Docs

Build a custom flow for handling user impersonation

Warning

This guide is for users who want to build a custom user interface using the Clerk API. To use a prebuilt UI, you should use Clerk's Account Portal pages or prebuilt components.

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
app/dashboard/page.tsx
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>
  }

  // 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} />
}
app/dashboard/_components.tsx
'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>
    </>
  )
}
app/dashboard/_actions.ts
'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 }
}

Warning

This example uses expo-router's experimental API routes for simplicity. However, you can use your own backend server to handle the API routes.

To use Expo's experimental API routes, you must follow the Expo deployment docs. Doing so allows your impersonation endpoints to access to your CLERK_SECRET_KEY.

Install the following dependencies:

terminal
npm i expo-linking @clerk/types
terminal
yarn add expo-linking @clerk/types
terminal
pnpm add expo-linking @clerk/types

Build the flow

The following example creates a basic dashboard for impersonating users.

Warning

It is recommended that you should build impersonation into an admin-only dashboard that only authorized users can access.

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:

app/(dashboard)/_layout.tsx
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.

app/(dashboard)/index.tsx
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.

hooks/useImpersonation.tsx
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.

app/generateActorToken+api.tsx
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.

hooks/useImpersonatedUser.tsx
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.

app/getImpersonatedUser+api.tsx
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:

  1. Get the ticket from the URL generated by your useImpersonation() hook.
  2. Pass the ticket to Clerk's signIn() 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:

app/(dashboard)/index.tsx
// 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.

app/(dashboard)/index.tsx
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:

app/(dashboard)/index.tsx
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

What did you think of this content?

Last updated on