[GH-ISSUE #2278] How to impersonate a user without the admin plugin #9128

Closed
opened 2026-04-13 04:28:51 -05:00 by GiteaMirror · 3 comments
Owner

Originally created by @SpeedOfSpin on GitHub (Apr 14, 2025).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/2278

Hi All,

Hoping someone can help me. I have implemented custom roles and permission in my nextjs 15 app rather than use the admin plugin because I needed more flexibility. I really like the impersonate user functionality but I havent been able to recreate it.

I've looked through the admin plugin source code and believe I have to close but it doesnt work.
I have created an endpoint and trying to set the cookies to the new user but I'm a bit lost.
Here is my endpoint code

// Example using Next.js API Route (adapt for Server Actions if preferred)
import { NextRequest, NextResponse } from "next/server";
import { getDate } from "date-fns"; // Adjust import paths
import { APIError, AuthContext, GenericEndpointContext, User } from "better-auth";
import { setSessionCookie } from "better-auth/cookies";
import { deleteSessionCookie } from "better-auth/cookies";
import { createAuthMiddleware, getSessionFromCtx } from "better-auth/api";
import { auth } from "@/auth";
import { isAdmin } from "@/permissions";
import { z } from "zod";
import { cookies } from "next/headers";
import { dbClient } from "@/shared/server/lib/server-action";
import { sessionTable } from "@/db/schemas/auth-schema";
import cuid from "cuid";
export const dynamic = "force-dynamic";
import { createRandomStringGenerator } from "@better-auth/utils/random";

// Define your admin roles/IDs - Replace plugin's hasPermission logic
const ADMIN_ROLES = ["admin"]; // Or check against specific user IDs

const impersonateSchema = z.object({
    targetUserId: z.string(),
});

interface AdminUser {
    role?: string;
    roles?: string[];
}

// Helper function to check if user has admin role/roles
function checkAdminAccess(user: AdminUser): boolean {
    if (user.role === "admin") return true;
    if (Array.isArray(user.roles) && user.roles.includes("admin")) return true;
    return false;
}

export async function POST(request: NextRequest) {
    try {
        console.log("impersonate");
        const session = await auth.api.getSession({
            headers: request.headers,
        });

        // if (!session?.user) {
        //     return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
        // }

        // Validate request body
        const body = await request.json();
        const result = impersonateSchema.safeParse(body);

        const ctx = await auth;

        if (!result.success) {
            return NextResponse.json({ error: "Invalid request" }, { status: 400 });
        }

        const { targetUserId } = result.data;

        const token = createRandomStringGenerator("a-z", "A-Z", "0-9")(32);
        console.log("token", token);
        await dbClient.insert(sessionTable).values({
            id: cuid(),
            impersonatedBy: targetUserId,
            token: token,
            userId: targetUserId,
            expiresAt: new Date(Date.now() + 60 * 60 * 1000), // 1 hour from now
            createdAt: new Date(),
            updatedAt: new Date(),
        });

        // Create response with success status
        const response = NextResponse.json({ success: true });

        // Set the cookie in the response headers
        response.cookies.set("better-auth.session_token", token, {
            httpOnly: true,
            secure: process.env.NODE_ENV === "production",
            sameSite: "lax",
            path: "/",
            // Set max age to 1 hour to match the session expiry
            maxAge: 60 * 60, // 1 hour in seconds
        });

        return response;
    } catch (error) {
        console.error("Impersonation error:", error);
        return NextResponse.json({ error: "Failed to impersonate user" }, { status: 500 });
    }
}

Any help would be appreciated.

Originally created by @SpeedOfSpin on GitHub (Apr 14, 2025). Original GitHub issue: https://github.com/better-auth/better-auth/issues/2278 Hi All, Hoping someone can help me. I have implemented custom roles and permission in my nextjs 15 app rather than use the admin plugin because I needed more flexibility. I really like the impersonate user functionality but I havent been able to recreate it. I've looked through the admin plugin source code and believe I have to close but it doesnt work. I have created an endpoint and trying to set the cookies to the new user but I'm a bit lost. Here is my endpoint code ``` // Example using Next.js API Route (adapt for Server Actions if preferred) import { NextRequest, NextResponse } from "next/server"; import { getDate } from "date-fns"; // Adjust import paths import { APIError, AuthContext, GenericEndpointContext, User } from "better-auth"; import { setSessionCookie } from "better-auth/cookies"; import { deleteSessionCookie } from "better-auth/cookies"; import { createAuthMiddleware, getSessionFromCtx } from "better-auth/api"; import { auth } from "@/auth"; import { isAdmin } from "@/permissions"; import { z } from "zod"; import { cookies } from "next/headers"; import { dbClient } from "@/shared/server/lib/server-action"; import { sessionTable } from "@/db/schemas/auth-schema"; import cuid from "cuid"; export const dynamic = "force-dynamic"; import { createRandomStringGenerator } from "@better-auth/utils/random"; // Define your admin roles/IDs - Replace plugin's hasPermission logic const ADMIN_ROLES = ["admin"]; // Or check against specific user IDs const impersonateSchema = z.object({ targetUserId: z.string(), }); interface AdminUser { role?: string; roles?: string[]; } // Helper function to check if user has admin role/roles function checkAdminAccess(user: AdminUser): boolean { if (user.role === "admin") return true; if (Array.isArray(user.roles) && user.roles.includes("admin")) return true; return false; } export async function POST(request: NextRequest) { try { console.log("impersonate"); const session = await auth.api.getSession({ headers: request.headers, }); // if (!session?.user) { // return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); // } // Validate request body const body = await request.json(); const result = impersonateSchema.safeParse(body); const ctx = await auth; if (!result.success) { return NextResponse.json({ error: "Invalid request" }, { status: 400 }); } const { targetUserId } = result.data; const token = createRandomStringGenerator("a-z", "A-Z", "0-9")(32); console.log("token", token); await dbClient.insert(sessionTable).values({ id: cuid(), impersonatedBy: targetUserId, token: token, userId: targetUserId, expiresAt: new Date(Date.now() + 60 * 60 * 1000), // 1 hour from now createdAt: new Date(), updatedAt: new Date(), }); // Create response with success status const response = NextResponse.json({ success: true }); // Set the cookie in the response headers response.cookies.set("better-auth.session_token", token, { httpOnly: true, secure: process.env.NODE_ENV === "production", sameSite: "lax", path: "/", // Set max age to 1 hour to match the session expiry maxAge: 60 * 60, // 1 hour in seconds }); return response; } catch (error) { console.error("Impersonation error:", error); return NextResponse.json({ error: "Failed to impersonate user" }, { status: 500 }); } } ``` Any help would be appreciated.
GiteaMirror added the locked label 2026-04-13 04:28:51 -05:00
Author
Owner

@s3f5 commented on GitHub (Apr 14, 2025):

Please take a look at nextjs docs: https://nextjs.org/docs/app/api-reference/functions/cookies#setting-a-cookie

<!-- gh-comment-id:2802767560 --> @s3f5 commented on GitHub (Apr 14, 2025): Please take a look at nextjs docs: https://nextjs.org/docs/app/api-reference/functions/cookies#setting-a-cookie
Author
Owner

@Kinfe123 commented on GitHub (Apr 15, 2025):

await dbClient.insert(sessionTable).values({
id: cuid(),
impersonatedBy: targetUserId,
token: token,
userId: targetUserId,
expiresAt: new Date(Date.now() + 60 * 60 * 1000), // 1 hour from now
createdAt: new Date(),
updatedAt: new Date(),
});

here make sure to use the impersonatedBy: session.user.id since they are the one impersonating

While impersonating, you will maintain two cookies: the admin cookie and the impersonated user's cookie. Make sure to store the admin session with the admin_session cookie name, including the session token and secret. By doing this, we reserve the admin session. Now, we can modify the regular session cookie (better-auth.session_token) with the impersonated session created by the admin. Later, when we stop impersonating, we will restore the original admin_session that we reserved earlier

<!-- gh-comment-id:2805640409 --> @Kinfe123 commented on GitHub (Apr 15, 2025): > await dbClient.insert(sessionTable).values({ > id: cuid(), > impersonatedBy: targetUserId, > token: token, > userId: targetUserId, > expiresAt: new Date(Date.now() + 60 * 60 * 1000), // 1 hour from now > createdAt: new Date(), > updatedAt: new Date(), > }); here make sure to use the `impersonatedBy: session.user.id` since they are the one impersonating While impersonating, you will maintain two cookies: the admin cookie and the impersonated user's cookie. Make sure to store the admin session with the admin_session cookie name, including the session token and secret. By doing this, we reserve the admin session. Now, we can modify the regular session cookie (better-auth.session_token) with the impersonated session created by the admin. Later, when we stop impersonating, we will restore the original admin_session that we reserved earlier
Author
Owner

@SpeedOfSpin commented on GitHub (Apr 16, 2025):

Thanks both. Managed to get it working. Here is the code for anyone else looking for this

export function signCookie(val: string, secret: string) {
    return val + "." + crypto.createHmac("sha256", secret).update(val).digest("base64");
}

export const startImpersonatingAction = serverAction.inputSchema(z.object({ userId: z.string() })).action(async ({ userId, tx }) => {
    const session = await auth.api.getSession({
        headers: await headers(),
    });

    const adminToken = session?.session.token; // Store the current admin token
    const impersonatedToken = createRandomStringGenerator("a-z", "A-Z", "0-9")(32);

    const ctx = await auth.$context;
    const signedAdminToken = signCookie(adminToken as string, ctx?.secret as string);
    const signedImpersonatedToken = signCookie(impersonatedToken, ctx?.secret as string);

    await tx.insert(sessionTable).values({
        id: cuid(),
        impersonatedBy: session?.user.id,
        token: impersonatedToken,
        userId: userId,
        expiresAt: new Date(Date.now() + 60 * 60 * 1000), // 1 hour from now
        createdAt: new Date(),
        updatedAt: new Date(),
    });

    const cookieStore = await cookies();

    cookieStore.set("admin_session", signedAdminToken, {
        httpOnly: true,
        secure: process.env.NODE_ENV === "production",
        sameSite: "lax",
        path: "/",
        maxAge: 60 * 60, // 1 hour in seconds
    });

    cookieStore.set("better-auth.session_token", signedImpersonatedToken, {
        httpOnly: true,
        secure: process.env.NODE_ENV === "production",
        sameSite: "lax",
        path: "/",
        maxAge: 60 * 60, // 1 hour in seconds
    });

    return;
});

export const stopImpersonatingAction = serverAction.inputSchema(z.void()).action(async ({ tx }) => {
    try {
        const session = await auth.api.getSession({
            headers: await headers(),
        });

        if (!session) {
            throw new ApplicationError("No session found");
        }

        const cookieStore = await cookies();
        const signedAdminToken = cookieStore.get("admin_session")?.value;
        cookieStore.delete("admin_session");
        cookieStore.delete("better-auth.session_token");

        await tx.delete(sessionTable).where(eq(sessionTable.id, session.session.id));

        if (signedAdminToken) {
            cookieStore.set("better-auth.session_token", signedAdminToken, {
                httpOnly: true,
                secure: process.env.NODE_ENV === "production",
                sameSite: "lax",
                path: "/",
                maxAge: 60 * 60, // 1 hour in seconds
            });
        }

        revalidatePath("/admin/users");

        return { success: true };
    } catch (error) {
        console.error("Failed to stop impersonating:", error);
        return { success: false, error: "Failed to stop impersonating" };
    }
});
<!-- gh-comment-id:2809282892 --> @SpeedOfSpin commented on GitHub (Apr 16, 2025): Thanks both. Managed to get it working. Here is the code for anyone else looking for this ``` export function signCookie(val: string, secret: string) { return val + "." + crypto.createHmac("sha256", secret).update(val).digest("base64"); } export const startImpersonatingAction = serverAction.inputSchema(z.object({ userId: z.string() })).action(async ({ userId, tx }) => { const session = await auth.api.getSession({ headers: await headers(), }); const adminToken = session?.session.token; // Store the current admin token const impersonatedToken = createRandomStringGenerator("a-z", "A-Z", "0-9")(32); const ctx = await auth.$context; const signedAdminToken = signCookie(adminToken as string, ctx?.secret as string); const signedImpersonatedToken = signCookie(impersonatedToken, ctx?.secret as string); await tx.insert(sessionTable).values({ id: cuid(), impersonatedBy: session?.user.id, token: impersonatedToken, userId: userId, expiresAt: new Date(Date.now() + 60 * 60 * 1000), // 1 hour from now createdAt: new Date(), updatedAt: new Date(), }); const cookieStore = await cookies(); cookieStore.set("admin_session", signedAdminToken, { httpOnly: true, secure: process.env.NODE_ENV === "production", sameSite: "lax", path: "/", maxAge: 60 * 60, // 1 hour in seconds }); cookieStore.set("better-auth.session_token", signedImpersonatedToken, { httpOnly: true, secure: process.env.NODE_ENV === "production", sameSite: "lax", path: "/", maxAge: 60 * 60, // 1 hour in seconds }); return; }); export const stopImpersonatingAction = serverAction.inputSchema(z.void()).action(async ({ tx }) => { try { const session = await auth.api.getSession({ headers: await headers(), }); if (!session) { throw new ApplicationError("No session found"); } const cookieStore = await cookies(); const signedAdminToken = cookieStore.get("admin_session")?.value; cookieStore.delete("admin_session"); cookieStore.delete("better-auth.session_token"); await tx.delete(sessionTable).where(eq(sessionTable.id, session.session.id)); if (signedAdminToken) { cookieStore.set("better-auth.session_token", signedAdminToken, { httpOnly: true, secure: process.env.NODE_ENV === "production", sameSite: "lax", path: "/", maxAge: 60 * 60, // 1 hour in seconds }); } revalidatePath("/admin/users"); return { success: true }; } catch (error) { console.error("Failed to stop impersonating:", error); return { success: false, error: "Failed to stop impersonating" }; } }); ```
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#9128