---
title: Use Vercel Passport with Node.js and Python backends
description: Read and verify a visitor's Vercel Passport identity in your backend, with examples for Express, Hono, Nitro, and FastAPI.
url: "https://vercel.com/kb/guide/vercel-passport-backend-frameworks"
published: 2026-10-14
last_updated: 2026-10-14
authors: Ben Sabic
install_vercel_plugin: npx plugins add vercel/vercel-plugin
---

Vercel Passport signs visitors in with your company identity provider, such as Okta or Microsoft Entra ID, before a request reaches your backend.

Node.js backends read the verified identity with the `@vercel/passport` SDK, and Python backends verify the same signed token with a standard JSON Web Token (JWT) library. Use that identity to secure internal apps, return the visitor's profile, restrict routes by group, and record who did what.

In this guide, you'll learn how to:

- Read Passport identity in a Node.js backend with `getIdentity()`
  
- Verify Passport identity in a Python backend with PyJWT
  
- Develop locally without a real Passport session
  
- Use Express, Hono, Nitro, or FastAPI to protect routes
  
- Forward identity from one backend to another
  

## Before you begin

Make sure you have:

- Your Vercel team on the Enterprise plan, which is required for Passport.
  
- Your Express, Hono, Nitro, or FastAPI backend deployed to Vercel.
  
- Passport enabled on the project with an identity provider configured. If you haven't set this up yet, follow [Set up Passport with an identity provider](https://vercel.com/docs/passport/set-up-identity-provider).
  

## How Vercel Passport identity reaches your backend

Vercel Passport runs in Vercel's network before your backend's routes and middleware execute. When a visitor without a session opens a protected deployment, Vercel redirects them to your identity provider. After the provider authenticates them, Vercel validates the response, stores a signed session token in a HttpOnly cookie, and forwards the same token to your deployment in the `x-vercel-oidc-passport-token` request header.

The token is a Vercel-signed JWT that combines standard OpenID Connect claims, deployment context, and identity claims from your provider. Vercel strips any client-supplied value for `x-vercel-oidc-passport-token` and injects its own verified token, so a visitor can't forge an identity by setting the header themselves.

Passport controls who can open your deployment. To control what each visitor can do inside it, read their verified identity in your route handlers.

## Read Passport identity in a Node.js backend

Install `@vercel/passport` in your Express, Hono, or Nitro project:

**pnpm**

```bash
pnpm i @vercel/passport
```

**npm**

```bash
npm i @vercel/passport
```

**yarn**

```bash
yarn add @vercel/passport
```

**bun**

```bash
bun add @vercel/passport
```

The `getIdentity()` function reads the Passport token, verifies it, and returns an identity object, or `null` when the request has no Passport identity. It accepts whatever request object your framework gives you:

- **Hono:** Pass `c.req.raw`, which is a standard `Request`.
  
- **Nitro:** Pass `event.headers`, which is a standard `Headers` object.
  
- **Express:** Pass `req.headers`, which is a plain object. The SDK matches header names without regard to case.
  

The identity object exposes these fields:

| Field             | Type             | Description                                                                                                                  |
| ----------------- | ---------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `subject`         | string           | Stable Passport principal (the `sub` claim). Use this as the visitor ID. Treat it as opaque and don't parse it.              |
| `externalSubject` | string           | Subject from your identity provider (the `external_sub` claim). Specific to the Connect application and not globally unique. |
| `email`           | string, optional | Email returned by your identity provider, if any.                                                                            |
| `name`            | string, optional | Display name returned by your identity provider.                                                                             |
| `token`           | string or null   | The raw Passport JWT, or null for the local development identity. Use it only to forward identity to a backend you operate.  |
| `payload`         | object           | All verified token claims, including `iss`, `aud`, `exp`, and any allowlisted claims such as `groups`.                       |

Each Node.js example in this guide uses a shared helper that reads `groups` defensively, because the claim shape depend on your identity provider. Save it as `src/authorization.ts` for Express and Hono, or as `utils/authorization.ts` for Nitro:

