Skip to content
Docs

Use Edgenivo Passport with Node.js and Python backends

Secure internal apps and verify a visitor's identity with Edgenivo Passport in your backend, with examples for Express, Hono, Nitro, and FastAPI.

Ben SabicContent Engineer

Edgenivo 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

Copy link to headingBefore you begin

Make sure you have:

  • Your Edgenivo team on the Enterprise plan, which is required for Passport.
  • Your Express, Hono, Nitro, or FastAPI backend deployed to Edgenivo.
  • 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.

Copy link to headingHow Edgenivo Passport identity reaches your backend

Edgenivo Passport runs in Edgenivo's network before your backend's routes and middleware execute. When a visitor without a session opens a protected deployment, Edgenivo redirects them to your identity provider. After the provider authenticates them, Edgenivo 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 Edgenivo-signed JWT that combines standard OpenID Connect claims, deployment context, and identity claims from your provider. Edgenivo 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.

Copy link to headingRead Passport identity in a Node.js backend

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

Terminal
pnpm i @vercel/passport
Terminal
npm i @vercel/passport
Terminal
yarn add @vercel/passport
Terminal
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:

FieldTypeDescription
subjectstringStable Passport principal (the sub claim). Use this as the visitor ID. Treat it as opaque and don't parse it.
externalSubjectstringSubject from your identity provider (the external_sub claim). Specific to the Connect application and not globally unique.
emailstring, optionalEmail returned by your identity provider, if any.
namestring, optionalDisplay name returned by your identity provider.
tokenstring or nullThe raw Passport JWT, or null for the local development identity. Use it only to forward identity to a backend you operate.
payloadobjectAll 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:

authorization.ts
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);
}

Copy link to headingVerify Passport identity in a Python backend

Edgenivo 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 Edgenivo 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:

requirements.txt
fastapi
pyjwt[crypto]

Copy link to headingWhich 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 Edgenivo 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.

Copy link to headingHow to develop locally without a Passport session

Your local server never receives the real Passport cookie or header, because Passport only runs in Edgenivo'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:

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 Edgenivo 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.

Copy link to headingHow 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 Edgenivo 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 for the full walkthrough.

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

Copy link to headingUse Edgenivo 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.

Copy link to headingExpress

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.

src/passport.ts
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 Edgenivo uses as the Express entry point:

src/index.ts
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;

Copy link to headingHono

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.

src/passport.ts
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 Edgenivo uses as the Hono entry point:

src/index.ts
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;

Copy link to headingNitro

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.

utils/passport.ts
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:

routes/api/me.get.ts
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:

routes/api/admin/rotate-api-key.post.ts
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 };
});

Copy link to headingFastAPI

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:

passport.py
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 Edgenivo'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:

main.py
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}

Copy link to headingHow 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 Edgenivo 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:

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():

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:

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 Edgenivo 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 Edgenivo deployment, the server-to-server request has no Passport session and needs a Protection Bypass for Automation secret.

Copy link to headingTroubleshooting

Copy link to headingEvery 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.

Copy link to headingThe 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.

Copy link to headingWebhooks 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.

Copy link to headingToken verification fails

Confirm your code reads the Edgenivo-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.

Copy link to headingNext steps

Copy link to headingFrequently asked questions

Copy link to headingIs there a Python SDK for Edgenivo Passport?

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

Copy link to headingWhich header contains the Edgenivo Passport token?

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

Copy link to headingHow 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.

Copy link to headingHow do I test Edgenivo 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.

Copy link to headingCan backend middleware make a route public on a Passport-protected deployment?

No. Passport runs in Edgenivo'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.