Skip to content

Backend API Quickstart

Ask about this page: Claude ChatGPT Grok

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

Ask:
  • 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

Ask:

Get these values from the workspace owner or API owner:

Value Example
SigID issuer the tenant_issuer returned by setup or your application handoff
API audience https://api.example.com/projects
Tenant ID the tenant UUID returned as tenant_id
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.

Resolve these values with Integration Settings. This guide assumes a server runtime with Web Crypto and the standard Request, Response, and fetch APIs, such as Node.js 22 or later. It is a resource-server guide; no client secret is required to verify a signature with public JWKS. For interactive server login, use Backend SDK.

Configure the server environment before starting it:

SIGID_ISSUER_URL=https://your-registered-issuer.example
SIGID_API_AUDIENCE=your-registered-api-audience
SIGID_TENANT_ID=your-tenant-uuid
SIGID_API_SCOPE=projects:read

Replace all example values with the application's registered configuration. Load the file through your framework's environment loader; a bare Node process does not automatically read .env. The guard below rejects missing settings when its module loads, so an absent tenant variable cannot disable tenant binding.

Install

Ask:
npm install @sigid/client

Validate The Token

Ask:

Create src/auth.ts (or your existing middleware module):

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

// Validate required settings when this server module loads.
const issuer = process.env.SIGID_ISSUER_URL?.trim();
const audience = process.env.SIGID_API_AUDIENCE?.trim();
const tenantId = process.env.SIGID_TENANT_ID?.trim();
const requiredScope = process.env.SIGID_API_SCOPE?.trim();
if (!issuer || !audience || !tenantId || !requiredScope) {
  throw new Error("Configure the SigID issuer, API audience, tenant UUID, and API scope.");
}
const tokenOptions = {
  issuer,
  audience,
  tenantId,
  scopes: [requiredScope],
  allowedSubjectTypes: ["human"],
};

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,
    headers: status === 401 ? { "WWW-Authenticate": 'Bearer error="invalid_token"' } : {},
  });
}

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], tokenOptions);

    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

Ask:

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(claims.tenantId);

  if (!project) {
    return Response.json({ ok: false, error: "not_found" }, { status: 404 });
  }

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

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

This resource-handler excerpt is application-specific: import authenticateProjectsRead from the auth module and replace loadProjectForTenant() with your tenant-scoped data lookup, including a 404 response when no record exists. Use claims.tenantId for that lookup. The ownerSubject comparison is one example of domain authorization; use your application's actual membership or ownership rules.

What To Log

Ask:

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

Ask:

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 SDK compares tenants only when tenantId is supplied. It still requires a tenant claim and performs the other token checks when the option is omitted. The startup check above prevents accidental omission in this single-tenant example. For a multi-tenant API, scope every data lookup by validated claims.tenantId and enforce membership for the requested resource.

For Next.js App Router, use requireAccessToken() from @sigid/next as shown in Next.js Quickstart.