Next.js + Auth.js Integration
Combine Twingate Identity Firewall with Auth.js sessions in Next.js.
This guide combines Twingate Identity Firewall with Auth.js (NextAuth v5) to give your Next.js app session-based authentication. To authenticate a user, the middleware verifies the Twingate JWT and mints an Auth.js session cookie. This gives you auth() in Server Components and useSession() in Client Components. For architecture details and the full JWT reference, see the Identity Firewall for Web Apps overview.
When to Use This Guide
Use the plain Next.js middleware if you only need identity in API route handlers or a few Server Components, and you don’t need Client Components to access user info.
Use this guide if you need useSession() in Client Components, want session persistence across page navigation, or are already using Auth.js in your project.
Prerequisites
- Next.js 14+ with App Router
Configure the Gateway to Inject the JWT
The Gateway injects no headers by default. On the Web App Resource, add a request header so the Gateway sends the signed JWT on every request. The recommended convention is a Bearer token in the Authorization header:
| Header Key | Value Template |
|---|---|
Authorization | Bearer {{jwt}} |
The header name and format are configurable at the Gateway/Resource level. This guide reads the JWT from Authorization using the Bearer scheme. See Request Headers for the full list of options and template variables.
Install Dependencies
npm install next-auth@beta joseAuth.js v5 (next-auth@beta) is required.
Generate the AUTH_SECRET that Auth.js uses to encrypt session cookies:
npx auth secretThis creates a .env.local file with a random AUTH_SECRET value.
Add the Middleware
Save this file as src/middleware.ts.
import { NextResponse, type NextRequest } from "next/server";import { encode, decode } from "next-auth/jwt";import { createRemoteJWKSet, jwtVerify } from "jose";
const jwks = createRemoteJWKSet( new URL(process.env.TWINGATE_JWKS_URL!));
const SESSION_COOKIE = process.env.NODE_ENV === "production" ? "__Secure-authjs.session-token" : "authjs.session-token";
const MAX_SESSION_AGE = 3600; // 1 hour
export async function middleware(request: NextRequest) { const existingSession = request.cookies.get(SESSION_COOKIE);
if (existingSession) { // If an existing session is found, attempt to decode and validate it before checking the Twingate JWT. try { const token = await decode({ token: existingSession.value, secret: process.env.AUTH_SECRET!, salt: SESSION_COOKIE, });
// If the session token is still valid, continue to the next middleware. if (token && token.exp && token.exp > Date.now() / 1000) { return NextResponse.next(); } } catch { // Handle caught error but fall through to re-verify the Twingate JWT. } }
const auth = request.headers.get("authorization"); if (!auth) { // No auth header, continue to the next middleware. return NextResponse.next(); }
const parts = auth.split(" "); if (parts.length !== 2 || parts[0].toLowerCase() !== "bearer") { return NextResponse.json( { error: "Malformed Authorization header" }, { status: 401 }, ); }
try { const { payload } = await jwtVerify(parts[1], jwks, { algorithms: ["ES256"], requiredClaims: ["exp", "iat"], clockTolerance: "30s", });
const user = payload.user as | { id: string; username: string; groups?: string[] } | undefined;
if (!user || !user.id || !user.username) { return NextResponse.json( { error: "Invalid token claims" }, { status: 401 }, ); }
// Encode the user information into a new session token const sessionToken = await encode({ token: { sub: user.id, email: user.username, name: user.username, twingateGroups: user.groups ?? [], }, secret: process.env.AUTH_SECRET!, salt: SESSION_COOKIE, maxAge: MAX_SESSION_AGE, });
// Make the new session cookie visible to downstream server components and // route handlers within this same request (auth() reads request cookies). request.cookies.set(SESSION_COOKIE, sessionToken);
// Set the headers to the original request for downstream middleware and route handlers. (current request) const response = NextResponse.next({ request: { headers: request.headers }, });
// Also set the session cookie on the response so the browser persists it. (future requests) response.cookies.set(SESSION_COOKIE, sessionToken, { httpOnly: true, secure: request.nextUrl.protocol === "https:", sameSite: "lax", path: "/", maxAge: MAX_SESSION_AGE, });
// Return the response to the client with the new session cookie set return response; } catch { return NextResponse.json({ error: "Invalid token" }, { status: 401 }); }}
export const config = { // Don't apply the middleware to static files, images, favicon, and health check endpoint. matcher: ["/((?!_next/static|_next/image|favicon.ico|api/health).*)"],};Add the Auth Configuration
Save this file as src/auth.ts.
import NextAuth from "next-auth";
declare module "next-auth" { interface Session { user: { id: string; email: string; name: string; twingateGroups: string[]; }; }}
export const { auth, handlers } = NextAuth({ providers: [], // Session is created by middleware, not a provider session: { strategy: "jwt" }, callbacks: { session({ session, token }) { session.user.id = token.sub!; session.user.twingateGroups = (token.twingateGroups as string[]) ?? []; return session; }, },});How It Works
Session Cookie Check
On each request, the middleware first checks for an existing Auth.js session cookie. If the cookie exists and hasn’t expired, the request passes through without re-verifying the Twingate JWT. This avoids redundant verification on every navigation. The cookie name changes between development (authjs.session-token) and production (__Secure-authjs.session-token).
JWT Verification and Session Minting
If no valid session cookie exists, the middleware verifies the Twingate JWT from the Authorization header. On success, it extracts the user’s identity and encodes an Auth.js session token with a 1-hour lifetime. The middleware sets the cookie on both the request (so auth() can read it within the same request) and the response (so the browser persists it for future requests).
Auth.js Configuration
The auth.ts file configures Auth.js with no providers because the session is created entirely by the middleware, not by an OAuth flow. The session callback maps the twingateGroups field from the token into the session object so it’s accessible via auth() and useSession(). The TypeScript module augmentation adds twingateGroups to the Session type.
Set Up the Session Provider
Wrap your root layout with SessionProvider to make the session available to Client Components via the useSession() hook.
// src/app/layout.tsximport { SessionProvider } from "next-auth/react";
export default function RootLayout({ children,}: { children: React.ReactNode;}) { return ( <html> <body> <SessionProvider>{children}</SessionProvider> </body> </html> );}Use Identity in Your App
Server Components
import { auth } from "@/auth";
export default async function ProfilePage() { const session = await auth(); if (!session) { return <p>Not authenticated</p>; }
return ( <div> <p>Hello, {session.user.name}</p> <p>Groups: {session.user.twingateGroups.join(", ")}</p> </div> );}Client Components
"use client";
import { useSession } from "next-auth/react";
export default function Dashboard() { const { data: session, status } = useSession();
if (status === "loading") return <p>Loading...</p>; if (!session) return <p>Not authenticated</p>;
return ( <div> <p>Hello, {session.user.name}</p> <p>Groups: {session.user.twingateGroups.join(", ")}</p> </div> );}Group-Based Authorization
import { auth } from "@/auth";import { redirect } from "next/navigation";
export default async function AdminPage() { const session = await auth(); if (!session || !session.user.twingateGroups.includes("admin")) { redirect("/"); }
return <p>Admin dashboard</p>;}Configuration
| Variable | Description |
|---|---|
TWINGATE_JWKS_URL | Your Twingate tenant’s JWKS endpoint: https://<your-tenant>.twingate.com/api/v1/jwk/ec |
AUTH_SECRET | A random secret used to encrypt session cookies. Generate with npx auth secret. |
Both variables go in .env.local for Next.js projects.
Next Steps
- Plain Next.js middleware if you don’t need session management
- JWT Payload Reference for the full token structure
- Request Headers for configuring which headers the Gateway injects
- Express.js and Django guides for other frameworks
Last updated 3 hours ago