@tillstack/auth-node 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -0
- package/dist/index.cjs +73 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +55 -1
- package/dist/index.d.ts +55 -1
- package/dist/index.js +70 -4
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -87,6 +87,38 @@ instead of letting the SDK fetch them. A rotation publishes the new key 15
|
|
|
87
87
|
minutes before it signs and the previous key verifies for 24 hours after, so
|
|
88
88
|
update the inline keys when you rotate.
|
|
89
89
|
|
|
90
|
+
### Internal apps
|
|
91
|
+
|
|
92
|
+
When a TillAuth internal app sits in front of your tool, the proxy forwards the
|
|
93
|
+
person's identity as `X-TillDev-Email`, `X-TillDev-Role` and friends, plus
|
|
94
|
+
`X-TillDev-Assertion`, a token signed for that internal app and that host only.
|
|
95
|
+
Verify the assertion instead of trusting the plain headers, so a request that
|
|
96
|
+
skipped the proxy can't claim to be someone:
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
import { createAccessVerifier, AccessAssertionError } from '@tillstack/auth-node'
|
|
100
|
+
|
|
101
|
+
const access = createAccessVerifier({
|
|
102
|
+
issuer: process.env.TILLDEV_ACCESS_ISSUER!, // shown on the internal app's page
|
|
103
|
+
audience: process.env.TILLDEV_ACCESS_AUDIENCE!, // its taac_… id
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
app.use(async (req, res, next) => {
|
|
107
|
+
try {
|
|
108
|
+
req.user = await access.verifyRequest(req.headers) // email, role, groups, mfa, auth_time, sub
|
|
109
|
+
next()
|
|
110
|
+
} catch (e) {
|
|
111
|
+
if (e instanceof AccessAssertionError) return res.status(401).send(e.code)
|
|
112
|
+
next(e)
|
|
113
|
+
}
|
|
114
|
+
})
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
`verifyRequest` takes Node, Express or Fastify headers, or a fetch `Headers`, and
|
|
118
|
+
refuses an assertion issued for another host. It lasts 5 minutes and is reused
|
|
119
|
+
across that person's requests for up to 4 of them, so it always arrives with a
|
|
120
|
+
minute or more left; it is not a one-time token.
|
|
121
|
+
|
|
90
122
|
## Docs
|
|
91
123
|
|
|
92
124
|
Full documentation: https://tilldev.dev/docs
|
package/dist/index.cjs
CHANGED
|
@@ -124,10 +124,10 @@ function expressCookieProxy(opts) {
|
|
|
124
124
|
res.json(JSON.parse(out.body));
|
|
125
125
|
};
|
|
126
126
|
}
|
|
127
|
-
function parseCookieHeader(
|
|
127
|
+
function parseCookieHeader(header2) {
|
|
128
128
|
const out = {};
|
|
129
|
-
if (!
|
|
130
|
-
for (const part of
|
|
129
|
+
if (!header2) return out;
|
|
130
|
+
for (const part of header2.split(";")) {
|
|
131
131
|
const [k, ...rest] = part.trim().split("=");
|
|
132
132
|
if (!k) continue;
|
|
133
133
|
out[k] = decodeURIComponent(rest.join("="));
|
|
@@ -180,6 +180,72 @@ async function verifyWebhook(rawBody, signatureHeader, secret, opts = {}) {
|
|
|
180
180
|
throw new TillAuthWebhookError("bad_payload");
|
|
181
181
|
}
|
|
182
182
|
}
|
|
183
|
+
var ACCESS_ASSERTION_HEADER = "x-tilldev-assertion";
|
|
184
|
+
var ASSERTION_TYP = "tilldev-access+jwt";
|
|
185
|
+
var AccessAssertionError = class extends Error {
|
|
186
|
+
constructor(message, code) {
|
|
187
|
+
super(message);
|
|
188
|
+
this.name = "AccessAssertionError";
|
|
189
|
+
this.code = code;
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
function header(headers, name) {
|
|
193
|
+
if (typeof headers.get === "function") return headers.get(name) ?? void 0;
|
|
194
|
+
const v = headers[name];
|
|
195
|
+
return Array.isArray(v) ? v[0] : v;
|
|
196
|
+
}
|
|
197
|
+
var AccessVerifier = class {
|
|
198
|
+
constructor(opts) {
|
|
199
|
+
if (!opts.issuer || !opts.audience) throw new Error("AccessVerifier: issuer and audience are required");
|
|
200
|
+
this.issuer = opts.issuer.replace(/\/+$/, "");
|
|
201
|
+
this.audience = opts.audience;
|
|
202
|
+
this.tolerance = opts.clockToleranceSec ?? 30;
|
|
203
|
+
this.keys = opts.publicJwks?.length ? Promise.all(opts.publicJwks.map((k) => jose.importJWK(k, "EdDSA"))) : jose.createRemoteJWKSet(new URL(opts.jwksUri ?? `${this.issuer}/.well-known/jwks.json`));
|
|
204
|
+
}
|
|
205
|
+
/** Verifies the signed identity the proxy forwarded. Pass `host` to refuse one checked for another host. */
|
|
206
|
+
async verify(token, expect = {}) {
|
|
207
|
+
if (!token) throw new AccessAssertionError("No identity assertion; is the request coming through the proxy?", "no_assertion");
|
|
208
|
+
const options = { algorithms: ["EdDSA"], issuer: this.issuer, audience: this.audience, typ: ASSERTION_TYP, clockTolerance: this.tolerance, requiredClaims: ["sub", "exp", "iat"] };
|
|
209
|
+
let payload;
|
|
210
|
+
try {
|
|
211
|
+
if (this.keys instanceof Promise) {
|
|
212
|
+
let last;
|
|
213
|
+
payload = {};
|
|
214
|
+
let ok = false;
|
|
215
|
+
for (const key of await this.keys) {
|
|
216
|
+
try {
|
|
217
|
+
payload = (await jose.jwtVerify(token, key, options)).payload;
|
|
218
|
+
ok = true;
|
|
219
|
+
break;
|
|
220
|
+
} catch (e) {
|
|
221
|
+
last = e;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (!ok) throw last ?? new Error("No key matched");
|
|
225
|
+
} else {
|
|
226
|
+
payload = (await jose.jwtVerify(token, this.keys, options)).payload;
|
|
227
|
+
}
|
|
228
|
+
} catch (e) {
|
|
229
|
+
throw new AccessAssertionError(e instanceof Error ? e.message : "Invalid assertion", "invalid_assertion");
|
|
230
|
+
}
|
|
231
|
+
if (typeof payload["email"] !== "string" || typeof payload["host"] !== "string" || payload["access_app"] !== this.audience) {
|
|
232
|
+
throw new AccessAssertionError("Assertion is missing its identity", "invalid_assertion");
|
|
233
|
+
}
|
|
234
|
+
if (expect.host !== void 0 && payload["host"] !== expect.host.toLowerCase().replace(/:\d+$/, "")) {
|
|
235
|
+
throw new AccessAssertionError("Assertion was issued for another host", "wrong_host");
|
|
236
|
+
}
|
|
237
|
+
const groups = Array.isArray(payload["groups"]) ? payload["groups"].filter((g) => typeof g === "string") : [];
|
|
238
|
+
return { ...payload, groups };
|
|
239
|
+
}
|
|
240
|
+
/** Reads the assertion and host from a request's headers (Node, Express, Fastify or fetch `Headers`). */
|
|
241
|
+
verifyRequest(headers) {
|
|
242
|
+
const host = header(headers, "host");
|
|
243
|
+
return this.verify(header(headers, ACCESS_ASSERTION_HEADER), host ? { host } : {});
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
function createAccessVerifier(opts) {
|
|
247
|
+
return new AccessVerifier(opts);
|
|
248
|
+
}
|
|
183
249
|
|
|
184
250
|
// src/index.ts
|
|
185
251
|
var DEFAULT_API2 = "https://auth.tilldev.dev";
|
|
@@ -343,9 +409,13 @@ function createVerifier(opts) {
|
|
|
343
409
|
return new TillAuthVerifier(opts);
|
|
344
410
|
}
|
|
345
411
|
|
|
412
|
+
exports.ACCESS_ASSERTION_HEADER = ACCESS_ASSERTION_HEADER;
|
|
413
|
+
exports.AccessAssertionError = AccessAssertionError;
|
|
414
|
+
exports.AccessVerifier = AccessVerifier;
|
|
346
415
|
exports.TillAuthVerifier = TillAuthVerifier;
|
|
347
416
|
exports.TillAuthVerifyError = TillAuthVerifyError;
|
|
348
417
|
exports.TillAuthWebhookError = TillAuthWebhookError;
|
|
418
|
+
exports.createAccessVerifier = createAccessVerifier;
|
|
349
419
|
exports.createFetchHandler = createFetchHandler;
|
|
350
420
|
exports.createVerifier = createVerifier;
|
|
351
421
|
exports.expressCookieProxy = expressCookieProxy;
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/cookie-proxy.ts","../src/webhooks.ts","../src/index.ts"],"names":["DEFAULT_API","importJWK","createRemoteJWKSet","payload","jwtVerify","decodeJwt"],"mappings":"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","file":"index.cjs","sourcesContent":["// Cookie-mode proxy for TillAuth.\n//\n// Mount this on the customer's backend (typically at /api/auth/*). It\n// forwards JSON bodies to TillAuth's /v1/* endpoints, then translates\n// access_token + refresh_token in the response into HttpOnly cookies on\n// the customer's domain. The browser's localStorage never sees the\n// tokens — XSS can't exfiltrate them.\n//\n// Two adapters: a generic web-fetch handler (Cloudflare Workers, Next.js\n// Route Handlers, Bun, Deno) and an Express middleware. Pick one.\n\nconst DEFAULT_API = 'https://auth.tilldev.dev'\n\nexport interface CookieProxyOptions {\n /** TillAuth public app id (`tau_…`). */\n appId: string\n /** Override the TillAuth API base. */\n apiBase?: string\n /** Cookie name for the access token. Default `tau_at`. */\n accessCookie?: string\n /** Cookie name for the refresh token. Default `tau_rt`. */\n refreshCookie?: string\n /** Cookie path. Default '/'. */\n cookiePath?: string\n /** Cookie domain (e.g. `.example.com` to share across subdomains). */\n cookieDomain?: string\n /** Secure flag. Defaults to true in production, false otherwise. */\n secure?: boolean\n}\n\ninterface NormalizedRequest {\n method: string\n pathname: string // path relative to the mount point (`/signin`, `/refresh`)\n body: string\n cookies: Record<string, string>\n}\ninterface NormalizedResponse {\n status: number\n body: string\n setCookies: string[]\n headers: Record<string, string>\n}\n\ninterface AuthBody {\n access_token?: string\n refresh_token?: string\n refresh_expires_at?: string\n [key: string]: unknown\n}\n\nfunction resolve(opts: CookieProxyOptions): Required<CookieProxyOptions> {\n return {\n appId: opts.appId,\n apiBase: (opts.apiBase ?? DEFAULT_API).replace(/\\/$/, ''),\n accessCookie: opts.accessCookie ?? 'tau_at',\n refreshCookie: opts.refreshCookie ?? 'tau_rt',\n cookiePath: opts.cookiePath ?? '/',\n cookieDomain: opts.cookieDomain ?? '',\n secure: opts.secure ?? process.env.NODE_ENV === 'production',\n }\n}\n\nfunction makeCookie(\n name: string,\n value: string,\n o: Required<CookieProxyOptions>,\n opts: { maxAge?: number; clear?: boolean } = {},\n): string {\n const parts = [`${name}=${opts.clear ? '' : value}`]\n parts.push('HttpOnly')\n if (o.secure) parts.push('Secure')\n parts.push('SameSite=Lax')\n parts.push(`Path=${o.cookiePath}`)\n if (o.cookieDomain) parts.push(`Domain=${o.cookieDomain}`)\n if (opts.clear) parts.push('Max-Age=0')\n else if (opts.maxAge !== undefined) parts.push(`Max-Age=${opts.maxAge}`)\n return parts.join('; ')\n}\n\nasync function handleProxy(req: NormalizedRequest, raw: CookieProxyOptions): Promise<NormalizedResponse> {\n const opts = resolve(raw)\n const path = req.pathname.replace(/^\\/+/, '')\n\n // Map proxy paths → TillAuth paths.\n const url = `${opts.apiBase}/v1/${path}`\n\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n 'X-TillAuth-App': opts.appId,\n 'User-Agent': 'tilldev-auth-node/cookie-proxy',\n }\n\n // /me, /refresh, /signout need to carry tokens from cookies, not the body.\n let body = req.body\n if (path === 'me' && req.cookies[opts.accessCookie]) {\n headers.Authorization = `Bearer ${req.cookies[opts.accessCookie]}`\n }\n if (path === 'refresh') {\n const rt = req.cookies[opts.refreshCookie]\n if (!rt) {\n return {\n status: 401,\n body: JSON.stringify({ error: 'no refresh cookie' }),\n setCookies: [],\n headers: { 'Content-Type': 'application/json' },\n }\n }\n body = JSON.stringify({ refresh_token: rt })\n }\n if (path === 'signout') {\n const rt = req.cookies[opts.refreshCookie]\n body = JSON.stringify({ refresh_token: rt ?? '' })\n }\n\n const upstream = await fetch(url, { method: req.method, headers, body })\n const text = await upstream.text()\n let json: AuthBody = {}\n try {\n json = JSON.parse(text) as AuthBody\n } catch {\n json = {}\n }\n\n const setCookies: string[] = []\n // Always strip token fields before returning to the SPA.\n const out = { ...json }\n\n if (typeof json.access_token === 'string') {\n setCookies.push(makeCookie(opts.accessCookie, json.access_token, opts, { maxAge: 60 * 10 }))\n delete out.access_token\n }\n if (typeof json.refresh_token === 'string') {\n // 30d in line with the Fastify REFRESH_TTL_MS.\n setCookies.push(makeCookie(opts.refreshCookie, json.refresh_token, opts, { maxAge: 60 * 60 * 24 * 30 }))\n delete out.refresh_token\n delete out.refresh_expires_at\n }\n if (path === 'signout') {\n setCookies.push(makeCookie(opts.accessCookie, '', opts, { clear: true }))\n setCookies.push(makeCookie(opts.refreshCookie, '', opts, { clear: true }))\n }\n\n return {\n status: upstream.status,\n body: JSON.stringify(out),\n setCookies,\n headers: { 'Content-Type': 'application/json' },\n }\n}\n\n// ── Adapter 1: web-fetch (Next.js Route Handlers, Workers, Bun, Deno) ───────\n\nexport function createFetchHandler(opts: CookieProxyOptions) {\n return async function handler(req: Request, ctx: { params?: { path?: string | string[] } } = {}): Promise<Response> {\n const url = new URL(req.url)\n // Derive the proxy-relative path. Prefer ctx.params.path (Next.js catch-all);\n // fall back to the part of the URL after the mount point heuristic.\n let pathname: string\n if (ctx.params?.path) {\n pathname = Array.isArray(ctx.params.path) ? `/${ctx.params.path.join('/')}` : `/${ctx.params.path}`\n } else {\n pathname = url.pathname.replace(/^\\/api\\/auth/, '')\n }\n const cookies = parseCookieHeader(req.headers.get('cookie'))\n const body = req.method === 'POST' ? await req.text() : '{}'\n const out = await handleProxy({ method: req.method, pathname, body, cookies }, opts)\n const headers = new Headers(out.headers)\n for (const c of out.setCookies) headers.append('Set-Cookie', c)\n return new Response(out.body, { status: out.status, headers })\n }\n}\n\n// ── Adapter 2: Express ──────────────────────────────────────────────────────\n\ninterface ExpReq {\n method: string\n url?: string\n originalUrl?: string\n path?: string\n body?: unknown\n headers: Record<string, string | string[] | undefined>\n}\ninterface ExpRes {\n status(n: number): ExpRes\n setHeader(name: string, value: string | string[]): void\n json(body: unknown): void\n}\n\nexport function expressCookieProxy(opts: CookieProxyOptions) {\n return async function (req: ExpReq, res: ExpRes): Promise<void> {\n const path =\n req.path ?? (req.originalUrl ?? req.url ?? '/').replace(/^\\/api\\/auth/, '').split('?')[0] ?? '/'\n const cookieHeader =\n typeof req.headers.cookie === 'string' ? req.headers.cookie : Array.isArray(req.headers.cookie) ? req.headers.cookie[0] : ''\n const body =\n typeof req.body === 'string' ? req.body : req.body ? JSON.stringify(req.body) : '{}'\n const out = await handleProxy(\n {\n method: req.method,\n pathname: path,\n body,\n cookies: parseCookieHeader(cookieHeader ?? null),\n },\n opts,\n )\n res.status(out.status)\n if (out.setCookies.length > 0) res.setHeader('Set-Cookie', out.setCookies)\n for (const [k, v] of Object.entries(out.headers)) res.setHeader(k, v)\n res.json(JSON.parse(out.body))\n }\n}\n\nfunction parseCookieHeader(header: string | null): Record<string, string> {\n const out: Record<string, string> = {}\n if (!header) return out\n for (const part of header.split(';')) {\n const [k, ...rest] = part.trim().split('=')\n if (!k) continue\n out[k] = decodeURIComponent(rest.join('='))\n }\n return out\n}\n","// Verifies TillAuth webhook deliveries (header `X-TillAuth-Signature: t=<unix>,v1=<hex>`).\n\nexport interface TillAuthWebhookEvent {\n type: string\n result: string\n ts: string\n app_id: string\n user_id: string | null\n ip: string | null\n user_agent: string | null\n metadata: Record<string, unknown>\n delivery_id: string\n attempt: number\n}\n\nexport type WebhookErrorReason = 'missing_signature' | 'malformed_signature' | 'stale' | 'bad_signature' | 'bad_payload'\n\nexport class TillAuthWebhookError extends Error {\n constructor(readonly reason: WebhookErrorReason) {\n super(`TillAuth webhook rejected: ${reason}`)\n this.name = 'TillAuthWebhookError'\n }\n}\n\nexport interface VerifyWebhookOptions {\n /** Max age of the signed timestamp, in seconds. Default 300. */\n toleranceSeconds?: number\n /** Unix seconds; for tests. */\n now?: number\n}\n\nconst enc = new TextEncoder()\n\nfunction hexBytes(hex: string): Uint8Array | null {\n if (hex.length === 0 || hex.length % 2 !== 0 || !/^[0-9a-f]+$/i.test(hex)) return null\n const out = new Uint8Array(hex.length / 2)\n for (let i = 0; i < out.length; i++) out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16)\n return out\n}\n\n/**\n * Checks the signature over the exact bytes received, then parses them.\n * Pass the raw body, never a re-serialized object.\n */\nexport async function verifyWebhook(\n rawBody: string | Uint8Array,\n signatureHeader: string | null | undefined,\n secret: string,\n opts: VerifyWebhookOptions = {},\n): Promise<TillAuthWebhookEvent> {\n if (!signatureHeader) throw new TillAuthWebhookError('missing_signature')\n\n let t: number | null = null\n const sigs: Uint8Array[] = []\n for (const part of signatureHeader.split(',')) {\n const [k, v] = part.trim().split('=', 2)\n if (k === 't' && v && /^\\d+$/.test(v)) t = Number(v)\n else if (k === 'v1' && v) {\n const b = hexBytes(v)\n if (b) sigs.push(b)\n }\n }\n if (t === null || sigs.length === 0) throw new TillAuthWebhookError('malformed_signature')\n\n const now = opts.now ?? Math.floor(Date.now() / 1000)\n if (Math.abs(now - t) > (opts.toleranceSeconds ?? 300)) throw new TillAuthWebhookError('stale')\n\n const body = typeof rawBody === 'string' ? enc.encode(rawBody) : rawBody\n const prefix = enc.encode(`${t}.`)\n const signed = new Uint8Array(prefix.length + body.length)\n signed.set(prefix)\n signed.set(body, prefix.length)\n\n const key = await crypto.subtle.importKey('raw', enc.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['verify'])\n let ok = false\n for (const s of sigs) if (await crypto.subtle.verify('HMAC', key, s, signed)) ok = true\n if (!ok) throw new TillAuthWebhookError('bad_signature')\n\n try {\n return JSON.parse(new TextDecoder().decode(body)) as TillAuthWebhookEvent\n } catch {\n throw new TillAuthWebhookError('bad_payload')\n }\n}\n","import { jwtVerify, createRemoteJWKSet, importJWK, decodeJwt, type JWK } from 'jose'\n\n// Server-side TillAuth verification.\n//\n// You configure once with the issuer + JWKS endpoint (or inline public keys),\n// then call `verify(token)` or use the framework-specific middleware.\n//\n// Verification is offline against the cached JWKS — no round-trip to TillAuth\n// on the hot path.\n\nconst DEFAULT_API = 'https://auth.tilldev.dev'\nconst REALM_SEGMENT = '/realms/'\n\nfunction trimSlash(s: string): string {\n return s.replace(/\\/+$/, '')\n}\n\n/**\n * Issuer acceptance check.\n *\n * A token this verifier may accept was minted for THIS app, so exactly two issuers\n * are legitimate: the canonical one (the app has no dedicated realm keys, so the\n * shared plane key signed it) or THIS app's own realm issuer. Nothing else.\n *\n * ⚠️ THIS USED TO ACCEPT ANY `{canonical}/realms/<anything>` when no issuer was\n * pinned. Together with a shared JWKS carrying every realm's key, that was a\n * cross-realm forgery (`atrium` `00` D13): holding any one realm's signing key was\n * enough to mint a token naming any OTHER realm. The signature verified (the key was\n * in the merged JWKS), the issuer passed this prefix test, and the `app` check\n * passed too — because the attacker wrote that claim as well.\n *\n * Note the `issuer` option was NOT the mitigation it looked like: pinning it\n * constrained the *claim* while the *key* still came from the merged set.\n *\n * The bare legacy `\"tillauth\"` string was retired 2026-07-20 and is not accepted.\n */\nfunction issuerAccepted(\n iss: unknown,\n canonical: string,\n realm: unknown,\n pinnedIssuer?: string,\n): boolean {\n if (typeof iss !== 'string' || iss.length === 0) return false\n if (pinnedIssuer) return iss === trimSlash(pinnedIssuer)\n const canon = trimSlash(canonical)\n return iss === canon || (typeof realm === 'string' && iss === `${canon}${REALM_SEGMENT}${realm}`)\n}\n\nexport interface VerifyOptions {\n /** The app's public id (`tau_…`), publishable key (`tau_pub_…`) or app id. Tokens for any other app are refused. */\n appId: string\n /** TillAuth API base. Defaults to https://auth.tilldev.dev */\n apiBase?: string\n /**\n * Pin verification to exactly one issuer. Once your app signs with its own key,\n * pin its issuer (`https://auth.tilldev.dev/realms/<app id>`) to refuse anything\n * signed with the shared key.\n *\n * When omitted, the verifier accepts the canonical HTTPS issuer (derived from\n * `apiBase`) and THIS app's own issuer — never another app's.\n */\n issuer?: string\n /**\n * Inline JWKS as a JWK array (the same `publicJwk` entries you'd find in\n * the TILLAUTH_JWT_KEYS env). Use this in environments where you want\n * fully air-gapped verification.\n */\n publicJwks?: JWK[]\n /** Require email_verified=true. Defaults to false. */\n requireVerifiedEmail?: boolean\n /** Require MFA was satisfied this session. Defaults to false. */\n requireMfa?: boolean\n}\n\nexport interface AuthClaims {\n /** User id */\n sub: string\n /** App id the token was issued for */\n app: string\n /** The app's public id. Absent on tokens minted before it existed. */\n pid?: string\n /** Lowercased email */\n email: string\n /** Whether MFA was satisfied this session */\n mfa: boolean\n /**\n * Whether the account's email was verified at issue time. Optional: tokens\n * minted before this claim existed omit it. When `requireVerifiedEmail` is\n * set, an absent claim is treated as unverified (fail-closed on the check).\n */\n email_verified?: boolean\n /** Token issuer — validated against the accepted set (see `issuerAccepted`). */\n iss?: string\n /** Issued-at (seconds) */\n iat: number\n /** Expires-at (seconds) */\n exp: number\n}\n\nexport class TillAuthVerifyError extends Error {\n code: 'no_token' | 'invalid_token' | 'wrong_app' | 'mfa_required' | 'email_not_verified' | 'invalid_issuer'\n constructor(message: string, code: TillAuthVerifyError['code']) {\n super(message)\n this.name = 'TillAuthVerifyError'\n this.code = code\n }\n}\n\nexport class TillAuthVerifier {\n private readonly opts: Required<Pick<VerifyOptions, 'appId' | 'apiBase' | 'requireVerifiedEmail' | 'requireMfa'>>\n private readonly pinnedIssuer: string | undefined\n private readonly remote: ReturnType<typeof createRemoteJWKSet> | null\n /** This app's own realm key set — see `keySetFor`. Null on the inline-keys path. */\n private readonly remoteRealm: ReturnType<typeof createRemoteJWKSet> | null = null\n private readonly inlineKeys: Promise<Array<{ kid?: string; key: Awaited<ReturnType<typeof importJWK>> }>> | null\n\n constructor(opts: VerifyOptions) {\n if (!opts.appId) throw new Error('TillAuthVerifier: appId is required')\n this.opts = {\n appId: opts.appId.startsWith('tau_pub_') ? 'tau_' + opts.appId.slice('tau_pub_'.length) : opts.appId,\n apiBase: (opts.apiBase ?? DEFAULT_API).replace(/\\/$/, ''),\n requireVerifiedEmail: opts.requireVerifiedEmail ?? false,\n requireMfa: opts.requireMfa ?? false,\n }\n this.pinnedIssuer = opts.issuer ? opts.issuer.replace(/\\/$/, '') : undefined\n if (opts.publicJwks && opts.publicJwks.length > 0) {\n this.remote = null\n this.inlineKeys = Promise.all(\n opts.publicJwks.map(async (k) => ({ kid: k.kid, key: await importJWK(k, 'EdDSA') })),\n )\n } else {\n /*\n * TWO key sets, selected by the token's issuer (`atrium` `00` D13 F4).\n *\n * `remote` is the shared plane's set — after the D13 F6 change it holds ONLY\n * the global signing keys, never a realm's. `remoteRealm` is this app's own\n * realm set, which holds only this realm's keys and 404s if the app has no\n * dedicated keys.\n *\n * Because neither set can contain ANOTHER realm's key, a forged token naming a\n * different realm cannot be verified from either — the isolation is structural\n * rather than a claim check. This replaced a single `createRemoteJWKSet` on the\n * merged JWKS, where every realm's key was resolvable by `kid` and the realm\n * checks below were therefore decorative.\n */\n this.remote = createRemoteJWKSet(new URL(`${this.opts.apiBase}/.well-known/jwks.json`))\n this.remoteRealm = createRemoteJWKSet(\n new URL(`${this.opts.apiBase}${REALM_SEGMENT}${this.opts.appId}/.well-known/jwks.json`),\n )\n this.inlineKeys = null\n }\n }\n\n /**\n * The key set entitled to have signed a token carrying `iss`, or `null` if no set\n * is. Selecting on the UNVERIFIED issuer is safe because the choice is then\n * enforced: `jwtVerify` pins `issuer` to the same value, so a token that lies about\n * its issuer to reach a different key set must still verify against that set — and\n * that set contains only keys entitled to the issuer it named.\n */\n private keySetFor(payload: { iss?: unknown; app?: unknown; pid?: unknown }): ReturnType<typeof createRemoteJWKSet> | null {\n if (typeof payload.iss !== 'string') return null\n if (payload.iss === this.opts.apiBase) return this.remote\n // The app's own key set is fetched by the configured app, so it holds only this app's keys.\n if (\n this.matchesApp(payload) &&\n typeof payload.app === 'string' &&\n payload.iss === `${this.opts.apiBase}${REALM_SEGMENT}${payload.app}`\n ) {\n return this.remoteRealm\n }\n return null\n }\n\n private matchesApp(claims: { app?: unknown; pid?: unknown }): boolean {\n return claims.app === this.opts.appId || claims.pid === this.opts.appId\n }\n\n async verify(token: string): Promise<AuthClaims> {\n if (!token) throw new TillAuthVerifyError('No token', 'no_token')\n\n let claims: AuthClaims\n try {\n if (this.inlineKeys) {\n const keys = await this.inlineKeys\n let lastErr: unknown\n for (const k of keys) {\n try {\n // No jose `issuer` pin — the issuer is one of several accepted values\n // (legacy string / canonical HTTPS / per-realm), checked below.\n const { payload } = await jwtVerify(token, k.key, { algorithms: ['EdDSA'] })\n claims = payload as unknown as AuthClaims\n return this.applyClaimsChecks(claims)\n } catch (e) {\n lastErr = e\n }\n }\n throw lastErr ?? new Error('No key matched')\n }\n /*\n * Pick the key set from the token's own issuer, then PIN that issuer during\n * verification. The pin is what makes reading an unverified claim safe: it\n * cannot be used to reach a key set and then disown the issuer that selected\n * it. A token naming a realm this verifier does not serve gets no key set at\n * all and is refused before any crypto runs.\n */\n const unverified = decodeJwt(token)\n const keySet = this.keySetFor(unverified)\n if (!keySet) {\n throw new TillAuthVerifyError('Untrusted token issuer', 'invalid_issuer')\n }\n const { payload } = await jwtVerify(token, keySet, {\n algorithms: ['EdDSA'],\n issuer: unverified.iss as string,\n })\n claims = payload as unknown as AuthClaims\n } catch (e) {\n // Preserve a specific claims verdict (wrong_app / invalid_issuer) thrown by\n // applyClaimsChecks on the inline path; only a genuine signature/JWKS\n // failure becomes the generic invalid_token.\n if (e instanceof TillAuthVerifyError) throw e\n throw new TillAuthVerifyError(\n e instanceof Error ? e.message : 'Invalid token',\n 'invalid_token',\n )\n }\n return this.applyClaimsChecks(claims)\n }\n\n private applyClaimsChecks(claims: AuthClaims): AuthClaims {\n // Issuer is validated here (jose's exact `issuer` pin is intentionally not\n // used) so per-realm HTTPS issuers verify alongside the canonical one.\n if (!this.matchesApp(claims)) {\n throw new TillAuthVerifyError(`Token issued for different app`, 'wrong_app')\n }\n if (!issuerAccepted(claims.iss, this.opts.apiBase, claims.app, this.pinnedIssuer)) {\n throw new TillAuthVerifyError('Untrusted token issuer', 'invalid_issuer')\n }\n if (this.opts.requireMfa && !claims.mfa) {\n throw new TillAuthVerifyError('MFA required', 'mfa_required')\n }\n // Absent claim (older tokens) is treated as unverified only when the check\n // is enabled — so requireVerifiedEmail fails closed rather than silently\n // passing when the issuer predates the claim.\n if (this.opts.requireVerifiedEmail && claims.email_verified !== true) {\n throw new TillAuthVerifyError('Email not verified', 'email_not_verified')\n }\n return claims\n }\n\n /**\n * Pull a Bearer token out of an Authorization header and verify. Returns\n * the claims or throws TillAuthVerifyError.\n */\n async verifyHeader(authHeader: string | undefined | null): Promise<AuthClaims> {\n if (!authHeader || !authHeader.startsWith('Bearer ')) {\n throw new TillAuthVerifyError('Missing Bearer token', 'no_token')\n }\n return this.verify(authHeader.slice(7))\n }\n}\n\n// ── Framework adapters ──────────────────────────────────────────────────────\n\n// Express-style middleware. Adds `req.user` on success.\nexport function expressMiddleware(verifier: TillAuthVerifier, opts: { onError?: (err: TillAuthVerifyError) => unknown } = {}) {\n return async function (req: ExpressLikeReq, res: ExpressLikeRes, next: () => void) {\n try {\n const h = req.headers?.authorization\n const claims = await verifier.verifyHeader(Array.isArray(h) ? h[0] : h)\n ;(req as ExpressLikeReq & { user: AuthClaims }).user = claims\n next()\n } catch (e) {\n const err = e instanceof TillAuthVerifyError ? e : new TillAuthVerifyError('Auth failed', 'invalid_token')\n if (opts.onError) {\n const out = opts.onError(err)\n if (out !== undefined) {\n res.status(401).json(out as Record<string, unknown>)\n return\n }\n }\n res.status(401).json({ error: err.message, code: err.code })\n }\n }\n}\n\ninterface ExpressLikeReq {\n headers?: { authorization?: string | string[] }\n}\ninterface ExpressLikeRes {\n status: (n: number) => { json: (b: Record<string, unknown>) => void }\n}\n\n// Fastify plugin shape — not bound to Fastify's types to avoid a peer dep.\nexport function fastifyHook(verifier: TillAuthVerifier) {\n return async function (\n req: { headers: Record<string, string | string[] | undefined>; user?: AuthClaims },\n reply: { code: (n: number) => { send: (b: unknown) => void } },\n ) {\n try {\n const h = req.headers['authorization']\n const auth = Array.isArray(h) ? h[0] : h\n const claims = await verifier.verifyHeader(auth)\n req.user = claims\n } catch (e) {\n const err = e instanceof TillAuthVerifyError ? e : new TillAuthVerifyError('Auth failed', 'invalid_token')\n reply.code(401).send({ error: err.message, code: err.code })\n }\n }\n}\n\n// Convenience entrypoint for users who want a one-call setup.\nexport function createVerifier(opts: VerifyOptions): TillAuthVerifier {\n return new TillAuthVerifier(opts)\n}\n\nexport { createFetchHandler, expressCookieProxy } from './cookie-proxy.js'\nexport type { CookieProxyOptions } from './cookie-proxy.js'\nexport { verifyWebhook, TillAuthWebhookError } from './webhooks.js'\nexport type { TillAuthWebhookEvent, VerifyWebhookOptions, WebhookErrorReason } from './webhooks.js'\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/cookie-proxy.ts","../src/webhooks.ts","../src/access.ts","../src/index.ts"],"names":["header","importJWK","createRemoteJWKSet","jwtVerify","DEFAULT_API","payload","decodeJwt"],"mappings":"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","file":"index.cjs","sourcesContent":["// Cookie-mode proxy for TillAuth.\n//\n// Mount this on the customer's backend (typically at /api/auth/*). It\n// forwards JSON bodies to TillAuth's /v1/* endpoints, then translates\n// access_token + refresh_token in the response into HttpOnly cookies on\n// the customer's domain. The browser's localStorage never sees the\n// tokens — XSS can't exfiltrate them.\n//\n// Two adapters: a generic web-fetch handler (Cloudflare Workers, Next.js\n// Route Handlers, Bun, Deno) and an Express middleware. Pick one.\n\nconst DEFAULT_API = 'https://auth.tilldev.dev'\n\nexport interface CookieProxyOptions {\n /** TillAuth public app id (`tau_…`). */\n appId: string\n /** Override the TillAuth API base. */\n apiBase?: string\n /** Cookie name for the access token. Default `tau_at`. */\n accessCookie?: string\n /** Cookie name for the refresh token. Default `tau_rt`. */\n refreshCookie?: string\n /** Cookie path. Default '/'. */\n cookiePath?: string\n /** Cookie domain (e.g. `.example.com` to share across subdomains). */\n cookieDomain?: string\n /** Secure flag. Defaults to true in production, false otherwise. */\n secure?: boolean\n}\n\ninterface NormalizedRequest {\n method: string\n pathname: string // path relative to the mount point (`/signin`, `/refresh`)\n body: string\n cookies: Record<string, string>\n}\ninterface NormalizedResponse {\n status: number\n body: string\n setCookies: string[]\n headers: Record<string, string>\n}\n\ninterface AuthBody {\n access_token?: string\n refresh_token?: string\n refresh_expires_at?: string\n [key: string]: unknown\n}\n\nfunction resolve(opts: CookieProxyOptions): Required<CookieProxyOptions> {\n return {\n appId: opts.appId,\n apiBase: (opts.apiBase ?? DEFAULT_API).replace(/\\/$/, ''),\n accessCookie: opts.accessCookie ?? 'tau_at',\n refreshCookie: opts.refreshCookie ?? 'tau_rt',\n cookiePath: opts.cookiePath ?? '/',\n cookieDomain: opts.cookieDomain ?? '',\n secure: opts.secure ?? process.env.NODE_ENV === 'production',\n }\n}\n\nfunction makeCookie(\n name: string,\n value: string,\n o: Required<CookieProxyOptions>,\n opts: { maxAge?: number; clear?: boolean } = {},\n): string {\n const parts = [`${name}=${opts.clear ? '' : value}`]\n parts.push('HttpOnly')\n if (o.secure) parts.push('Secure')\n parts.push('SameSite=Lax')\n parts.push(`Path=${o.cookiePath}`)\n if (o.cookieDomain) parts.push(`Domain=${o.cookieDomain}`)\n if (opts.clear) parts.push('Max-Age=0')\n else if (opts.maxAge !== undefined) parts.push(`Max-Age=${opts.maxAge}`)\n return parts.join('; ')\n}\n\nasync function handleProxy(req: NormalizedRequest, raw: CookieProxyOptions): Promise<NormalizedResponse> {\n const opts = resolve(raw)\n const path = req.pathname.replace(/^\\/+/, '')\n\n // Map proxy paths → TillAuth paths.\n const url = `${opts.apiBase}/v1/${path}`\n\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n 'X-TillAuth-App': opts.appId,\n 'User-Agent': 'tilldev-auth-node/cookie-proxy',\n }\n\n // /me, /refresh, /signout need to carry tokens from cookies, not the body.\n let body = req.body\n if (path === 'me' && req.cookies[opts.accessCookie]) {\n headers.Authorization = `Bearer ${req.cookies[opts.accessCookie]}`\n }\n if (path === 'refresh') {\n const rt = req.cookies[opts.refreshCookie]\n if (!rt) {\n return {\n status: 401,\n body: JSON.stringify({ error: 'no refresh cookie' }),\n setCookies: [],\n headers: { 'Content-Type': 'application/json' },\n }\n }\n body = JSON.stringify({ refresh_token: rt })\n }\n if (path === 'signout') {\n const rt = req.cookies[opts.refreshCookie]\n body = JSON.stringify({ refresh_token: rt ?? '' })\n }\n\n const upstream = await fetch(url, { method: req.method, headers, body })\n const text = await upstream.text()\n let json: AuthBody = {}\n try {\n json = JSON.parse(text) as AuthBody\n } catch {\n json = {}\n }\n\n const setCookies: string[] = []\n // Always strip token fields before returning to the SPA.\n const out = { ...json }\n\n if (typeof json.access_token === 'string') {\n setCookies.push(makeCookie(opts.accessCookie, json.access_token, opts, { maxAge: 60 * 10 }))\n delete out.access_token\n }\n if (typeof json.refresh_token === 'string') {\n // 30d in line with the Fastify REFRESH_TTL_MS.\n setCookies.push(makeCookie(opts.refreshCookie, json.refresh_token, opts, { maxAge: 60 * 60 * 24 * 30 }))\n delete out.refresh_token\n delete out.refresh_expires_at\n }\n if (path === 'signout') {\n setCookies.push(makeCookie(opts.accessCookie, '', opts, { clear: true }))\n setCookies.push(makeCookie(opts.refreshCookie, '', opts, { clear: true }))\n }\n\n return {\n status: upstream.status,\n body: JSON.stringify(out),\n setCookies,\n headers: { 'Content-Type': 'application/json' },\n }\n}\n\n// ── Adapter 1: web-fetch (Next.js Route Handlers, Workers, Bun, Deno) ───────\n\nexport function createFetchHandler(opts: CookieProxyOptions) {\n return async function handler(req: Request, ctx: { params?: { path?: string | string[] } } = {}): Promise<Response> {\n const url = new URL(req.url)\n // Derive the proxy-relative path. Prefer ctx.params.path (Next.js catch-all);\n // fall back to the part of the URL after the mount point heuristic.\n let pathname: string\n if (ctx.params?.path) {\n pathname = Array.isArray(ctx.params.path) ? `/${ctx.params.path.join('/')}` : `/${ctx.params.path}`\n } else {\n pathname = url.pathname.replace(/^\\/api\\/auth/, '')\n }\n const cookies = parseCookieHeader(req.headers.get('cookie'))\n const body = req.method === 'POST' ? await req.text() : '{}'\n const out = await handleProxy({ method: req.method, pathname, body, cookies }, opts)\n const headers = new Headers(out.headers)\n for (const c of out.setCookies) headers.append('Set-Cookie', c)\n return new Response(out.body, { status: out.status, headers })\n }\n}\n\n// ── Adapter 2: Express ──────────────────────────────────────────────────────\n\ninterface ExpReq {\n method: string\n url?: string\n originalUrl?: string\n path?: string\n body?: unknown\n headers: Record<string, string | string[] | undefined>\n}\ninterface ExpRes {\n status(n: number): ExpRes\n setHeader(name: string, value: string | string[]): void\n json(body: unknown): void\n}\n\nexport function expressCookieProxy(opts: CookieProxyOptions) {\n return async function (req: ExpReq, res: ExpRes): Promise<void> {\n const path =\n req.path ?? (req.originalUrl ?? req.url ?? '/').replace(/^\\/api\\/auth/, '').split('?')[0] ?? '/'\n const cookieHeader =\n typeof req.headers.cookie === 'string' ? req.headers.cookie : Array.isArray(req.headers.cookie) ? req.headers.cookie[0] : ''\n const body =\n typeof req.body === 'string' ? req.body : req.body ? JSON.stringify(req.body) : '{}'\n const out = await handleProxy(\n {\n method: req.method,\n pathname: path,\n body,\n cookies: parseCookieHeader(cookieHeader ?? null),\n },\n opts,\n )\n res.status(out.status)\n if (out.setCookies.length > 0) res.setHeader('Set-Cookie', out.setCookies)\n for (const [k, v] of Object.entries(out.headers)) res.setHeader(k, v)\n res.json(JSON.parse(out.body))\n }\n}\n\nfunction parseCookieHeader(header: string | null): Record<string, string> {\n const out: Record<string, string> = {}\n if (!header) return out\n for (const part of header.split(';')) {\n const [k, ...rest] = part.trim().split('=')\n if (!k) continue\n out[k] = decodeURIComponent(rest.join('='))\n }\n return out\n}\n","// Verifies TillAuth webhook deliveries (header `X-TillAuth-Signature: t=<unix>,v1=<hex>`).\n\nexport interface TillAuthWebhookEvent {\n type: string\n result: string\n ts: string\n app_id: string\n user_id: string | null\n ip: string | null\n user_agent: string | null\n metadata: Record<string, unknown>\n delivery_id: string\n attempt: number\n}\n\nexport type WebhookErrorReason = 'missing_signature' | 'malformed_signature' | 'stale' | 'bad_signature' | 'bad_payload'\n\nexport class TillAuthWebhookError extends Error {\n constructor(readonly reason: WebhookErrorReason) {\n super(`TillAuth webhook rejected: ${reason}`)\n this.name = 'TillAuthWebhookError'\n }\n}\n\nexport interface VerifyWebhookOptions {\n /** Max age of the signed timestamp, in seconds. Default 300. */\n toleranceSeconds?: number\n /** Unix seconds; for tests. */\n now?: number\n}\n\nconst enc = new TextEncoder()\n\nfunction hexBytes(hex: string): Uint8Array | null {\n if (hex.length === 0 || hex.length % 2 !== 0 || !/^[0-9a-f]+$/i.test(hex)) return null\n const out = new Uint8Array(hex.length / 2)\n for (let i = 0; i < out.length; i++) out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16)\n return out\n}\n\n/**\n * Checks the signature over the exact bytes received, then parses them.\n * Pass the raw body, never a re-serialized object.\n */\nexport async function verifyWebhook(\n rawBody: string | Uint8Array,\n signatureHeader: string | null | undefined,\n secret: string,\n opts: VerifyWebhookOptions = {},\n): Promise<TillAuthWebhookEvent> {\n if (!signatureHeader) throw new TillAuthWebhookError('missing_signature')\n\n let t: number | null = null\n const sigs: Uint8Array[] = []\n for (const part of signatureHeader.split(',')) {\n const [k, v] = part.trim().split('=', 2)\n if (k === 't' && v && /^\\d+$/.test(v)) t = Number(v)\n else if (k === 'v1' && v) {\n const b = hexBytes(v)\n if (b) sigs.push(b)\n }\n }\n if (t === null || sigs.length === 0) throw new TillAuthWebhookError('malformed_signature')\n\n const now = opts.now ?? Math.floor(Date.now() / 1000)\n if (Math.abs(now - t) > (opts.toleranceSeconds ?? 300)) throw new TillAuthWebhookError('stale')\n\n const body = typeof rawBody === 'string' ? enc.encode(rawBody) : rawBody\n const prefix = enc.encode(`${t}.`)\n const signed = new Uint8Array(prefix.length + body.length)\n signed.set(prefix)\n signed.set(body, prefix.length)\n\n const key = await crypto.subtle.importKey('raw', enc.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['verify'])\n let ok = false\n for (const s of sigs) if (await crypto.subtle.verify('HMAC', key, s, signed)) ok = true\n if (!ok) throw new TillAuthWebhookError('bad_signature')\n\n try {\n return JSON.parse(new TextDecoder().decode(body)) as TillAuthWebhookEvent\n } catch {\n throw new TillAuthWebhookError('bad_payload')\n }\n}\n","import { jwtVerify, createRemoteJWKSet, importJWK, type JWK, type JWTVerifyGetKey } from 'jose'\n\nexport const ACCESS_ASSERTION_HEADER = 'x-tilldev-assertion'\nconst ASSERTION_TYP = 'tilldev-access+jwt'\n\nexport interface AccessVerifyOptions {\n /** The issuer shown on the internal app's page, or by `tilldev auth access <app> show <id>`. */\n issuer: string\n /** The internal app's audience (its `taac_…` client id). */\n audience: string\n /** Defaults to `{issuer}/.well-known/jwks.json`. */\n jwksUri?: string\n /** Inline public keys instead of fetching the key set. */\n publicJwks?: JWK[]\n /** Seconds of clock skew to allow. Defaults to 30. */\n clockToleranceSec?: number\n}\n\nexport interface AccessIdentity {\n sub: string\n email: string\n email_verified: boolean\n role: string\n /** Group names, sorted; at most 100. Empty when the person is in none. */\n groups: string[]\n mfa: boolean\n /** When the person signed in, in seconds. */\n auth_time: number\n sid: string\n /** The host the proxy checked the request for. */\n host: string\n access_app: string\n iss: string\n iat: number\n exp: number\n jti: string\n}\n\nexport class AccessAssertionError extends Error {\n code: 'no_assertion' | 'invalid_assertion' | 'wrong_host'\n constructor(message: string, code: AccessAssertionError['code']) {\n super(message)\n this.name = 'AccessAssertionError'\n this.code = code\n }\n}\n\ntype HeaderBag = Record<string, string | string[] | undefined> | { get(name: string): string | null }\n\nfunction header(headers: HeaderBag, name: string): string | undefined {\n if (typeof (headers as { get?: unknown }).get === 'function') return (headers as { get(n: string): string | null }).get(name) ?? undefined\n const v = (headers as Record<string, string | string[] | undefined>)[name]\n return Array.isArray(v) ? v[0] : v\n}\n\nexport class AccessVerifier {\n private readonly issuer: string\n private readonly audience: string\n private readonly tolerance: number\n private readonly keys: JWTVerifyGetKey | Promise<Array<Awaited<ReturnType<typeof importJWK>>>>\n\n constructor(opts: AccessVerifyOptions) {\n if (!opts.issuer || !opts.audience) throw new Error('AccessVerifier: issuer and audience are required')\n this.issuer = opts.issuer.replace(/\\/+$/, '')\n this.audience = opts.audience\n this.tolerance = opts.clockToleranceSec ?? 30\n this.keys = opts.publicJwks?.length ? Promise.all(opts.publicJwks.map((k) => importJWK(k, 'EdDSA'))) : createRemoteJWKSet(new URL(opts.jwksUri ?? `${this.issuer}/.well-known/jwks.json`))\n }\n\n /** Verifies the signed identity the proxy forwarded. Pass `host` to refuse one checked for another host. */\n async verify(token: string | undefined | null, expect: { host?: string } = {}): Promise<AccessIdentity> {\n if (!token) throw new AccessAssertionError('No identity assertion; is the request coming through the proxy?', 'no_assertion')\n const options = { algorithms: ['EdDSA'], issuer: this.issuer, audience: this.audience, typ: ASSERTION_TYP, clockTolerance: this.tolerance, requiredClaims: ['sub', 'exp', 'iat'] }\n let payload: Record<string, unknown>\n try {\n if (this.keys instanceof Promise) {\n let last: unknown\n payload = {}\n let ok = false\n for (const key of await this.keys) {\n try {\n payload = (await jwtVerify(token, key, options)).payload as Record<string, unknown>\n ok = true\n break\n } catch (e) {\n last = e\n }\n }\n if (!ok) throw last ?? new Error('No key matched')\n } else {\n payload = (await jwtVerify(token, this.keys, options)).payload as Record<string, unknown>\n }\n } catch (e) {\n throw new AccessAssertionError(e instanceof Error ? e.message : 'Invalid assertion', 'invalid_assertion')\n }\n if (typeof payload['email'] !== 'string' || typeof payload['host'] !== 'string' || payload['access_app'] !== this.audience) {\n throw new AccessAssertionError('Assertion is missing its identity', 'invalid_assertion')\n }\n if (expect.host !== undefined && payload['host'] !== expect.host.toLowerCase().replace(/:\\d+$/, '')) {\n throw new AccessAssertionError('Assertion was issued for another host', 'wrong_host')\n }\n const groups = Array.isArray(payload['groups']) ? payload['groups'].filter((g): g is string => typeof g === 'string') : []\n return { ...payload, groups } as unknown as AccessIdentity\n }\n\n /** Reads the assertion and host from a request's headers (Node, Express, Fastify or fetch `Headers`). */\n verifyRequest(headers: HeaderBag): Promise<AccessIdentity> {\n const host = header(headers, 'host')\n return this.verify(header(headers, ACCESS_ASSERTION_HEADER), host ? { host } : {})\n }\n}\n\nexport function createAccessVerifier(opts: AccessVerifyOptions): AccessVerifier {\n return new AccessVerifier(opts)\n}\n","import { jwtVerify, createRemoteJWKSet, importJWK, decodeJwt, type JWK } from 'jose'\n\n// Server-side TillAuth verification.\n//\n// You configure once with the issuer + JWKS endpoint (or inline public keys),\n// then call `verify(token)` or use the framework-specific middleware.\n//\n// Verification is offline against the cached JWKS — no round-trip to TillAuth\n// on the hot path.\n\nconst DEFAULT_API = 'https://auth.tilldev.dev'\nconst REALM_SEGMENT = '/realms/'\n\nfunction trimSlash(s: string): string {\n return s.replace(/\\/+$/, '')\n}\n\n/**\n * Issuer acceptance check.\n *\n * A token this verifier may accept was minted for THIS app, so exactly two issuers\n * are legitimate: the canonical one (the app has no dedicated realm keys, so the\n * shared plane key signed it) or THIS app's own realm issuer. Nothing else.\n *\n * ⚠️ THIS USED TO ACCEPT ANY `{canonical}/realms/<anything>` when no issuer was\n * pinned. Together with a shared JWKS carrying every realm's key, that was a\n * cross-realm forgery (`atrium` `00` D13): holding any one realm's signing key was\n * enough to mint a token naming any OTHER realm. The signature verified (the key was\n * in the merged JWKS), the issuer passed this prefix test, and the `app` check\n * passed too — because the attacker wrote that claim as well.\n *\n * Note the `issuer` option was NOT the mitigation it looked like: pinning it\n * constrained the *claim* while the *key* still came from the merged set.\n *\n * The bare legacy `\"tillauth\"` string was retired 2026-07-20 and is not accepted.\n */\nfunction issuerAccepted(\n iss: unknown,\n canonical: string,\n realm: unknown,\n pinnedIssuer?: string,\n): boolean {\n if (typeof iss !== 'string' || iss.length === 0) return false\n if (pinnedIssuer) return iss === trimSlash(pinnedIssuer)\n const canon = trimSlash(canonical)\n return iss === canon || (typeof realm === 'string' && iss === `${canon}${REALM_SEGMENT}${realm}`)\n}\n\nexport interface VerifyOptions {\n /** The app's public id (`tau_…`), publishable key (`tau_pub_…`) or app id. Tokens for any other app are refused. */\n appId: string\n /** TillAuth API base. Defaults to https://auth.tilldev.dev */\n apiBase?: string\n /**\n * Pin verification to exactly one issuer. Once your app signs with its own key,\n * pin its issuer (`https://auth.tilldev.dev/realms/<app id>`) to refuse anything\n * signed with the shared key.\n *\n * When omitted, the verifier accepts the canonical HTTPS issuer (derived from\n * `apiBase`) and THIS app's own issuer — never another app's.\n */\n issuer?: string\n /**\n * Inline JWKS as a JWK array (the same `publicJwk` entries you'd find in\n * the TILLAUTH_JWT_KEYS env). Use this in environments where you want\n * fully air-gapped verification.\n */\n publicJwks?: JWK[]\n /** Require email_verified=true. Defaults to false. */\n requireVerifiedEmail?: boolean\n /** Require MFA was satisfied this session. Defaults to false. */\n requireMfa?: boolean\n}\n\nexport interface AuthClaims {\n /** User id */\n sub: string\n /** App id the token was issued for */\n app: string\n /** The app's public id. Absent on tokens minted before it existed. */\n pid?: string\n /** Lowercased email */\n email: string\n /** Whether MFA was satisfied this session */\n mfa: boolean\n /**\n * Whether the account's email was verified at issue time. Optional: tokens\n * minted before this claim existed omit it. When `requireVerifiedEmail` is\n * set, an absent claim is treated as unverified (fail-closed on the check).\n */\n email_verified?: boolean\n /** Token issuer — validated against the accepted set (see `issuerAccepted`). */\n iss?: string\n /** Issued-at (seconds) */\n iat: number\n /** Expires-at (seconds) */\n exp: number\n}\n\nexport class TillAuthVerifyError extends Error {\n code: 'no_token' | 'invalid_token' | 'wrong_app' | 'mfa_required' | 'email_not_verified' | 'invalid_issuer'\n constructor(message: string, code: TillAuthVerifyError['code']) {\n super(message)\n this.name = 'TillAuthVerifyError'\n this.code = code\n }\n}\n\nexport class TillAuthVerifier {\n private readonly opts: Required<Pick<VerifyOptions, 'appId' | 'apiBase' | 'requireVerifiedEmail' | 'requireMfa'>>\n private readonly pinnedIssuer: string | undefined\n private readonly remote: ReturnType<typeof createRemoteJWKSet> | null\n /** This app's own realm key set — see `keySetFor`. Null on the inline-keys path. */\n private readonly remoteRealm: ReturnType<typeof createRemoteJWKSet> | null = null\n private readonly inlineKeys: Promise<Array<{ kid?: string; key: Awaited<ReturnType<typeof importJWK>> }>> | null\n\n constructor(opts: VerifyOptions) {\n if (!opts.appId) throw new Error('TillAuthVerifier: appId is required')\n this.opts = {\n appId: opts.appId.startsWith('tau_pub_') ? 'tau_' + opts.appId.slice('tau_pub_'.length) : opts.appId,\n apiBase: (opts.apiBase ?? DEFAULT_API).replace(/\\/$/, ''),\n requireVerifiedEmail: opts.requireVerifiedEmail ?? false,\n requireMfa: opts.requireMfa ?? false,\n }\n this.pinnedIssuer = opts.issuer ? opts.issuer.replace(/\\/$/, '') : undefined\n if (opts.publicJwks && opts.publicJwks.length > 0) {\n this.remote = null\n this.inlineKeys = Promise.all(\n opts.publicJwks.map(async (k) => ({ kid: k.kid, key: await importJWK(k, 'EdDSA') })),\n )\n } else {\n /*\n * TWO key sets, selected by the token's issuer (`atrium` `00` D13 F4).\n *\n * `remote` is the shared plane's set — after the D13 F6 change it holds ONLY\n * the global signing keys, never a realm's. `remoteRealm` is this app's own\n * realm set, which holds only this realm's keys and 404s if the app has no\n * dedicated keys.\n *\n * Because neither set can contain ANOTHER realm's key, a forged token naming a\n * different realm cannot be verified from either — the isolation is structural\n * rather than a claim check. This replaced a single `createRemoteJWKSet` on the\n * merged JWKS, where every realm's key was resolvable by `kid` and the realm\n * checks below were therefore decorative.\n */\n this.remote = createRemoteJWKSet(new URL(`${this.opts.apiBase}/.well-known/jwks.json`))\n this.remoteRealm = createRemoteJWKSet(\n new URL(`${this.opts.apiBase}${REALM_SEGMENT}${this.opts.appId}/.well-known/jwks.json`),\n )\n this.inlineKeys = null\n }\n }\n\n /**\n * The key set entitled to have signed a token carrying `iss`, or `null` if no set\n * is. Selecting on the UNVERIFIED issuer is safe because the choice is then\n * enforced: `jwtVerify` pins `issuer` to the same value, so a token that lies about\n * its issuer to reach a different key set must still verify against that set — and\n * that set contains only keys entitled to the issuer it named.\n */\n private keySetFor(payload: { iss?: unknown; app?: unknown; pid?: unknown }): ReturnType<typeof createRemoteJWKSet> | null {\n if (typeof payload.iss !== 'string') return null\n if (payload.iss === this.opts.apiBase) return this.remote\n // The app's own key set is fetched by the configured app, so it holds only this app's keys.\n if (\n this.matchesApp(payload) &&\n typeof payload.app === 'string' &&\n payload.iss === `${this.opts.apiBase}${REALM_SEGMENT}${payload.app}`\n ) {\n return this.remoteRealm\n }\n return null\n }\n\n private matchesApp(claims: { app?: unknown; pid?: unknown }): boolean {\n return claims.app === this.opts.appId || claims.pid === this.opts.appId\n }\n\n async verify(token: string): Promise<AuthClaims> {\n if (!token) throw new TillAuthVerifyError('No token', 'no_token')\n\n let claims: AuthClaims\n try {\n if (this.inlineKeys) {\n const keys = await this.inlineKeys\n let lastErr: unknown\n for (const k of keys) {\n try {\n // No jose `issuer` pin — the issuer is one of several accepted values\n // (legacy string / canonical HTTPS / per-realm), checked below.\n const { payload } = await jwtVerify(token, k.key, { algorithms: ['EdDSA'] })\n claims = payload as unknown as AuthClaims\n return this.applyClaimsChecks(claims)\n } catch (e) {\n lastErr = e\n }\n }\n throw lastErr ?? new Error('No key matched')\n }\n /*\n * Pick the key set from the token's own issuer, then PIN that issuer during\n * verification. The pin is what makes reading an unverified claim safe: it\n * cannot be used to reach a key set and then disown the issuer that selected\n * it. A token naming a realm this verifier does not serve gets no key set at\n * all and is refused before any crypto runs.\n */\n const unverified = decodeJwt(token)\n const keySet = this.keySetFor(unverified)\n if (!keySet) {\n throw new TillAuthVerifyError('Untrusted token issuer', 'invalid_issuer')\n }\n const { payload } = await jwtVerify(token, keySet, {\n algorithms: ['EdDSA'],\n issuer: unverified.iss as string,\n })\n claims = payload as unknown as AuthClaims\n } catch (e) {\n // Preserve a specific claims verdict (wrong_app / invalid_issuer) thrown by\n // applyClaimsChecks on the inline path; only a genuine signature/JWKS\n // failure becomes the generic invalid_token.\n if (e instanceof TillAuthVerifyError) throw e\n throw new TillAuthVerifyError(\n e instanceof Error ? e.message : 'Invalid token',\n 'invalid_token',\n )\n }\n return this.applyClaimsChecks(claims)\n }\n\n private applyClaimsChecks(claims: AuthClaims): AuthClaims {\n // Issuer is validated here (jose's exact `issuer` pin is intentionally not\n // used) so per-realm HTTPS issuers verify alongside the canonical one.\n if (!this.matchesApp(claims)) {\n throw new TillAuthVerifyError(`Token issued for different app`, 'wrong_app')\n }\n if (!issuerAccepted(claims.iss, this.opts.apiBase, claims.app, this.pinnedIssuer)) {\n throw new TillAuthVerifyError('Untrusted token issuer', 'invalid_issuer')\n }\n if (this.opts.requireMfa && !claims.mfa) {\n throw new TillAuthVerifyError('MFA required', 'mfa_required')\n }\n // Absent claim (older tokens) is treated as unverified only when the check\n // is enabled — so requireVerifiedEmail fails closed rather than silently\n // passing when the issuer predates the claim.\n if (this.opts.requireVerifiedEmail && claims.email_verified !== true) {\n throw new TillAuthVerifyError('Email not verified', 'email_not_verified')\n }\n return claims\n }\n\n /**\n * Pull a Bearer token out of an Authorization header and verify. Returns\n * the claims or throws TillAuthVerifyError.\n */\n async verifyHeader(authHeader: string | undefined | null): Promise<AuthClaims> {\n if (!authHeader || !authHeader.startsWith('Bearer ')) {\n throw new TillAuthVerifyError('Missing Bearer token', 'no_token')\n }\n return this.verify(authHeader.slice(7))\n }\n}\n\n// ── Framework adapters ──────────────────────────────────────────────────────\n\n// Express-style middleware. Adds `req.user` on success.\nexport function expressMiddleware(verifier: TillAuthVerifier, opts: { onError?: (err: TillAuthVerifyError) => unknown } = {}) {\n return async function (req: ExpressLikeReq, res: ExpressLikeRes, next: () => void) {\n try {\n const h = req.headers?.authorization\n const claims = await verifier.verifyHeader(Array.isArray(h) ? h[0] : h)\n ;(req as ExpressLikeReq & { user: AuthClaims }).user = claims\n next()\n } catch (e) {\n const err = e instanceof TillAuthVerifyError ? e : new TillAuthVerifyError('Auth failed', 'invalid_token')\n if (opts.onError) {\n const out = opts.onError(err)\n if (out !== undefined) {\n res.status(401).json(out as Record<string, unknown>)\n return\n }\n }\n res.status(401).json({ error: err.message, code: err.code })\n }\n }\n}\n\ninterface ExpressLikeReq {\n headers?: { authorization?: string | string[] }\n}\ninterface ExpressLikeRes {\n status: (n: number) => { json: (b: Record<string, unknown>) => void }\n}\n\n// Fastify plugin shape — not bound to Fastify's types to avoid a peer dep.\nexport function fastifyHook(verifier: TillAuthVerifier) {\n return async function (\n req: { headers: Record<string, string | string[] | undefined>; user?: AuthClaims },\n reply: { code: (n: number) => { send: (b: unknown) => void } },\n ) {\n try {\n const h = req.headers['authorization']\n const auth = Array.isArray(h) ? h[0] : h\n const claims = await verifier.verifyHeader(auth)\n req.user = claims\n } catch (e) {\n const err = e instanceof TillAuthVerifyError ? e : new TillAuthVerifyError('Auth failed', 'invalid_token')\n reply.code(401).send({ error: err.message, code: err.code })\n }\n }\n}\n\n// Convenience entrypoint for users who want a one-call setup.\nexport function createVerifier(opts: VerifyOptions): TillAuthVerifier {\n return new TillAuthVerifier(opts)\n}\n\nexport { createFetchHandler, expressCookieProxy } from './cookie-proxy.js'\nexport type { CookieProxyOptions } from './cookie-proxy.js'\nexport { verifyWebhook, TillAuthWebhookError } from './webhooks.js'\nexport { AccessVerifier, AccessAssertionError, createAccessVerifier, ACCESS_ASSERTION_HEADER } from './access.js'\nexport type { AccessIdentity, AccessVerifyOptions } from './access.js'\nexport type { TillAuthWebhookEvent, VerifyWebhookOptions, WebhookErrorReason } from './webhooks.js'\n"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -65,6 +65,60 @@ interface VerifyWebhookOptions {
|
|
|
65
65
|
*/
|
|
66
66
|
declare function verifyWebhook(rawBody: string | Uint8Array, signatureHeader: string | null | undefined, secret: string, opts?: VerifyWebhookOptions): Promise<TillAuthWebhookEvent>;
|
|
67
67
|
|
|
68
|
+
declare const ACCESS_ASSERTION_HEADER = "x-tilldev-assertion";
|
|
69
|
+
interface AccessVerifyOptions {
|
|
70
|
+
/** The issuer shown on the internal app's page, or by `tilldev auth access <app> show <id>`. */
|
|
71
|
+
issuer: string;
|
|
72
|
+
/** The internal app's audience (its `taac_…` client id). */
|
|
73
|
+
audience: string;
|
|
74
|
+
/** Defaults to `{issuer}/.well-known/jwks.json`. */
|
|
75
|
+
jwksUri?: string;
|
|
76
|
+
/** Inline public keys instead of fetching the key set. */
|
|
77
|
+
publicJwks?: JWK[];
|
|
78
|
+
/** Seconds of clock skew to allow. Defaults to 30. */
|
|
79
|
+
clockToleranceSec?: number;
|
|
80
|
+
}
|
|
81
|
+
interface AccessIdentity {
|
|
82
|
+
sub: string;
|
|
83
|
+
email: string;
|
|
84
|
+
email_verified: boolean;
|
|
85
|
+
role: string;
|
|
86
|
+
/** Group names, sorted; at most 100. Empty when the person is in none. */
|
|
87
|
+
groups: string[];
|
|
88
|
+
mfa: boolean;
|
|
89
|
+
/** When the person signed in, in seconds. */
|
|
90
|
+
auth_time: number;
|
|
91
|
+
sid: string;
|
|
92
|
+
/** The host the proxy checked the request for. */
|
|
93
|
+
host: string;
|
|
94
|
+
access_app: string;
|
|
95
|
+
iss: string;
|
|
96
|
+
iat: number;
|
|
97
|
+
exp: number;
|
|
98
|
+
jti: string;
|
|
99
|
+
}
|
|
100
|
+
declare class AccessAssertionError extends Error {
|
|
101
|
+
code: 'no_assertion' | 'invalid_assertion' | 'wrong_host';
|
|
102
|
+
constructor(message: string, code: AccessAssertionError['code']);
|
|
103
|
+
}
|
|
104
|
+
type HeaderBag = Record<string, string | string[] | undefined> | {
|
|
105
|
+
get(name: string): string | null;
|
|
106
|
+
};
|
|
107
|
+
declare class AccessVerifier {
|
|
108
|
+
private readonly issuer;
|
|
109
|
+
private readonly audience;
|
|
110
|
+
private readonly tolerance;
|
|
111
|
+
private readonly keys;
|
|
112
|
+
constructor(opts: AccessVerifyOptions);
|
|
113
|
+
/** Verifies the signed identity the proxy forwarded. Pass `host` to refuse one checked for another host. */
|
|
114
|
+
verify(token: string | undefined | null, expect?: {
|
|
115
|
+
host?: string;
|
|
116
|
+
}): Promise<AccessIdentity>;
|
|
117
|
+
/** Reads the assertion and host from a request's headers (Node, Express, Fastify or fetch `Headers`). */
|
|
118
|
+
verifyRequest(headers: HeaderBag): Promise<AccessIdentity>;
|
|
119
|
+
}
|
|
120
|
+
declare function createAccessVerifier(opts: AccessVerifyOptions): AccessVerifier;
|
|
121
|
+
|
|
68
122
|
interface VerifyOptions {
|
|
69
123
|
/** The app's public id (`tau_…`), publishable key (`tau_pub_…`) or app id. Tokens for any other app are refused. */
|
|
70
124
|
appId: string;
|
|
@@ -166,4 +220,4 @@ declare function fastifyHook(verifier: TillAuthVerifier): (req: {
|
|
|
166
220
|
}) => Promise<void>;
|
|
167
221
|
declare function createVerifier(opts: VerifyOptions): TillAuthVerifier;
|
|
168
222
|
|
|
169
|
-
export { type AuthClaims, type CookieProxyOptions, TillAuthVerifier, TillAuthVerifyError, TillAuthWebhookError, type TillAuthWebhookEvent, type VerifyOptions, type VerifyWebhookOptions, type WebhookErrorReason, createFetchHandler, createVerifier, expressCookieProxy, expressMiddleware, fastifyHook, verifyWebhook };
|
|
223
|
+
export { ACCESS_ASSERTION_HEADER, AccessAssertionError, type AccessIdentity, AccessVerifier, type AccessVerifyOptions, type AuthClaims, type CookieProxyOptions, TillAuthVerifier, TillAuthVerifyError, TillAuthWebhookError, type TillAuthWebhookEvent, type VerifyOptions, type VerifyWebhookOptions, type WebhookErrorReason, createAccessVerifier, createFetchHandler, createVerifier, expressCookieProxy, expressMiddleware, fastifyHook, verifyWebhook };
|
package/dist/index.d.ts
CHANGED
|
@@ -65,6 +65,60 @@ interface VerifyWebhookOptions {
|
|
|
65
65
|
*/
|
|
66
66
|
declare function verifyWebhook(rawBody: string | Uint8Array, signatureHeader: string | null | undefined, secret: string, opts?: VerifyWebhookOptions): Promise<TillAuthWebhookEvent>;
|
|
67
67
|
|
|
68
|
+
declare const ACCESS_ASSERTION_HEADER = "x-tilldev-assertion";
|
|
69
|
+
interface AccessVerifyOptions {
|
|
70
|
+
/** The issuer shown on the internal app's page, or by `tilldev auth access <app> show <id>`. */
|
|
71
|
+
issuer: string;
|
|
72
|
+
/** The internal app's audience (its `taac_…` client id). */
|
|
73
|
+
audience: string;
|
|
74
|
+
/** Defaults to `{issuer}/.well-known/jwks.json`. */
|
|
75
|
+
jwksUri?: string;
|
|
76
|
+
/** Inline public keys instead of fetching the key set. */
|
|
77
|
+
publicJwks?: JWK[];
|
|
78
|
+
/** Seconds of clock skew to allow. Defaults to 30. */
|
|
79
|
+
clockToleranceSec?: number;
|
|
80
|
+
}
|
|
81
|
+
interface AccessIdentity {
|
|
82
|
+
sub: string;
|
|
83
|
+
email: string;
|
|
84
|
+
email_verified: boolean;
|
|
85
|
+
role: string;
|
|
86
|
+
/** Group names, sorted; at most 100. Empty when the person is in none. */
|
|
87
|
+
groups: string[];
|
|
88
|
+
mfa: boolean;
|
|
89
|
+
/** When the person signed in, in seconds. */
|
|
90
|
+
auth_time: number;
|
|
91
|
+
sid: string;
|
|
92
|
+
/** The host the proxy checked the request for. */
|
|
93
|
+
host: string;
|
|
94
|
+
access_app: string;
|
|
95
|
+
iss: string;
|
|
96
|
+
iat: number;
|
|
97
|
+
exp: number;
|
|
98
|
+
jti: string;
|
|
99
|
+
}
|
|
100
|
+
declare class AccessAssertionError extends Error {
|
|
101
|
+
code: 'no_assertion' | 'invalid_assertion' | 'wrong_host';
|
|
102
|
+
constructor(message: string, code: AccessAssertionError['code']);
|
|
103
|
+
}
|
|
104
|
+
type HeaderBag = Record<string, string | string[] | undefined> | {
|
|
105
|
+
get(name: string): string | null;
|
|
106
|
+
};
|
|
107
|
+
declare class AccessVerifier {
|
|
108
|
+
private readonly issuer;
|
|
109
|
+
private readonly audience;
|
|
110
|
+
private readonly tolerance;
|
|
111
|
+
private readonly keys;
|
|
112
|
+
constructor(opts: AccessVerifyOptions);
|
|
113
|
+
/** Verifies the signed identity the proxy forwarded. Pass `host` to refuse one checked for another host. */
|
|
114
|
+
verify(token: string | undefined | null, expect?: {
|
|
115
|
+
host?: string;
|
|
116
|
+
}): Promise<AccessIdentity>;
|
|
117
|
+
/** Reads the assertion and host from a request's headers (Node, Express, Fastify or fetch `Headers`). */
|
|
118
|
+
verifyRequest(headers: HeaderBag): Promise<AccessIdentity>;
|
|
119
|
+
}
|
|
120
|
+
declare function createAccessVerifier(opts: AccessVerifyOptions): AccessVerifier;
|
|
121
|
+
|
|
68
122
|
interface VerifyOptions {
|
|
69
123
|
/** The app's public id (`tau_…`), publishable key (`tau_pub_…`) or app id. Tokens for any other app are refused. */
|
|
70
124
|
appId: string;
|
|
@@ -166,4 +220,4 @@ declare function fastifyHook(verifier: TillAuthVerifier): (req: {
|
|
|
166
220
|
}) => Promise<void>;
|
|
167
221
|
declare function createVerifier(opts: VerifyOptions): TillAuthVerifier;
|
|
168
222
|
|
|
169
|
-
export { type AuthClaims, type CookieProxyOptions, TillAuthVerifier, TillAuthVerifyError, TillAuthWebhookError, type TillAuthWebhookEvent, type VerifyOptions, type VerifyWebhookOptions, type WebhookErrorReason, createFetchHandler, createVerifier, expressCookieProxy, expressMiddleware, fastifyHook, verifyWebhook };
|
|
223
|
+
export { ACCESS_ASSERTION_HEADER, AccessAssertionError, type AccessIdentity, AccessVerifier, type AccessVerifyOptions, type AuthClaims, type CookieProxyOptions, TillAuthVerifier, TillAuthVerifyError, TillAuthWebhookError, type TillAuthWebhookEvent, type VerifyOptions, type VerifyWebhookOptions, type WebhookErrorReason, createAccessVerifier, createFetchHandler, createVerifier, expressCookieProxy, expressMiddleware, fastifyHook, verifyWebhook };
|
package/dist/index.js
CHANGED
|
@@ -122,10 +122,10 @@ function expressCookieProxy(opts) {
|
|
|
122
122
|
res.json(JSON.parse(out.body));
|
|
123
123
|
};
|
|
124
124
|
}
|
|
125
|
-
function parseCookieHeader(
|
|
125
|
+
function parseCookieHeader(header2) {
|
|
126
126
|
const out = {};
|
|
127
|
-
if (!
|
|
128
|
-
for (const part of
|
|
127
|
+
if (!header2) return out;
|
|
128
|
+
for (const part of header2.split(";")) {
|
|
129
129
|
const [k, ...rest] = part.trim().split("=");
|
|
130
130
|
if (!k) continue;
|
|
131
131
|
out[k] = decodeURIComponent(rest.join("="));
|
|
@@ -178,6 +178,72 @@ async function verifyWebhook(rawBody, signatureHeader, secret, opts = {}) {
|
|
|
178
178
|
throw new TillAuthWebhookError("bad_payload");
|
|
179
179
|
}
|
|
180
180
|
}
|
|
181
|
+
var ACCESS_ASSERTION_HEADER = "x-tilldev-assertion";
|
|
182
|
+
var ASSERTION_TYP = "tilldev-access+jwt";
|
|
183
|
+
var AccessAssertionError = class extends Error {
|
|
184
|
+
constructor(message, code) {
|
|
185
|
+
super(message);
|
|
186
|
+
this.name = "AccessAssertionError";
|
|
187
|
+
this.code = code;
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
function header(headers, name) {
|
|
191
|
+
if (typeof headers.get === "function") return headers.get(name) ?? void 0;
|
|
192
|
+
const v = headers[name];
|
|
193
|
+
return Array.isArray(v) ? v[0] : v;
|
|
194
|
+
}
|
|
195
|
+
var AccessVerifier = class {
|
|
196
|
+
constructor(opts) {
|
|
197
|
+
if (!opts.issuer || !opts.audience) throw new Error("AccessVerifier: issuer and audience are required");
|
|
198
|
+
this.issuer = opts.issuer.replace(/\/+$/, "");
|
|
199
|
+
this.audience = opts.audience;
|
|
200
|
+
this.tolerance = opts.clockToleranceSec ?? 30;
|
|
201
|
+
this.keys = opts.publicJwks?.length ? Promise.all(opts.publicJwks.map((k) => importJWK(k, "EdDSA"))) : createRemoteJWKSet(new URL(opts.jwksUri ?? `${this.issuer}/.well-known/jwks.json`));
|
|
202
|
+
}
|
|
203
|
+
/** Verifies the signed identity the proxy forwarded. Pass `host` to refuse one checked for another host. */
|
|
204
|
+
async verify(token, expect = {}) {
|
|
205
|
+
if (!token) throw new AccessAssertionError("No identity assertion; is the request coming through the proxy?", "no_assertion");
|
|
206
|
+
const options = { algorithms: ["EdDSA"], issuer: this.issuer, audience: this.audience, typ: ASSERTION_TYP, clockTolerance: this.tolerance, requiredClaims: ["sub", "exp", "iat"] };
|
|
207
|
+
let payload;
|
|
208
|
+
try {
|
|
209
|
+
if (this.keys instanceof Promise) {
|
|
210
|
+
let last;
|
|
211
|
+
payload = {};
|
|
212
|
+
let ok = false;
|
|
213
|
+
for (const key of await this.keys) {
|
|
214
|
+
try {
|
|
215
|
+
payload = (await jwtVerify(token, key, options)).payload;
|
|
216
|
+
ok = true;
|
|
217
|
+
break;
|
|
218
|
+
} catch (e) {
|
|
219
|
+
last = e;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
if (!ok) throw last ?? new Error("No key matched");
|
|
223
|
+
} else {
|
|
224
|
+
payload = (await jwtVerify(token, this.keys, options)).payload;
|
|
225
|
+
}
|
|
226
|
+
} catch (e) {
|
|
227
|
+
throw new AccessAssertionError(e instanceof Error ? e.message : "Invalid assertion", "invalid_assertion");
|
|
228
|
+
}
|
|
229
|
+
if (typeof payload["email"] !== "string" || typeof payload["host"] !== "string" || payload["access_app"] !== this.audience) {
|
|
230
|
+
throw new AccessAssertionError("Assertion is missing its identity", "invalid_assertion");
|
|
231
|
+
}
|
|
232
|
+
if (expect.host !== void 0 && payload["host"] !== expect.host.toLowerCase().replace(/:\d+$/, "")) {
|
|
233
|
+
throw new AccessAssertionError("Assertion was issued for another host", "wrong_host");
|
|
234
|
+
}
|
|
235
|
+
const groups = Array.isArray(payload["groups"]) ? payload["groups"].filter((g) => typeof g === "string") : [];
|
|
236
|
+
return { ...payload, groups };
|
|
237
|
+
}
|
|
238
|
+
/** Reads the assertion and host from a request's headers (Node, Express, Fastify or fetch `Headers`). */
|
|
239
|
+
verifyRequest(headers) {
|
|
240
|
+
const host = header(headers, "host");
|
|
241
|
+
return this.verify(header(headers, ACCESS_ASSERTION_HEADER), host ? { host } : {});
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
function createAccessVerifier(opts) {
|
|
245
|
+
return new AccessVerifier(opts);
|
|
246
|
+
}
|
|
181
247
|
|
|
182
248
|
// src/index.ts
|
|
183
249
|
var DEFAULT_API2 = "https://auth.tilldev.dev";
|
|
@@ -341,6 +407,6 @@ function createVerifier(opts) {
|
|
|
341
407
|
return new TillAuthVerifier(opts);
|
|
342
408
|
}
|
|
343
409
|
|
|
344
|
-
export { TillAuthVerifier, TillAuthVerifyError, TillAuthWebhookError, createFetchHandler, createVerifier, expressCookieProxy, expressMiddleware, fastifyHook, verifyWebhook };
|
|
410
|
+
export { ACCESS_ASSERTION_HEADER, AccessAssertionError, AccessVerifier, TillAuthVerifier, TillAuthVerifyError, TillAuthWebhookError, createAccessVerifier, createFetchHandler, createVerifier, expressCookieProxy, expressMiddleware, fastifyHook, verifyWebhook };
|
|
345
411
|
//# sourceMappingURL=index.js.map
|
|
346
412
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/cookie-proxy.ts","../src/webhooks.ts","../src/index.ts"],"names":["DEFAULT_API","payload"],"mappings":"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","file":"index.js","sourcesContent":["// Cookie-mode proxy for TillAuth.\n//\n// Mount this on the customer's backend (typically at /api/auth/*). It\n// forwards JSON bodies to TillAuth's /v1/* endpoints, then translates\n// access_token + refresh_token in the response into HttpOnly cookies on\n// the customer's domain. The browser's localStorage never sees the\n// tokens — XSS can't exfiltrate them.\n//\n// Two adapters: a generic web-fetch handler (Cloudflare Workers, Next.js\n// Route Handlers, Bun, Deno) and an Express middleware. Pick one.\n\nconst DEFAULT_API = 'https://auth.tilldev.dev'\n\nexport interface CookieProxyOptions {\n /** TillAuth public app id (`tau_…`). */\n appId: string\n /** Override the TillAuth API base. */\n apiBase?: string\n /** Cookie name for the access token. Default `tau_at`. */\n accessCookie?: string\n /** Cookie name for the refresh token. Default `tau_rt`. */\n refreshCookie?: string\n /** Cookie path. Default '/'. */\n cookiePath?: string\n /** Cookie domain (e.g. `.example.com` to share across subdomains). */\n cookieDomain?: string\n /** Secure flag. Defaults to true in production, false otherwise. */\n secure?: boolean\n}\n\ninterface NormalizedRequest {\n method: string\n pathname: string // path relative to the mount point (`/signin`, `/refresh`)\n body: string\n cookies: Record<string, string>\n}\ninterface NormalizedResponse {\n status: number\n body: string\n setCookies: string[]\n headers: Record<string, string>\n}\n\ninterface AuthBody {\n access_token?: string\n refresh_token?: string\n refresh_expires_at?: string\n [key: string]: unknown\n}\n\nfunction resolve(opts: CookieProxyOptions): Required<CookieProxyOptions> {\n return {\n appId: opts.appId,\n apiBase: (opts.apiBase ?? DEFAULT_API).replace(/\\/$/, ''),\n accessCookie: opts.accessCookie ?? 'tau_at',\n refreshCookie: opts.refreshCookie ?? 'tau_rt',\n cookiePath: opts.cookiePath ?? '/',\n cookieDomain: opts.cookieDomain ?? '',\n secure: opts.secure ?? process.env.NODE_ENV === 'production',\n }\n}\n\nfunction makeCookie(\n name: string,\n value: string,\n o: Required<CookieProxyOptions>,\n opts: { maxAge?: number; clear?: boolean } = {},\n): string {\n const parts = [`${name}=${opts.clear ? '' : value}`]\n parts.push('HttpOnly')\n if (o.secure) parts.push('Secure')\n parts.push('SameSite=Lax')\n parts.push(`Path=${o.cookiePath}`)\n if (o.cookieDomain) parts.push(`Domain=${o.cookieDomain}`)\n if (opts.clear) parts.push('Max-Age=0')\n else if (opts.maxAge !== undefined) parts.push(`Max-Age=${opts.maxAge}`)\n return parts.join('; ')\n}\n\nasync function handleProxy(req: NormalizedRequest, raw: CookieProxyOptions): Promise<NormalizedResponse> {\n const opts = resolve(raw)\n const path = req.pathname.replace(/^\\/+/, '')\n\n // Map proxy paths → TillAuth paths.\n const url = `${opts.apiBase}/v1/${path}`\n\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n 'X-TillAuth-App': opts.appId,\n 'User-Agent': 'tilldev-auth-node/cookie-proxy',\n }\n\n // /me, /refresh, /signout need to carry tokens from cookies, not the body.\n let body = req.body\n if (path === 'me' && req.cookies[opts.accessCookie]) {\n headers.Authorization = `Bearer ${req.cookies[opts.accessCookie]}`\n }\n if (path === 'refresh') {\n const rt = req.cookies[opts.refreshCookie]\n if (!rt) {\n return {\n status: 401,\n body: JSON.stringify({ error: 'no refresh cookie' }),\n setCookies: [],\n headers: { 'Content-Type': 'application/json' },\n }\n }\n body = JSON.stringify({ refresh_token: rt })\n }\n if (path === 'signout') {\n const rt = req.cookies[opts.refreshCookie]\n body = JSON.stringify({ refresh_token: rt ?? '' })\n }\n\n const upstream = await fetch(url, { method: req.method, headers, body })\n const text = await upstream.text()\n let json: AuthBody = {}\n try {\n json = JSON.parse(text) as AuthBody\n } catch {\n json = {}\n }\n\n const setCookies: string[] = []\n // Always strip token fields before returning to the SPA.\n const out = { ...json }\n\n if (typeof json.access_token === 'string') {\n setCookies.push(makeCookie(opts.accessCookie, json.access_token, opts, { maxAge: 60 * 10 }))\n delete out.access_token\n }\n if (typeof json.refresh_token === 'string') {\n // 30d in line with the Fastify REFRESH_TTL_MS.\n setCookies.push(makeCookie(opts.refreshCookie, json.refresh_token, opts, { maxAge: 60 * 60 * 24 * 30 }))\n delete out.refresh_token\n delete out.refresh_expires_at\n }\n if (path === 'signout') {\n setCookies.push(makeCookie(opts.accessCookie, '', opts, { clear: true }))\n setCookies.push(makeCookie(opts.refreshCookie, '', opts, { clear: true }))\n }\n\n return {\n status: upstream.status,\n body: JSON.stringify(out),\n setCookies,\n headers: { 'Content-Type': 'application/json' },\n }\n}\n\n// ── Adapter 1: web-fetch (Next.js Route Handlers, Workers, Bun, Deno) ───────\n\nexport function createFetchHandler(opts: CookieProxyOptions) {\n return async function handler(req: Request, ctx: { params?: { path?: string | string[] } } = {}): Promise<Response> {\n const url = new URL(req.url)\n // Derive the proxy-relative path. Prefer ctx.params.path (Next.js catch-all);\n // fall back to the part of the URL after the mount point heuristic.\n let pathname: string\n if (ctx.params?.path) {\n pathname = Array.isArray(ctx.params.path) ? `/${ctx.params.path.join('/')}` : `/${ctx.params.path}`\n } else {\n pathname = url.pathname.replace(/^\\/api\\/auth/, '')\n }\n const cookies = parseCookieHeader(req.headers.get('cookie'))\n const body = req.method === 'POST' ? await req.text() : '{}'\n const out = await handleProxy({ method: req.method, pathname, body, cookies }, opts)\n const headers = new Headers(out.headers)\n for (const c of out.setCookies) headers.append('Set-Cookie', c)\n return new Response(out.body, { status: out.status, headers })\n }\n}\n\n// ── Adapter 2: Express ──────────────────────────────────────────────────────\n\ninterface ExpReq {\n method: string\n url?: string\n originalUrl?: string\n path?: string\n body?: unknown\n headers: Record<string, string | string[] | undefined>\n}\ninterface ExpRes {\n status(n: number): ExpRes\n setHeader(name: string, value: string | string[]): void\n json(body: unknown): void\n}\n\nexport function expressCookieProxy(opts: CookieProxyOptions) {\n return async function (req: ExpReq, res: ExpRes): Promise<void> {\n const path =\n req.path ?? (req.originalUrl ?? req.url ?? '/').replace(/^\\/api\\/auth/, '').split('?')[0] ?? '/'\n const cookieHeader =\n typeof req.headers.cookie === 'string' ? req.headers.cookie : Array.isArray(req.headers.cookie) ? req.headers.cookie[0] : ''\n const body =\n typeof req.body === 'string' ? req.body : req.body ? JSON.stringify(req.body) : '{}'\n const out = await handleProxy(\n {\n method: req.method,\n pathname: path,\n body,\n cookies: parseCookieHeader(cookieHeader ?? null),\n },\n opts,\n )\n res.status(out.status)\n if (out.setCookies.length > 0) res.setHeader('Set-Cookie', out.setCookies)\n for (const [k, v] of Object.entries(out.headers)) res.setHeader(k, v)\n res.json(JSON.parse(out.body))\n }\n}\n\nfunction parseCookieHeader(header: string | null): Record<string, string> {\n const out: Record<string, string> = {}\n if (!header) return out\n for (const part of header.split(';')) {\n const [k, ...rest] = part.trim().split('=')\n if (!k) continue\n out[k] = decodeURIComponent(rest.join('='))\n }\n return out\n}\n","// Verifies TillAuth webhook deliveries (header `X-TillAuth-Signature: t=<unix>,v1=<hex>`).\n\nexport interface TillAuthWebhookEvent {\n type: string\n result: string\n ts: string\n app_id: string\n user_id: string | null\n ip: string | null\n user_agent: string | null\n metadata: Record<string, unknown>\n delivery_id: string\n attempt: number\n}\n\nexport type WebhookErrorReason = 'missing_signature' | 'malformed_signature' | 'stale' | 'bad_signature' | 'bad_payload'\n\nexport class TillAuthWebhookError extends Error {\n constructor(readonly reason: WebhookErrorReason) {\n super(`TillAuth webhook rejected: ${reason}`)\n this.name = 'TillAuthWebhookError'\n }\n}\n\nexport interface VerifyWebhookOptions {\n /** Max age of the signed timestamp, in seconds. Default 300. */\n toleranceSeconds?: number\n /** Unix seconds; for tests. */\n now?: number\n}\n\nconst enc = new TextEncoder()\n\nfunction hexBytes(hex: string): Uint8Array | null {\n if (hex.length === 0 || hex.length % 2 !== 0 || !/^[0-9a-f]+$/i.test(hex)) return null\n const out = new Uint8Array(hex.length / 2)\n for (let i = 0; i < out.length; i++) out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16)\n return out\n}\n\n/**\n * Checks the signature over the exact bytes received, then parses them.\n * Pass the raw body, never a re-serialized object.\n */\nexport async function verifyWebhook(\n rawBody: string | Uint8Array,\n signatureHeader: string | null | undefined,\n secret: string,\n opts: VerifyWebhookOptions = {},\n): Promise<TillAuthWebhookEvent> {\n if (!signatureHeader) throw new TillAuthWebhookError('missing_signature')\n\n let t: number | null = null\n const sigs: Uint8Array[] = []\n for (const part of signatureHeader.split(',')) {\n const [k, v] = part.trim().split('=', 2)\n if (k === 't' && v && /^\\d+$/.test(v)) t = Number(v)\n else if (k === 'v1' && v) {\n const b = hexBytes(v)\n if (b) sigs.push(b)\n }\n }\n if (t === null || sigs.length === 0) throw new TillAuthWebhookError('malformed_signature')\n\n const now = opts.now ?? Math.floor(Date.now() / 1000)\n if (Math.abs(now - t) > (opts.toleranceSeconds ?? 300)) throw new TillAuthWebhookError('stale')\n\n const body = typeof rawBody === 'string' ? enc.encode(rawBody) : rawBody\n const prefix = enc.encode(`${t}.`)\n const signed = new Uint8Array(prefix.length + body.length)\n signed.set(prefix)\n signed.set(body, prefix.length)\n\n const key = await crypto.subtle.importKey('raw', enc.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['verify'])\n let ok = false\n for (const s of sigs) if (await crypto.subtle.verify('HMAC', key, s, signed)) ok = true\n if (!ok) throw new TillAuthWebhookError('bad_signature')\n\n try {\n return JSON.parse(new TextDecoder().decode(body)) as TillAuthWebhookEvent\n } catch {\n throw new TillAuthWebhookError('bad_payload')\n }\n}\n","import { jwtVerify, createRemoteJWKSet, importJWK, decodeJwt, type JWK } from 'jose'\n\n// Server-side TillAuth verification.\n//\n// You configure once with the issuer + JWKS endpoint (or inline public keys),\n// then call `verify(token)` or use the framework-specific middleware.\n//\n// Verification is offline against the cached JWKS — no round-trip to TillAuth\n// on the hot path.\n\nconst DEFAULT_API = 'https://auth.tilldev.dev'\nconst REALM_SEGMENT = '/realms/'\n\nfunction trimSlash(s: string): string {\n return s.replace(/\\/+$/, '')\n}\n\n/**\n * Issuer acceptance check.\n *\n * A token this verifier may accept was minted for THIS app, so exactly two issuers\n * are legitimate: the canonical one (the app has no dedicated realm keys, so the\n * shared plane key signed it) or THIS app's own realm issuer. Nothing else.\n *\n * ⚠️ THIS USED TO ACCEPT ANY `{canonical}/realms/<anything>` when no issuer was\n * pinned. Together with a shared JWKS carrying every realm's key, that was a\n * cross-realm forgery (`atrium` `00` D13): holding any one realm's signing key was\n * enough to mint a token naming any OTHER realm. The signature verified (the key was\n * in the merged JWKS), the issuer passed this prefix test, and the `app` check\n * passed too — because the attacker wrote that claim as well.\n *\n * Note the `issuer` option was NOT the mitigation it looked like: pinning it\n * constrained the *claim* while the *key* still came from the merged set.\n *\n * The bare legacy `\"tillauth\"` string was retired 2026-07-20 and is not accepted.\n */\nfunction issuerAccepted(\n iss: unknown,\n canonical: string,\n realm: unknown,\n pinnedIssuer?: string,\n): boolean {\n if (typeof iss !== 'string' || iss.length === 0) return false\n if (pinnedIssuer) return iss === trimSlash(pinnedIssuer)\n const canon = trimSlash(canonical)\n return iss === canon || (typeof realm === 'string' && iss === `${canon}${REALM_SEGMENT}${realm}`)\n}\n\nexport interface VerifyOptions {\n /** The app's public id (`tau_…`), publishable key (`tau_pub_…`) or app id. Tokens for any other app are refused. */\n appId: string\n /** TillAuth API base. Defaults to https://auth.tilldev.dev */\n apiBase?: string\n /**\n * Pin verification to exactly one issuer. Once your app signs with its own key,\n * pin its issuer (`https://auth.tilldev.dev/realms/<app id>`) to refuse anything\n * signed with the shared key.\n *\n * When omitted, the verifier accepts the canonical HTTPS issuer (derived from\n * `apiBase`) and THIS app's own issuer — never another app's.\n */\n issuer?: string\n /**\n * Inline JWKS as a JWK array (the same `publicJwk` entries you'd find in\n * the TILLAUTH_JWT_KEYS env). Use this in environments where you want\n * fully air-gapped verification.\n */\n publicJwks?: JWK[]\n /** Require email_verified=true. Defaults to false. */\n requireVerifiedEmail?: boolean\n /** Require MFA was satisfied this session. Defaults to false. */\n requireMfa?: boolean\n}\n\nexport interface AuthClaims {\n /** User id */\n sub: string\n /** App id the token was issued for */\n app: string\n /** The app's public id. Absent on tokens minted before it existed. */\n pid?: string\n /** Lowercased email */\n email: string\n /** Whether MFA was satisfied this session */\n mfa: boolean\n /**\n * Whether the account's email was verified at issue time. Optional: tokens\n * minted before this claim existed omit it. When `requireVerifiedEmail` is\n * set, an absent claim is treated as unverified (fail-closed on the check).\n */\n email_verified?: boolean\n /** Token issuer — validated against the accepted set (see `issuerAccepted`). */\n iss?: string\n /** Issued-at (seconds) */\n iat: number\n /** Expires-at (seconds) */\n exp: number\n}\n\nexport class TillAuthVerifyError extends Error {\n code: 'no_token' | 'invalid_token' | 'wrong_app' | 'mfa_required' | 'email_not_verified' | 'invalid_issuer'\n constructor(message: string, code: TillAuthVerifyError['code']) {\n super(message)\n this.name = 'TillAuthVerifyError'\n this.code = code\n }\n}\n\nexport class TillAuthVerifier {\n private readonly opts: Required<Pick<VerifyOptions, 'appId' | 'apiBase' | 'requireVerifiedEmail' | 'requireMfa'>>\n private readonly pinnedIssuer: string | undefined\n private readonly remote: ReturnType<typeof createRemoteJWKSet> | null\n /** This app's own realm key set — see `keySetFor`. Null on the inline-keys path. */\n private readonly remoteRealm: ReturnType<typeof createRemoteJWKSet> | null = null\n private readonly inlineKeys: Promise<Array<{ kid?: string; key: Awaited<ReturnType<typeof importJWK>> }>> | null\n\n constructor(opts: VerifyOptions) {\n if (!opts.appId) throw new Error('TillAuthVerifier: appId is required')\n this.opts = {\n appId: opts.appId.startsWith('tau_pub_') ? 'tau_' + opts.appId.slice('tau_pub_'.length) : opts.appId,\n apiBase: (opts.apiBase ?? DEFAULT_API).replace(/\\/$/, ''),\n requireVerifiedEmail: opts.requireVerifiedEmail ?? false,\n requireMfa: opts.requireMfa ?? false,\n }\n this.pinnedIssuer = opts.issuer ? opts.issuer.replace(/\\/$/, '') : undefined\n if (opts.publicJwks && opts.publicJwks.length > 0) {\n this.remote = null\n this.inlineKeys = Promise.all(\n opts.publicJwks.map(async (k) => ({ kid: k.kid, key: await importJWK(k, 'EdDSA') })),\n )\n } else {\n /*\n * TWO key sets, selected by the token's issuer (`atrium` `00` D13 F4).\n *\n * `remote` is the shared plane's set — after the D13 F6 change it holds ONLY\n * the global signing keys, never a realm's. `remoteRealm` is this app's own\n * realm set, which holds only this realm's keys and 404s if the app has no\n * dedicated keys.\n *\n * Because neither set can contain ANOTHER realm's key, a forged token naming a\n * different realm cannot be verified from either — the isolation is structural\n * rather than a claim check. This replaced a single `createRemoteJWKSet` on the\n * merged JWKS, where every realm's key was resolvable by `kid` and the realm\n * checks below were therefore decorative.\n */\n this.remote = createRemoteJWKSet(new URL(`${this.opts.apiBase}/.well-known/jwks.json`))\n this.remoteRealm = createRemoteJWKSet(\n new URL(`${this.opts.apiBase}${REALM_SEGMENT}${this.opts.appId}/.well-known/jwks.json`),\n )\n this.inlineKeys = null\n }\n }\n\n /**\n * The key set entitled to have signed a token carrying `iss`, or `null` if no set\n * is. Selecting on the UNVERIFIED issuer is safe because the choice is then\n * enforced: `jwtVerify` pins `issuer` to the same value, so a token that lies about\n * its issuer to reach a different key set must still verify against that set — and\n * that set contains only keys entitled to the issuer it named.\n */\n private keySetFor(payload: { iss?: unknown; app?: unknown; pid?: unknown }): ReturnType<typeof createRemoteJWKSet> | null {\n if (typeof payload.iss !== 'string') return null\n if (payload.iss === this.opts.apiBase) return this.remote\n // The app's own key set is fetched by the configured app, so it holds only this app's keys.\n if (\n this.matchesApp(payload) &&\n typeof payload.app === 'string' &&\n payload.iss === `${this.opts.apiBase}${REALM_SEGMENT}${payload.app}`\n ) {\n return this.remoteRealm\n }\n return null\n }\n\n private matchesApp(claims: { app?: unknown; pid?: unknown }): boolean {\n return claims.app === this.opts.appId || claims.pid === this.opts.appId\n }\n\n async verify(token: string): Promise<AuthClaims> {\n if (!token) throw new TillAuthVerifyError('No token', 'no_token')\n\n let claims: AuthClaims\n try {\n if (this.inlineKeys) {\n const keys = await this.inlineKeys\n let lastErr: unknown\n for (const k of keys) {\n try {\n // No jose `issuer` pin — the issuer is one of several accepted values\n // (legacy string / canonical HTTPS / per-realm), checked below.\n const { payload } = await jwtVerify(token, k.key, { algorithms: ['EdDSA'] })\n claims = payload as unknown as AuthClaims\n return this.applyClaimsChecks(claims)\n } catch (e) {\n lastErr = e\n }\n }\n throw lastErr ?? new Error('No key matched')\n }\n /*\n * Pick the key set from the token's own issuer, then PIN that issuer during\n * verification. The pin is what makes reading an unverified claim safe: it\n * cannot be used to reach a key set and then disown the issuer that selected\n * it. A token naming a realm this verifier does not serve gets no key set at\n * all and is refused before any crypto runs.\n */\n const unverified = decodeJwt(token)\n const keySet = this.keySetFor(unverified)\n if (!keySet) {\n throw new TillAuthVerifyError('Untrusted token issuer', 'invalid_issuer')\n }\n const { payload } = await jwtVerify(token, keySet, {\n algorithms: ['EdDSA'],\n issuer: unverified.iss as string,\n })\n claims = payload as unknown as AuthClaims\n } catch (e) {\n // Preserve a specific claims verdict (wrong_app / invalid_issuer) thrown by\n // applyClaimsChecks on the inline path; only a genuine signature/JWKS\n // failure becomes the generic invalid_token.\n if (e instanceof TillAuthVerifyError) throw e\n throw new TillAuthVerifyError(\n e instanceof Error ? e.message : 'Invalid token',\n 'invalid_token',\n )\n }\n return this.applyClaimsChecks(claims)\n }\n\n private applyClaimsChecks(claims: AuthClaims): AuthClaims {\n // Issuer is validated here (jose's exact `issuer` pin is intentionally not\n // used) so per-realm HTTPS issuers verify alongside the canonical one.\n if (!this.matchesApp(claims)) {\n throw new TillAuthVerifyError(`Token issued for different app`, 'wrong_app')\n }\n if (!issuerAccepted(claims.iss, this.opts.apiBase, claims.app, this.pinnedIssuer)) {\n throw new TillAuthVerifyError('Untrusted token issuer', 'invalid_issuer')\n }\n if (this.opts.requireMfa && !claims.mfa) {\n throw new TillAuthVerifyError('MFA required', 'mfa_required')\n }\n // Absent claim (older tokens) is treated as unverified only when the check\n // is enabled — so requireVerifiedEmail fails closed rather than silently\n // passing when the issuer predates the claim.\n if (this.opts.requireVerifiedEmail && claims.email_verified !== true) {\n throw new TillAuthVerifyError('Email not verified', 'email_not_verified')\n }\n return claims\n }\n\n /**\n * Pull a Bearer token out of an Authorization header and verify. Returns\n * the claims or throws TillAuthVerifyError.\n */\n async verifyHeader(authHeader: string | undefined | null): Promise<AuthClaims> {\n if (!authHeader || !authHeader.startsWith('Bearer ')) {\n throw new TillAuthVerifyError('Missing Bearer token', 'no_token')\n }\n return this.verify(authHeader.slice(7))\n }\n}\n\n// ── Framework adapters ──────────────────────────────────────────────────────\n\n// Express-style middleware. Adds `req.user` on success.\nexport function expressMiddleware(verifier: TillAuthVerifier, opts: { onError?: (err: TillAuthVerifyError) => unknown } = {}) {\n return async function (req: ExpressLikeReq, res: ExpressLikeRes, next: () => void) {\n try {\n const h = req.headers?.authorization\n const claims = await verifier.verifyHeader(Array.isArray(h) ? h[0] : h)\n ;(req as ExpressLikeReq & { user: AuthClaims }).user = claims\n next()\n } catch (e) {\n const err = e instanceof TillAuthVerifyError ? e : new TillAuthVerifyError('Auth failed', 'invalid_token')\n if (opts.onError) {\n const out = opts.onError(err)\n if (out !== undefined) {\n res.status(401).json(out as Record<string, unknown>)\n return\n }\n }\n res.status(401).json({ error: err.message, code: err.code })\n }\n }\n}\n\ninterface ExpressLikeReq {\n headers?: { authorization?: string | string[] }\n}\ninterface ExpressLikeRes {\n status: (n: number) => { json: (b: Record<string, unknown>) => void }\n}\n\n// Fastify plugin shape — not bound to Fastify's types to avoid a peer dep.\nexport function fastifyHook(verifier: TillAuthVerifier) {\n return async function (\n req: { headers: Record<string, string | string[] | undefined>; user?: AuthClaims },\n reply: { code: (n: number) => { send: (b: unknown) => void } },\n ) {\n try {\n const h = req.headers['authorization']\n const auth = Array.isArray(h) ? h[0] : h\n const claims = await verifier.verifyHeader(auth)\n req.user = claims\n } catch (e) {\n const err = e instanceof TillAuthVerifyError ? e : new TillAuthVerifyError('Auth failed', 'invalid_token')\n reply.code(401).send({ error: err.message, code: err.code })\n }\n }\n}\n\n// Convenience entrypoint for users who want a one-call setup.\nexport function createVerifier(opts: VerifyOptions): TillAuthVerifier {\n return new TillAuthVerifier(opts)\n}\n\nexport { createFetchHandler, expressCookieProxy } from './cookie-proxy.js'\nexport type { CookieProxyOptions } from './cookie-proxy.js'\nexport { verifyWebhook, TillAuthWebhookError } from './webhooks.js'\nexport type { TillAuthWebhookEvent, VerifyWebhookOptions, WebhookErrorReason } from './webhooks.js'\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/cookie-proxy.ts","../src/webhooks.ts","../src/access.ts","../src/index.ts"],"names":["header","DEFAULT_API","importJWK","createRemoteJWKSet","payload","jwtVerify"],"mappings":"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","file":"index.js","sourcesContent":["// Cookie-mode proxy for TillAuth.\n//\n// Mount this on the customer's backend (typically at /api/auth/*). It\n// forwards JSON bodies to TillAuth's /v1/* endpoints, then translates\n// access_token + refresh_token in the response into HttpOnly cookies on\n// the customer's domain. The browser's localStorage never sees the\n// tokens — XSS can't exfiltrate them.\n//\n// Two adapters: a generic web-fetch handler (Cloudflare Workers, Next.js\n// Route Handlers, Bun, Deno) and an Express middleware. Pick one.\n\nconst DEFAULT_API = 'https://auth.tilldev.dev'\n\nexport interface CookieProxyOptions {\n /** TillAuth public app id (`tau_…`). */\n appId: string\n /** Override the TillAuth API base. */\n apiBase?: string\n /** Cookie name for the access token. Default `tau_at`. */\n accessCookie?: string\n /** Cookie name for the refresh token. Default `tau_rt`. */\n refreshCookie?: string\n /** Cookie path. Default '/'. */\n cookiePath?: string\n /** Cookie domain (e.g. `.example.com` to share across subdomains). */\n cookieDomain?: string\n /** Secure flag. Defaults to true in production, false otherwise. */\n secure?: boolean\n}\n\ninterface NormalizedRequest {\n method: string\n pathname: string // path relative to the mount point (`/signin`, `/refresh`)\n body: string\n cookies: Record<string, string>\n}\ninterface NormalizedResponse {\n status: number\n body: string\n setCookies: string[]\n headers: Record<string, string>\n}\n\ninterface AuthBody {\n access_token?: string\n refresh_token?: string\n refresh_expires_at?: string\n [key: string]: unknown\n}\n\nfunction resolve(opts: CookieProxyOptions): Required<CookieProxyOptions> {\n return {\n appId: opts.appId,\n apiBase: (opts.apiBase ?? DEFAULT_API).replace(/\\/$/, ''),\n accessCookie: opts.accessCookie ?? 'tau_at',\n refreshCookie: opts.refreshCookie ?? 'tau_rt',\n cookiePath: opts.cookiePath ?? '/',\n cookieDomain: opts.cookieDomain ?? '',\n secure: opts.secure ?? process.env.NODE_ENV === 'production',\n }\n}\n\nfunction makeCookie(\n name: string,\n value: string,\n o: Required<CookieProxyOptions>,\n opts: { maxAge?: number; clear?: boolean } = {},\n): string {\n const parts = [`${name}=${opts.clear ? '' : value}`]\n parts.push('HttpOnly')\n if (o.secure) parts.push('Secure')\n parts.push('SameSite=Lax')\n parts.push(`Path=${o.cookiePath}`)\n if (o.cookieDomain) parts.push(`Domain=${o.cookieDomain}`)\n if (opts.clear) parts.push('Max-Age=0')\n else if (opts.maxAge !== undefined) parts.push(`Max-Age=${opts.maxAge}`)\n return parts.join('; ')\n}\n\nasync function handleProxy(req: NormalizedRequest, raw: CookieProxyOptions): Promise<NormalizedResponse> {\n const opts = resolve(raw)\n const path = req.pathname.replace(/^\\/+/, '')\n\n // Map proxy paths → TillAuth paths.\n const url = `${opts.apiBase}/v1/${path}`\n\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n 'X-TillAuth-App': opts.appId,\n 'User-Agent': 'tilldev-auth-node/cookie-proxy',\n }\n\n // /me, /refresh, /signout need to carry tokens from cookies, not the body.\n let body = req.body\n if (path === 'me' && req.cookies[opts.accessCookie]) {\n headers.Authorization = `Bearer ${req.cookies[opts.accessCookie]}`\n }\n if (path === 'refresh') {\n const rt = req.cookies[opts.refreshCookie]\n if (!rt) {\n return {\n status: 401,\n body: JSON.stringify({ error: 'no refresh cookie' }),\n setCookies: [],\n headers: { 'Content-Type': 'application/json' },\n }\n }\n body = JSON.stringify({ refresh_token: rt })\n }\n if (path === 'signout') {\n const rt = req.cookies[opts.refreshCookie]\n body = JSON.stringify({ refresh_token: rt ?? '' })\n }\n\n const upstream = await fetch(url, { method: req.method, headers, body })\n const text = await upstream.text()\n let json: AuthBody = {}\n try {\n json = JSON.parse(text) as AuthBody\n } catch {\n json = {}\n }\n\n const setCookies: string[] = []\n // Always strip token fields before returning to the SPA.\n const out = { ...json }\n\n if (typeof json.access_token === 'string') {\n setCookies.push(makeCookie(opts.accessCookie, json.access_token, opts, { maxAge: 60 * 10 }))\n delete out.access_token\n }\n if (typeof json.refresh_token === 'string') {\n // 30d in line with the Fastify REFRESH_TTL_MS.\n setCookies.push(makeCookie(opts.refreshCookie, json.refresh_token, opts, { maxAge: 60 * 60 * 24 * 30 }))\n delete out.refresh_token\n delete out.refresh_expires_at\n }\n if (path === 'signout') {\n setCookies.push(makeCookie(opts.accessCookie, '', opts, { clear: true }))\n setCookies.push(makeCookie(opts.refreshCookie, '', opts, { clear: true }))\n }\n\n return {\n status: upstream.status,\n body: JSON.stringify(out),\n setCookies,\n headers: { 'Content-Type': 'application/json' },\n }\n}\n\n// ── Adapter 1: web-fetch (Next.js Route Handlers, Workers, Bun, Deno) ───────\n\nexport function createFetchHandler(opts: CookieProxyOptions) {\n return async function handler(req: Request, ctx: { params?: { path?: string | string[] } } = {}): Promise<Response> {\n const url = new URL(req.url)\n // Derive the proxy-relative path. Prefer ctx.params.path (Next.js catch-all);\n // fall back to the part of the URL after the mount point heuristic.\n let pathname: string\n if (ctx.params?.path) {\n pathname = Array.isArray(ctx.params.path) ? `/${ctx.params.path.join('/')}` : `/${ctx.params.path}`\n } else {\n pathname = url.pathname.replace(/^\\/api\\/auth/, '')\n }\n const cookies = parseCookieHeader(req.headers.get('cookie'))\n const body = req.method === 'POST' ? await req.text() : '{}'\n const out = await handleProxy({ method: req.method, pathname, body, cookies }, opts)\n const headers = new Headers(out.headers)\n for (const c of out.setCookies) headers.append('Set-Cookie', c)\n return new Response(out.body, { status: out.status, headers })\n }\n}\n\n// ── Adapter 2: Express ──────────────────────────────────────────────────────\n\ninterface ExpReq {\n method: string\n url?: string\n originalUrl?: string\n path?: string\n body?: unknown\n headers: Record<string, string | string[] | undefined>\n}\ninterface ExpRes {\n status(n: number): ExpRes\n setHeader(name: string, value: string | string[]): void\n json(body: unknown): void\n}\n\nexport function expressCookieProxy(opts: CookieProxyOptions) {\n return async function (req: ExpReq, res: ExpRes): Promise<void> {\n const path =\n req.path ?? (req.originalUrl ?? req.url ?? '/').replace(/^\\/api\\/auth/, '').split('?')[0] ?? '/'\n const cookieHeader =\n typeof req.headers.cookie === 'string' ? req.headers.cookie : Array.isArray(req.headers.cookie) ? req.headers.cookie[0] : ''\n const body =\n typeof req.body === 'string' ? req.body : req.body ? JSON.stringify(req.body) : '{}'\n const out = await handleProxy(\n {\n method: req.method,\n pathname: path,\n body,\n cookies: parseCookieHeader(cookieHeader ?? null),\n },\n opts,\n )\n res.status(out.status)\n if (out.setCookies.length > 0) res.setHeader('Set-Cookie', out.setCookies)\n for (const [k, v] of Object.entries(out.headers)) res.setHeader(k, v)\n res.json(JSON.parse(out.body))\n }\n}\n\nfunction parseCookieHeader(header: string | null): Record<string, string> {\n const out: Record<string, string> = {}\n if (!header) return out\n for (const part of header.split(';')) {\n const [k, ...rest] = part.trim().split('=')\n if (!k) continue\n out[k] = decodeURIComponent(rest.join('='))\n }\n return out\n}\n","// Verifies TillAuth webhook deliveries (header `X-TillAuth-Signature: t=<unix>,v1=<hex>`).\n\nexport interface TillAuthWebhookEvent {\n type: string\n result: string\n ts: string\n app_id: string\n user_id: string | null\n ip: string | null\n user_agent: string | null\n metadata: Record<string, unknown>\n delivery_id: string\n attempt: number\n}\n\nexport type WebhookErrorReason = 'missing_signature' | 'malformed_signature' | 'stale' | 'bad_signature' | 'bad_payload'\n\nexport class TillAuthWebhookError extends Error {\n constructor(readonly reason: WebhookErrorReason) {\n super(`TillAuth webhook rejected: ${reason}`)\n this.name = 'TillAuthWebhookError'\n }\n}\n\nexport interface VerifyWebhookOptions {\n /** Max age of the signed timestamp, in seconds. Default 300. */\n toleranceSeconds?: number\n /** Unix seconds; for tests. */\n now?: number\n}\n\nconst enc = new TextEncoder()\n\nfunction hexBytes(hex: string): Uint8Array | null {\n if (hex.length === 0 || hex.length % 2 !== 0 || !/^[0-9a-f]+$/i.test(hex)) return null\n const out = new Uint8Array(hex.length / 2)\n for (let i = 0; i < out.length; i++) out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16)\n return out\n}\n\n/**\n * Checks the signature over the exact bytes received, then parses them.\n * Pass the raw body, never a re-serialized object.\n */\nexport async function verifyWebhook(\n rawBody: string | Uint8Array,\n signatureHeader: string | null | undefined,\n secret: string,\n opts: VerifyWebhookOptions = {},\n): Promise<TillAuthWebhookEvent> {\n if (!signatureHeader) throw new TillAuthWebhookError('missing_signature')\n\n let t: number | null = null\n const sigs: Uint8Array[] = []\n for (const part of signatureHeader.split(',')) {\n const [k, v] = part.trim().split('=', 2)\n if (k === 't' && v && /^\\d+$/.test(v)) t = Number(v)\n else if (k === 'v1' && v) {\n const b = hexBytes(v)\n if (b) sigs.push(b)\n }\n }\n if (t === null || sigs.length === 0) throw new TillAuthWebhookError('malformed_signature')\n\n const now = opts.now ?? Math.floor(Date.now() / 1000)\n if (Math.abs(now - t) > (opts.toleranceSeconds ?? 300)) throw new TillAuthWebhookError('stale')\n\n const body = typeof rawBody === 'string' ? enc.encode(rawBody) : rawBody\n const prefix = enc.encode(`${t}.`)\n const signed = new Uint8Array(prefix.length + body.length)\n signed.set(prefix)\n signed.set(body, prefix.length)\n\n const key = await crypto.subtle.importKey('raw', enc.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['verify'])\n let ok = false\n for (const s of sigs) if (await crypto.subtle.verify('HMAC', key, s, signed)) ok = true\n if (!ok) throw new TillAuthWebhookError('bad_signature')\n\n try {\n return JSON.parse(new TextDecoder().decode(body)) as TillAuthWebhookEvent\n } catch {\n throw new TillAuthWebhookError('bad_payload')\n }\n}\n","import { jwtVerify, createRemoteJWKSet, importJWK, type JWK, type JWTVerifyGetKey } from 'jose'\n\nexport const ACCESS_ASSERTION_HEADER = 'x-tilldev-assertion'\nconst ASSERTION_TYP = 'tilldev-access+jwt'\n\nexport interface AccessVerifyOptions {\n /** The issuer shown on the internal app's page, or by `tilldev auth access <app> show <id>`. */\n issuer: string\n /** The internal app's audience (its `taac_…` client id). */\n audience: string\n /** Defaults to `{issuer}/.well-known/jwks.json`. */\n jwksUri?: string\n /** Inline public keys instead of fetching the key set. */\n publicJwks?: JWK[]\n /** Seconds of clock skew to allow. Defaults to 30. */\n clockToleranceSec?: number\n}\n\nexport interface AccessIdentity {\n sub: string\n email: string\n email_verified: boolean\n role: string\n /** Group names, sorted; at most 100. Empty when the person is in none. */\n groups: string[]\n mfa: boolean\n /** When the person signed in, in seconds. */\n auth_time: number\n sid: string\n /** The host the proxy checked the request for. */\n host: string\n access_app: string\n iss: string\n iat: number\n exp: number\n jti: string\n}\n\nexport class AccessAssertionError extends Error {\n code: 'no_assertion' | 'invalid_assertion' | 'wrong_host'\n constructor(message: string, code: AccessAssertionError['code']) {\n super(message)\n this.name = 'AccessAssertionError'\n this.code = code\n }\n}\n\ntype HeaderBag = Record<string, string | string[] | undefined> | { get(name: string): string | null }\n\nfunction header(headers: HeaderBag, name: string): string | undefined {\n if (typeof (headers as { get?: unknown }).get === 'function') return (headers as { get(n: string): string | null }).get(name) ?? undefined\n const v = (headers as Record<string, string | string[] | undefined>)[name]\n return Array.isArray(v) ? v[0] : v\n}\n\nexport class AccessVerifier {\n private readonly issuer: string\n private readonly audience: string\n private readonly tolerance: number\n private readonly keys: JWTVerifyGetKey | Promise<Array<Awaited<ReturnType<typeof importJWK>>>>\n\n constructor(opts: AccessVerifyOptions) {\n if (!opts.issuer || !opts.audience) throw new Error('AccessVerifier: issuer and audience are required')\n this.issuer = opts.issuer.replace(/\\/+$/, '')\n this.audience = opts.audience\n this.tolerance = opts.clockToleranceSec ?? 30\n this.keys = opts.publicJwks?.length ? Promise.all(opts.publicJwks.map((k) => importJWK(k, 'EdDSA'))) : createRemoteJWKSet(new URL(opts.jwksUri ?? `${this.issuer}/.well-known/jwks.json`))\n }\n\n /** Verifies the signed identity the proxy forwarded. Pass `host` to refuse one checked for another host. */\n async verify(token: string | undefined | null, expect: { host?: string } = {}): Promise<AccessIdentity> {\n if (!token) throw new AccessAssertionError('No identity assertion; is the request coming through the proxy?', 'no_assertion')\n const options = { algorithms: ['EdDSA'], issuer: this.issuer, audience: this.audience, typ: ASSERTION_TYP, clockTolerance: this.tolerance, requiredClaims: ['sub', 'exp', 'iat'] }\n let payload: Record<string, unknown>\n try {\n if (this.keys instanceof Promise) {\n let last: unknown\n payload = {}\n let ok = false\n for (const key of await this.keys) {\n try {\n payload = (await jwtVerify(token, key, options)).payload as Record<string, unknown>\n ok = true\n break\n } catch (e) {\n last = e\n }\n }\n if (!ok) throw last ?? new Error('No key matched')\n } else {\n payload = (await jwtVerify(token, this.keys, options)).payload as Record<string, unknown>\n }\n } catch (e) {\n throw new AccessAssertionError(e instanceof Error ? e.message : 'Invalid assertion', 'invalid_assertion')\n }\n if (typeof payload['email'] !== 'string' || typeof payload['host'] !== 'string' || payload['access_app'] !== this.audience) {\n throw new AccessAssertionError('Assertion is missing its identity', 'invalid_assertion')\n }\n if (expect.host !== undefined && payload['host'] !== expect.host.toLowerCase().replace(/:\\d+$/, '')) {\n throw new AccessAssertionError('Assertion was issued for another host', 'wrong_host')\n }\n const groups = Array.isArray(payload['groups']) ? payload['groups'].filter((g): g is string => typeof g === 'string') : []\n return { ...payload, groups } as unknown as AccessIdentity\n }\n\n /** Reads the assertion and host from a request's headers (Node, Express, Fastify or fetch `Headers`). */\n verifyRequest(headers: HeaderBag): Promise<AccessIdentity> {\n const host = header(headers, 'host')\n return this.verify(header(headers, ACCESS_ASSERTION_HEADER), host ? { host } : {})\n }\n}\n\nexport function createAccessVerifier(opts: AccessVerifyOptions): AccessVerifier {\n return new AccessVerifier(opts)\n}\n","import { jwtVerify, createRemoteJWKSet, importJWK, decodeJwt, type JWK } from 'jose'\n\n// Server-side TillAuth verification.\n//\n// You configure once with the issuer + JWKS endpoint (or inline public keys),\n// then call `verify(token)` or use the framework-specific middleware.\n//\n// Verification is offline against the cached JWKS — no round-trip to TillAuth\n// on the hot path.\n\nconst DEFAULT_API = 'https://auth.tilldev.dev'\nconst REALM_SEGMENT = '/realms/'\n\nfunction trimSlash(s: string): string {\n return s.replace(/\\/+$/, '')\n}\n\n/**\n * Issuer acceptance check.\n *\n * A token this verifier may accept was minted for THIS app, so exactly two issuers\n * are legitimate: the canonical one (the app has no dedicated realm keys, so the\n * shared plane key signed it) or THIS app's own realm issuer. Nothing else.\n *\n * ⚠️ THIS USED TO ACCEPT ANY `{canonical}/realms/<anything>` when no issuer was\n * pinned. Together with a shared JWKS carrying every realm's key, that was a\n * cross-realm forgery (`atrium` `00` D13): holding any one realm's signing key was\n * enough to mint a token naming any OTHER realm. The signature verified (the key was\n * in the merged JWKS), the issuer passed this prefix test, and the `app` check\n * passed too — because the attacker wrote that claim as well.\n *\n * Note the `issuer` option was NOT the mitigation it looked like: pinning it\n * constrained the *claim* while the *key* still came from the merged set.\n *\n * The bare legacy `\"tillauth\"` string was retired 2026-07-20 and is not accepted.\n */\nfunction issuerAccepted(\n iss: unknown,\n canonical: string,\n realm: unknown,\n pinnedIssuer?: string,\n): boolean {\n if (typeof iss !== 'string' || iss.length === 0) return false\n if (pinnedIssuer) return iss === trimSlash(pinnedIssuer)\n const canon = trimSlash(canonical)\n return iss === canon || (typeof realm === 'string' && iss === `${canon}${REALM_SEGMENT}${realm}`)\n}\n\nexport interface VerifyOptions {\n /** The app's public id (`tau_…`), publishable key (`tau_pub_…`) or app id. Tokens for any other app are refused. */\n appId: string\n /** TillAuth API base. Defaults to https://auth.tilldev.dev */\n apiBase?: string\n /**\n * Pin verification to exactly one issuer. Once your app signs with its own key,\n * pin its issuer (`https://auth.tilldev.dev/realms/<app id>`) to refuse anything\n * signed with the shared key.\n *\n * When omitted, the verifier accepts the canonical HTTPS issuer (derived from\n * `apiBase`) and THIS app's own issuer — never another app's.\n */\n issuer?: string\n /**\n * Inline JWKS as a JWK array (the same `publicJwk` entries you'd find in\n * the TILLAUTH_JWT_KEYS env). Use this in environments where you want\n * fully air-gapped verification.\n */\n publicJwks?: JWK[]\n /** Require email_verified=true. Defaults to false. */\n requireVerifiedEmail?: boolean\n /** Require MFA was satisfied this session. Defaults to false. */\n requireMfa?: boolean\n}\n\nexport interface AuthClaims {\n /** User id */\n sub: string\n /** App id the token was issued for */\n app: string\n /** The app's public id. Absent on tokens minted before it existed. */\n pid?: string\n /** Lowercased email */\n email: string\n /** Whether MFA was satisfied this session */\n mfa: boolean\n /**\n * Whether the account's email was verified at issue time. Optional: tokens\n * minted before this claim existed omit it. When `requireVerifiedEmail` is\n * set, an absent claim is treated as unverified (fail-closed on the check).\n */\n email_verified?: boolean\n /** Token issuer — validated against the accepted set (see `issuerAccepted`). */\n iss?: string\n /** Issued-at (seconds) */\n iat: number\n /** Expires-at (seconds) */\n exp: number\n}\n\nexport class TillAuthVerifyError extends Error {\n code: 'no_token' | 'invalid_token' | 'wrong_app' | 'mfa_required' | 'email_not_verified' | 'invalid_issuer'\n constructor(message: string, code: TillAuthVerifyError['code']) {\n super(message)\n this.name = 'TillAuthVerifyError'\n this.code = code\n }\n}\n\nexport class TillAuthVerifier {\n private readonly opts: Required<Pick<VerifyOptions, 'appId' | 'apiBase' | 'requireVerifiedEmail' | 'requireMfa'>>\n private readonly pinnedIssuer: string | undefined\n private readonly remote: ReturnType<typeof createRemoteJWKSet> | null\n /** This app's own realm key set — see `keySetFor`. Null on the inline-keys path. */\n private readonly remoteRealm: ReturnType<typeof createRemoteJWKSet> | null = null\n private readonly inlineKeys: Promise<Array<{ kid?: string; key: Awaited<ReturnType<typeof importJWK>> }>> | null\n\n constructor(opts: VerifyOptions) {\n if (!opts.appId) throw new Error('TillAuthVerifier: appId is required')\n this.opts = {\n appId: opts.appId.startsWith('tau_pub_') ? 'tau_' + opts.appId.slice('tau_pub_'.length) : opts.appId,\n apiBase: (opts.apiBase ?? DEFAULT_API).replace(/\\/$/, ''),\n requireVerifiedEmail: opts.requireVerifiedEmail ?? false,\n requireMfa: opts.requireMfa ?? false,\n }\n this.pinnedIssuer = opts.issuer ? opts.issuer.replace(/\\/$/, '') : undefined\n if (opts.publicJwks && opts.publicJwks.length > 0) {\n this.remote = null\n this.inlineKeys = Promise.all(\n opts.publicJwks.map(async (k) => ({ kid: k.kid, key: await importJWK(k, 'EdDSA') })),\n )\n } else {\n /*\n * TWO key sets, selected by the token's issuer (`atrium` `00` D13 F4).\n *\n * `remote` is the shared plane's set — after the D13 F6 change it holds ONLY\n * the global signing keys, never a realm's. `remoteRealm` is this app's own\n * realm set, which holds only this realm's keys and 404s if the app has no\n * dedicated keys.\n *\n * Because neither set can contain ANOTHER realm's key, a forged token naming a\n * different realm cannot be verified from either — the isolation is structural\n * rather than a claim check. This replaced a single `createRemoteJWKSet` on the\n * merged JWKS, where every realm's key was resolvable by `kid` and the realm\n * checks below were therefore decorative.\n */\n this.remote = createRemoteJWKSet(new URL(`${this.opts.apiBase}/.well-known/jwks.json`))\n this.remoteRealm = createRemoteJWKSet(\n new URL(`${this.opts.apiBase}${REALM_SEGMENT}${this.opts.appId}/.well-known/jwks.json`),\n )\n this.inlineKeys = null\n }\n }\n\n /**\n * The key set entitled to have signed a token carrying `iss`, or `null` if no set\n * is. Selecting on the UNVERIFIED issuer is safe because the choice is then\n * enforced: `jwtVerify` pins `issuer` to the same value, so a token that lies about\n * its issuer to reach a different key set must still verify against that set — and\n * that set contains only keys entitled to the issuer it named.\n */\n private keySetFor(payload: { iss?: unknown; app?: unknown; pid?: unknown }): ReturnType<typeof createRemoteJWKSet> | null {\n if (typeof payload.iss !== 'string') return null\n if (payload.iss === this.opts.apiBase) return this.remote\n // The app's own key set is fetched by the configured app, so it holds only this app's keys.\n if (\n this.matchesApp(payload) &&\n typeof payload.app === 'string' &&\n payload.iss === `${this.opts.apiBase}${REALM_SEGMENT}${payload.app}`\n ) {\n return this.remoteRealm\n }\n return null\n }\n\n private matchesApp(claims: { app?: unknown; pid?: unknown }): boolean {\n return claims.app === this.opts.appId || claims.pid === this.opts.appId\n }\n\n async verify(token: string): Promise<AuthClaims> {\n if (!token) throw new TillAuthVerifyError('No token', 'no_token')\n\n let claims: AuthClaims\n try {\n if (this.inlineKeys) {\n const keys = await this.inlineKeys\n let lastErr: unknown\n for (const k of keys) {\n try {\n // No jose `issuer` pin — the issuer is one of several accepted values\n // (legacy string / canonical HTTPS / per-realm), checked below.\n const { payload } = await jwtVerify(token, k.key, { algorithms: ['EdDSA'] })\n claims = payload as unknown as AuthClaims\n return this.applyClaimsChecks(claims)\n } catch (e) {\n lastErr = e\n }\n }\n throw lastErr ?? new Error('No key matched')\n }\n /*\n * Pick the key set from the token's own issuer, then PIN that issuer during\n * verification. The pin is what makes reading an unverified claim safe: it\n * cannot be used to reach a key set and then disown the issuer that selected\n * it. A token naming a realm this verifier does not serve gets no key set at\n * all and is refused before any crypto runs.\n */\n const unverified = decodeJwt(token)\n const keySet = this.keySetFor(unverified)\n if (!keySet) {\n throw new TillAuthVerifyError('Untrusted token issuer', 'invalid_issuer')\n }\n const { payload } = await jwtVerify(token, keySet, {\n algorithms: ['EdDSA'],\n issuer: unverified.iss as string,\n })\n claims = payload as unknown as AuthClaims\n } catch (e) {\n // Preserve a specific claims verdict (wrong_app / invalid_issuer) thrown by\n // applyClaimsChecks on the inline path; only a genuine signature/JWKS\n // failure becomes the generic invalid_token.\n if (e instanceof TillAuthVerifyError) throw e\n throw new TillAuthVerifyError(\n e instanceof Error ? e.message : 'Invalid token',\n 'invalid_token',\n )\n }\n return this.applyClaimsChecks(claims)\n }\n\n private applyClaimsChecks(claims: AuthClaims): AuthClaims {\n // Issuer is validated here (jose's exact `issuer` pin is intentionally not\n // used) so per-realm HTTPS issuers verify alongside the canonical one.\n if (!this.matchesApp(claims)) {\n throw new TillAuthVerifyError(`Token issued for different app`, 'wrong_app')\n }\n if (!issuerAccepted(claims.iss, this.opts.apiBase, claims.app, this.pinnedIssuer)) {\n throw new TillAuthVerifyError('Untrusted token issuer', 'invalid_issuer')\n }\n if (this.opts.requireMfa && !claims.mfa) {\n throw new TillAuthVerifyError('MFA required', 'mfa_required')\n }\n // Absent claim (older tokens) is treated as unverified only when the check\n // is enabled — so requireVerifiedEmail fails closed rather than silently\n // passing when the issuer predates the claim.\n if (this.opts.requireVerifiedEmail && claims.email_verified !== true) {\n throw new TillAuthVerifyError('Email not verified', 'email_not_verified')\n }\n return claims\n }\n\n /**\n * Pull a Bearer token out of an Authorization header and verify. Returns\n * the claims or throws TillAuthVerifyError.\n */\n async verifyHeader(authHeader: string | undefined | null): Promise<AuthClaims> {\n if (!authHeader || !authHeader.startsWith('Bearer ')) {\n throw new TillAuthVerifyError('Missing Bearer token', 'no_token')\n }\n return this.verify(authHeader.slice(7))\n }\n}\n\n// ── Framework adapters ──────────────────────────────────────────────────────\n\n// Express-style middleware. Adds `req.user` on success.\nexport function expressMiddleware(verifier: TillAuthVerifier, opts: { onError?: (err: TillAuthVerifyError) => unknown } = {}) {\n return async function (req: ExpressLikeReq, res: ExpressLikeRes, next: () => void) {\n try {\n const h = req.headers?.authorization\n const claims = await verifier.verifyHeader(Array.isArray(h) ? h[0] : h)\n ;(req as ExpressLikeReq & { user: AuthClaims }).user = claims\n next()\n } catch (e) {\n const err = e instanceof TillAuthVerifyError ? e : new TillAuthVerifyError('Auth failed', 'invalid_token')\n if (opts.onError) {\n const out = opts.onError(err)\n if (out !== undefined) {\n res.status(401).json(out as Record<string, unknown>)\n return\n }\n }\n res.status(401).json({ error: err.message, code: err.code })\n }\n }\n}\n\ninterface ExpressLikeReq {\n headers?: { authorization?: string | string[] }\n}\ninterface ExpressLikeRes {\n status: (n: number) => { json: (b: Record<string, unknown>) => void }\n}\n\n// Fastify plugin shape — not bound to Fastify's types to avoid a peer dep.\nexport function fastifyHook(verifier: TillAuthVerifier) {\n return async function (\n req: { headers: Record<string, string | string[] | undefined>; user?: AuthClaims },\n reply: { code: (n: number) => { send: (b: unknown) => void } },\n ) {\n try {\n const h = req.headers['authorization']\n const auth = Array.isArray(h) ? h[0] : h\n const claims = await verifier.verifyHeader(auth)\n req.user = claims\n } catch (e) {\n const err = e instanceof TillAuthVerifyError ? e : new TillAuthVerifyError('Auth failed', 'invalid_token')\n reply.code(401).send({ error: err.message, code: err.code })\n }\n }\n}\n\n// Convenience entrypoint for users who want a one-call setup.\nexport function createVerifier(opts: VerifyOptions): TillAuthVerifier {\n return new TillAuthVerifier(opts)\n}\n\nexport { createFetchHandler, expressCookieProxy } from './cookie-proxy.js'\nexport type { CookieProxyOptions } from './cookie-proxy.js'\nexport { verifyWebhook, TillAuthWebhookError } from './webhooks.js'\nexport { AccessVerifier, AccessAssertionError, createAccessVerifier, ACCESS_ASSERTION_HEADER } from './access.js'\nexport type { AccessIdentity, AccessVerifyOptions } from './access.js'\nexport type { TillAuthWebhookEvent, VerifyWebhookOptions, WebhookErrorReason } from './webhooks.js'\n"]}
|