---
title: Verifying tokens
description: Checking an access token on your backend, in any language.
---

import Callout from '../../../components/Callout.astro';


A ZevAuth access token is an ordinary JWT. You verify it with your
environment's public key, which we publish. **Your backend never calls us to
check a token**, so an outage of ours is not an outage of yours.

## Discovery

Every environment publishes an OIDC discovery document:

```
https://api.zevauth.net/v1/{environmentId}/.well-known/openid-configuration
```

It gives you the `issuer` and the `jwks_uri`. Read both from there rather than
building them yourself. The issuer is an identity, not a URL you can assume,
and a self-hosted deployment will not match a guess.

## Node

```ts
import { verifyZevAuthToken } from '@zevauth/nextjs/server';

const claims = await verifyZevAuthToken(token, {
  environmentId: process.env.ZEVAUTH_ENVIRONMENT_ID!,
});
```

Or with `jose` directly, in any framework:

```ts
import { createRemoteJWKSet, jwtVerify } from 'jose';

const issuer = `https://api.zevauth.net/v1/${environmentId}`;
const jwks = createRemoteJWKSet(new URL(`${issuer}/.well-known/jwks.json`));

const { payload } = await jwtVerify(token, jwks, {
  issuer,
  audience: environmentId,
});
```

## Python

```python
from jwt import PyJWKClient
import jwt

issuer = f"https://api.zevauth.net/v1/{environment_id}"
jwks = PyJWKClient(f"{issuer}/.well-known/jwks.json")

claims = jwt.decode(
    token,
    jwks.get_signing_key_from_jwt(token).key,
    algorithms=["RS256"],
    issuer=issuer,
    audience=environment_id,
)
```

## Always check the audience

<Callout type="warning">
Verify `aud` against your environment id. Each environment signs with its own
key, so a token cannot verify against the wrong one. But a project with
development and production both live has two valid tokens, and without an
audience check a test-mode token would be accepted by a production endpoint.
</Callout>

## Cache the key set

Fetch the JWKS once and keep it. Libraries like `jose` and `PyJWKClient` do
this for you, and refetch when they meet a key id they do not know, which is
what a key rotation looks like. Building a new client per request defeats the
cache and fetches the document on every call.

Retiring keys stay published for a while after a rotation, so tokens signed a
minute before it keep verifying for the rest of their life.