---
summary: Step-by-step backend API quickstart for validating SigID bearer tokens, checking tenant, audience, scopes, subject type, and returning safe 401 or 403 responses.
tags:
  - developers
  - quickstart
  - backend
  - token-validation
categories:
  - For Developers
---

# Backend API Quickstart

<!-- agent:page
You are a coding agent adding SigID bearer-token validation to the user's backend API by following this guide end to end.
First collect from the workspace or API owner: SigID issuer URL, API audience, tenant ID, the required scope (e.g. projects:read), and allowed subject types (usually ["human"]; add "agent" only when the route supports agents).
Implementation order:
- npm install @sigid/client
- create an auth guard that extracts the Authorization: Bearer header, calls validateAccessToken from @sigid/client with issuer, audience, tenantId, scopes, and allowedSubjectTypes from env vars (SIGID_ISSUER_URL, SIGID_API_AUDIENCE, SIGID_TENANT_ID, SIGID_API_SCOPE), and maps AccessTokenValidationError to a JSON error response using its status and code
- apply the guard to each protected route, then add per-resource ownership checks that return 403 for valid tokens that cannot access the resource
Adapt the Request/Response examples to the user's framework; do not weaken existing auth middleware.
Never accept ID tokens, UserInfo responses, email addresses, or frontend session state as API authorization, and never log tokens or secrets.
Verify with the page's table: no header gives 401 missing_bearer_token; wrong issuer, audience, tenant, or missing scope each give 401; a valid token with the wrong resource owner gives 403; a valid token on an allowed resource gives 200.
For Next.js App Router, use requireAccessToken() from @sigid/next instead of hand-rolling the guard.
-->

Use this guide when your backend receives `Authorization: Bearer <token>` from a
frontend, mobile app, CLI, or service client.

Time: 15-25 minutes after you know the issuer, API audience, tenant, and
required scope.

## What You Will Build

- a bearer-token guard for one API route
- issuer, audience, tenant, expiry, signature, scope, and subject-type checks
- safe `401` responses for invalid tokens
- safe `403` responses for valid tokens that cannot access a resource

## Before You Start

Get these values from the workspace owner or API owner:

| Value | Example |
|---|---|
| SigID issuer | `https://identity.example.com` |
| API audience | `https://api.example.com/projects` |
| Tenant ID | `tenant_123` |
| Required scope | `projects:read` |
| Allowed subject types | usually `human`; add `agent` only when the route supports agents |

Do not use ID tokens, UserInfo responses, email addresses, or frontend session
state as API authorization.

## Install

<!-- agent:action Install the SDK
Run: npm install @sigid/client
Run it at the backend project root, using the project's existing package manager if it is not npm.
Verify @sigid/client appears in package.json dependencies before continuing.
-->

```bash
npm install @sigid/client
```

## Validate The Token

<!-- agent:action Create the token guard
Create an authenticateProjectsRead(request) helper modeled on this section: extract the Authorization header, return 401 missing_bearer_token when absent, 401 invalid_token_format when it is not Bearer <token>, then call validateAccessToken from @sigid/client with issuer SIGID_ISSUER_URL, audience SIGID_API_AUDIENCE, tenantId SIGID_TENANT_ID, scopes [SIGID_API_SCOPE], allowedSubjectTypes ["human"].
Catch AccessTokenValidationError and return a JSON error built from error.status, error.code, and error.message; rethrow anything else.
Place the helper in the project's existing auth or middleware directory and return the validated claims on success.
-->