```typescript
import type { PassportIdentity } from '@vercel/passport';

export const ADMIN_GROUP = 'platform-admins';

export function getGroups(identity: PassportIdentity): string[] {
  const groups = identity.payload.groups;

  if (!Array.isArray(groups)) return [];
  return groups.filter((group): group is string => typeof group === 'string');
}

export function isInGroup(identity: PassportIdentity, group: string): boolean {
  return getGroups(identity).includes(group);
}
```

## Verify Passport identity in a Python backend

Vercel doesn't publish a Python SDK for Passport, so Python backends verify the token themselves as a standard JWT. The FastAPI example in this guide uses PyJWT and applies similar checks as `@vercel/passport`:

- **Signature:** Verify the RS256 signature against the keys at `https://oidc.vercel.com/.well-known/jwks`, which serves the same key set as `https://passport.vercel.com/.well-known/jwks.json`.
  
- **Issuer:** Confirm `iss` is `https://passport.vercel.com` or scoped under it, such as `https://passport.vercel.com/acme`.
  
- **Token type:** Confirm the `typ` claim equals `passport`.
  
- **Project and environment:** Confirm `project_id` matches `VERCEL_PROJECT_ID` and `environment` matches `VERCEL_TARGET_ENV` (or `VERCEL_ENV`).
  
- **Lifetime:** Reject tokens whose `exp` or `nbf` claims put them outside their valid window.
  

The Vercel Passport SDK doesn't check the `aud` claim by default, so the Python example doesn't either.

Add PyJWT with its cryptography extra to your `requirements.txt`:

```plaintext
fastapi
pyjwt[crypto]
```

## Which claim to use as the visitor identifier

Use the `sub` claim (`identity.subject` in Node.js) as the visitor identifier when your backend accepts tokens from one Passport issuer. If your backend accepts tokens from more than one Vercel team, store the `iss` and `sub` claims together.

Don't use `email`, `name`, `sid`, or `external_sub` alone as a global identifier. These values can change, be absent, or overlap across identity providers and Connect applications. When you record who created or changed data, store the combined `iss` and `sub` value as the actor and treat `email` as a display value.

## How to develop locally without a Passport session

Your local server never receives the real Passport cookie or header, because Passport only runs in Vercel's network.

In Node.js, `getIdentity()` returns a development identity when there's no token and the process isn't a preview or production deployment. This covers `vercel dev` and any process where `VERCEL_ENV` is unset, including a production build you run on your own machine. The development identity looks like this:

```plaintext
subject: owner:local:connector:local:principal:test-user
externalSubject: test-user
email: test-user@passport.local
name: Test User
```

The development identity has `verified: false`, no `token`, and no `groups`, so treat it as a fixture for your application logic. Customize it with `VERCEL_PASSPORT_DEV_*` environment variables, such as `VERCEL_PASSPORT_DEV_EXTERNAL_SUB`, or set `VERCEL_PASSPORT_DEV=0` to make `getIdentity()` return `null`.

In Python, the FastAPI example returns a matching development visitor only when you opt in with `VERCEL_PASSPORT_DEV=1`. Set that variable in your local environment file only, never in your Vercel project settings. The example ignores it whenever `VERCEL_ENV` reports a preview or production deployment.

To test the real redirect, session cookie, and verified claims, use a Passport-protected preview deployment.

## How to request the `groups` claim

Passport requests the standard `openid`, `profile`, and `email` scopes by default. The group checks in this guide also need a `groups` claim.

To include group membership in the Passport token:

1. In the Vercel dashboard, open the Connect application that Passport uses from your project's Passport settings.
   
2. Under Grant Types, enable User Authorization.
   
3. Add `groups` to the User Authorization scopes.
   
4. Under Forwarded ID Token Claims, add `groups`. This is the allowlist of extra claims Connect can include in the Passport token.
   
5. Save the application.
   

Your identity provider must also return the claim. In Okta, for example, you configure a `groups` claim on the authorization server that matches your Connect application's issuer. See [Configure additional identity scopes](https://vercel.com/docs/passport/additional-identity-scopes) for the full walkthrough.

After changing the configuration, start a new Passport session in a private browser window so the new claim appears.

## Use Vercel Passport with your backend framework

