accounts
Version:
Tempo Accounts SDK
147 lines • 6.92 kB
JavaScript
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