*** title: Set up JWT subtitle: Self-managed authentication integrated with your login system ----------------------------------------------------------------------- With JWT, you manage the entire auth flow. This involves building and signing a [`fern_token`](/learn/docs/authentication/overview#how-authentication-works) cookie that integrates your docs with your existing login system. Like [OAuth](/learn/docs/authentication/setup/oauth), JWT enables: * **Login only** — gate docs behind authentication * **[RBAC](/learn/docs/authentication/features/rbac)** — restrict content by user role * **[API key injection](/learn/docs/authentication/features/api-key-injection)** — pre-fill API keys in the [API Explorer](/learn/docs/api-references/api-explorer) ## How it works 1. A user clicks **Login** on your docs site and is redirected to your authentication page. 2. After authentication, your system signs a [JWT](https://jwt.io) with a secret key from Fern and sets it as a `fern_token` cookie. 3. Fern reads the token to determine the user's access and credentials. ```mermaid sequenceDiagram participant U as User participant F as Fern Docs participant R as Redirect URL participant A as Auth System U->>F: Visit restricted page F->>F: Check fern_token cookie alt Cookie exists F->>F: Decode JWT with secret key F->>F: Extract roles from JWT F->>F: Check if user has required role alt User has required role F->>U: Show restricted content else User lacks required role F->>U: User is shown a 404 page end else No cookie F->>R: Redirect to login page R->>A: Authenticate user end Note over A: User logs in A->>A: Generate JWT with roles A->>F: Set fern_token cookie F->>F: Validate JWT and roles F->>U: Show restricted content ``` ## Configuration Reach out to Fern to get your secret key and send them the URL of your authentication page. This is where users are redirected after clicking **Login**. The JWT payload must include a `fern` claim. What you include in the token's `fern` claim controls which features are enabled: login only, RBAC, or API key injection. ```json Login only { "fern": {} } ``` ```json RBAC { "fern": { "roles": ["partners"] } } ``` ```json API key injection { "fern": { "playground": { "initial_state": { "auth": { "bearer_token": "eyJhbGciOiJIUzI1c" } } } } } ``` ```json API key injection + RBAC { "fern": { "roles": ["partners"], "playground": { "initial_state": { "auth": { "bearer_token": "eyJhbGciOiJIUzI1c" } } } } } ``` Add logic to your service to sign the JWT and set it as a `fern_token` cookie when a user logs in. This Next.js endpoint handles the callback from your authentication page. It reads the `state` parameter to determine where to redirect the user, mints a `fern_token` JWT using [jose](https://github.com/panva/jose), sets it as a cookie, and redirects the user back to the docs. ```typescript title="app/api/fern-token/route.ts" import { SignJWT } from "jose"; import { cookies } from "next/headers"; import { type NextRequest, NextResponse } from "next/server"; export async function GET(req: NextRequest): Promise { const domain = getDomain(req); // your logic to determine the docs domain // use the state param to determine redirect location const returnTo = req.nextUrl.searchParams.get("state"); const redirectLocation = returnTo ?? `https://${domain}`; // fetch the user's API key, roles, and secret (from your config or database) const apiKey = await getApiKeyForUser(); const roles = await getRolesForUser(); const secret = await getSecretForDomain(domain); if (!secret) { // redirect with an error if credentials are missing const url = new URL(redirectLocation); url.searchParams.set("error", "missing_credentials"); return NextResponse.redirect(url); } // mint the JWT using the secret key const fernToken = await mintFernToken({ secret, apiKey, roles }); if (!fernToken) { const url = new URL(redirectLocation); url.searchParams.set("error", "token_creation_failed"); return NextResponse.redirect(url); } // set the fern_token as a cookie on the docs domain const cookieJar = await cookies(); cookieJar.set("fern_token", fernToken, { httpOnly: true, secure: true, sameSite: "lax", domain, }); // redirect the user back to the docs return NextResponse.redirect(redirectLocation); } const encoder = new TextEncoder(); async function mintFernToken({ secret, apiKey, roles, }: { secret: string; apiKey?: string; roles?: string[]; }): Promise { const fern: Record = {}; if (roles) { fern.roles = roles; } if (apiKey) { fern.playground = { initial_state: { auth: { bearer_token: apiKey, }, }, }; } return await new SignJWT({ fern }) .setProtectedHeader({ alg: "HS256", typ: "JWT" }) .setIssuedAt() .setExpirationTime("1d") // set to any value .setIssuer("https://buildwithfern.com") .sign(encoder.encode(secret)); // sign using the secret provided by Fern } ``` Once your `fern_token` is working, configure the features you need: * **[Role-based access control](/learn/docs/authentication/features/rbac)** — define roles in `docs.yml` and restrict navigation items or page content by role. * **[API key injection](/learn/docs/authentication/features/api-key-injection)** — configure the `playground` payload, including custom headers, multiple API keys, and per-environment credentials.