@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 + Path | Purpose |
|---|---|
GET /login | Generate state + PKCE + nonce, persist them, 302 to IdP's authorize endpoint. Honors ?return_to= for deep links. |
GET /callback | Validate state, exchange code, verify id_token, call onAuthenticated, then issueSession. |
POST /backchannel-logout | Receive 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—mountSsoreturnsRoute[]you spread into your router- External: none. Uses
fetchand 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.