# How to identify and authorize visitors with the Vercel Passport token in Next.js

**Author:** Ben Sabic

---

After Vercel Passport authenticates a visitor, you can identify that visitor in your [Next.js](https://nextjs.org/) app and decide what data they see. Vercel injects a signed token into the `x-vercel-oidc-passport-token` request header, and that token carries the visitor's identity claims from your identity provider. Because Vercel validates the session and injects the token server-side, your code can read the visitor's identity without running its own sign-in flow. Use the `external_sub` claim as the visitor's stable identifier to scope database queries, gate routes, and personalize responses.

This guide will show you how to read the Passport token from a server-side request in Next.js, decode it to access the visitor's identity claims, and use the `external_sub` claim to authorize a visitor and scope their data. It also covers how to handle requests where the token is missing.

## Prerequisites

Before you begin, make sure you have:

- Passport enabled on your project, currently available for Enterprise customers. See [Restrict access to deployments with Passport](https://vercel.com/docs/passport) to turn it on.
  
- A Next.js app deployed to Vercel. Vercel injects the Passport token only for protected deployments, so the header isn't present in local development.
  
- Code that runs on the server, such as a Route Handler, a Server Component, or a server action. The token is never available in client components or in the browser.
  

## How it works

Vercel forwards the Passport session token to your deployment in the `x-vercel-oidc-passport-token` request header. The token is a Vercel-signed JWT that carries deployment context and the visitor's identity claims. The reliable user identifier is the `external_sub` claim, which comes from the external subject your identity provider returns.

You don't need to verify the token's signature in your own code. Vercel strips any client-supplied value from this header and injects the verified token after validating the session, so a token present in a server-side request is already trustworthy. Your job is to read the header and decode the claims.

The token carries the following claims:

| Claim           | Description                                                                                                                 |
| --------------- | --------------------------------------------------------------------------------------------------------------------------- |
| `external_sub`  | The stable visitor identifier returned by your identity provider. Use this to identify the visitor.                         |
| `sub`           | The owner, `connector_id`, and `external_sub` in a stable Vercel format.                                                    |
| `scope`         | Includes the owner, `connector_id`, and `external_sub`.                                                                     |
| `email`, `name` | Profile fields. Not guaranteed. They appear only if your identity provider returns them in the Passport user info response. |

The Passport token is separate from the OIDC federation token in the `x-vercel-oidc-token` header. The federation token is for exchanging for short-lived cloud credentials with the `@vercel/oidc` helpers, while the Passport token identifies the human visiting your protected deployment.

## Steps

### 1\. Read the token from the request

Read the `x-vercel-oidc-passport-token` header in a server-side Route Handler. If the header is missing, the request didn't pass through Passport, so return a `401` response.

`export async function GET(request: Request) { const token = request.headers.get('x-vercel-oidc-passport-token'); if (!token) { return new Response('Unauthorized', { status: 401 }); } // Next: decode the token to read the visitor's claims. return Response.json({ authenticated: true }); }`

Missing tokens usually mean the request came from local development or an unprotected deployment. Guarding for it keeps your code working in both places.

### 2\. Decode the token to read its claims

Decode the token's payload to read the claims. Vercel has already validated the session, so you decode the JWT rather than verifying its signature. The `jose` library's `decodeJwt` reads the claims and works regardless of runtime.

Install `jose`:

Then add a helper that reads and decodes the token in one place:

`import { decodeJwt } from 'jose'; export interface PassportClaims { external_sub: string; sub: string; scope: string; email?: string; name?: string; } export function readPassportClaims(request: Request): PassportClaims | null { const token = request.headers.get('x-vercel-oidc-passport-token'); if (!token) { return null; } return decodeJwt(token) as PassportClaims; }`

If you'd rather avoid a dependency and you're on the Node.js runtime, decode the payload segment directly:

`const payload = JSON.parse( Buffer.from(token.split('.')[1], 'base64url').toString(), ); const visitorId = payload.external_sub;` `Buffer` is available on the Node.js runtime. The `jose` approach works across runtimes, so prefer it if your route runs anywhere other than Node.js. ### 3\. Authorize the visitor and scope their data Use the `external_sub` claim to decide what the visitor can access, and key your queries on it so each visitor reads only their own records. Derive the identity from the token, not from request parameters. A `userId` taken from the query string or request body lets a visitor request another visitor's data. `import { readPassportClaims } from '@/lib/passport'; import { getDocumentsForUser } from '@/lib/db'; export async function GET(request: Request) { const claims = readPassportClaims(request); if (!claims) { return new Response('Unauthorized', { status: 401 }); } // Scope the query to the authenticated visitor. const documents = await getDocumentsForUser(claims.external_sub); return Response.json({ documents }); }` To restrict a route to specific visitors, check `external_sub` against an allowlist and return `403` for everyone else: `import { readPassportClaims } from '@/lib/passport'; const ADMINS = new Set(['external_subject_id_1', 'external_subject_id_2']); export async function GET(request: Request) { const claims = readPassportClaims(request); if (!claims) { return new Response('Unauthorized', { status: 401 }); } if (!ADMINS.has(claims.external_sub)) { return new Response('Forbidden', { status: 403 }); } // Return admin-only data. return Response.json({ ok: true }); }` ### 4\. Read the token in a Server Component Server Components read request headers through `headers()` from `next/headers`, so you can identify the visitor when rendering a page. `headers()` returns a promise, so await it. `import { headers } from 'next/headers'; import { decodeJwt } from 'jose'; export default async function DashboardPage() { const token = (await headers()).get('x-vercel-oidc-passport-token'); if (!token) { return <p>This page is available on a Passport-protected deployment.</p>; } const { external_sub } = decodeJwt(token) as { external_sub: string }; return <p>Signed in as {external_sub}</p>; }` ## Best practices - **Use** `**external_sub**` **as the identifier.** It's stable and comes from your identity provider. Treat `email` and `name` as optional, since they appear only when your provider returns them.    - **Always read the token server-side.** Vercel strips client-supplied header values, so the value you read in server code is the one Vercel injected and validated.    - **Derive identity from the token, not from request parameters.** Scoping a query to a `userId` from the query string or body lets a visitor request another visitor's data.    - **Use the right status codes.** Return `401` when the token is missing and `403` when the visitor is authenticated but not allowed.    ## Troubleshooting ### The token header is missing - Confirm Passport is enabled on the project and that you're requesting a [protected deployment](https://vercel.com/docs/passport) URL.
  
- Confirm your code runs on the server. The header isn't available in client components or in the browser.
  
- In local development, the header isn't present because requests don't pass through Passport. Guard for a missing token so local development still works.
  

### Profile fields are undefined

`email` and `name` appear only if your identity provider returns them in the Passport user info response. Use `external_sub` to identify the visitor and treat profile fields as optional.

## Resources and next steps

- Learn how Passport fits alongside other methods in [Deployment Protection](https://vercel.com/docs/deployment-protection).
  
- Compare [methods to protect deployments](https://vercel.com/docs/deployment-protection/methods-to-protect-deployments) to choose the right one for your project.
  
- Read the [Passport documentation](https://vercel.com/docs/passport) to enable and configure the feature.

---

[View full KB sitemap](/kb/sitemap.md)
