Skip to main content

    NextAuth → Behest

    NextAuth (Auth.js v5) is the most common auth library for Next.js App Router apps. Pair it with a Behest token route handler.


    1. NextAuth config (standard)

    ts
    // auth.ts
    import NextAuth from "next-auth";
    import Google from "next-auth/providers/google";
     
    export const { handlers, auth, signIn, signOut } = NextAuth({
      providers: [Google],
      session: { strategy: "jwt" },
      callbacks: {
        async session({ session, token }) {
          // Expose a stable user id and tier to the client and to our token route
          session.user.id = token.sub!;
          (session.user as any).tier = (token as any).tier ?? "free";
          return session;
        },
        async jwt({ token, user, trigger, session }) {
          if (user) (token as any).tier = (user as any).tier ?? "free";
          if (trigger === "update" && session?.tier)
            (token as any).tier = session.tier;
          return token;
        },
      },
    });

    2. Behest token route handler

    ts
    // app/api/behest/token/route.ts
    import { auth } from "@/auth";
    import { NextResponse } from "next/server";
    import { Behest } from "@behest/client-ts";
     
    const behest = new Behest(); // reads BEHEST_KEY + BEHEST_BASE_URL from env
     
    export async function POST() {
      const session = await auth();
      if (!session?.user?.id)
        return new NextResponse("Unauthorized", { status: 401 });
     
      try {
        const { token, ttl, sessionId, expiresAt } = await behest.auth.mint({
          user_id: session.user.id,
          tier: (session.user as any).tier ?? 1,
          ttl: 900,
        });
        return NextResponse.json({ token, ttl, sessionId, expiresAt });
      } catch (err) {
        return NextResponse.json({ error: String(err) }, { status: 500 });
      }
    }

    3. Client helper

    ts
    // lib/behestToken.ts
    "use client";
    import OpenAI from "openai";
     
    type TokenBundle = {
      token: string;
      ttl: number;
      sessionId: string;
      expiresAt: number;
    };
    let cached: TokenBundle | null = null;
     
    async function getToken(): Promise<TokenBundle> {
      const now = Math.floor(Date.now() / 1000);
      if (cached && cached.expiresAt - now > 60) return cached;
      const r = await fetch("/api/behest/token", { method: "POST" });
      if (!r.ok) throw new Error(`token fetch failed: ${r.status}`);
      cached = (await r.json()) as TokenBundle;
      return cached;
    }
     
    export async function getOpenAI() {
      const { token, sessionId } = await getToken();
      return new OpenAI({
        apiKey: token,
        baseURL: `${process.env.NEXT_PUBLIC_BEHEST_BASE_URL}/v1`,
        dangerouslyAllowBrowser: true,
        defaultHeaders: { "X-Session-Id": sessionId },
      });
    }

    4. Tier updates without sign-out

    After a Stripe webhook upgrades the user:

    ts
    import { useSession } from "next-auth/react";
    const { update } = useSession();
     
    await update({ tier: "pro" }); // re-issues NextAuth JWT with new tier
    // next call to /api/behest/token reads the new tier

    Combined with Behest's short JWT TTL (15 min), users get upgraded limits on their very next request.


    5. Local signing option

    Skip the Behest round-trip entirely. Swap BEHEST_KEY=behest_sk_live_... for BEHEST_KEY=behest_pk_... (a tenant RSA private key from dashboard → Keys → Signing) and add BEHEST_KID, BEHEST_TENANT_ID, BEHEST_PROJECT_ID to env. The SDK auto-detects the mode by key prefix — the route handler code above is unchanged.

    See auth modes for when this pays off.


    6. Edge runtime

    The SDK uses jose for signing, which works on Vercel / Cloudflare edge runtimes.

    ts
    export const runtime = "edge";

    Perfect for Vercel / Cloudflare Pages Functions / etc.


    7. Middleware consideration

    If you put NextAuth in middleware, keep the Behest token route outside middleware or ensure the middleware lets it through — it's called from authenticated clients and only needs the NextAuth cookie.

    ts
    // middleware.ts
    export const config = {
      matcher: ["/((?!api/behest/token|_next/static|_next/image).*)"],
    };

    See also

    Enterprise Token FinOps: Enforce hard budgets and attribute costs per session.

    Learn more