@wess/atlas/sso#

Drop-in OIDC relying-party. Any Atlas app gains "Sign in with $IdP" by calling mountSso(cfg) and providing two callbacks: onAuthenticated (claims → local user row) and issueSession (set whatever session the host app uses). The package handles discovery, PKCE, state, code exchange, id_token verification, and back-channel logout.

Exports#

mountSso(cfg: SsoConfig) → readonly Route[]      mount /login + /callback + /backchannel-logout
ensureSsoStateTable(db, table?)                  idempotent CREATE TABLE IF NOT EXISTS
sweepExpiredSsoState(db, table?)                 delete expired state rows
clearDiscoveryCache()                            for tests / hot-reload

Types#

IdTokenClaims { sub, iss, aud, email?, email_verified?, name?, preferred_username?, picture?, ... }

AuthenticatedUser { localUserId: number|string, displayName? }

SessionIssuer = (conn, user, claims) => Promise<Conn> | Conn

SsoConfig {
  db: Connection
  issuerUrl: string                          // base URL — discovery doc lives at ${issuerUrl}/.well-known/openid-configuration
  clientId: string
  clientSecret: string
  redirectUri?: string                       // default: ${request.origin}${basePath}/callback — set it in production (the default trusts Host)
  scopes?: readonly string[]                 // default: ["openid","email","profile"] — openid is always added
  basePath?: string                          // default: "/auth/sso"
  defaultPostLoginPath?: string              // default: "/"
  stateTable?: string                        // default: "sso_state"

  onAuthenticated(db, claims) → AuthenticatedUser      // upsert local user, return its id
  issueSession(conn, user, claims) → Conn              // set cookie/JWT/etc., redirect home

  findLocalUserBySub?(db, sub) → number|string|null    // for back-channel logout
  invalidateSessions?(db, { sub, localUserId }) → void // revoke all sessions for that user
}

DiscoveryDoc { issuer, authorization_endpoint, token_endpoint, jwks_uri, userinfo_endpoint?, end_session_endpoint?, id_token_signing_alg_values_supported? }

Routes mounted#

mountSso returns three routes under basePath (default /auth/sso):

Method + PathPurpose
GET /loginGenerate state + PKCE + nonce, persist them, 302 to IdP's authorize endpoint. Honors ?return_to= for deep links.
GET /callbackValidate state, exchange code, verify id_token, call onAuthenticated, then issueSession.
POST /backchannel-logoutReceive IdP-pushed logout token, verify, look up localUserId via findLocalUserBySub, call invalidateSessions.

Wire up#

import { connect, from } from "@wess/atlas/db"
import { router, serve } from "@wess/atlas/server"
import { ensureSsoStateTable, mountSso, type IdTokenClaims, type SsoConfig } from "@wess/atlas/sso"

const db = connect({ driver: "sqlite", path: "./app.db" })
await ensureSsoStateTable(db)

const cfg: SsoConfig = {
  db,
  issuerUrl: process.env.SSO_ISSUER!,
  clientId: process.env.SSO_CLIENT_ID!,
  clientSecret: process.env.SSO_CLIENT_SECRET!,
  onAuthenticated: async (db, claims) => {
    const id = await upsertUserFromClaims(db, claims)
    return { localUserId: id, displayName: claims.name ?? claims.preferred_username }
  },
  issueSession: async (conn, user, _claims) => issueLocalSession(conn, user.localUserId),
}

serve({ routes: router(...mountSso(cfg)) })

State table#

ensureSsoStateTable creates the transient row store that bridges the /login → /callback redirect:

CREATE TABLE IF NOT EXISTS sso_state (
  state       TEXT PRIMARY KEY,
  verifier    TEXT NOT NULL,
  nonce       TEXT NOT NULL,
  return_to   TEXT NOT NULL DEFAULT '/',
  expires_at  TIMESTAMPTZ NOT NULL,
  created_at  TIMESTAMPTZ NOT NULL DEFAULT NOW()
)

Schedule sweepExpiredSsoState(db) to run periodically (rows TTL is 10 min):

setInterval(() => sweepExpiredSsoState(db), 60 * 60 * 1000)  // hourly

Discovery#

@wess/atlas/sso fetches the IdP's discovery doc lazily on the first request (and caches it). Tests can call clearDiscoveryCache() between runs.

Dependencies#

  • @wess/atlas/auth — JWT verification for id_tokens
  • @wess/atlas/db — state-row persistence
  • @wess/atlas/server — mountSso returns Route[] you spread into your router
  • External: none. Uses fetch and Web Crypto.

Compared to @wess/atlas/auth/social#

  • @wess/atlas/auth/social — provider-specific factories (Google, GitHub, Apple, …). Best for consumer apps with named providers.
  • @wess/atlas/sso — generic OIDC relying-party. Best for enterprise SSO (Keycloak, Auth0, Okta, Castle, anything OIDC-conformant).

Login state is bound to an HttpOnly SameSite=Lax browser cookie and consumed once. Post-login destinations must be local paths, checked by localReturnPath from @wess/atlas/auth/social: control characters (a tab would turn /\t/evil.example into //evil.example), backslashes, //host, and anything that resolves off the site fall back to defaultPostLoginPath. Discovery, id_token, back-channel and onAuthenticated failures answer a fixed error_description and log the detail server-side, so IdP responses and provisioning errors never reach the browser. Without redirectUri the callback URL is built from the Host header; set it explicitly in production. Discovery validates the issuer and HTTPS endpoints (HTTP is permitted on loopback), shares concurrent fetches, and bounds its cache. ID tokens require issuer, audience, subject, expiry, issued-at, and nonce bindings. Back-channel logout validates issuer/audience, rejects tokens older than five minutes, and tracks replays in process memory. Hosts with multiple instances should also deduplicate logout jti values in shared storage.

Canonical sourcepackages/sso/AGENTS.md
Type to search guides and package references.