```typescript
import {
  AccessTokenValidationError,
  validateAccessToken,
  type ValidatedAccessTokenClaims,
} from "@sigid/client";

type AuthResult =
  | { ok: true; claims: ValidatedAccessTokenClaims }
  | { ok: false; response: Response };

function errorResponse(
  status: number,
  error: string,
  detail?: string,
): Response {
  return Response.json({ ok: false, error, detail }, { status });
}

export async function authenticateProjectsRead(
  request: Request,
): Promise<AuthResult> {
  const authorization = request.headers.get("authorization");

  if (!authorization) {
    return {
      ok: false,
      response: errorResponse(401, "missing_bearer_token"),
    };
  }

  const match = authorization.match(/^Bearer\s+(\S+)$/i);

  if (!match) {
    return {
      ok: false,
      response: errorResponse(401, "invalid_token_format"),
    };
  }

  try {
    const claims = await validateAccessToken(match[1], {
      issuer: process.env.SIGID_ISSUER_URL!,
      audience: process.env.SIGID_API_AUDIENCE!,
      tenantId: process.env.SIGID_TENANT_ID!,
      scopes: [process.env.SIGID_API_SCOPE ?? "projects:read"],
      allowedSubjectTypes: ["human"],
    });

    return { ok: true, claims };
  } catch (error) {
    if (error instanceof AccessTokenValidationError) {
      return {
        ok: false,
        response: errorResponse(error.status, error.code, error.message),
      };
    }

    throw error;
  }
}
```

## Enforce Resource Access

<!-- agent:action Enforce resource access
In each protected route handler, call the guard first and return its error response when validation fails.
Then check the specific resource: compare ownership or membership against the validated claims.subject and return 403 forbidden when it does not match.
Replace loadProjectForTenant() with the application's real data lookup; a valid token alone must never grant access to every resource.
-->

Token validation proves the caller has a valid token for this API. It does not
automatically prove the caller can read every resource.

```typescript
export async function GET(request: Request) {
  const auth = await authenticateProjectsRead(request);

  if (!auth.ok) {
    return auth.response;
  }

  const { claims } = auth;
  const project = await loadProjectForTenant(process.env.SIGID_TENANT_ID!);

  if (project.ownerSubject !== claims.subject) {
    return Response.json(
      { ok: false, error: "forbidden" },
      { status: 403 },
    );
  }

  return Response.json({ ok: true, project });
}
```

Replace `loadProjectForTenant()` with your application data lookup.

## What To Log

<!-- agent:action Configure safe logging
In the guard and route handlers, log request ID, tenant ID, route name, validated subject, subject type, and the missing scope or policy reason on denials.
Never log access tokens, refresh tokens, ID tokens, authorization codes, client secrets, webhook secrets, MFA codes, or full sensitive payloads.
Check the project's existing logger configuration so these fields fit its structured-logging format.
-->

Log enough to debug without leaking secrets:

- request ID
- tenant ID
- route name
- validated subject
- subject type
- missing scope or policy reason

Do not log access tokens, refresh tokens, ID tokens, authorization codes, client
secrets, webhook secrets, MFA codes, or full sensitive payloads.

## Verify

<!-- agent:action Verify the guard
Exercise the route with every case in this table: no Authorization header, malformed bearer header, wrong issuer, wrong audience, wrong tenant, missing scope, valid token with wrong resource owner, and valid token with allowed resource.
Expect 401 with the matching error code for the token failures, 403 forbidden for the ownership failure, and 200 only for the final case.
Add these cases to the project's API test suite so the guard cannot regress silently.
-->

Call the route with these cases:

| Request | Expected result |
|---|---|
| No `Authorization` header | `401 missing_bearer_token` |
| Malformed bearer header | `401 invalid_token_format` or equivalent local error |
| Token from wrong issuer | `401 wrong_issuer` |
| Token for wrong audience | `401 wrong_audience` |
| Token from another tenant | `401 wrong_tenant` *(only when `tenantId` is configured – see note)* |
| Token missing required scope | `401 insufficient_scope` |
| Valid token but wrong resource owner | `403 forbidden` |
| Valid token and allowed resource | `200` |

!!! warning "The tenant check is opt-in"
    The `wrong_tenant` row above only fires when you pass `tenantId` to
    `validateAccessToken`. If `SIGID_TENANT_ID` is unset, the SDK performs no
    tenant binding and a token from any tenant is accepted – so that row
    silently becomes `200`. Always set `tenantId` for a single-tenant resource
    server; for a multi-tenant server, scope every data lookup by the validated
    `claims.tenantId` rather than trusting a tenant ID supplied by the client.

For Next.js App Router, use `requireAccessToken()` from `@sigid/next` as shown
in [Next.js Quickstart](quickstart-nextjs.md#protect-an-api-route).
