Build a custom flow for handling user impersonation
Clerk's user impersonation feature allows you to sign in to your application as one of your users, enabling you to directly reproduce and remedy any issues they're experiencing. It's a helpful feature for customer support and debugging.
This guide will walk you through how to build a custom flow that handles user impersonation.
The following example creates a basic dashboard for impersonating users.
- Create the
dashboard/directory. - In the
dashboard/directory, create a_layout.tsxfile with the following code. The useAuth() hook is used to access the user's authentication state. If the user is already signed in, they'll be redirected to the home page. The <Show > component is used to ensure that only users with theorg:dashboard:accessPermission can access it. You can modify thewhen: {{ permission: '...' }}attribute to fit your use case.
import { Redirect, Stack } from 'expo-router'
import { Show, useAuth } from '@clerk/expo'
import { Text } from 'react-native'
export default function GuestLayout() {
const { isSignedIn } = useAuth()
if (!isSignedIn) {
return <Redirect href={'/'} />
}
return (
<Show
when={{ permission: 'org:dashboard:access' }}
fallback={<Text>You don't have the permissions to access the dashboard.</Text>}
>
<Stack />
</Show>
)
}Create an API route to generate actor tokens
To sign in as a different user, you must supply an actor token when creating a session.
Create the generateActorToken+api.tsx file with the following code. This creates an API route that will call Clerk's Backend API /actor_tokens endpoint to create an actor token.
export async function POST(req: Request) {
const { actorId, userId } = await req.json()
try {
// 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: JSON.stringify({
user_id: userId,
actor: {
sub: actorId,
},
}),
})
const data = await res.json()
return Response.json(data)
} catch (err) {
return Response.json({ error: 'Failed to generate actor token' }, { status: 500 })
}
}Create a hook to get users
To impersonate a user, you need a list of your application's users to be able to select one for impersonation.
- Create the
hooks/directory. - In the
hooks/directory, create theuseUsers.tsxfile with the following code. This creates a hook that will fetch the list of your application's users.
import { UserJSON } from '@clerk/shared/types'
import { useEffect, useState } from 'react'
type UseUsersReturn = {
users: UserJSON[] | null
isLoading: boolean
error: Error | null
}
/**
* Returns a list of users for the application.
*
* Until the users are fetched, `isLoading` will be set to `true`.
*
* @example
*
* import { useUsers } from '@/app/hooks/useUsers';
*
* function Hello() {
* const { users, isLoading, error } = useUsers();
* if(isLoading) {
* return <div>Loading...</div>;
* }
* return <div>Users: {users?.map((user) => user.firstName).join(', ')}</div>
* }
*/
export default function useUsers(): UseUsersReturn {
const [users, setUsers] = useState<UserJSON[] | null>(null)
const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState<Error | null>(null)
useEffect(() => {
const getUsers = async () => {
try {
const res = await fetch('/getUsers')
if (!res.ok) {
throw new Error('Failed to fetch users')
}
const data = await res.json()
setUsers(data)
} catch (err) {
setError(err instanceof Error ? err : new Error('Unknown error'))
} finally {
setIsLoading(false)
}
}
getUsers()
}, []) // Remove users from dependency array to prevent infinite loop
return { users, isLoading, error }
}Create the dashboard UI
- In the
dashboard/directory, create theindex.tsxfile with the following code. This creates the UI for the dashboard, which displays a list of users and allows you to impersonate one.
import React, { useState } from 'react'
import { Button, Text, View } from 'react-native'
import { useRouter } from 'expo-router'
import { useUser, useSignIn } from '@clerk/expo'
import useUsers from '../hooks/useUsers'
export default function Dashboard() {
const [error, setError] = useState<string | null>(null)
const { isLoaded, signIn, setActive } = useSignIn()
const { isSignedIn, user } = useUser()
const router = useRouter()
const { users, isLoading } = useUsers()
if (!isSignedIn) {
// Handle signed out state
return null
}
// Create an actor token for the impersonation
async function createActorToken(actorId: string, userId: string) {
setError(null)
try {
const res = await fetch('/generateActorToken', {
method: 'POST',
body: JSON.stringify({
actorId,
userId,
}),
})
const data = await res.json()
if (data.errors) {
setError(data.errors[0].long_message)
return null
}
return data.token
} catch (err) {
setError('Failed to generate actor token')
return null
}
}
// Handle "Impersonate" button click
async function impersonateUser(actorId: string, userId: string) {
setError(null)
if (!isLoaded) return
// Calls your /generateActorToken API route
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) {
setError('Failed to impersonate user')
// See https://clerk.com/docs/guides/development/custom-flows/error-handling
// for more info on error handling
console.error(JSON.stringify(err, null, 2))
}
}
}
return (
<View>
<Text style={{ fontSize: 24, fontWeight: 'bold', padding: 10 }}>
Welcome to the dashboard, {user?.firstName}!
</Text>
<Text style={{ fontSize: 16, padding: 10 }}>Your user ID is {user?.id}</Text>
{isLoading && <Text>Loading your users...</Text>}
{!isLoading && users && (
<View style={{ padding: 10 }}>
<View style={{ flexDirection: 'row', padding: 5, borderBottomWidth: 1 }}>
<Text style={{ flex: 1, fontWeight: 'bold' }}>User ID</Text>
<Text style={{ flex: 1, fontWeight: 'bold' }}>Email ID</Text>
<Text style={{ flex: 1, fontWeight: 'bold' }}>First Name</Text>
<Text style={{ flex: 1, fontWeight: 'bold' }}>Actions</Text>
</View>
{users.map((userFromList) => {
const primaryEmail = userFromList.email_addresses?.find(
(email) => email.id === userFromList.primary_email_address_id,
)
return (
<View key={userFromList.id} style={{ flexDirection: 'row', padding: 5 }}>
<Text style={{ flex: 1 }}>{userFromList.id}</Text>
<Text style={{ flex: 1 }}>{primaryEmail?.id || 'N/A'}</Text>
<Text style={{ flex: 1 }}>{userFromList.first_name || 'N/A'}</Text>
<View style={{ flex: 1 }}>
{/* Don't allow impersonation of yourself */}
{userFromList.id !== user.id ? (
<Button
title="Impersonate"
onPress={async () => await impersonateUser(user.id, userFromList.id)}
/>
) : (
<Text>You cannot impersonate yourself</Text>
)}
</View>
</View>
)
})}
</View>
)}
{error && (
<View style={{ padding: 10, backgroundColor: '#ffebee' }}>
<Text style={{ color: '#c62828' }}>{error}</Text>
</View>
)}
</View>
)
}Feedback
Last updated on