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