Build a custom flow for handling user impersonation
User impersonation is a way to offer customer support by logging into your application from their accounts. Doing so enables you to directly reproduce and remedy any issues they're experiencing.
This guide will walk you through how to build a custom flow that handles user impersonation.
The following example builds a dashboard that is only accessible to users with the admin:impersonate
permission. You can modify the authorization checks to fit your use case.
In the dashboard, the user will see a list of the application's users. When the user chooses to impersonate a user, they will be signed in as that user and redirected to the homepage.
Use the following tabs to view the code for:
- The main page that gets the list of the application's users using Clerk's Backend SDK
- The Client Component that has the UI for displaying the users and the ability to impersonate them
- The Server Action that generates the actor token using Clerk's Backend API
import { auth, clerkClient } from '@clerk/nextjs/server'
import ImpersonateUsers from './_components'
export default async function AccountPage() {
const { has } = auth()
// Protect the page
if (!has({ permission: 'org:admin:impersonate' })) {
return <p>You do not have permission to access this page.</p>
}
// Fetch list of application's users using Clerk's Backend SDK
const users = await clerkClient.users.getUserList()
// This page needs to be a server component to use clerkClient.users.getUserList()
// You must pass the list of users to the client for the rest of the logic
// But you cannot pass the entire User object to the client,
// because its too complex. So grab the data you need, like so:
const parsedUsers = []
for (const user of users.data) {
parsedUsers.push({
id: user.id,
email: user.primaryEmailAddress?.emailAddress,
})
}
// Pass the parsed users to the Client Component
return <ImpersonateUsers users={parsedUsers} />
}
'use client'
import React from 'react'
import { useUser, useSignIn } from '@clerk/nextjs'
import { generateActorToken } from './_actions'
import { useRouter } from 'next/navigation'
type ParsedUser = {
id: string
email: string | undefined
}
export type Actor = {
object: string
id: string
status: 'pending' | 'accepted' | 'revoked'
user_id: string
actor: object
token: string | null
url: string | null
created_at: Number
updated_at: Number
}
// Create an actor token for the impersonation
async function createActorToken(actorId: string, userId: string) {
const res = await generateActorToken(actorId, userId) // The Server Action to generate the actor token
if (!res.ok) console.log('Error', res.message)
return res.token
}
export default function ImpersonateUsers({ users }: { users: ParsedUser[] }) {
const { isLoaded, signIn, setActive } = useSignIn()
const router = useRouter()
const { user } = useUser()
if (!user?.id) return null
// Handle "Impersonate" button click
async function impersonateUser(actorId: string, userId: string) {
if (!isLoaded) return
const actorToken = await createActorToken(actorId, userId)
// Sign in as the impersonated user
if (actorToken) {
try {
const { createdSessionId } = await signIn.create({
strategy: 'ticket',
ticket: actorToken,
})
await setActive({ session: createdSessionId })
router.push('/')
} catch (err) {
// See https://clerk.com/docs/custom-flows/error-handling
// for more info on error handling
console.error(JSON.stringify(err, null, 2))
}
}
}
return (
<>
<p>Hello {user?.primaryEmailAddress?.emailAddress}</p>
<h1>Users</h1>
<ul>
{users?.map((userFromUserList) => {
return (
<li key={userFromUserList.id} style={{ display: 'flex', gap: '4px' }}>
<p>{userFromUserList?.email ? userFromUserList.email : userFromUserList.id}</p>
<button onClick={async () => await impersonateUser(user.id, userFromUserList.id)}>
Impersonate
</button>
</li>
)
})}
</ul>
</>
)
}
'use server'
import { auth } from '@clerk/nextjs/server'
export async function generateActorToken(actorId: string, userId: string) {
// Check if the user has the permission to impersonate
if (!auth().has({ permission: 'org:admin:impersonate' })) {
return {
ok: false,
message: 'You do not have permission to access this page.',
}
}
const params = JSON.stringify({
user_id: userId,
actor: {
sub: actorId,
},
})
// Create an actor token using Clerk's Backend API
const res = await fetch('https://api.clerk.com/v1/actor_tokens', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.CLERK_SECRET_KEY}`,
'Content-type': 'application/json',
},
body: params,
})
if (!res.ok) {
return { ok: false, message: 'Failed to generate actor token' }
}
const data = await res.json()
return { ok: true, token: data.token }
}
Install the following dependencies:
npm i expo-linking @clerk/types
yarn add expo-linking @clerk/types
pnpm add expo-linking @clerk/types
Build the flow
The following example creates a basic dashboard for impersonating users.
The person visiting this dashboard should not be able to see it unless signed in. To protect the dashboard, create the dashboard
route group with a layout that redirects users if they're already signed in, as shown in the following example:
import { Redirect, Stack } from 'expo-router'
import { useAuth } from '@clerk/clerk-expo'
export default function GuestLayout() {
const { isSignedIn } = useAuth()
if (!isSignedIn) {
return <Redirect href={'/dashboard'} />
}
return <Stack />
}
Create the dashboard UI
Create the main page of the dashboard, which will hold most of your impersonation code. This example contains the skeleton of the final result, which will be fleshed out with each step in this guide. To skip the step-by-step process, see the full example at the end of this guide.
import React from 'react'
import { Button, StyleSheet, Text, TouchableOpacity, View } from 'react-native'
import { Link, useRouter } from 'expo-router'
import { useAuth, useUser, useSignIn, useSessionList } from '@clerk/clerk-expo'
import { UserDataJSON } from '@clerk/types'
import * as Linking from 'expo-linking'
import useImpersonatedUser from '@/hooks/useImpersonatedUser'
import useImpersonation from '@/hooks/useImpersonation'
export default function Dashboard() {
// This state will allow us to display some information about the current impersonator
const [impersonator, setImpersonator] = React.useState<UserDataJSON | string>('')
// Our Clerk hooks provide us with variables and functions to handle our authentication flows
const { signOut, actor } = useAuth()
const { isLoaded, signIn, setActive } = useSignIn()
const { user } = useUser()
const { sessions } = useSessionList()
// Using expo's router will allow us to navigate properly once we've newly authenticated
const router = useRouter()
const actorRes = useImpersonation(actor?.sub || undefined, user?.id)
const actorUserData = useImpersonatedUser(actor?.sub || '', setImpersonator)
// Our actor endpoint will return a URL containing the ticket value we need to pass to our signIn method
// This function will get fleshed out later in the doc
function extractTicketValue() {}
// `onSignoutPress` will determine which session we are attempting to sign out of and clear the session
// This function will get fleshed out later in the doc
async function onSignoutPress() {}
return (
<View>
<Link href="/account">
<Text>Account</Text>
</Link>
<Text>Hello {user?.firstName}</Text>
{sessions?.map((sesh) => (
<TouchableOpacity onPress={() => onSignOutPress(sesh.id)} key={sesh.id}>
<Text>Sign out of {sesh?.user?.primaryEmailAddress?.emailAddress}</Text>
</TouchableOpacity>
))}
{actorRes && <Button title="Impersonate" onPress={async () => await impersonateUser()} />}
</View>
)
}
Create a hook to generate an actor token
Create a custom hook called useImpersonation()
that will call an API route to generate and return an actor token from Clerk.
import { useState, useEffect } from 'react'
// This is the entire return type for actor token generation,
// Though we are only using a couple of the properties in the example
export type Actor = {
object: string
id: string
status: 'pending' | 'accepted' | 'revoked'
user_id: string
actor: object
token: string | null
url: string | null
created_at: Number
updated_at: Number
}
export default function useImpersonation(actorId: string | undefined, userId: string | undefined) {
const [actor, setActor] = useState<Actor>()
useEffect(() => {
async function generateAndSetToken() {
if (typeof actorId !== 'string') {
const res = await fetch('/generateActorToken', {
method: 'POST',
body: JSON.stringify({
// This is the user ID of the use you're going to impersonate
user_id: userId,
actor: {
// This is the ID of the impersonator
sub: actorId,
},
}),
})
const data = await res.json()
setActor(data)
}
}
generateAndSetToken()
}, [])
return actor
}
Create an API route to generate actor tokens
Now create an endpoint that will call Clerk's create actor token endpoint at /actor_tokens
and pass in the Clerk secret key for authorization. In your API, you should build in permission checks to make sure this is only being accessed from a trusted source.
export async function POST(request: Request) {
const body: { user_id: string; actor: { sub: string } } = await request.json()
const { user_id, actor } = body
const res = await fetch('https://api.clerk.com/v1/actor_tokens', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.CLERK_SECRET_KEY}`,
'Content-type': 'application/json',
},
body: JSON.stringify({
user_id,
actor: {
sub: actor.sub,
},
}),
})
const data = await res.json()
return Response.json(data)
}
Create a hook to get impersonated user data
Create a second custom hook called useImpersonatedUser()
that will fetch data about the impersonator. You can use this to display UI only the impersonator will see for specific actions, like showing who is currently impersonating and matching the session to the right user for logout.
function useImpersonatedUser(
actorSub: string,
setImpersonator: React.Dispatch<React.SetStateAction<string | UserDataJSON>>,
) {
React.useEffect(() => {
const getImpersonatedUser = async () => {
const res = await fetch(`/getImpersonatedUser`, {
method: 'POST',
body: JSON.stringify({
impersonator_id: actorSub,
}),
})
const data = await res.json()
setImpersonator(data)
getImpersonatedUser()
}
}, [actorSub])
}
Create an API route to retrieve user data
With your hook setup you can now create an API route that will call Clerk's retrieve user endpoint at /users
and get back the Impersonated user's full User
object.
export async function POST(request: Request) {
const body: { impersonator_id: string } = await request.json()
const { impersonator_id } = body
const res = await fetch(`https://api.clerk.com/v1/users/${impersonator_id}`, {
headers: {
Authorization: `Bearer ${process.env.CLERK_SECRET_KEY}`,
},
})
const data = await res.json()
return Response.json(data)
}
Create functions to generate an impersonated session
Now that you have the functionality for generating an impersonated session & getting the impersonated user's data, there are a few more functions to complete that will:
- Get the
ticket
from the URL generated by youruseImpersonation()
hook. - Pass the
ticket
to Clerk'ssignIn()
function to create a new impersonated session, allowing you to sign in the impersonated user or impersonator.
The following code should be placed in the Page
component of your app/(dashboard)/index.tsx
file:
// Use this function to get the ticket ID from the response of our generateActorToken API
function extractTicketValue(input: string): string | undefined {
const index = input.indexOf('ticket=')
if (index !== -1) {
return input.slice(index + 7)
}
return undefined
}
// Passing the ticket ID to our `signIn.create` function will create a new impersonated session that we can set to the active session
async function impersonateUser() {
if (!isLoaded) return
if (typeof actorRes?.url === 'string') {
const ticket = extractTicketValue(actorRes.url)
if (ticket) {
try {
const { createdSessionId } = await signIn.create({
strategy: 'ticket',
ticket,
})
await setActive({ session: createdSessionId })
await user?.reload()
router.replace('/dashboard')
} catch (err) {
// See https://clerk.com/docs/custom-flows/error-handling
// for more info on error handling
console.error(JSON.stringify(err, null, 2))
}
}
}
}
Create a hook for signing out
Finally, create a helper function for signing out. Usually this can be as simple as calling Clerk's signOut()
function, but since you're handling multiple sessions, you must add some checks to ensure you're signing out of the right session.
const onSignOutPress = async (sessionId: string) => {
try {
if (isLoaded && sessions && sessions?.length > 0) {
const noActiveSessions = sessions.filter((session) => session.user?.id !== user?.id)
await setActive({ session: noActiveSessions[0].id })
}
const redirectUrl = Linking.createURL('/dashboard', { scheme: 'myapp' })
await signOut({
sessionId,
})
router.replace(redirectUrl)
} catch (err: any) {
// See https://clerk.com/docs/custom-flows/error-handling
// for more info on error handling
console.error(JSON.stringify(err, null, 2))
}
}
Full example
The full code for the dashboard page will resemble the following, excluding the API routes:
import React from 'react'
import { Button, StyleSheet, Text, TouchableOpacity, View } from 'react-native'
import { Link, useRouter } from 'expo-router'
import { useAuth, useUser, useSignIn, useSessionList } from '@clerk/clerk-expo'
import { UserDataJSON } from '@clerk/types'
import * as Linking from 'expo-linking'
export type Actor = {
object: string
id: string
status: 'pending' | 'accepted' | 'revoked'
user_id: string
actor: object
token: string | null
url: string | null
created_at: Number
updated_at: Number
}
function useImpersonation(actorId: string | undefined, userId: string | undefined) {
const [actor, setActor] = React.useState<Actor>()
React.useEffect(() => {
async function generateAndSetToken() {
if (typeof actorId !== 'string') {
const res = await fetch('/generateActorToken', {
method: 'POST',
body: JSON.stringify({
user_id: userId, // This is the user ID of the use you're going to impersonate,
actor: {
sub: actorId, // This is the ID of the impersonator,
},
}),
})
const data = await res.json()
setActor(data)
}
}
generateAndSetToken()
}, [])
return actor
}
function useImpersonatedUser(
actorSub: string,
setImpersonator: React.Dispatch<React.SetStateAction<string | UserDataJSON>>,
) {
React.useEffect(() => {
const getImpersonatedUser = async () => {
const res = await fetch(`/getImpersonatedUser`, {
method: 'POST',
body: JSON.stringify({
impersonator_id: actorSub,
}),
})
const data = await res.json()
setImpersonator(data)
getImpersonatedUser()
}
}, [actorSub])
}
export default function Page() {
const [impersonator, setImpersonator] = React.useState<UserDataJSON | string>('')
const { signOut, actor } = useAuth()
const { isLoaded, signIn, setActive } = useSignIn()
const { user } = useUser()
const router = useRouter()
const { sessions } = useSessionList()
const actorRes = useImpersonation(actor?.sub || undefined, user?.id)
const actorUserData = useImpersonatedUser(actor?.sub || '', setImpersonator)
function extractTicketValue(input: string): string | undefined {
const index = input.indexOf('ticket=')
if (index !== -1) {
return input.slice(index + 7)
}
return undefined
}
async function impersonateUser() {
if (!isLoaded) return
if (typeof actorRes?.url === 'string') {
const ticket = extractTicketValue(actorRes.url)
if (ticket) {
try {
const { createdSessionId } = await signIn.create({
strategy: 'ticket',
ticket,
})
await setActive({ session: createdSessionId })
await user?.reload()
router.replace('/dashboard')
} catch (err) {
// See https://clerk.com/docs/custom-flows/error-handling
// for more info on error handling
console.error(JSON.stringify(err, null, 2))
}
}
}
}
const onSignOutPress = async (sessionId: string) => {
try {
if (isLoaded && sessions && sessions?.length > 0) {
const noActiveSessions = sessions.filter((session) => session.user?.id !== user?.id)
await setActive({ session: noActiveSessions[0].id })
}
const redirectUrl = Linking.createURL('/dashboard', { scheme: 'myapp' })
await signOut({
sessionId,
})
router.replace(redirectUrl)
} catch (err: any) {
// See https://clerk.com/docs/custom-flows/error-handling
// for more info on error handling
console.error(JSON.stringify(err, null, 2))
}
}
return (
<View>
<Link href="/account">
<Text>Account</Text>
</Link>
<Text>Hello {user?.firstName}</Text>
{sessions?.map((sesh) => (
<TouchableOpacity onPress={() => onSignOutPress(sesh.id)} key={sesh.id}>
<Text>Sign out of {sesh?.user?.primaryEmailAddress?.emailAddress}</Text>
</TouchableOpacity>
))}
{actorRes && <Button title="Impersonate" onPress={async () => await impersonateUser()} />}
</View>
)
}
Feedback
Last updated on