Skip to content

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

npm install @sigid/backend

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