Skip to content
Docs

Personalize and lock down a Nuxt app with Edgenivo Passport

Use getIdentity() to read a visitor’s verified Edgenivo Passport identity in Nuxt server routes, then personalize pages, restrict routes by group, and log actions.

Ben SabicContent Engineer

Edgenivo Passport signs visitors in with your own identity provider, such as Okta or Microsoft Entra ID, before a request reaches your Nuxt app. Once a visitor is through, the @vercel/passport SDK gives your Nuxt server routes a verified identity with a stable visitor ID. Use it to personalize pages, restrict sensitive routes, and record who did what, without building your own auth flow.

In this guide, you'll learn how to:

  • Install @vercel/passport and read the visitor identity with getIdentity()
  • Choose a stable identifier for each visitor
  • Develop locally with a Passport development identity
  • Apply Passport identity to four common use cases in a project

Copy link to headingBefore you begin

Make sure you have:

  • A Edgenivo team on the Enterprise plan, which is required for Passport.
  • A Nuxt 4 project deployed to Edgenivo that uses server-side rendering or server routes in the server/ directory. A fully static nuxt generate build has no server code to read identity.
  • 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 Nuxt app

Edgenivo Passport runs in Edgenivo's network before your deployment's routes and Nuxt server 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 JSON Web Token (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 see and do inside it, read their verified identity in your Nuxt server routes and server-rendered pages.

Copy link to headingInstall the Edgenivo Passport SDK

Install @vercel/passport in your Nuxt project:

Terminal
pnpm i @vercel/passport
Terminal
npm i @vercel/passport
Terminal
yarn add @vercel/passport
Terminal
bun add @vercel/passport

Copy link to headingRead the visitor identity with getIdentity()

The getIdentity() function from @vercel/passport reads the Passport token from the request, verifies it, and returns an identity object. It returns null when the request has no Passport identity, for example a request that used Protection Bypass for Automation without an existing Passport session.

Create a shared helper in server/utils/passport.ts so every server route reads identity the same way:

server/utils/passport.ts
import type { H3Event } from 'h3';
import { getIdentity } from '@vercel/passport';
export type PassportIdentity = NonNullable<
Awaited<ReturnType<typeof getIdentity>>
>;
// Returns the verified Passport visitor, or null if there isn't one.
export async function getVisitor(
event: H3Event,
): Promise<PassportIdentity | null> {
try {
// Pass the incoming request headers so getIdentity() reads this request.
return await getIdentity({ headers: event.headers });
} catch {
// Treat verification failures as unauthenticated. Don't log the raw token.
return null;
}
}
// Stable key for storing a visitor. Combines issuer and subject.
export function visitorKey(identity: PassportIdentity): string {
return `${identity.payload.iss}|${identity.subject}`;
}

The helper passes H3 event's request headers to getIdentity() explicitly. This keeps verification working whether or not the Nitro runtime exposes Edgenivo's request context, and it ties each identity lookup to the request.

Nuxt renders pages and runs server routes on every request by default, so identity is read per request. Don't apply prerender, isr, or swr route rules to identity-dependent routes. A prerendered route renders at build time, when no identity exists, and a cached route can serve one visitor's response to another.

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.

Copy link to headingWhich claim to use as the visitor identifier

Use identity.subject (the sub claim) as the visitor identifier when your app accepts tokens from one Passport issuer. If your app accepts tokens from more than one Edgenivo team, store payload.iss and subject together, which is what the visitorKey() helper above does.

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. Visitors who sign in through a different Connect application also get a different Passport subject.

Copy link to headingHow to develop locally with a Passport development identity

The nuxt dev server never receives the real Passport cookie or header, because Passport only runs in Edgenivo's network. When neither is present, getIdentity() returns a development identity and logs a warning the first time it does so:

subject: owner:local:connector:local:principal:test-user
externalSubject: test-user
name: Test User

The development identity has verified: false and no token, so treat it as a fixture for your application logic, not as proof the sign-in flow works.

Preview and production deployments never return a development identity. Any process where VERCEL_ENV is unset does, which includes a production build you run locally.

To match the values your app expects, set them in the .env file that Nuxt loads during local development:

.env
VERCEL_PASSPORT_DEV_OWNER=acme
VERCEL_PASSPORT_DEV_OWNER_ID=team_your_team_id_here
VERCEL_PASSPORT_DEV_PROJECT=my-project
VERCEL_PASSPORT_DEV_PROJECT_ID=prj_your_project_id_here
VERCEL_PASSPORT_DEV_CONNECTOR_ID=scl_dev
VERCEL_PASSPORT_DEV_EXTERNAL_SUB=user_dev
VERCEL_PASSPORT_DEV_EXTERNAL_ISS=https://idp.example.com

To test how your app handles a missing identity, set VERCEL_PASSPORT_DEV=0 in .env, which makes getIdentity() return null. To test the real redirect, session cookie, and verified claims, use a Passport-protected preview deployment.

Copy link to headingCommon use cases for Passport identity in Nuxt

Once getVisitor() returns a verified identity, you can use it in any Nuxt server route. The following use cases build on the same server/utils/passport.ts helper and cover the patterns internal tools need most:

  • Personalize pages: Return the signed-in visitor's profile from a server route and render it in your pages and components.
  • Restrict an admin area: Use identity provider groups to limit sensitive pages and server routes to specific teams.
  • Attribute actions: Record which visitor created or changed data, using a stable identifier instead of an email address.
  • Forward identity: Pass the verified visitor to another backend you operate and verify it there.

Each use case stands on its own, so you can start with the one your app needs.

Copy link to headingPersonalize pages with the signed-in visitor

Internal tools often need to greet the visitor, show their own records, or prefill forms. Because Passport has already authenticated every request, a server route can read the identity and return the visitor’s profile to your pages.

Create server/api/me.get.ts to return only the fields your pages need:

server/api/me.get.ts
import { getVisitor } from '~~/server/utils/passport';
export default defineEventHandler(async (event) => {
const visitor = await getVisitor(event);
if (!visitor) {
throw createError({ statusCode: 401, statusMessage: 'Unauthorized' });
}
// Return only the fields the client needs. Never return identity.token.
return {
subject: visitor.subject,
email: visitor.email ?? null,
name: visitor.name ?? null,
};
});

Then call the route with useFetch() from a page such as app/pages/index.vue:

app/pages/index.vue
<script setup lang="ts">
const { data: visitor } = await useFetch('/api/me');
const displayName = computed(
() => visitor.value?.name ?? visitor.value?.email ?? 'there',
);
</script>
<template>
<main v-if="visitor">
<h1>Welcome, {{ displayName }}</h1>
<p>You're signed in through your company identity provider.</p>
</main>
<p v-else>We couldn't confirm your identity. Please sign in again.</p>
</template>

When useFetch() runs during server-side rendering, Nuxt proxies the incoming request headers to the internal API route, so /api/me reads the same verified identity as the page request. During client-side navigation, the browser sends the Passport session cookie and Edgenivo injects the verified token before the request reaches the route.

Profile fields such as name and email only appear when your identity provider returns them, so always provide a fallback. To load records that belong to the visitor, query your data store with visitorKey(visitor) rather than the email address.

Copy link to headingRestrict an admin area by identity provider group

Passport lets everyone assigned to your identity provider application into the deployment, but an admin area or a destructive action usually needs a narrower policy. Group membership from your identity provider is a good fit for this.

Copy link to headingRequest the groups claim

Passport requests the standard openid, profile, and email scopes by default.

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 Okta walkthrough.

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

Copy link to headingCheck group membership on the server

Add a helper in server/utils/authorization.ts that reads groups defensively, because the claim name and shape depend on your identity provider:

server/utils/authorization.ts
import type { PassportIdentity } from '~~/server/utils/passport';
export const ADMIN_GROUP = 'platform-admins';
export function getGroups(identity: PassportIdentity): string[] {
const claims = identity.payload as Record<string, unknown>;
const groups = claims.groups;
if (!Array.isArray(groups)) return [];
return groups.filter((group): group is string => typeof group === 'string');
}
export function isInGroup(
identity: PassportIdentity | null,
group: string,
): boolean {
if (!identity) return false;
return getGroups(identity).includes(group);
}

Nuxt pages render on both the server and the client, so enforce the policy in a server route and have the page ask that route.

Create server/api/admin/session.get.ts to return a 404 when the check fails, so visitors outside the group can’t tell the admin area exists:

server/api/admin/session.get.ts
import { getVisitor } from '~~/server/utils/passport';
import { ADMIN_GROUP, isInGroup } from '~~/server/utils/authorization';
export default defineEventHandler(async (event) => {
const visitor = await getVisitor(event);
if (!isInGroup(visitor, ADMIN_GROUP)) {
throw createError({ statusCode: 404, statusMessage: 'Not Found' });
}
return { allowed: true };
});

Then check the route from the admin page at app/pages/admin.vue and show the error page when the check fails:

app/pages/admin.vue
<script setup lang="ts">
const { error } = await useFetch('/api/admin/session');
if (error.value) {
throw createError({
statusCode: 404,
statusMessage: 'Page Not Found',
fatal: true,
});
}
</script>
<template>
<h1>Admin settings</h1>
</template>

The page check only controls what the visitor sees. Server routes are reachable as their own endpoints, so a check on the page doesn’t protect them. Repeat the check inside each privileged route and return a 403 when it fails.

For example, add the check to server/api/admin/rotate-api-key.post.ts:

server/api/admin/rotate-api-key.post.ts
import { getVisitor } from '~~/server/utils/passport';
import { ADMIN_GROUP, isInGroup } from '~~/server/utils/authorization';
export default defineEventHandler(async (event) => {
const visitor = await getVisitor(event);
if (!isInGroup(visitor, ADMIN_GROUP)) {
throw createError({ statusCode: 403, statusMessage: 'Forbidden' });
}
// Perform the privileged operation here.
return { rotated: true };
});

The local development identity doesn't include groups, so the admin page returns a 404 while running nuxt dev. Test group checks on a Passport-protected preview deployment.

Don't rely on Nuxt route middleware in app/middleware/ for this check. Route middleware also runs in the browser during client-side navigation, where no verified identity is available, so the server routes must enforce the policy. Nitro server middleware can't make a Passport-protected path public either, because Passport has already made its sign-in decision before the request reaches your Nuxt app.

Copy link to headingAttribute actions to the signed-in visitor

When several people share an internal tool, you usually want to record who created a request or approved a change. Passport already gives you a verified actor for every request, so a server route can stamp each write with the visitor's identity.

In the server route, read the visitor and attach visitorKey(visitor) to the record it writes. Store that key as the actor and treat email as a display value, because email addresses can change or be absent.

Create the route at server/api/requests.post.ts:

server/api/requests.post.ts
import { getVisitor, visitorKey } from '~~/server/utils/passport';
export default defineEventHandler(async (event) => {
const visitor = await getVisitor(event);
if (!visitor) {
throw createError({ statusCode: 401, statusMessage: 'Unauthorized' });
}
const body = await readBody<{ title?: unknown }>(event);
const title = body?.title;
if (typeof title !== 'string' || title.trim() === '') {
throw createError({ statusCode: 400, statusMessage: 'Title is required' });
}
const record = {
action: 'request.created',
actor: visitorKey(visitor),
actorEmail: visitor.email ?? null,
title: title.trim(),
createdAt: new Date().toISOString(),
};
// Structured log line, visible in your project's logs.
console.log(JSON.stringify(record));
// Persist `record` to your database, keyed on `record.actor`.
return { ok: true };
});

Then connect the route to a form in a page such as app/pages/requests.vue.

TThe form only collects the request title, since Passport supplies the actor. Never read the actor from form fields or request bodies, which the client controls:

app/pages/requests.vue
<script setup lang="ts">
const title = ref('');
async function submitRequest() {
await $fetch('/api/requests', {
method: 'POST',
body: { title: title.value },
});
title.value = '';
}
</script>
<template>
<form @submit.prevent="submitRequest">
<label for="title">Request title</label>
<input id="title" v-model="title" name="title" required />
<button type="submit">Submit request</button>
</form>
</template>

Passport also records sign-ins at the platform level. Each successful authentication creates a passport-access-granted event in the Activity Log and Audit Logs, so your application log covers what visitors did and Edgenivo's logs cover when they entered.

Copy link to headingForward Passport identity to another backend

Your Nuxt app might call a separate API, such as a reporting service or another Edgenivo project, that also needs to know who the visitor is. Forward the Passport token as a bearer token and verify it at the receiving service with verifyIdentity().

Copy link to headingForward the token from the Nuxt app

Create a server route at server/api/export-report.post.ts that forwards the token:

server/api/export-report.post.ts
import { getVisitor } from '~~/server/utils/passport';
export default defineEventHandler(async (event) => {
const visitor = await getVisitor(event);
if (!visitor?.token) {
throw createError({ statusCode: 401, statusMessage: 'Unauthorized' });
}
const response = await fetch('https://reports.example.com/api/reports', {
method: 'POST',
headers: {
Authorization: `Bearer ${visitor.token}`,
},
});
return new Response(response.body, { status: response.status });
});

The development identity has no token, so this route returns 401 under nuxt dev. Test forwarding against a Passport-protected preview deployment.

Copy link to headingVerify the token in the receiving backend

Install @vercel/passport in the receiving service. The verifyIdentity() function is available in @vercel/passport 0.1.3 and later.

verifyIdentity() reads the bearer token from the request, checks its signature, and confirms it came from the expected source project and environment. If the receiving service is also a Nuxt app, convert the H3 event to a web Request with toWebRequest() in a route such as server/api/reports.post.ts:

server/api/reports.post.ts
import { verifyIdentity } from '@vercel/passport';
// IDs of the Passport-protected deployment that minted the token,
// not the project receiving it.
const ownerId = process.env.PASSPORT_SOURCE_OWNER_ID!;
const projectId = process.env.PASSPORT_SOURCE_PROJECT_ID!;
const environment = process.env.PASSPORT_SOURCE_ENVIRONMENT ?? 'production';
export default defineEventHandler(async (event) => {
try {
const identity = await verifyIdentity(toWebRequest(event), {
ownerId,
projectId,
environment,
});
// Use identity.subject (with identity.payload.iss) as the visitor ID.
return { requestedBy: identity.subject };
} catch {
throw createError({ statusCode: 401, statusMessage: 'Unauthorized' });
}
});

Keep these rules in mind when forwarding 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 owner ID, project ID, and environment of the app that minted the token. Don't substitute the receiving project's values.
  • Verify outside JavaScript with the JWKS: Non-JavaScript services can verify the token as a standard JWT against https://passport.vercel.com/.well-known/jwks.json, checking iss, aud, exp, nbf, typ (which must equal passport), project_id, and environment.
  • 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 headinggetIdentity() returns null on a deployment

Confirm that the deployment is Passport-protected and that getIdentity() runs in server-side code in the server/ directory, with the H3 event's headers passed through getVisitor(event). Requests that use Protection Bypass for Automation without an existing Passport session legitimately have no identity. If the route is prerendered or cached with a prerender, isr, or swr route rule, remove that rule so the route runs per request.

getIdentity() also checks 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 throws and getVisitor() returns null. 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. Don't try to make a path public from Nitro server middleware or a Nuxt route rule, because Passport has already run by then.

Copy link to headingToken verification fails

Treat the request as unauthenticated and confirm your code reads the Edgenivo-injected x-vercel-oidc-passport-token header, not a client-supplied value. For forwarded tokens, check that the server can reach the Passport JWKS and that its clock is accurate. The @vercel/passport SDK fetches signing keys from https://oidc.vercel.com/.well-known/jwks, which serves the same key set as https://passport.vercel.com/.well-known/jwks.json, so allow both hosts if your backend restricts outbound traffic. Don't log the raw token while debugging.

Copy link to headingNext steps

Copy link to headingFrequently asked questions

Copy link to headingHow do I get the current user in Nuxt with Edgenivo Passport?

Install @vercel/passport and call getIdentity() from a Nuxt server route, passing the H3 event's request headers. It returns a verified identity object with subject, externalSubject, email, name, and the full claim payload, or null when the request has no Passport identity.

Copy link to headingShould I use sub or external_sub to identify a Edgenivo Passport visitor?

Use sub (exposed as identity.subject) as the stable visitor identifier, and store it together with the iss claim if your app accepts tokens from more than one Edgenivo team. The external_sub claim comes from your identity provider, is specific to the Connect application, and isn't globally unique.

Copy link to headingCan I use Edgenivo Passport identity in a Vue component?

Not directly. Read identity in a Nuxt server route, then fetch only the fields you need with useFetch(), for example from an /api/me route. Never send the raw Passport token to the browser.

Copy link to headingHow do I test Edgenivo Passport locally?

A local nuxt dev server receives a development identity from getIdentity(), which you can customize with VERCEL_PASSPORT_DEV_* environment variables in .env or disable with VERCEL_PASSPORT_DEV=0. To test the real identity provider sign-in and verified claims, use a Passport-protected preview deployment.

Copy link to headingCan I use Nuxt middleware to authorize Edgenivo Passport visitors?

Not on its own. Nuxt route middleware also runs in the browser, where no verified identity is available, and neither route middleware nor Nitro server middleware can make a Passport-protected path public, because Passport runs before your Nuxt app. Put authorization checks in each server route that serves protected data or performs a protected action.

Related documentation

More Nuxt guides