@tillstack/auth-node 0.3.1 → 0.4.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 +39 -3
- package/dist/index.cjs +67 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +39 -9
- package/dist/index.d.ts +39 -9
- package/dist/index.js +66 -15
- package/dist/index.js.map +1 -1
- package/package.json +9 -10
package/README.md
CHANGED
|
@@ -17,8 +17,9 @@ npm install @tillstack/auth-node
|
|
|
17
17
|
import { createVerifier } from '@tillstack/auth-node'
|
|
18
18
|
|
|
19
19
|
const verifier = createVerifier({
|
|
20
|
-
appId: 'tau_your_app_id', //
|
|
20
|
+
appId: 'tau_your_app_id', // the app's public id (its publishable key or app id works too)
|
|
21
21
|
// apiBase: 'https://auth.tilldev.dev', // default
|
|
22
|
+
// issuer: 'https://auth.tilldev.dev/realms/<app id>', // refuse the shared key once the app has its own
|
|
22
23
|
// requireMfa: true,
|
|
23
24
|
})
|
|
24
25
|
|
|
@@ -48,8 +49,43 @@ const verifier = createVerifier({ appId: 'tau_your_app_id' })
|
|
|
48
49
|
fastify.addHook('preHandler', fastifyHook(verifier)) // sets req.user
|
|
49
50
|
```
|
|
50
51
|
|
|
52
|
+
### Webhooks
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
import express from 'express'
|
|
56
|
+
import { verifyWebhook, TillAuthWebhookError } from '@tillstack/auth-node'
|
|
57
|
+
|
|
58
|
+
const app = express()
|
|
59
|
+
|
|
60
|
+
// Express: mount with express.raw({ type: 'application/json' }) so req.body is the raw bytes.
|
|
61
|
+
app.post('/webhooks/tillauth', express.raw({ type: 'application/json' }), async (req, res) => {
|
|
62
|
+
try {
|
|
63
|
+
const event = await verifyWebhook(req.body, req.get('x-tillauth-signature'), process.env.TILLAUTH_WEBHOOK_SECRET!)
|
|
64
|
+
// event.type, event.user_id, event.delivery_id
|
|
65
|
+
res.sendStatus(204)
|
|
66
|
+
} catch (e) {
|
|
67
|
+
if (e instanceof TillAuthWebhookError) return res.status(400).send(e.reason)
|
|
68
|
+
throw e
|
|
69
|
+
}
|
|
70
|
+
})
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Rejects a bad signature, a timestamp older than five minutes, and a body that was re-serialized.
|
|
74
|
+
|
|
75
|
+
### Signing keys
|
|
76
|
+
|
|
77
|
+
The verifier picks the key set from the token's issuer: tokens signed with the
|
|
78
|
+
app's own key are checked against
|
|
79
|
+
`https://auth.tilldev.dev/realms/<appId>/.well-known/jwks.json`, which holds only
|
|
80
|
+
that app's keys; tokens from an app still on the shared key are checked against
|
|
81
|
+
`https://auth.tilldev.dev/.well-known/jwks.json`. Another app's tokens are
|
|
82
|
+
refused either way. Pin `issuer` to the app's issuer to refuse the shared key
|
|
83
|
+
entirely.
|
|
84
|
+
|
|
51
85
|
For fully air-gapped verification, pass inline public keys via `publicJwks`
|
|
52
|
-
instead of letting the SDK fetch the
|
|
86
|
+
instead of letting the SDK fetch them. A rotation publishes the new key 15
|
|
87
|
+
minutes before it signs and the previous key verifies for 24 hours after, so
|
|
88
|
+
update the inline keys when you rotate.
|
|
53
89
|
|
|
54
90
|
## Docs
|
|
55
91
|
|
|
@@ -57,4 +93,4 @@ Full documentation: https://tilldev.dev/docs
|
|
|
57
93
|
|
|
58
94
|
## License
|
|
59
95
|
|
|
60
|
-
|
|
96
|
+
Apache-2.0
|
package/dist/index.cjs
CHANGED
|
@@ -135,17 +135,63 @@ function parseCookieHeader(header) {
|
|
|
135
135
|
return out;
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
+
// src/webhooks.ts
|
|
139
|
+
var TillAuthWebhookError = class extends Error {
|
|
140
|
+
constructor(reason) {
|
|
141
|
+
super(`TillAuth webhook rejected: ${reason}`);
|
|
142
|
+
this.reason = reason;
|
|
143
|
+
this.name = "TillAuthWebhookError";
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
var enc = new TextEncoder();
|
|
147
|
+
function hexBytes(hex) {
|
|
148
|
+
if (hex.length === 0 || hex.length % 2 !== 0 || !/^[0-9a-f]+$/i.test(hex)) return null;
|
|
149
|
+
const out = new Uint8Array(hex.length / 2);
|
|
150
|
+
for (let i = 0; i < out.length; i++) out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
151
|
+
return out;
|
|
152
|
+
}
|
|
153
|
+
async function verifyWebhook(rawBody, signatureHeader, secret, opts = {}) {
|
|
154
|
+
if (!signatureHeader) throw new TillAuthWebhookError("missing_signature");
|
|
155
|
+
let t = null;
|
|
156
|
+
const sigs = [];
|
|
157
|
+
for (const part of signatureHeader.split(",")) {
|
|
158
|
+
const [k, v] = part.trim().split("=", 2);
|
|
159
|
+
if (k === "t" && v && /^\d+$/.test(v)) t = Number(v);
|
|
160
|
+
else if (k === "v1" && v) {
|
|
161
|
+
const b = hexBytes(v);
|
|
162
|
+
if (b) sigs.push(b);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (t === null || sigs.length === 0) throw new TillAuthWebhookError("malformed_signature");
|
|
166
|
+
const now = opts.now ?? Math.floor(Date.now() / 1e3);
|
|
167
|
+
if (Math.abs(now - t) > (opts.toleranceSeconds ?? 300)) throw new TillAuthWebhookError("stale");
|
|
168
|
+
const body = typeof rawBody === "string" ? enc.encode(rawBody) : rawBody;
|
|
169
|
+
const prefix = enc.encode(`${t}.`);
|
|
170
|
+
const signed = new Uint8Array(prefix.length + body.length);
|
|
171
|
+
signed.set(prefix);
|
|
172
|
+
signed.set(body, prefix.length);
|
|
173
|
+
const key = await crypto.subtle.importKey("raw", enc.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["verify"]);
|
|
174
|
+
let ok = false;
|
|
175
|
+
for (const s of sigs) if (await crypto.subtle.verify("HMAC", key, s, signed)) ok = true;
|
|
176
|
+
if (!ok) throw new TillAuthWebhookError("bad_signature");
|
|
177
|
+
try {
|
|
178
|
+
return JSON.parse(new TextDecoder().decode(body));
|
|
179
|
+
} catch {
|
|
180
|
+
throw new TillAuthWebhookError("bad_payload");
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
138
184
|
// src/index.ts
|
|
139
185
|
var DEFAULT_API2 = "https://auth.tilldev.dev";
|
|
140
186
|
var REALM_SEGMENT = "/realms/";
|
|
141
187
|
function trimSlash(s) {
|
|
142
188
|
return s.replace(/\/+$/, "");
|
|
143
189
|
}
|
|
144
|
-
function issuerAccepted(iss, canonical,
|
|
190
|
+
function issuerAccepted(iss, canonical, realm, pinnedIssuer) {
|
|
145
191
|
if (typeof iss !== "string" || iss.length === 0) return false;
|
|
146
192
|
if (pinnedIssuer) return iss === trimSlash(pinnedIssuer);
|
|
147
193
|
const canon = trimSlash(canonical);
|
|
148
|
-
return iss === canon || iss === `${canon}${REALM_SEGMENT}${
|
|
194
|
+
return iss === canon || typeof realm === "string" && iss === `${canon}${REALM_SEGMENT}${realm}`;
|
|
149
195
|
}
|
|
150
196
|
var TillAuthVerifyError = class extends Error {
|
|
151
197
|
constructor(message, code) {
|
|
@@ -160,7 +206,7 @@ var TillAuthVerifier = class {
|
|
|
160
206
|
this.remoteRealm = null;
|
|
161
207
|
if (!opts.appId) throw new Error("TillAuthVerifier: appId is required");
|
|
162
208
|
this.opts = {
|
|
163
|
-
appId: opts.appId,
|
|
209
|
+
appId: opts.appId.startsWith("tau_pub_") ? "tau_" + opts.appId.slice("tau_pub_".length) : opts.appId,
|
|
164
210
|
apiBase: (opts.apiBase ?? DEFAULT_API2).replace(/\/$/, ""),
|
|
165
211
|
requireVerifiedEmail: opts.requireVerifiedEmail ?? false,
|
|
166
212
|
requireMfa: opts.requireMfa ?? false
|
|
@@ -186,12 +232,17 @@ var TillAuthVerifier = class {
|
|
|
186
232
|
* its issuer to reach a different key set must still verify against that set — and
|
|
187
233
|
* that set contains only keys entitled to the issuer it named.
|
|
188
234
|
*/
|
|
189
|
-
keySetFor(
|
|
190
|
-
if (typeof iss !== "string") return null;
|
|
191
|
-
if (iss === this.opts.apiBase) return this.remote;
|
|
192
|
-
if (iss === `${this.opts.apiBase}${REALM_SEGMENT}${
|
|
235
|
+
keySetFor(payload) {
|
|
236
|
+
if (typeof payload.iss !== "string") return null;
|
|
237
|
+
if (payload.iss === this.opts.apiBase) return this.remote;
|
|
238
|
+
if (this.matchesApp(payload) && typeof payload.app === "string" && payload.iss === `${this.opts.apiBase}${REALM_SEGMENT}${payload.app}`) {
|
|
239
|
+
return this.remoteRealm;
|
|
240
|
+
}
|
|
193
241
|
return null;
|
|
194
242
|
}
|
|
243
|
+
matchesApp(claims) {
|
|
244
|
+
return claims.app === this.opts.appId || claims.pid === this.opts.appId;
|
|
245
|
+
}
|
|
195
246
|
async verify(token) {
|
|
196
247
|
if (!token) throw new TillAuthVerifyError("No token", "no_token");
|
|
197
248
|
let claims;
|
|
@@ -210,14 +261,14 @@ var TillAuthVerifier = class {
|
|
|
210
261
|
}
|
|
211
262
|
throw lastErr ?? new Error("No key matched");
|
|
212
263
|
}
|
|
213
|
-
const
|
|
214
|
-
const keySet = this.keySetFor(
|
|
264
|
+
const unverified = jose.decodeJwt(token);
|
|
265
|
+
const keySet = this.keySetFor(unverified);
|
|
215
266
|
if (!keySet) {
|
|
216
267
|
throw new TillAuthVerifyError("Untrusted token issuer", "invalid_issuer");
|
|
217
268
|
}
|
|
218
269
|
const { payload } = await jose.jwtVerify(token, keySet, {
|
|
219
270
|
algorithms: ["EdDSA"],
|
|
220
|
-
issuer:
|
|
271
|
+
issuer: unverified.iss
|
|
221
272
|
});
|
|
222
273
|
claims = payload;
|
|
223
274
|
} catch (e) {
|
|
@@ -230,12 +281,12 @@ var TillAuthVerifier = class {
|
|
|
230
281
|
return this.applyClaimsChecks(claims);
|
|
231
282
|
}
|
|
232
283
|
applyClaimsChecks(claims) {
|
|
233
|
-
if (!
|
|
234
|
-
throw new TillAuthVerifyError("Untrusted token issuer", "invalid_issuer");
|
|
235
|
-
}
|
|
236
|
-
if (claims.app !== this.opts.appId) {
|
|
284
|
+
if (!this.matchesApp(claims)) {
|
|
237
285
|
throw new TillAuthVerifyError(`Token issued for different app`, "wrong_app");
|
|
238
286
|
}
|
|
287
|
+
if (!issuerAccepted(claims.iss, this.opts.apiBase, claims.app, this.pinnedIssuer)) {
|
|
288
|
+
throw new TillAuthVerifyError("Untrusted token issuer", "invalid_issuer");
|
|
289
|
+
}
|
|
239
290
|
if (this.opts.requireMfa && !claims.mfa) {
|
|
240
291
|
throw new TillAuthVerifyError("MFA required", "mfa_required");
|
|
241
292
|
}
|
|
@@ -294,10 +345,12 @@ function createVerifier(opts) {
|
|
|
294
345
|
|
|
295
346
|
exports.TillAuthVerifier = TillAuthVerifier;
|
|
296
347
|
exports.TillAuthVerifyError = TillAuthVerifyError;
|
|
348
|
+
exports.TillAuthWebhookError = TillAuthWebhookError;
|
|
297
349
|
exports.createFetchHandler = createFetchHandler;
|
|
298
350
|
exports.createVerifier = createVerifier;
|
|
299
351
|
exports.expressCookieProxy = expressCookieProxy;
|
|
300
352
|
exports.expressMiddleware = expressMiddleware;
|
|
301
353
|
exports.fastifyHook = fastifyHook;
|
|
354
|
+
exports.verifyWebhook = verifyWebhook;
|
|
302
355
|
//# sourceMappingURL=index.cjs.map
|
|
303
356
|
//# sourceMappingURL=index.cjs.map
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/cookie-proxy.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","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 appId: string,\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 || iss === `${canon}${REALM_SEGMENT}${appId}`\n}\n\nexport interface VerifyOptions {\n /** TillAuth public app id (`tau_…`). Used to enforce app scoping. */\n appId: string\n /** TillAuth API base. Defaults to https://auth.tilldev.dev */\n apiBase?: string\n /**\n * Pin verification to exactly one issuer (e.g. a dedicated realm's\n * `https://auth.tilldev.dev/realms/<app>`).\n *\n * Rarely needed now: when omitted, the verifier accepts the canonical HTTPS issuer\n * (derived from `apiBase`) and THIS app's own realm issuer — never another realm's.\n * It previously accepted any realm under `apiBase`, which is what made this option\n * look load-bearing; it was not, because the key still came from a merged JWKS\n * (`atrium` `00` D13). The retired legacy `\"tillauth\"` string is never accepted.\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 /** 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,\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(iss: unknown): ReturnType<typeof createRemoteJWKSet> | null {\n if (typeof iss !== 'string') return null\n if (iss === this.opts.apiBase) return this.remote\n if (iss === `${this.opts.apiBase}${REALM_SEGMENT}${this.opts.appId}`) return this.remoteRealm\n return null\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 unverifiedIss = decodeJwt(token).iss\n const keySet = this.keySetFor(unverifiedIss)\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: unverifiedIss 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 (!issuerAccepted(claims.iss, this.opts.apiBase, this.opts.appId, this.pinnedIssuer)) {\n throw new TillAuthVerifyError('Untrusted token issuer', 'invalid_issuer')\n }\n if (claims.app !== this.opts.appId) {\n throw new TillAuthVerifyError(`Token issued for different app`, 'wrong_app')\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'\n"]}
|
|
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"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -36,20 +36,47 @@ interface ExpRes {
|
|
|
36
36
|
}
|
|
37
37
|
declare function expressCookieProxy(opts: CookieProxyOptions): (req: ExpReq, res: ExpRes) => Promise<void>;
|
|
38
38
|
|
|
39
|
+
interface TillAuthWebhookEvent {
|
|
40
|
+
type: string;
|
|
41
|
+
result: string;
|
|
42
|
+
ts: string;
|
|
43
|
+
app_id: string;
|
|
44
|
+
user_id: string | null;
|
|
45
|
+
ip: string | null;
|
|
46
|
+
user_agent: string | null;
|
|
47
|
+
metadata: Record<string, unknown>;
|
|
48
|
+
delivery_id: string;
|
|
49
|
+
attempt: number;
|
|
50
|
+
}
|
|
51
|
+
type WebhookErrorReason = 'missing_signature' | 'malformed_signature' | 'stale' | 'bad_signature' | 'bad_payload';
|
|
52
|
+
declare class TillAuthWebhookError extends Error {
|
|
53
|
+
readonly reason: WebhookErrorReason;
|
|
54
|
+
constructor(reason: WebhookErrorReason);
|
|
55
|
+
}
|
|
56
|
+
interface VerifyWebhookOptions {
|
|
57
|
+
/** Max age of the signed timestamp, in seconds. Default 300. */
|
|
58
|
+
toleranceSeconds?: number;
|
|
59
|
+
/** Unix seconds; for tests. */
|
|
60
|
+
now?: number;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Checks the signature over the exact bytes received, then parses them.
|
|
64
|
+
* Pass the raw body, never a re-serialized object.
|
|
65
|
+
*/
|
|
66
|
+
declare function verifyWebhook(rawBody: string | Uint8Array, signatureHeader: string | null | undefined, secret: string, opts?: VerifyWebhookOptions): Promise<TillAuthWebhookEvent>;
|
|
67
|
+
|
|
39
68
|
interface VerifyOptions {
|
|
40
|
-
/**
|
|
69
|
+
/** The app's public id (`tau_…`), publishable key (`tau_pub_…`) or app id. Tokens for any other app are refused. */
|
|
41
70
|
appId: string;
|
|
42
71
|
/** TillAuth API base. Defaults to https://auth.tilldev.dev */
|
|
43
72
|
apiBase?: string;
|
|
44
73
|
/**
|
|
45
|
-
* Pin verification to exactly one issuer
|
|
46
|
-
* `https://auth.tilldev.dev/realms/<app>`)
|
|
74
|
+
* Pin verification to exactly one issuer. Once your app signs with its own key,
|
|
75
|
+
* pin its issuer (`https://auth.tilldev.dev/realms/<app id>`) to refuse anything
|
|
76
|
+
* signed with the shared key.
|
|
47
77
|
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
* It previously accepted any realm under `apiBase`, which is what made this option
|
|
51
|
-
* look load-bearing; it was not, because the key still came from a merged JWKS
|
|
52
|
-
* (`atrium` `00` D13). The retired legacy `"tillauth"` string is never accepted.
|
|
78
|
+
* When omitted, the verifier accepts the canonical HTTPS issuer (derived from
|
|
79
|
+
* `apiBase`) and THIS app's own issuer — never another app's.
|
|
53
80
|
*/
|
|
54
81
|
issuer?: string;
|
|
55
82
|
/**
|
|
@@ -68,6 +95,8 @@ interface AuthClaims {
|
|
|
68
95
|
sub: string;
|
|
69
96
|
/** App id the token was issued for */
|
|
70
97
|
app: string;
|
|
98
|
+
/** The app's public id. Absent on tokens minted before it existed. */
|
|
99
|
+
pid?: string;
|
|
71
100
|
/** Lowercased email */
|
|
72
101
|
email: string;
|
|
73
102
|
/** Whether MFA was satisfied this session */
|
|
@@ -105,6 +134,7 @@ declare class TillAuthVerifier {
|
|
|
105
134
|
* that set contains only keys entitled to the issuer it named.
|
|
106
135
|
*/
|
|
107
136
|
private keySetFor;
|
|
137
|
+
private matchesApp;
|
|
108
138
|
verify(token: string): Promise<AuthClaims>;
|
|
109
139
|
private applyClaimsChecks;
|
|
110
140
|
/**
|
|
@@ -136,4 +166,4 @@ declare function fastifyHook(verifier: TillAuthVerifier): (req: {
|
|
|
136
166
|
}) => Promise<void>;
|
|
137
167
|
declare function createVerifier(opts: VerifyOptions): TillAuthVerifier;
|
|
138
168
|
|
|
139
|
-
export { type AuthClaims, type CookieProxyOptions, TillAuthVerifier, TillAuthVerifyError, type VerifyOptions, createFetchHandler, createVerifier, expressCookieProxy, expressMiddleware, fastifyHook };
|
|
169
|
+
export { type AuthClaims, type CookieProxyOptions, TillAuthVerifier, TillAuthVerifyError, TillAuthWebhookError, type TillAuthWebhookEvent, type VerifyOptions, type VerifyWebhookOptions, type WebhookErrorReason, createFetchHandler, createVerifier, expressCookieProxy, expressMiddleware, fastifyHook, verifyWebhook };
|
package/dist/index.d.ts
CHANGED
|
@@ -36,20 +36,47 @@ interface ExpRes {
|
|
|
36
36
|
}
|
|
37
37
|
declare function expressCookieProxy(opts: CookieProxyOptions): (req: ExpReq, res: ExpRes) => Promise<void>;
|
|
38
38
|
|
|
39
|
+
interface TillAuthWebhookEvent {
|
|
40
|
+
type: string;
|
|
41
|
+
result: string;
|
|
42
|
+
ts: string;
|
|
43
|
+
app_id: string;
|
|
44
|
+
user_id: string | null;
|
|
45
|
+
ip: string | null;
|
|
46
|
+
user_agent: string | null;
|
|
47
|
+
metadata: Record<string, unknown>;
|
|
48
|
+
delivery_id: string;
|
|
49
|
+
attempt: number;
|
|
50
|
+
}
|
|
51
|
+
type WebhookErrorReason = 'missing_signature' | 'malformed_signature' | 'stale' | 'bad_signature' | 'bad_payload';
|
|
52
|
+
declare class TillAuthWebhookError extends Error {
|
|
53
|
+
readonly reason: WebhookErrorReason;
|
|
54
|
+
constructor(reason: WebhookErrorReason);
|
|
55
|
+
}
|
|
56
|
+
interface VerifyWebhookOptions {
|
|
57
|
+
/** Max age of the signed timestamp, in seconds. Default 300. */
|
|
58
|
+
toleranceSeconds?: number;
|
|
59
|
+
/** Unix seconds; for tests. */
|
|
60
|
+
now?: number;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Checks the signature over the exact bytes received, then parses them.
|
|
64
|
+
* Pass the raw body, never a re-serialized object.
|
|
65
|
+
*/
|
|
66
|
+
declare function verifyWebhook(rawBody: string | Uint8Array, signatureHeader: string | null | undefined, secret: string, opts?: VerifyWebhookOptions): Promise<TillAuthWebhookEvent>;
|
|
67
|
+
|
|
39
68
|
interface VerifyOptions {
|
|
40
|
-
/**
|
|
69
|
+
/** The app's public id (`tau_…`), publishable key (`tau_pub_…`) or app id. Tokens for any other app are refused. */
|
|
41
70
|
appId: string;
|
|
42
71
|
/** TillAuth API base. Defaults to https://auth.tilldev.dev */
|
|
43
72
|
apiBase?: string;
|
|
44
73
|
/**
|
|
45
|
-
* Pin verification to exactly one issuer
|
|
46
|
-
* `https://auth.tilldev.dev/realms/<app>`)
|
|
74
|
+
* Pin verification to exactly one issuer. Once your app signs with its own key,
|
|
75
|
+
* pin its issuer (`https://auth.tilldev.dev/realms/<app id>`) to refuse anything
|
|
76
|
+
* signed with the shared key.
|
|
47
77
|
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
* It previously accepted any realm under `apiBase`, which is what made this option
|
|
51
|
-
* look load-bearing; it was not, because the key still came from a merged JWKS
|
|
52
|
-
* (`atrium` `00` D13). The retired legacy `"tillauth"` string is never accepted.
|
|
78
|
+
* When omitted, the verifier accepts the canonical HTTPS issuer (derived from
|
|
79
|
+
* `apiBase`) and THIS app's own issuer — never another app's.
|
|
53
80
|
*/
|
|
54
81
|
issuer?: string;
|
|
55
82
|
/**
|
|
@@ -68,6 +95,8 @@ interface AuthClaims {
|
|
|
68
95
|
sub: string;
|
|
69
96
|
/** App id the token was issued for */
|
|
70
97
|
app: string;
|
|
98
|
+
/** The app's public id. Absent on tokens minted before it existed. */
|
|
99
|
+
pid?: string;
|
|
71
100
|
/** Lowercased email */
|
|
72
101
|
email: string;
|
|
73
102
|
/** Whether MFA was satisfied this session */
|
|
@@ -105,6 +134,7 @@ declare class TillAuthVerifier {
|
|
|
105
134
|
* that set contains only keys entitled to the issuer it named.
|
|
106
135
|
*/
|
|
107
136
|
private keySetFor;
|
|
137
|
+
private matchesApp;
|
|
108
138
|
verify(token: string): Promise<AuthClaims>;
|
|
109
139
|
private applyClaimsChecks;
|
|
110
140
|
/**
|
|
@@ -136,4 +166,4 @@ declare function fastifyHook(verifier: TillAuthVerifier): (req: {
|
|
|
136
166
|
}) => Promise<void>;
|
|
137
167
|
declare function createVerifier(opts: VerifyOptions): TillAuthVerifier;
|
|
138
168
|
|
|
139
|
-
export { type AuthClaims, type CookieProxyOptions, TillAuthVerifier, TillAuthVerifyError, type VerifyOptions, createFetchHandler, createVerifier, expressCookieProxy, expressMiddleware, fastifyHook };
|
|
169
|
+
export { type AuthClaims, type CookieProxyOptions, TillAuthVerifier, TillAuthVerifyError, TillAuthWebhookError, type TillAuthWebhookEvent, type VerifyOptions, type VerifyWebhookOptions, type WebhookErrorReason, createFetchHandler, createVerifier, expressCookieProxy, expressMiddleware, fastifyHook, verifyWebhook };
|
package/dist/index.js
CHANGED
|
@@ -133,17 +133,63 @@ function parseCookieHeader(header) {
|
|
|
133
133
|
return out;
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
+
// src/webhooks.ts
|
|
137
|
+
var TillAuthWebhookError = class extends Error {
|
|
138
|
+
constructor(reason) {
|
|
139
|
+
super(`TillAuth webhook rejected: ${reason}`);
|
|
140
|
+
this.reason = reason;
|
|
141
|
+
this.name = "TillAuthWebhookError";
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
var enc = new TextEncoder();
|
|
145
|
+
function hexBytes(hex) {
|
|
146
|
+
if (hex.length === 0 || hex.length % 2 !== 0 || !/^[0-9a-f]+$/i.test(hex)) return null;
|
|
147
|
+
const out = new Uint8Array(hex.length / 2);
|
|
148
|
+
for (let i = 0; i < out.length; i++) out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
149
|
+
return out;
|
|
150
|
+
}
|
|
151
|
+
async function verifyWebhook(rawBody, signatureHeader, secret, opts = {}) {
|
|
152
|
+
if (!signatureHeader) throw new TillAuthWebhookError("missing_signature");
|
|
153
|
+
let t = null;
|
|
154
|
+
const sigs = [];
|
|
155
|
+
for (const part of signatureHeader.split(",")) {
|
|
156
|
+
const [k, v] = part.trim().split("=", 2);
|
|
157
|
+
if (k === "t" && v && /^\d+$/.test(v)) t = Number(v);
|
|
158
|
+
else if (k === "v1" && v) {
|
|
159
|
+
const b = hexBytes(v);
|
|
160
|
+
if (b) sigs.push(b);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (t === null || sigs.length === 0) throw new TillAuthWebhookError("malformed_signature");
|
|
164
|
+
const now = opts.now ?? Math.floor(Date.now() / 1e3);
|
|
165
|
+
if (Math.abs(now - t) > (opts.toleranceSeconds ?? 300)) throw new TillAuthWebhookError("stale");
|
|
166
|
+
const body = typeof rawBody === "string" ? enc.encode(rawBody) : rawBody;
|
|
167
|
+
const prefix = enc.encode(`${t}.`);
|
|
168
|
+
const signed = new Uint8Array(prefix.length + body.length);
|
|
169
|
+
signed.set(prefix);
|
|
170
|
+
signed.set(body, prefix.length);
|
|
171
|
+
const key = await crypto.subtle.importKey("raw", enc.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["verify"]);
|
|
172
|
+
let ok = false;
|
|
173
|
+
for (const s of sigs) if (await crypto.subtle.verify("HMAC", key, s, signed)) ok = true;
|
|
174
|
+
if (!ok) throw new TillAuthWebhookError("bad_signature");
|
|
175
|
+
try {
|
|
176
|
+
return JSON.parse(new TextDecoder().decode(body));
|
|
177
|
+
} catch {
|
|
178
|
+
throw new TillAuthWebhookError("bad_payload");
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
136
182
|
// src/index.ts
|
|
137
183
|
var DEFAULT_API2 = "https://auth.tilldev.dev";
|
|
138
184
|
var REALM_SEGMENT = "/realms/";
|
|
139
185
|
function trimSlash(s) {
|
|
140
186
|
return s.replace(/\/+$/, "");
|
|
141
187
|
}
|
|
142
|
-
function issuerAccepted(iss, canonical,
|
|
188
|
+
function issuerAccepted(iss, canonical, realm, pinnedIssuer) {
|
|
143
189
|
if (typeof iss !== "string" || iss.length === 0) return false;
|
|
144
190
|
if (pinnedIssuer) return iss === trimSlash(pinnedIssuer);
|
|
145
191
|
const canon = trimSlash(canonical);
|
|
146
|
-
return iss === canon || iss === `${canon}${REALM_SEGMENT}${
|
|
192
|
+
return iss === canon || typeof realm === "string" && iss === `${canon}${REALM_SEGMENT}${realm}`;
|
|
147
193
|
}
|
|
148
194
|
var TillAuthVerifyError = class extends Error {
|
|
149
195
|
constructor(message, code) {
|
|
@@ -158,7 +204,7 @@ var TillAuthVerifier = class {
|
|
|
158
204
|
this.remoteRealm = null;
|
|
159
205
|
if (!opts.appId) throw new Error("TillAuthVerifier: appId is required");
|
|
160
206
|
this.opts = {
|
|
161
|
-
appId: opts.appId,
|
|
207
|
+
appId: opts.appId.startsWith("tau_pub_") ? "tau_" + opts.appId.slice("tau_pub_".length) : opts.appId,
|
|
162
208
|
apiBase: (opts.apiBase ?? DEFAULT_API2).replace(/\/$/, ""),
|
|
163
209
|
requireVerifiedEmail: opts.requireVerifiedEmail ?? false,
|
|
164
210
|
requireMfa: opts.requireMfa ?? false
|
|
@@ -184,12 +230,17 @@ var TillAuthVerifier = class {
|
|
|
184
230
|
* its issuer to reach a different key set must still verify against that set — and
|
|
185
231
|
* that set contains only keys entitled to the issuer it named.
|
|
186
232
|
*/
|
|
187
|
-
keySetFor(
|
|
188
|
-
if (typeof iss !== "string") return null;
|
|
189
|
-
if (iss === this.opts.apiBase) return this.remote;
|
|
190
|
-
if (iss === `${this.opts.apiBase}${REALM_SEGMENT}${
|
|
233
|
+
keySetFor(payload) {
|
|
234
|
+
if (typeof payload.iss !== "string") return null;
|
|
235
|
+
if (payload.iss === this.opts.apiBase) return this.remote;
|
|
236
|
+
if (this.matchesApp(payload) && typeof payload.app === "string" && payload.iss === `${this.opts.apiBase}${REALM_SEGMENT}${payload.app}`) {
|
|
237
|
+
return this.remoteRealm;
|
|
238
|
+
}
|
|
191
239
|
return null;
|
|
192
240
|
}
|
|
241
|
+
matchesApp(claims) {
|
|
242
|
+
return claims.app === this.opts.appId || claims.pid === this.opts.appId;
|
|
243
|
+
}
|
|
193
244
|
async verify(token) {
|
|
194
245
|
if (!token) throw new TillAuthVerifyError("No token", "no_token");
|
|
195
246
|
let claims;
|
|
@@ -208,14 +259,14 @@ var TillAuthVerifier = class {
|
|
|
208
259
|
}
|
|
209
260
|
throw lastErr ?? new Error("No key matched");
|
|
210
261
|
}
|
|
211
|
-
const
|
|
212
|
-
const keySet = this.keySetFor(
|
|
262
|
+
const unverified = decodeJwt(token);
|
|
263
|
+
const keySet = this.keySetFor(unverified);
|
|
213
264
|
if (!keySet) {
|
|
214
265
|
throw new TillAuthVerifyError("Untrusted token issuer", "invalid_issuer");
|
|
215
266
|
}
|
|
216
267
|
const { payload } = await jwtVerify(token, keySet, {
|
|
217
268
|
algorithms: ["EdDSA"],
|
|
218
|
-
issuer:
|
|
269
|
+
issuer: unverified.iss
|
|
219
270
|
});
|
|
220
271
|
claims = payload;
|
|
221
272
|
} catch (e) {
|
|
@@ -228,12 +279,12 @@ var TillAuthVerifier = class {
|
|
|
228
279
|
return this.applyClaimsChecks(claims);
|
|
229
280
|
}
|
|
230
281
|
applyClaimsChecks(claims) {
|
|
231
|
-
if (!
|
|
232
|
-
throw new TillAuthVerifyError("Untrusted token issuer", "invalid_issuer");
|
|
233
|
-
}
|
|
234
|
-
if (claims.app !== this.opts.appId) {
|
|
282
|
+
if (!this.matchesApp(claims)) {
|
|
235
283
|
throw new TillAuthVerifyError(`Token issued for different app`, "wrong_app");
|
|
236
284
|
}
|
|
285
|
+
if (!issuerAccepted(claims.iss, this.opts.apiBase, claims.app, this.pinnedIssuer)) {
|
|
286
|
+
throw new TillAuthVerifyError("Untrusted token issuer", "invalid_issuer");
|
|
287
|
+
}
|
|
237
288
|
if (this.opts.requireMfa && !claims.mfa) {
|
|
238
289
|
throw new TillAuthVerifyError("MFA required", "mfa_required");
|
|
239
290
|
}
|
|
@@ -290,6 +341,6 @@ function createVerifier(opts) {
|
|
|
290
341
|
return new TillAuthVerifier(opts);
|
|
291
342
|
}
|
|
292
343
|
|
|
293
|
-
export { TillAuthVerifier, TillAuthVerifyError, createFetchHandler, createVerifier, expressCookieProxy, expressMiddleware, fastifyHook };
|
|
344
|
+
export { TillAuthVerifier, TillAuthVerifyError, TillAuthWebhookError, createFetchHandler, createVerifier, expressCookieProxy, expressMiddleware, fastifyHook, verifyWebhook };
|
|
294
345
|
//# sourceMappingURL=index.js.map
|
|
295
346
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/cookie-proxy.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","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 appId: string,\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 || iss === `${canon}${REALM_SEGMENT}${appId}`\n}\n\nexport interface VerifyOptions {\n /** TillAuth public app id (`tau_…`). Used to enforce app scoping. */\n appId: string\n /** TillAuth API base. Defaults to https://auth.tilldev.dev */\n apiBase?: string\n /**\n * Pin verification to exactly one issuer (e.g. a dedicated realm's\n * `https://auth.tilldev.dev/realms/<app>`).\n *\n * Rarely needed now: when omitted, the verifier accepts the canonical HTTPS issuer\n * (derived from `apiBase`) and THIS app's own realm issuer — never another realm's.\n * It previously accepted any realm under `apiBase`, which is what made this option\n * look load-bearing; it was not, because the key still came from a merged JWKS\n * (`atrium` `00` D13). The retired legacy `\"tillauth\"` string is never accepted.\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 /** 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,\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(iss: unknown): ReturnType<typeof createRemoteJWKSet> | null {\n if (typeof iss !== 'string') return null\n if (iss === this.opts.apiBase) return this.remote\n if (iss === `${this.opts.apiBase}${REALM_SEGMENT}${this.opts.appId}`) return this.remoteRealm\n return null\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 unverifiedIss = decodeJwt(token).iss\n const keySet = this.keySetFor(unverifiedIss)\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: unverifiedIss 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 (!issuerAccepted(claims.iss, this.opts.apiBase, this.opts.appId, this.pinnedIssuer)) {\n throw new TillAuthVerifyError('Untrusted token issuer', 'invalid_issuer')\n }\n if (claims.app !== this.opts.appId) {\n throw new TillAuthVerifyError(`Token issued for different app`, 'wrong_app')\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'\n"]}
|
|
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"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tillstack/auth-node",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Node SDK for TillAuth — JWT verification + middleware",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -25,14 +25,6 @@
|
|
|
25
25
|
"files": [
|
|
26
26
|
"dist"
|
|
27
27
|
],
|
|
28
|
-
"scripts": {
|
|
29
|
-
"build": "tsup",
|
|
30
|
-
"dev": "tsup --watch",
|
|
31
|
-
"typecheck": "tsc --noEmit",
|
|
32
|
-
"prepack": "tsup",
|
|
33
|
-
"clean": "rm -rf dist",
|
|
34
|
-
"test": "node --import tsx --test test/*.test.ts"
|
|
35
|
-
},
|
|
36
28
|
"dependencies": {
|
|
37
29
|
"jose": "^5.9.6"
|
|
38
30
|
},
|
|
@@ -52,5 +44,12 @@
|
|
|
52
44
|
},
|
|
53
45
|
"publishConfig": {
|
|
54
46
|
"access": "public"
|
|
47
|
+
},
|
|
48
|
+
"scripts": {
|
|
49
|
+
"build": "tsup",
|
|
50
|
+
"dev": "tsup --watch",
|
|
51
|
+
"typecheck": "tsc --noEmit",
|
|
52
|
+
"clean": "rm -rf dist",
|
|
53
|
+
"test": "node --import tsx --test test/*.test.ts"
|
|
55
54
|
}
|
|
56
|
-
}
|
|
55
|
+
}
|