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. You will build a dashboard that is only accessible to users with the org:admin:impersonate permission. The user visiting the dashboard will see a list of the application's users. When they choose to impersonate a user, they will be signed in as that user and redirected to the homepage.
Before you start
This example builds a dashboard that is only accessible to users with the org:admin:impersonate permission. To use this example, you must first create the custom org:admin:impersonate permission. Or you can modify the to fit your use case.
Build the custom flow
This example is written for Next.js App Router but it can be adapted for any React-based framework, such as React Router or Tanstack React Start.
Use the following tabs to view the code for:
- The main page that gets the list of the application's users using the JS 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 the Backend API
import { auth, clerkClient } from '@clerk/nextjs/server'
import ImpersonateUsers from './_components'
export default async function AccountPage() {
const { has } = await auth()
// Protect the page
if (!has({ permission: 'org:admin:impersonate' })) {
return <p>You do not have permission to access this page.</p>
}
const client = await clerkClient()
// Fetch list of application's users using Clerk's JS Backend SDK
const users = await client.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 { isSignedIn, user } = useUser()
if (!isSignedIn) {
// Handle signed out state
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/guides/development/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 }
}- Create a
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. The <Protect> component is used to ensure that only users with theorg:admin:impersonatePermission can access it. You can modify thepermissionattribute to fit your use case.
import { Redirect, Stack } from 'expo-router'
import { Protect, useAuth } from '@clerk/clerk-expo'
import { Text } from 'react-native'
export default function GuestLayout() {
const { isSignedIn } = useAuth()
// Protect the route from unauthenticated users
if (!isSignedIn) return <Redirect href={'/'} />
return (
<Protect
permission="org:admin:impersonate"
fallback={<Text>You don't have the permissions to access the dashboard.</Text>}
>
<Stack />
</Protect>
)
}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 a 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 a
hooks/directory. - In the
hooks/directory, create auseUsers.tsxfile with the following code. This creates a hook that will fetch the list of your application's users.
import { UserJSON } from '@clerk/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 an index.tsx file with the following code. This creates the UI for the dashboard, which displays a list of users and allows you to impersonate one.
import { ThemedText } from '@/components/themed-text'
import { ThemedView } from '@/components/themed-view'
import { useSignIn, useUser } from '@clerk/clerk-expo'
import { Redirect, useRouter } from 'expo-router'
import React, { useState } from 'react'
import { Pressable, ScrollView, StyleSheet, View } from 'react-native'
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()
// Protect the route from unauthenticated users
if (!isSignedIn) return <Redirect href={'/'} />
// 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 (
<ScrollView>
<ThemedView style={styles.container}>
<ThemedText type="title" style={styles.welcomeText}>
Welcome to the dashboard, {user?.firstName}!
</ThemedText>
<ThemedText style={styles.userIdText}>Your user ID is {user?.id}</ThemedText>
{isLoading && <ThemedText style={styles.loadingText}>Loading your users...</ThemedText>}
{!isLoading && users && (
<View style={styles.tableContainer}>
<View style={styles.tableHeader}>
<ThemedText style={[styles.tableHeaderText, styles.tableCell]}>User ID</ThemedText>
<ThemedText style={[styles.tableHeaderText, styles.tableCell]}>Email ID</ThemedText>
<ThemedText style={[styles.tableHeaderText, styles.tableCell]}>First Name</ThemedText>
<ThemedText style={[styles.tableHeaderText, styles.tableCell]}>Actions</ThemedText>
</View>
{users.map((userFromList) => {
const primaryEmail = userFromList.email_addresses?.find(
(email) => email.id === userFromList.primary_email_address_id,
)
return (
<View key={userFromList.id} style={styles.tableRow}>
<ThemedText style={styles.tableCell}>{userFromList.id}</ThemedText>
<ThemedText style={styles.tableCell}>{primaryEmail?.id || 'N/A'}</ThemedText>
<ThemedText style={styles.tableCell}>
{userFromList.first_name || 'N/A'}
</ThemedText>
<View style={styles.tableCell}>
{/* Don't allow impersonation of yourself */}
{userFromList.id !== user.id ? (
<Pressable
style={({ pressed }) => [styles.button, pressed && styles.buttonPressed]}
onPress={async () => await impersonateUser(user.id, userFromList.id)}
>
<ThemedText style={styles.buttonText}>Impersonate</ThemedText>
</Pressable>
) : (
<ThemedText style={styles.disabledText}>
Cannot impersonate yourself
</ThemedText>
)}
</View>
</View>
)
})}
</View>
)}
{error && (
<View style={styles.errorContainer}>
<ThemedText style={styles.errorText}>{error}</ThemedText>
</View>
)}
</ThemedView>
</ScrollView>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
gap: 16,
},
welcomeText: {
marginBottom: 8,
},
userIdText: {
fontSize: 16,
marginBottom: 16,
},
loadingText: {
fontSize: 14,
opacity: 0.8,
},
tableContainer: {
marginTop: 16,
gap: 4,
},
tableHeader: {
flexDirection: 'row',
paddingVertical: 12,
paddingHorizontal: 8,
borderBottomWidth: 2,
borderBottomColor: '#ccc',
backgroundColor: '#f5f5f5',
},
tableHeaderText: {
fontWeight: '600',
},
tableRow: {
flexDirection: 'row',
paddingVertical: 12,
paddingHorizontal: 8,
borderBottomWidth: 1,
borderBottomColor: '#eee',
},
tableCell: {
flex: 1,
fontSize: 14,
},
button: {
backgroundColor: '#0a7ea4',
paddingVertical: 8,
paddingHorizontal: 12,
borderRadius: 6,
alignItems: 'center',
},
buttonPressed: {
opacity: 0.7,
},
buttonText: {
color: '#fff',
fontWeight: '600',
fontSize: 12,
},
disabledText: {
fontSize: 12,
opacity: 0.6,
fontStyle: 'italic',
},
errorContainer: {
padding: 12,
backgroundColor: '#ffebee',
borderRadius: 8,
marginTop: 16,
},
errorText: {
color: '#c62828',
fontWeight: '500',
},
})Examples for this SDK aren't available yet. For now, try switching to a supported SDK, such as Next.js, and converting the code to fit your SDK.
Feedback
Last updated on