Next.js Middleware
Verify Twingate Identity Firewall JWTs in Next.js Edge Middleware.
This guide shows how to verify Twingate Identity Firewall JWTs in Next.js using the jose library. A thin Edge Middleware gates the routes you choose, and a small helper verifies the JWT wherever you need the user’s identity. For architecture details and the full JWT reference, see the Identity Firewall for Web Apps overview.
For session-based integration with useSession() and auth(), see the Next.js + Auth.js guide.
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. The examples below read the JWT from Authorization using the Bearer scheme. If you configure a different header, adjust the helper to read that header instead. See Request Headers for the full list of options and template variables.
Install Dependencies
npm install josejose runs in the Edge Runtime, which is required for Next.js middleware.
Add the Verification Helper
Save this file as src/twingate.ts. It verifies the JWT and returns the identity, and you reuse it in both the middleware and your route handlers and Server Components.
import { createRemoteJWKSet, jwtVerify, type JWTPayload } from "jose";
const jwks = createRemoteJWKSet(new URL(process.env.TWINGATE_JWKS_URL!));
export interface TwingateIdentity extends JWTPayload { user: { id: string; username: string; email?: string; groups: string[]; }; device?: { id: string }; resource?: { id: string; type: string; address: string; aliases: string[] };}
// Reads the JWT from the Authorization header (Bearer scheme) and verifies it.// Returns the identity on success, or null when the header is missing,// malformed, or the token fails verification.export async function verifyTwingateJWT( authHeader: string | null,): Promise<TwingateIdentity | null> { if (!authHeader) return null;
const parts = authHeader.split(" "); if (parts.length !== 2 || parts[0].toLowerCase() !== "bearer") { return null; }
try { const { payload } = await jwtVerify(parts[1], jwks, { algorithms: ["ES256"], requiredClaims: ["exp", "iat"], clockTolerance: "30s", }); return payload as TwingateIdentity; } catch { return null; }}Add the Middleware
The middleware is a thin gate: it verifies the JWT on the routes you match and returns 401 when the token is missing or invalid. It does not forward identity downstream. Your handlers and Server Components re-verify with the same helper, which is cheap because jose caches the JWKS.
Save this file as src/middleware.ts (or middleware.ts at your project root).
import { NextResponse, type NextRequest } from "next/server";import { verifyTwingateJWT } from "@/twingate";
export async function middleware(request: NextRequest) { const identity = await verifyTwingateJWT( request.headers.get("authorization"), );
if (!identity) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); }
return NextResponse.next();}
export const config = { matcher: ["/api/:path*"],};Toggle Auth per Route
config.matcher controls which routes the middleware gates. List one or more patterns to protect only those paths:
export const config = { matcher: ["/api/:path*", "/dashboard/:path*"],};Or invert it with a negative lookahead to gate everything except public paths such as health checks and static assets:
export const config = { matcher: ["/((?!health|_next/static|_next/image|favicon.ico).*)"],};For finer control than path patterns allow, branch inside the middleware on request.nextUrl.pathname and return early for routes that should stay public:
export async function middleware(request: NextRequest) { if (request.nextUrl.pathname.startsWith("/public")) { return NextResponse.next(); } // ...verify and gate}How It Works
JWKS and Verification
createRemoteJWKSet fetches and caches the Gateway’s public keys, so verifyTwingateJWT checks the ES256 signature against a cached key with no network call after the first. It requires the exp and iat claims and allows 30 seconds of clock tolerance, returning the identity on success or null on any failure.
Middleware and Helper
The middleware gates matched routes with a 401. The helper is what actually reads identity, and you call it again in your handlers and Server Components, re-verifying there rather than forwarding an identity header from the middleware. This keeps the verified JWT as the single source of truth for who the user is.
Read Identity in Route Handlers
Call the helper with the request’s Authorization header and the Gateway attaches the JWT to every request. This allows you to get the verified identity:
import { NextResponse, type NextRequest } from "next/server";import { verifyTwingateJWT } from "@/twingate";
export async function GET(request: NextRequest) { const identity = await verifyTwingateJWT( request.headers.get("authorization"), ); if (!identity) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); }
return NextResponse.json({ username: identity.user.username, groups: identity.user.groups, });}Read the identity in a Server Component with next/headers:
import { headers } from "next/headers";import { verifyTwingateJWT } from "@/twingate";
export default async function DashboardPage() { const headersList = await headers(); const identity = await verifyTwingateJWT( headersList.get("authorization"), );
if (!identity) { return <p>Not authenticated</p>; }
return <p>Hello, {identity.user.username}</p>;}Because the Gateway attaches the JWT to every request, any route handler or Server Component can verify it with the helper. You are not limited to the routes matched by the middleware. Verifying in each place keeps the JWT as the single source of truth with no forwarded identity header to manage.
Group-Based Authorization
import { NextResponse, type NextRequest } from "next/server";import { verifyTwingateJWT } from "@/twingate";
export async function GET(request: NextRequest) { const identity = await verifyTwingateJWT( request.headers.get("authorization"), ); if (!identity) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } if (!identity.user.groups.includes("engineering")) { return NextResponse.json({ error: "Forbidden" }, { status: 403 }); }
return NextResponse.json({ message: "Welcome" });}Configuration
| Variable | Description |
|---|---|
TWINGATE_JWKS_URL | Your Twingate tenant’s JWKS endpoint: https://<your-tenant>.twingate.com/api/v1/jwk/ec |
Add this variable to .env.local in your Next.js project root.
Next Steps
- Next.js + Auth.js guide if you need
useSession()in Client Components or session persistence across navigation - 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