The Express, Hono, Nitro, and FastAPI examples all follow the same pattern, with a helper that reads the Passport identity, a profile route that returns the signed-in visitor, and an admin route that checks group membership.

When the group check fails, the admin route returns a 404 instead of a 403. Visitors outside the group get the same response as for a nonexistent route, so they can’t tell it’s there or probe which admin routes your backend has.

### Express

Express passes headers as a plain object, so the helper hands `req.headers` straight to `getIdentity()`. The `requireVisitor` middleware stores the verified visitor on `res.locals` for the route handler.

```typescript
import type { NextFunction, Request, Response } from 'express';
import { getIdentity, type PassportIdentity } from '@vercel/passport';

// Returns the verified Passport visitor, or null if there isn't one.
export async function getVisitor(req: Request): Promise<PassportIdentity | null> {
  try {
    return await getIdentity(req.headers);
  } catch {
    // Treat verification failures as unauthenticated. Don't log the raw token.
    return null;
  }
}

// Express middleware that requires a Passport visitor.
export async function requireVisitor(
  req: Request,
  res: Response,
  next: NextFunction,
) {
  const visitor = await getVisitor(req);

  if (!visitor) {
    res.status(401).json({ error: 'Unauthorized' });
    return;
  }

  res.locals.visitor = visitor;
  next();
}
```

Add the routes to `src/index.ts`, which Vercel uses as the Express entry point:

```typescript
import express from 'express';
import type { PassportIdentity } from '@vercel/passport';
import { requireVisitor } from './passport.js';
import { ADMIN_GROUP, isInGroup } from './authorization.js';

const app = express();

app.get('/api/me', requireVisitor, (req, res) => {
  const visitor: PassportIdentity = res.locals.visitor;

  // Return only the fields the client needs. Never return visitor.token.
  res.json({
    subject: visitor.subject,
    email: visitor.email ?? null,
    name: visitor.name ?? null,
  });
});

app.post('/api/admin/rotate-api-key', requireVisitor, (req, res) => {
  const visitor: PassportIdentity = res.locals.visitor;

  if (!isInGroup(visitor, ADMIN_GROUP)) {
    res.status(404).json({ error: 'Not Found' });
    return;
  }

  // Perform the privileged operation here.
  res.json({ rotated: true });
});

export default app;
```

### Hono

Hono exposes the standard `Request` as `c.req.raw`, which `getIdentity()` accepts directly. The typed middleware stores the visitor on the context, so `c.get('visitor')` is typed in every route that uses it.

```typescript
import { createMiddleware } from 'hono/factory';
import { getIdentity, type PassportIdentity } from '@vercel/passport';

// Hono middleware that requires a Passport visitor and stores it on the context.
export const requireVisitor = createMiddleware<{
  Variables: { visitor: PassportIdentity };
}>(async (c, next) => {
  let visitor: PassportIdentity | null = null;

  try {
    visitor = await getIdentity(c.req.raw);
  } catch {
    // Treat verification failures as unauthenticated. Don't log the raw token.
  }

  if (!visitor) {
    return c.json({ error: 'Unauthorized' }, 401);
  }

  c.set('visitor', visitor);
  await next();
});
```

Add the routes to `src/index.ts`, which Vercel uses as the Hono entry point:

```typescript
import { Hono } from 'hono';
import { requireVisitor } from './passport.js';
import { ADMIN_GROUP, isInGroup } from './authorization.js';

const app = new Hono();

app.get('/api/me', requireVisitor, (c) => {
  const visitor = c.get('visitor');

  // Return only the fields the client needs. Never return visitor.token.
  return c.json({
    subject: visitor.subject,
    email: visitor.email ?? null,
    name: visitor.name ?? null,
  });
});

app.post('/api/admin/rotate-api-key', requireVisitor, (c) => {
  if (!isInGroup(c.get('visitor'), ADMIN_GROUP)) {
    return c.json({ error: 'Not Found' }, 404);
  }

  // Perform the privileged operation here.
  return c.json({ rotated: true });
});

export default app;
```

### Nitro

