@merediv/sso-sdk
v0.3.0
Published
First-party OIDC relying-party SDK for mmadwn-sso. Runtime-agnostic BFF auth (authorization-code + PKCE, RS256 id_token verification, central logout) for Cloudflare Workers, Node and React Native. Zero runtime dependencies.
Downloads
259
Maintainers
Readme
@merediv/sso-sdk
First-party OIDC relying-party SDK for mmadwn-sso. It extracts the
proven Backend-For-Frontend (BFF) auth pattern — authorization-code + PKCE, RS256 id_token
verification, server-side refresh/id_token storage, and RP-initiated central logout — into a
reusable, runtime-agnostic, zero-dependency package.
- Runs anywhere: Cloudflare Workers / Pages Functions, Node 18+, Deno, Bun, React Native.
Web-standard APIs only (WebCrypto +
fetch) — nonode:*, noBuffer, noprocess. - Secure by default, not configurable down: RS256-only id_token verification, S256-only PKCE,
full claim checks (
iss/aud/exp/iat/nonce),state+iss(RFC 9207) CSRF/mix-up defense, single-use transactions,__Host-HttpOnly cookies, tokens never reach the browser, denylist revocation, and CWE-601 open-redirect hardening. - Tokens stay server-side: the browser only ever holds an HttpOnly session cookie.
Confidential (BFF) client model. For pure SPA/mobile without a server, register a public client and use the low-level primitives directly; the four handlers assume a server-side secret.
Install
bun add @merediv/sso-sdk # or: npm i @merediv/sso-sdkQuickstart (Cloudflare Pages Functions)
// functions/auth/[[route]].ts — one file wires all four endpoints.
import { createSsoClient, cloudflareKvStore, subAllowlist } from "@merediv/sso-sdk";
interface Env {
AUTH_KV: KVNamespace;
SSO_ISSUER: string;
SSO_CLIENT_ID: string;
SSO_CLIENT_SECRET: string;
SSO_REDIRECT_URI: string;
SESSION_SECRET: string;
ADMIN_SUBS: string;
APP_ORIGIN: string;
}
const clientFor = (env: Env) =>
createSsoClient({
issuer: env.SSO_ISSUER, // e.g. https://sso.mmadwn.com/api/auth
clientId: env.SSO_CLIENT_ID,
clientSecret: env.SSO_CLIENT_SECRET,
redirectUri: env.SSO_REDIRECT_URI,
sessionSecret: env.SESSION_SECRET,
appOrigin: env.APP_ORIGIN,
store: cloudflareKvStore(env.AUTH_KV),
deriveSession: subAllowlist(env.ADMIN_SUBS), // adds { isAdmin } live on every probe
});
export const onRequest: PagesFunction<Env> = ({ request, env }) => {
const sso = clientFor(env);
const { pathname } = new URL(request.url);
if (pathname.endsWith("/auth/login")) return sso.handleLogin(request);
if (pathname.endsWith("/auth/callback")) return sso.handleCallback(request);
if (pathname.endsWith("/auth/logout")) return sso.handleLogout(request);
if (pathname.endsWith("/auth/me")) return sso.handleMe(request);
if (pathname.endsWith("/auth/refresh")) return sso.handleRefresh(request);
return new Response("not found", { status: 404 });
};Guard your own API routes with getSession (it verifies the cookie + denylist and returns the
live session, with derived fields, or null):
const session = await clientFor(env).getSession(request);
if (!session) return new Response("unauthorized", { status: 401 });
if (!session.isAdmin) return new Response("forbidden", { status: 403 });Authorization: the roles claim (SSO M5)
The SSO now emits a real per-application roles claim (plus an org slug) in the id_token
and /userinfo — the authorization data an RP used to lack. Enable it per client in the SSO
dashboard (roles are defined + assigned there), then gate on roles instead of a hardcoded
sub-allowlist:
import { authorize, hasRole } from "@merediv/sso-sdk";
createSsoClient({
// …
// Persists the `roles` (+ `org`) claim onto the session and sets `isAuthorized`
// live on every probe. `mode: "any"` requires at least one; default requires all.
deriveSession: authorize({ require: "admin" }),
});
// or inspect roles directly off the verified session / id_token claims:
if (!hasRole(session, "billing")) return new Response("forbidden", { status: 403 });subAllowlist still works for apps already on it, but role-based authz is preferred: roles are
managed centrally in the SSO and take effect on the next login, with no per-RP redeploy. Only
clients that opt in receive the claim — everyone else is unaffected.
SSO client registration (prerequisites)
Register a confidential client in the SSO dashboard and provide:
redirect_uri= yourSSO_REDIRECT_URI(exact match)- a post-logout redirect URI = your
APP_ORIGIN(required for central logout to return) - scopes
openid profile email offline_access(offline_accessenables refresh; PKCE is always sent and is required wheneveroffline_accessis requested)
API
| Export | Purpose |
| --- | --- |
| createSsoClient(config) | The four handlers + handleRefresh + getSession. |
| authorize({ require?, mode?, field?, flag? }) | A deriveSession role gate backed by the roles claim (M5). |
| rolesFromClaims(claims) / hasRole(claims, role) / orgFromClaims(claims) | Read the roles / org claim off a session or id_token. |
| subAllowlist(subs, field?) | A deriveSession admin gate keyed on sub (pre-M5; still supported). |
| cloudflareKvStore(kv) / memoryKvStore() | KVStore adapters. |
| discoverEndpoints / resolveEndpoints / defaultEndpoints | Endpoint resolution. |
| verifyIdToken, signSession, verifySession, createPkcePair, safeReturnTo, … | Low-level primitives for custom flows. |
License
MIT.
