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 | 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¶
Validate The Token¶
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¶
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¶
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 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.