Nitro 2 event handlers expose request headers as a standard `Headers` object on `event.headers`. Nitro also auto-imports functions exported from `utils/` and h3 helpers such as `defineEventHandler` and `createError`, so route files don't need import statements. The paths below are relative to your Nitro source directory.

```typescript
import type { H3Event } from 'h3';
import { getIdentity, type PassportIdentity } from '@vercel/passport';

// Returns the verified Passport visitor, or null if there isn't one.
export async function getVisitor(
  event: H3Event,
): Promise<PassportIdentity | null> {
  try {
    return await getIdentity(event.headers);
  } catch {
    // Treat verification failures as unauthenticated.
    // Don't log the raw token.
    return null;
  }
}

// Returns the verified visitor, or throws a 401 if there isn't one.
export async function requireVisitor(
  event: H3Event,
): Promise<PassportIdentity> {
  const visitor = await getVisitor(event);

  if (!visitor) {
    throw createError({
      statusCode: 401,
      statusMessage: 'Unauthorized',
    });
  }

  return visitor;
}
```

Create the profile route at `routes/api/me.get.ts`:

```typescript
export default defineEventHandler(async (event) => {
  const visitor = await requireVisitor(event);

  // Return only the fields the client needs. Never return visitor.token.
  return {
    subject: visitor.subject,
    email: visitor.email ?? null,
    name: visitor.name ?? null,
  };
});
```

Create the admin route at `routes/api/admin/rotate-api-key.post.ts`:

```typescript
export default defineEventHandler(async (event) => {
  const visitor = await requireVisitor(event);

  if (!isInGroup(visitor, ADMIN_GROUP)) {
    throw createError({ statusCode: 404, statusMessage: 'Not Found' });
  }

  // Perform the privileged operation here.
  return { rotated: true };
});
```

### FastAPI

FastAPI verifies the token with PyJWT, checking the same signature, issuer, token type, project, and environment claims as `@vercel/passport`. The `get_visitor` dependency returns the verified claims as a dictionary, so any route can require a visitor by adding it to its parameters.

Create `passport.py` next to your FastAPI app:

```python
import os

import jwt
from fastapi import HTTPException, Request
from jwt import PyJWKClient

PASSPORT_HEADER = "x-vercel-oidc-passport-token"
PASSPORT_ISSUER = "https://passport.vercel.com"

# Fetches Vercel's signing keys and caches them between requests.
jwks_client = PyJWKClient("https://oidc.vercel.com/.well-known/jwks")


def verify_passport_token(token, project_id=None, environment=None):
    """Verifies a Passport token and returns its claims, or raises jwt.PyJWTError."""
    expected_project = project_id or os.environ.get("VERCEL_PROJECT_ID")
    expected_env = (
        environment
        or os.environ.get("VERCEL_TARGET_ENV")
        or os.environ.get("VERCEL_ENV")
    )
    if not expected_project or not expected_env:
        raise jwt.InvalidTokenError("Missing expected project ID or environment")

    signing_key = jwks_client.get_signing_key_from_jwt(token)
    claims = jwt.decode(
        token,
        signing_key.key,
        algorithms=["RS256"],
        options={"require": ["exp", "iss", "sub"], "verify_aud": False},
    )

    issuer = claims["iss"]
    if issuer != PASSPORT_ISSUER and not issuer.startswith(f"{PASSPORT_ISSUER}/"):
        raise jwt.InvalidIssuerError("Unexpected Passport issuer")
    if claims.get("typ") != "passport":
        raise jwt.InvalidTokenError("Token isn't a Passport token")
    if claims.get("project_id") != expected_project:
        raise jwt.InvalidTokenError("Unexpected project_id claim")
    if claims.get("environment") != expected_env:
        raise jwt.InvalidTokenError("Unexpected environment claim")

    return claims


DEV_VISITOR = {
    "iss": "https://passport.vercel.com/local",
    "sub": "owner:local:connector:local:principal:test-user",
    "email": "test-user@passport.local",
    "name": "Test User",
}


def use_dev_visitor():
    # Opt in with VERCEL_PASSPORT_DEV=1. Never applies to preview or production.
    if os.environ.get("VERCEL_PASSPORT_DEV") != "1":
        return False
    return os.environ.get("VERCEL_ENV") in (None, "", "development")


def get_visitor(request: Request) -> dict:
    """FastAPI dependency that returns the verified Passport claims."""
    token = request.headers.get(PASSPORT_HEADER)

    if not token:
        if use_dev_visitor():
            return DEV_VISITOR
        raise HTTPException(status_code=401, detail="Unauthorized")

    try:
        return verify_passport_token(token)
    except jwt.PyJWTError:
        # Treat verification failures as unauthenticated. Don't log the raw token.
        raise HTTPException(status_code=401, detail="Unauthorized")


def visitor_key(claims: dict) -> str:
    """Stable key for storing a visitor. Combines issuer and subject."""
    return f"{claims['iss']}|{claims['sub']}"


def get_groups(claims: dict) -> list[str]:
    groups = claims.get("groups")
    if not isinstance(groups, list):
        return []
    return [group for group in groups if isinstance(group, str)]
```

