UNPKG

accounts

Version:

Tempo Accounts SDK

147 lines 6.92 kB
import { sign } from 'hono/jwt'; import * as z from 'zod/mini'; import { from } from '../../Handler.js'; import * as Hono from '../hono.js'; const defaults = { claimsSupported: ['iss', 'aud', 'sub', 'iat', 'exp', 'nonce'], kid: 'oidc-1', path: '/', ttl: 5 * 60, // 5 minutes }; /** Zod schemas for the OIDC provider's request and response payloads. */ export var schema; (function (schema) { /** Schemas for `POST {path}/token`. */ let token; (function (token) { /** Request body schema. */ token.parameters = z.object({ /** Audience the token is minted for — the requesting app's origin. */ audience: z.string(), /** One-time value bound into the token (OIDC `nonce`). */ nonce: z.optional(z.string()), /** * Subject the token is minted for. Ignored when an `authenticate` * callback is configured (the resolved subject wins); required * otherwise. */ subject: z.optional(z.string()), }); /** Response body schema. */ token.returns = z.object({ idToken: z.string(), }); })(token = schema.token || (schema.token = {})); })(schema || (schema = {})); /** * OpenID Connect provider (issuer) request handler. Mounts three routes * under `path`: * * - `POST {path}/token` → mints + signs an EdDSA id_token, returns `{ idToken }` * - `GET {path}/.well-known/openid-configuration` → OIDC discovery document * - `GET {path}/.well-known/jwks.json` → public signing keys (JWKS) * * The handler owns the OIDC protocol mechanics (claim set, EdDSA signing, * discovery + JWKS shape); a deployment supplies its key material and the * claim source via callbacks, so nothing here is deployment-specific: * * - `authenticate(request)` resolves the authenticated subject (e.g. mapping * a session cookie to an account address). When omitted, the request body's * `subject` is trusted instead — only safe behind an authenticating gateway. * - `getClaims({ subject, audience, nonce, request })` returns the claim set * embedded into the token (e.g. `{ email, email_verified: true }`). Throwing * rejects issuance (e.g. no verified email) with `400`. * * Tokens carry `iss`, `aud`, `sub`, `iat`, `exp`, an optional `nonce`, and any * claims returned by `getClaims`. They are signed EdDSA (Ed25519) with the * provided `signingKey`; the public counterpart is served at the JWKS route so * relying parties can verify them. */ export function oidcProvider(options) { const { authenticate, claimsSupported = defaults.claimsSupported, getClaims, issuer, jwksUri, kid = defaults.kid, path = defaults.path, publicKey, signingKey, ttl = defaults.ttl, ...rest } = options; const key = parseJwk(signingKey); const jwk = { ...parseJwk(publicKey), kid, use: 'sig', alg: 'EdDSA' }; const tokenPath = path === '/' ? '/token' : `${path}/token`; const discoveryPath = path === '/' ? '/.well-known/openid-configuration' : `${path}/.well-known/openid-configuration`; const jwksPath = path === '/' ? '/.well-known/jwks.json' : `${path}/.well-known/jwks.json`; // The advertised `jwks_uri` is issuer-relative (OIDC serves discovery at // `{issuer}/.well-known/openid-configuration`), not origin + routing path — // those differ when `issuer` is path-based (e.g. `{origin}/api/oidc`). const resolvedJwksUri = jwksUri ?? `${trimTrailingSlash(issuer)}/.well-known/jwks.json`; const router = from(rest); router.post(tokenPath, Hono.validate('json', schema.token.parameters), async (c) => { const { audience, nonce, subject: subject_body } = c.req.valid('json'); // Resolve the subject the token is minted for. An `authenticate` callback // (e.g. session cookie → address) always wins so the server never trusts a // client-supplied subject when it can prove one itself. const subject = await (async () => { try { const resolved = authenticate ? await authenticate(c.req.raw) : (subject_body ?? ''); if (!resolved) return c.json({ error: 'missing subject' }, 400); return resolved; } catch (error) { return c.json({ error: error instanceof Error ? error.message : 'unauthenticated' }, 401); } })(); if (subject instanceof Response) return subject; // Resolve the claim set (e.g. verified email). Throwing here rejects // issuance — the deployment owns the policy (e.g. `email_verified`). const claims = await (async () => { try { return await getClaims({ audience, nonce, request: c.req.raw, subject }); } catch (error) { return c.json({ error: error instanceof Error ? error.message : 'claims unavailable' }, 400); } })(); if (claims instanceof Response) return claims; const now = Math.floor(Date.now() / 1000); const payload = { iss: issuer, aud: audience, sub: subject, iat: now, exp: now + ttl, ...(nonce ? { nonce } : {}), ...claims, }; const idToken = await sign(payload, key, 'EdDSA'); return c.json(z.encode(schema.token.returns, { idToken })); }); // Verify-only discovery: this is a verifiable ID-token issuer (JWKS + JWT), // not a full interactive OIDC OP. Apps integrate by verifying the token (the // `jwks_uri` is the field that matters), not by running a redirect login — so // `authorization_endpoint`/`token_endpoint` are intentionally omitted. The // shape otherwise mirrors workload-identity issuers (GitHub Actions, GitLab // CI, Kubernetes), which advertise `response_types_supported: ["id_token"]` // + JWKS only. router.get(discoveryPath, (c) => c.json({ issuer, jwks_uri: resolvedJwksUri, response_types_supported: ['id_token'], subject_types_supported: ['public'], id_token_signing_alg_values_supported: ['EdDSA'], claims_supported: claimsSupported, })); router.get(jwksPath, (c) => c.json({ keys: [jwk] }, { headers: { 'Cache-Control': 'public, max-age=31536000, immutable' } })); return router; } /** * Parse a JWK string, normalizing the `Ed25519` algorithm name to the JWT * `EdDSA` value expected by `hono/jwt`. */ function parseJwk(jwk) { const parsed = JSON.parse(jwk); if (parsed.alg === 'Ed25519') parsed.alg = 'EdDSA'; return parsed; } /** Strip a single trailing slash so URL joins don't double up. */ function trimTrailingSlash(value) { return value.endsWith('/') ? value.slice(0, -1) : value; } //# sourceMappingURL=oidcProvider.js.map