Docs
User Sessions
Experimental

User Sessions

Allows the user to review and manage their active sessions

When working with Authorization Servers and Web Applications, it is important to remember that there is always more than one session involved in the process of tracking the authentication state of a user.

Logging users out implies invalidating all sessions related to this authentication state. It is beacuse of this that the OIDC Back-channel Logout comes to play an important role to guarantee safely invalidating all user's active sessions.

You can learn more about the topic of logging users out safely at:

    Active Sessions

    Installation

    Install the following dependencies:

    npx shadcn-ui@latest add badge button card label separator toast
    pnpm install moment ua-parser-js

    Copy and paste the following code into your project.

    "use client"; import moment from "moment"; import { useCallback, useEffect, useState } from "react"; import { UAParser } from "ua-parser-js"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Label } from "@/components/ui/label"; import { Separator } from "@/components/ui/separator"; import { Toaster } from "@/components/ui/toaster"; import { useToast } from "@/components/ui/use-toast"; function Spinner() { return ( <svg xmlns="http://www.w3.org/2000/svg" width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="mr-2 animate-spin" > <path d="M21 12a9 9 0 1 1-6.219-8.56"></path> </svg> ); } interface KeyValueMap { [key: string]: any; } type UserSessionsProps = { user: KeyValueMap; sessions?: KeyValueMap[]; onFetch: () => Promise<{ sessions?: KeyValueMap[]; status: number }>; onDelete: (sessionId: string) => Promise<{ id?: string; status: number; }>; }; export default function UserSessions({ user, sessions, onFetch, onDelete, }: UserSessionsProps) { const { toast } = useToast(); const [currentSessions, setCurrentSessions] = useState< KeyValueMap[] | undefined >(sessions); const [fetching, setFetching] = useState(false); const [isRevokingSession, setIsRevokingSession] = useState<string | null>( null ); const handleRevokeSession = (sessionId: string) => async () => { setIsRevokingSession(sessionId); const response = await onDelete(sessionId); if (response.status !== 200) { setIsRevokingSession(null); return toast({ title: "Info", description: "There was a problem removing the session. Try again later.", }); } const { id } = response; setCurrentSessions((prev) => prev?.filter((session) => session.id !== id)); setIsRevokingSession(null); }; const handleFetchSessions = useCallback( async function handleFetchSessions() { setFetching(true); const response = await onFetch(); if (response.status !== 200) { return setFetching(false); } setCurrentSessions(response.sessions); setFetching(false); }, [onFetch] ); useEffect(() => { (async () => { if (!sessions) { await handleFetchSessions(); } })(); }, [sessions, handleFetchSessions]); return ( <> <Toaster /> <Card> <CardHeader className="p-4 md:p-6"> <CardTitle className="text-lg font-normal">Active Sessions</CardTitle> <CardDescription></CardDescription> </CardHeader> <CardContent className="grid gap-6 p-4 pt-0 md:p-6 md:pt-0"> {fetching && ( <div className="flex w-full items-center justify-left"> <Spinner /> <span className="text-sm text-muted-foreground"> Retrieving your active sessions... </span> </div> )} {!currentSessions && !fetching && ( <div className="flex flex-col gap-6"> <Separator /> <div className="flex items-center justify-between space-x-2"> <Label className="flex flex-col space-y-2"> <p className="font-normal leading-snug text-muted-foreground max-w-fit"> There was a problem listing all user sessions. Try again later. </p> </Label> </div> </div> )} {currentSessions && currentSessions .sort(({ id }) => (id === user.sid ? -1 : 1)) .map((session, idx) => { const { id } = session; const lastUA = new UAParser( session.device?.last_user_agent || "unknown" ).getResult(); return ( <div key={`session-${idx}-${id}`} className="flex flex-col gap-6" > {idx > 0 && <Separator />} <div key={id} className="flex flex-col md:flex-row items-center justify-between md:space-x-2 space-y-6 md:space-y-0" > <Label className="flex flex-col space-y-1"> <span className="leading-6"> {`Session on ${lastUA.browser.name} - ${lastUA.os.name} [${lastUA.os.version}]`} {id === user.sid && ( <Badge variant="default" className="h-fit bg-green-300 text-black ml-3 font-light hover:bg-green-300" > Current </Badge> )} </span> <p className="font-normal leading-snug text-muted-foreground max-w-fit"> Last activity{" "} <span className="underline decoration-dotted cursor-help" title={session.updated_at} > {moment(session.updated_at).fromNow()} </span>{" "} from location{" "} <span className="underline decoration-dotted cursor-help" title={session.device?.last_ip} > {session.device?.last_ip} </span> . <br /> First sign-in on{" "} <span className="underline decoration-dotted cursor-help" title={session.created_at} > {moment(session.created_at).format( "MMMM DD, YYYY \\a\\t HH:MM:SS" )} </span> . </p> </Label> <div className="flex space-x-24 items-center justify-end md:min-w-24"> <Button className="h-fit min-w-24" variant="outline" onClick={handleRevokeSession(id)} disabled={isRevokingSession === id} > {isRevokingSession === id && <Spinner />} Sign out </Button> </div> </div> </div> ); })} </CardContent> </Card> </> ); }

    Component behavior

    By design, our components provide basic behavior without making any requests to the Auth0 Management API. To help you implement the full feature, we've also included React Hooks and NextJS routers for calling and proxying the Auth0 Management API.

    Update the import paths to match your project setup.

    React Hooks

    useUserSessions

    A hook to manage current user sessions.

    import { useCallback } from "react"; interface KeyValueMap { [key: string]: any; } export default function useUserSessions() { const fetchUserSessions = useCallback(async (): Promise<{ sessions?: KeyValueMap[]; status: number; }> => { try { /** * '/api/auth/user/sessions' is a custom endpoint which will proxy * the request to the Auth0 Management API. * * Proxy sample at: * - https://components.lab.auth0.com/docs/components/user-sessions#nextjs-routers */ const response = await fetch("/api/auth/user/sessions", { method: "GET", headers: { "Content-Type": "application/json", }, }); // TODO: Better handling rate limits if (response.status === 429) { return { status: 429 }; } const sessions: KeyValueMap[] = await response.json(); return { sessions: sessions, status: response.status, }; } catch (error) { console.error(error); return { status: 500 }; } }, []); const deleteUserSession = useCallback( async ( sessionId: string ): Promise<{ id?: string; status: number; }> => { try { /** * '/api/auth/user/sessions/[id]' is a custom endpoint which will proxy * the request to the Auth0 Management API. * * Proxy sample at: * - https://components.lab.auth0.com/docs/components/user-sessions#nextjs-routers */ const response = await fetch(`/api/auth/user/sessions/${sessionId}`, { method: "DELETE", }); // TODO: Better handling rate limits if (response.status === 429) { return { status: 429 }; } return { id: sessionId, status: response.status }; } catch (error) { console.error(error); return { status: 500 }; } }, [] ); return { fetchUserSessions, deleteUserSession }; }

    NextJS routers

    UserSessions router

    Handles user sessions fetch and revocation.

    import { ManagementClient } from "auth0"; import { NextResponse } from "next/server"; import { getSession, withApiAuthRequired } from "@auth0/nextjs-auth0"; /** * Make sure to install the withRateLimit from: * - https://components.lab.auth0.com/docs/rate-limit#helpers */ import { withRateLimit } from "./helpers/rate-limit"; const client = new ManagementClient({ domain: new URL(process.env.AUTH0_ISSUER_BASE_URL!).host, clientId: process.env.AUTH0_CLIENT_ID_MGMT!, clientSecret: process.env.AUTH0_CLIENT_SECRET_MGMT!, }); /** * @example export const GET = handleUserSessionsFetch(); */ export function handleUserSessionsFetch() { return withRateLimit( withApiAuthRequired(async (): Promise<NextResponse> => { try { const session = await getSession(); const user_id = session?.user.sub; const response = await client.users.getSessions({ user_id, }); const { data } = response; return NextResponse.json(data.sessions || [], { status: response.status, }); } catch (error) { console.error(error); return NextResponse.json( { error: "Error fetching user metadata" }, { status: 500 } ); } }) ); } /** * @example export const DELETE = handleDeleteUserSession(); */ export function handleDeleteUserSession() { return withRateLimit( withApiAuthRequired( async (request: Request, { params }: any): Promise<NextResponse> => { try { const { id }: { id: string } = params; await client.sessions.delete({ id, }); return NextResponse.json( { id }, { status: 200, } ); } catch (error) { console.error(error); return NextResponse.json( { error: "Error deleting MFA Enrollment" }, { status: 500 } ); } } ) ); }