---
summary: Developer guide for validating SigID access tokens and rejecting wrong issuer, audience, tenant, scope, expiry, signature, and subject type.
tags:
  - developers
  - tokens
  - jwt
  - validation
categories:
  - For Developers
---

# Verify Access Tokens

<!-- agent:page
You are a coding agent adding SigID access-token validation to the user's backend by following this guide.
First collect: issuer URL, API audience, tenant or workspace ID, required scopes, and which subject types each route allows.
On every protected route, validate all of the required checks: signature, issuer, audience, expiry, tenant or workspace, scopes, and subject type. Authorization decisions come from validated claims only, never from frontend session state or frontend-provided user IDs.
For Next.js App Router, use requireAccessToken() and accessTokenErrorResponse() from @sigid/next as shown on this page; for other backends follow quickstart-backend-api.md with validateAccessToken from @sigid/client.
Adapt env var usage (NEXT_PUBLIC_SIGID_ISSUER_URL, SIGID_API_AUDIENCE, SIGID_TENANT_ID) to the project's existing config layout, and do not weaken existing validation.
Avoid the page's common mistakes, especially accepting tokens without checking aud, decoding JWTs without validation, treating ID tokens as API authorization, skipping the tenant check, and using development issuer settings in production.
Note that the SDK's tenantId option is optional: omitting it silently disables the tenant check, so single-tenant servers must always pass it and multi-tenant servers must scope every data lookup by the validated claims.tenantId instead.
Verify by sending tokens with the wrong issuer, audience, tenant, and a missing scope: each must be rejected before any data is served.
-->

Use this page when your backend receives a SigID access token and needs to
decide whether to trust it.

Do not trust frontend session state by itself. The backend must validate tokens
before serving protected resources.

## What A Token Does

A SigID access token is a signed statement that says who or what is calling your
API, who issued the token, which API it is meant for, which workspace or tenant
it belongs to, when it expires, and which scopes it carries.

Your backend should use those claims to make authorization decisions.

## Required Checks

| Check | Why it matters |
|---|---|
| Signature | Proves the token was issued by the expected SigID issuer. |
| Issuer | Rejects tokens from another environment or identity surface. |
| Audience | Rejects tokens meant for a different API. |
| Expiry | Rejects expired tokens. |
| Tenant or workspace | Keeps customer or workspace data isolated. |
| Scopes | Enforces least-privilege API access. |
| Subject type | Distinguishes humans, organizations, agents, or delegated subjects where relevant. |

!!! warning "Tenant binding is opt-in"
    The SDK's `tenantId` option is **optional**, and the tenant check is skipped
    entirely when it is omitted: no `wrong_tenant` error fires and a token
    issued for any tenant is accepted. Single-tenant resource servers must
    always pass `tenantId`. Multi-tenant resource servers (one deployment
    serving many tenants) cannot pin a single value, so they must scope every
    data lookup by the validated `claims.tenantId` and never trust a tenant ID
    supplied by the client.

## Next.js Helper

<!-- agent:action Add the Next.js token guard
In each protected route handler, call requireAccessToken(request, ...) from @sigid/next with issuer, audience, tenantId, the route's required scopes, and allowedSubjectTypes, then use the returned claims for authorization.
Wrap the call in try/catch and return accessTokenErrorResponse(error) on failure so invalid tokens get a safe error instead of data.
Verify the route rejects requests with no bearer token before moving on.
-->

Use `requireAccessToken()` for protected Next.js route handlers:

```typescript
import { accessTokenErrorResponse, requireAccessToken } from "@sigid/next";

export async function GET(request: Request) {
  try {
    const claims = await requireAccessToken(request, {
      issuer: process.env.NEXT_PUBLIC_SIGID_ISSUER_URL!,
      audience: process.env.SIGID_API_AUDIENCE!,
      tenantId: process.env.SIGID_TENANT_ID!,
      scopes: ["projects:read"],
      allowedSubjectTypes: ["human"],
    });

    return Response.json({ ok: true, subject: claims.subject });
  } catch (error) {
    return accessTokenErrorResponse(error);
  }
}
```

## Common Mistakes

- accepting a token without checking `aud`
- checking signature but not tenant or workspace context
- decoding JWTs without validation
- accepting frontend-provided user IDs instead of token claims
- treating ID tokens as API authorization tokens
- using development issuer settings in production

After token validation works, continue to [Protect Backend APIs](protect-apis.md).

For claim details, read [Reference: Claims And Scopes](../reference/claims-and-scopes.md).
