Backend SDK For Confidential Servers¶
Use this page when your app is a confidential server with its own session (B2B SaaS, server-rendered apps, an existing cookie/auth boundary that must stay authoritative) and you want SigID login without hand-rolling the OAuth/OIDC code flow.
@sigid/client is public-only – it never sends a client_secret. For server
apps that hold a secret, @sigid/backend provides the confidential-side
primitives browser apps already get from @sigid/client: PKCE/state/nonce,
confidential code exchange, ID-token verification, RP-initiated logout, webhook
verification, and access-token validation.
Install¶
Configure¶
import { createSigIdBackend } from "@sigid/backend";
const sigid = createSigIdBackend({
issuer: process.env.SIGID_ISSUER!,
clientId: process.env.SIGID_CLIENT_ID!,
clientSecret: process.env.SIGID_CLIENT_SECRET!, // server-side only
redirectUri: "https://app.example.com/auth/callback",
});
| Value | Where | Notes |
|---|---|---|
| Issuer | Dashboard / discovery | e.g. https://auth.sigid.org |
| Client ID | Dashboard Applications | The confidential application |
| Client secret | Dashboard Applications | Never ship to the browser |
| Redirect URI | Dashboard Applications | Exact match (scheme/host/port/path) |
1. Begin Login¶
// Persist a fresh random value in an HttpOnly, Secure, SameSite=Lax cookie
// when the authorization response returns via the default GET callback.
const sessionBinding = appSession.oauthBinding;
const { url, state, nonce } = await sigid.buildAuthorizeUrl({ sessionBinding });
// Redirect the user to `url`. PKCE verifier, state, and nonce are generated
// and stored in the transaction store, keyed by `state`.
The store defaults to an in-memory store (single-process). For multi-instance deployments, pass a shared store:
import { type TransactionStore } from "@sigid/backend";
class RedisTransactionStore implements TransactionStore {
async set(key, record, ttlSeconds) { /* redis SET ... EX ttlSeconds */ }
async get(key) { /* redis GET */ }
async remove(key) { /* redis DEL */ }
}
const sigid = createSigIdBackend({ /* ... */, store: new RedisTransactionStore() });
2. Handle The Callback¶
On your /auth/callback route, exchange the code and verify the ID token in one
call:
const result = await sigid.exchangeAuthorizationCode({ code, state, sessionBinding });
// result.idTokenClaims is already signature/issuer/audience/nonce verified.
// Mint YOUR OWN app session cookie from result.idTokenClaims.
// Retain result.idToken only if you want RP-initiated logout.
exchangeAuthorizationCode uses client_secret_basic by default (SigID's
backend default); pass authMethod: "client_secret_post" to use the body
method. Nonce binding is enforced automatically from the stored transaction.
The mandatory sessionBinding must be an unguessable value read from the
initiating browser's cookie (or equivalent server-side session). For the
default query response mode, whose callback is a top-level GET navigation, use
an HttpOnly; Secure; SameSite=Lax cookie. A cross-site POST callback such as
response_mode=form_post requires SameSite=None; Secure, or a same-site GET
trampoline that restores the stricter cookie before exchange. Never derive the
binding from a callback parameter.
3. Logout¶
const endSessionUrl = sigid.buildEndSessionUrl({
idTokenHint: result.idToken,
postLogoutRedirectUri: "https://app.example.com/",
});
// Redirect the user to `endSessionUrl`.
SigID's end-session flow decodes the id_token_hint to locate the application
and verify the post-logout redirect, so the ID token must be retained and passed
through. buildEndSessionUrl handles id_token_hint + client_id +
post_logout_redirect_uri for you.
4. Protect Backend APIs¶
const claims = await sigid.validateAccessToken(token, {
audience: "https://api.example.com",
tenantId: process.env.SIGID_TENANT_ID!,
scopes: ["projects:read"],
});
Single-tenant servers must always pass tenantId; multi-tenant servers must
scope every data lookup by claims.tenantId. See
Verify Access Tokens and Protect Backend APIs.
5. Verify Webhooks¶
const { deliveryId, eventType } = await sigid.verifyWebhook({
secret: process.env.SIGID_WEBHOOK_SECRET!,
headers,
rawBody, // the VERBATIM request body – do not re-serialize JSON
});
Suite is sigid-webhook-v1 (HKDF-SHA256 over a canonical string). See
Receive Webhooks.
Design Principle: Enable, Don't Take Over¶
@sigid/backend stays framework-agnostic and never owns your session. It exposes
pluggable store interfaces and tells you what to persist and how to verify – it
does not mandate a session implementation. Framework adapter packages
(@sigid/express, @sigid/hono, …) may wrap the guards on demand.
Errors¶
All failures throw SigIdBackendError with a stable code and a remediation
hint. Handle them at your framework's error boundary:
import { isSigIdBackendError } from "@sigid/backend";
try {
await sigid.exchangeAuthorizationCode({ code, state, sessionBinding });
} catch (error) {
if (isSigIdBackendError(error)) {
console.error(error.code, error.remediation);
}
}
Next Pages¶
| Goal | Page |
|---|---|
| Token validation details | Verify Access Tokens |
| Protecting routes | Protect Backend APIs |
| Events | Receive Webhooks |
| OAuth reference | OAuth And OIDC |