Then add the routes to your FastAPI entry point, such as `main.py`:

```python
from fastapi import Depends, FastAPI, HTTPException

from passport import get_groups, get_visitor

ADMIN_GROUP = "platform-admins"

app = FastAPI()


@app.get("/api/me")
def read_me(visitor: dict = Depends(get_visitor)):
    # Return only the fields the client needs. Never return the raw token.
    return {
        "subject": visitor["sub"],
        "email": visitor.get("email"),
        "name": visitor.get("name"),
    }


@app.post("/api/admin/rotate-api-key")
def rotate_api_key(visitor: dict = Depends(get_visitor)):
    if ADMIN_GROUP not in get_groups(visitor):
        raise HTTPException(status_code=404, detail="Not Found")

    # Perform the privileged operation here.
    return {"rotated": True}
```

## How to forward Passport identity to another backend

Some requests need identity beyond the backend that receives them. Your API might call a reporting service that generates exports for the signed-in visitor, a data service that only returns the records that person can see, or another Vercel project that logs who triggered an action.

To pass the visitor along, forward the Passport token as a bearer token. The receiving service then verifies it against the project that minted the token, so it can trust the identity without running its own sign-in.

In Node.js, send `visitor.token` in the `Authorization` header:

```typescript
const response = await fetch('https://reports.example.com/api/reports', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${visitor.token}`,
  },
});
```

Node.js receivers verify the token with `verifyIdentity()`, which is available in `@vercel/passport` 0.1.3 and later. It reads the bearer token from the request and accepts the same request objects as `getIdentity()`:

```typescript
import { verifyIdentity } from '@vercel/passport';

// IDs of the Passport-protected deployment that minted the token,
// not the project receiving it.
const identity = await verifyIdentity(request, {
  ownerId: process.env.PASSPORT_SOURCE_OWNER_ID!,
  projectId: process.env.PASSPORT_SOURCE_PROJECT_ID!,
  environment: process.env.PASSPORT_SOURCE_ENVIRONMENT ?? 'production',
});
```

Python receivers call `verify_passport_token()` with the source project's values:

```python
import os

from fastapi import HTTPException, Request
import jwt

from passport import verify_passport_token


def get_forwarded_visitor(request: Request) -> dict:
    authorization = request.headers.get("authorization", "")
    scheme, _, token = authorization.partition(" ")
    if scheme.lower() != "bearer" or not token:
        raise HTTPException(status_code=401, detail="Unauthorized")

    try:
        return verify_passport_token(
            token,
            project_id=os.environ["PASSPORT_SOURCE_PROJECT_ID"],
            environment=os.environ.get(
                "PASSPORT_SOURCE_ENVIRONMENT", "production"
            ),
        )
    except jwt.PyJWTError:
        raise HTTPException(status_code=401, detail="Unauthorized")
