Skip to content

Backend API Quickstart

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

npm install @sigid/client

Validate The Token

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

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

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

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

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

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.