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
401responses for invalid tokens - safe
403responses 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¶
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.