```

Keep these rules in mind when forwarding the visitor’s identity:

- **Use HTTPS to backends you operate:** The Passport token authenticates the visitor to your own services. It isn't an access token for your identity provider or for third-party APIs. Use Vercel Connect when a backend needs credentials for another service.
  
- **Validate against the source deployment:** Pass the project ID and environment of the app that minted the token. Don't substitute the receiving project's values.
  
- **Account for Deployment Protection on the receiver:** If the receiving backend is itself a protected Vercel deployment, the server-to-server request has no Passport session and needs a [Protection Bypass for Automation](https://vercel.com/docs/deployment-protection/methods-to-bypass-deployment-protection/protection-bypass-automation) secret.
  

## Troubleshooting

### Every request returns 401 on a deployment

Confirm the deployment is Passport-protected. Requests sent with Protection Bypass for Automation have no identity unless they also carry a Passport session.

Both the SDK and the Python example check the token's `project_id` and `environment` claims against the `VERCEL_PROJECT_ID` and `VERCEL_TARGET_ENV` (or `VERCEL_ENV`) System Environment Variables. If those variables aren't exposed to your functions, verification fails for every request. Confirm that **Enable access to System Environment Variables** is checked in your project’s [environment variable settings.](https://vercel.com/d?to=%2F%5Bteam%5D%2F%5Bproject%5D%2Fsettings%2Fenvironment-variables%3Fenv%3Dsystem)

### The `groups`, `email`, or `name` claim is missing

These claims are optional. Confirm that your identity provider returns the claim, that the Connect application requests the scope and lists the claim under Forwarded ID Token Claims, and then start a new Passport session. Existing sessions keep the claims from when they were issued.

### Webhooks or cron jobs can't reach a Passport-protected route

Passport redirects any request without a session to your identity provider, including webhooks and Cron Jobs. The original request must include a Protection Bypass for Automation secret. Middleware in your backend can't make a path public, because Passport has already run by the time the request reaches it.

### Token verification fails

Confirm your code reads the Vercel-injected `x-vercel-oidc-passport-token` header for direct requests, or the bearer token for forwarded requests. Check that your server can reach `https://oidc.vercel.com/.well-known/jwks` and that its clock is accurate, since `exp` and `nbf` checks depend on it. Don't log the raw token while debugging.

## Next steps

- Review every claim in the [Passport token claims reference](https://vercel.com/docs/passport/token-claims).
  
- Read the full [Read Passport identity in your application](https://vercel.com/docs/passport/read-identity) docs for SDK options.
  
- Set up additional claims with [Configure additional identity scopes](https://vercel.com/docs/passport/additional-identity-scopes).
  
- Learn how [Express](https://vercel.com/docs/frameworks/backend/express), [Hono](https://vercel.com/docs/frameworks/backend/hono), [Nitro](https://vercel.com/docs/frameworks/backend/nitro), and [FastAPI](https://vercel.com/docs/frameworks/backend/fastapi) run on Vercel.
  
- Use Passport in a full-stack app with [Identify users and control access in Next.js with Vercel Passport](https://vercel.com/kb/guide/vercel-passport-nextjs) or [Personalize and lock down a Nuxt app with Vercel Passport](https://vercel.com/kb/guide/vercel-passport-nuxt).
  

## Frequently asked questions

### Is there a Python SDK for Vercel Passport?

No. `@vercel/passport` is a Node.js SDK. Python backends verify the Passport token as a standard JWT, checking its RS256 signature against Vercel's published keys, the issuer, the `typ` claim, and the `project_id` and `environment` claims.

### Which header contains the Vercel Passport token?

Vercel forwards the verified token in the `x-vercel-oidc-passport-token` request header. Vercel strips any value that a client sends for this header, so the token your backend receives always comes from Vercel.

### How do I pass an Express request to `getIdentity()`?

Pass `req.headers`. The SDK accepts a plain header object as well as a standard `Request` or `Headers` instance, and it matches header names without regard to case.

### How do I test Vercel Passport in a backend locally?

In Node.js, `getIdentity()` returns a development identity when you run your backend outside a preview or production deployment, such as with `vercel dev`. In the Python example, set `VERCEL_PASSPORT_DEV=1` in your local environment to get a matching development visitor. To test the real sign-in and verified claims, use a Passport-protected preview deployment.

### Can backend middleware make a route public on a Passport-protected deployment?

No. Passport runs in Vercel's network before your backend receives the request, so middleware can only add checks after Passport has made its sign-in decision. Automated requests need a Protection Bypass for Automation secret instead.