@uln/auth 0.4.2 → 0.6.1

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/dist/hono.js CHANGED
@@ -33,6 +33,8 @@ function base64urlEncode(data) {
33
33
  return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
34
34
  }
35
35
  function base64urlDecode(str) {
36
+ if (!/^[A-Za-z0-9_-]*$/.test(str))
37
+ throw new Error("Invalid base64url");
36
38
  const padded = str.replace(/-/g, "+").replace(/_/g, "/");
37
39
  const binary = atob(padded);
38
40
  const bytes = new Uint8Array(binary.length);
@@ -60,8 +62,8 @@ async function importPrivateKey(jwk) {
60
62
  async function importPublicKey(jwk) {
61
63
  return crypto.subtle.importKey("jwk", jwk, ECDSA_PARAMS, false, ["verify"]);
62
64
  }
63
- async function createToken(config, user) {
64
- const now = Math.floor(Date.now() / 1000);
65
+ async function createToken(config, user, options = {}) {
66
+ const now = options.issuedAt ?? Math.floor(Date.now() / 1000);
65
67
  const payload = {
66
68
  sub: user.id,
67
69
  email: user.email,
@@ -71,7 +73,10 @@ async function createToken(config, user) {
71
73
  flags: user.flags,
72
74
  kid: config.kid,
73
75
  iat: now,
74
- exp: now + config.maxAgeSeconds
76
+ exp: now + config.maxAgeSeconds,
77
+ ...user.audience === undefined ? {} : { aud: user.audience },
78
+ ...user.sessionId === undefined ? {} : { sid: user.sessionId },
79
+ ...user.deviceBound ? { dbsc: true } : {}
75
80
  };
76
81
  const payloadB64 = base64urlEncode(encoder.encode(JSON.stringify(payload)));
77
82
  const payloadBytes = encoder.encode(payloadB64);
@@ -79,25 +84,46 @@ async function createToken(config, user) {
79
84
  const sigB64 = base64urlEncode(new Uint8Array(signature));
80
85
  return `${payloadB64}.${sigB64}`;
81
86
  }
87
+ var CLOCK_SKEW_SECONDS = 60;
88
+ function isStringArray(value) {
89
+ return Array.isArray(value) && value.every((v) => typeof v === "string");
90
+ }
91
+ function isFlags(value) {
92
+ if (typeof value !== "object" || value === null || Array.isArray(value))
93
+ return false;
94
+ return Object.values(value).every(isStringArray);
95
+ }
96
+ function isTokenPayload(value) {
97
+ if (typeof value !== "object" || value === null)
98
+ return false;
99
+ if (!("sub" in value) || !("email" in value) || !("name" in value) || !("admin" in value) || !("scopes" in value) || !("flags" in value) || !("kid" in value) || !("iat" in value) || !("exp" in value)) {
100
+ return false;
101
+ }
102
+ return typeof value.sub === "string" && typeof value.email === "string" && typeof value.name === "string" && typeof value.admin === "boolean" && isStringArray(value.scopes) && isFlags(value.flags) && typeof value.kid === "string" && Number.isSafeInteger(value.iat) && Number.isSafeInteger(value.exp) && (!("aud" in value) || value.aud === undefined || typeof value.aud === "string") && (!("sid" in value) || value.sid === undefined || typeof value.sid === "string") && (!("dbsc" in value) || value.dbsc === undefined || typeof value.dbsc === "boolean");
103
+ }
82
104
  async function validateToken(config, token) {
83
- const dotIndex = token.indexOf(".");
84
- if (dotIndex === -1)
105
+ const parts = token.split(".");
106
+ if (parts.length !== 2)
107
+ return null;
108
+ const [payloadB64 = "", sigB64 = ""] = parts;
109
+ if (!payloadB64 || !sigB64)
85
110
  return null;
86
- const payloadB64 = token.substring(0, dotIndex);
87
- const sigB64 = token.substring(dotIndex + 1);
88
- let payload;
111
+ let decoded;
89
112
  try {
90
- const decoded = new TextDecoder().decode(base64urlDecode(payloadB64));
91
- payload = JSON.parse(decoded);
113
+ decoded = JSON.parse(new TextDecoder().decode(base64urlDecode(payloadB64)));
92
114
  } catch {
93
115
  return null;
94
116
  }
95
- if (typeof payload.sub !== "string" || typeof payload.exp !== "number" || typeof payload.kid !== "string") {
117
+ if (!isTokenPayload(decoded))
96
118
  return null;
97
- }
119
+ const payload = decoded;
98
120
  const now = Math.floor(Date.now() / 1000);
99
121
  if (now > payload.exp)
100
122
  return null;
123
+ if (payload.iat > now + CLOCK_SKEW_SECONDS)
124
+ return null;
125
+ if (now - payload.iat > config.maxAgeSeconds)
126
+ return null;
101
127
  const entry = config.publicKeys.find((k) => k.kid === payload.kid);
102
128
  if (!entry)
103
129
  return null;
@@ -112,10 +138,257 @@ async function validateToken(config, token) {
112
138
  }
113
139
  return payload;
114
140
  }
141
+ function peekTokenPayload(token) {
142
+ const payloadB64 = token.split(".")[0];
143
+ if (!payloadB64)
144
+ return null;
145
+ try {
146
+ const decoded = JSON.parse(new TextDecoder().decode(base64urlDecode(payloadB64)));
147
+ return isTokenPayload(decoded) ? decoded : null;
148
+ } catch {
149
+ return null;
150
+ }
151
+ }
152
+ function peekTokenKid(token) {
153
+ const payloadB64 = token.split(".")[0];
154
+ if (!payloadB64)
155
+ return null;
156
+ try {
157
+ const decoded = JSON.parse(new TextDecoder().decode(base64urlDecode(payloadB64)));
158
+ if (typeof decoded !== "object" || decoded === null || !("kid" in decoded))
159
+ return null;
160
+ return typeof decoded.kid === "string" ? decoded.kid : null;
161
+ } catch {
162
+ return null;
163
+ }
164
+ }
115
165
 
116
- // src/hono.ts
166
+ // src/remote.ts
167
+ function publicFetcher(authUrl) {
168
+ return { fetch: (path, init) => fetch(new URL(path, authUrl), init) };
169
+ }
170
+ function bindingFetcher(binding) {
171
+ return { fetch: (path, init) => binding.fetch(new URL(path, "https://auth").toString(), init) };
172
+ }
173
+ function isLoopbackHostname(hostname) {
174
+ return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]";
175
+ }
176
+ function toOrigin(value) {
177
+ try {
178
+ return new URL(value).origin;
179
+ } catch {
180
+ return null;
181
+ }
182
+ }
183
+ function hasSiteAccess(payload, origin) {
184
+ return hasSiteAccessFor(payload, { audiences: [origin], scopeOrigin: origin });
185
+ }
186
+ function hasSiteAccessFor(payload, site) {
187
+ const scopeOrigin = toOrigin(site.scopeOrigin);
188
+ if (!scopeOrigin || payload.aud === undefined)
189
+ return false;
190
+ if (!site.audiences.some((audience) => toOrigin(audience) === payload.aud))
191
+ return false;
192
+ if (payload.admin)
193
+ return true;
194
+ if (isLoopbackHostname(new URL(scopeOrigin).hostname))
195
+ return true;
196
+ return payload.scopes.includes(scopeOrigin);
197
+ }
198
+ async function readJsonObject(res) {
199
+ try {
200
+ const body = await res.json();
201
+ return typeof body === "object" && body !== null ? body : null;
202
+ } catch {
203
+ return null;
204
+ }
205
+ }
206
+ async function exchangeAuthCode(auth, code, redirectOrigin, codeVerifier) {
207
+ const res = await auth.fetch("/api/exchange-code", {
208
+ method: "POST",
209
+ headers: { "Content-Type": "application/json" },
210
+ body: JSON.stringify(codeVerifier === undefined ? { code, redirectOrigin } : { code, redirectOrigin, codeVerifier })
211
+ });
212
+ if (!res.ok)
213
+ return null;
214
+ const body = await readJsonObject(res);
215
+ if (!body || !("token" in body) || typeof body.token !== "string")
216
+ return null;
217
+ return body.token;
218
+ }
219
+ async function checkTokenRemote(auth, token, audience) {
220
+ let res;
221
+ try {
222
+ res = await auth.fetch("/api/verify-token", {
223
+ method: "POST",
224
+ headers: { "Content-Type": "application/json" },
225
+ body: JSON.stringify(audience === undefined ? { token } : { token, audience })
226
+ });
227
+ } catch {
228
+ return { status: "unavailable" };
229
+ }
230
+ if (res.status >= 500)
231
+ return { status: "unavailable" };
232
+ if (!res.ok)
233
+ return { status: "invalid" };
234
+ const body = await readJsonObject(res);
235
+ if (!body || !("user" in body) || typeof body.user !== "object" || body.user === null)
236
+ return { status: "invalid" };
237
+ const user = body.user;
238
+ if (!("id" in user))
239
+ return { status: "invalid" };
240
+ const candidate = { ...user, sub: user.id, iat: "iat" in user ? user.iat : 0, exp: "exp" in user ? user.exp : 0 };
241
+ if (!isTokenPayload(candidate))
242
+ return { status: "invalid" };
243
+ return {
244
+ status: "valid",
245
+ payload: {
246
+ sub: candidate.sub,
247
+ email: candidate.email,
248
+ name: candidate.name,
249
+ admin: candidate.admin,
250
+ scopes: candidate.scopes,
251
+ flags: candidate.flags,
252
+ kid: candidate.kid,
253
+ iat: candidate.iat,
254
+ exp: candidate.exp,
255
+ ...candidate.aud === undefined ? {} : { aud: candidate.aud }
256
+ }
257
+ };
258
+ }
259
+ async function verifyTokenRemote(auth, token, audience) {
260
+ const result = await checkTokenRemote(auth, token, audience);
261
+ return result.status === "valid" ? result.payload : null;
262
+ }
263
+ async function fetchJwks(auth) {
264
+ const res = await auth.fetch("/.well-known/jwk.json");
265
+ if (!res.ok)
266
+ throw new Error(`Failed to fetch JWKS: ${res.status}`);
267
+ const body = await readJsonObject(res);
268
+ if (!body || !("keys" in body) || !Array.isArray(body.keys))
269
+ throw new Error("Malformed JWKS response");
270
+ const entries = [];
271
+ for (const jwk of body.keys) {
272
+ if (typeof jwk !== "object" || jwk === null || !("kid" in jwk) || typeof jwk.kid !== "string")
273
+ continue;
274
+ if (!("kty" in jwk) || typeof jwk.kty !== "string")
275
+ continue;
276
+ const { kid, ...keyData } = jwk;
277
+ entries.push({ kid, key: await importPublicKey({ ...keyData, kty: jwk.kty }) });
278
+ }
279
+ return entries;
280
+ }
281
+
282
+ // src/version.ts
283
+ var LIBRARY_VERSION = "0.6.1";
284
+
285
+ // src/flow.ts
117
286
  var DEFAULT_AUTH_URL = "https://auth.uln.industries";
118
- var DEFAULT_COOKIE = "uln_auth";
287
+ var STATE_COOKIE = "__Host-uln_auth_state";
288
+ var INSECURE_STATE_COOKIE = "uln_auth_state";
289
+ var STATE_PARAM = "uln_state";
290
+ var CHALLENGE_PARAM = "uln_challenge";
291
+ var VERSION_PARAM = "uln_v";
292
+ var STATE_MAX_AGE_SECONDS = 600;
293
+ function base64url(bytes) {
294
+ let binary = "";
295
+ for (const byte of bytes)
296
+ binary += String.fromCharCode(byte);
297
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
298
+ }
299
+ function randomToken() {
300
+ return base64url(crypto.getRandomValues(new Uint8Array(32)));
301
+ }
302
+ async function pkceChallenge(verifier) {
303
+ return base64url(new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier))));
304
+ }
305
+ function stateCookie(value, secure, maxAgeSeconds) {
306
+ const name = secure ? STATE_COOKIE : INSECURE_STATE_COOKIE;
307
+ const parts = [`${name}=${value}`, `Max-Age=${maxAgeSeconds}`, "Path=/", "HttpOnly", "SameSite=Lax"];
308
+ if (secure)
309
+ parts.push("Secure");
310
+ return parts.join("; ");
311
+ }
312
+ function readStateCookie(request, secure) {
313
+ const name = secure ? STATE_COOKIE : INSECURE_STATE_COOKIE;
314
+ const [state = "", verifier = "", ...rest] = (extractCookie(request.headers.get("Cookie") ?? "", name) ?? "").split(".");
315
+ return state && verifier && rest.length === 0 ? { state, verifier } : null;
316
+ }
317
+ function stripCallbackParams(url) {
318
+ url.searchParams.delete("code");
319
+ url.searchParams.delete(STATE_PARAM);
320
+ url.searchParams.delete(CHALLENGE_PARAM);
321
+ url.searchParams.delete(VERSION_PARAM);
322
+ }
323
+ async function redirectToLogin(currentUrl, authUrl = DEFAULT_AUTH_URL) {
324
+ const returnUrl = new URL(currentUrl);
325
+ stripCallbackParams(returnUrl);
326
+ const state = randomToken();
327
+ const verifier = randomToken();
328
+ returnUrl.searchParams.set(STATE_PARAM, state);
329
+ returnUrl.searchParams.set(CHALLENGE_PARAM, await pkceChallenge(verifier));
330
+ returnUrl.searchParams.set(VERSION_PARAM, LIBRARY_VERSION);
331
+ const loginUrl = new URL("/login", authUrl);
332
+ loginUrl.searchParams.set("redirect", returnUrl.toString());
333
+ loginUrl.searchParams.set("response_type", "code");
334
+ const headers = new Headers({ Location: loginUrl.toString() });
335
+ headers.append("Set-Cookie", stateCookie(`${state}.${verifier}`, returnUrl.protocol === "https:", STATE_MAX_AGE_SECONDS));
336
+ return new Response(null, { status: 302, headers });
337
+ }
338
+ async function handleLoginCallback(request, options) {
339
+ const url = new URL(request.url);
340
+ const code = url.searchParams.get("code");
341
+ if (!code)
342
+ return null;
343
+ const state = url.searchParams.get(STATE_PARAM);
344
+ const secure = url.protocol === "https:";
345
+ const expected = readStateCookie(request, secure);
346
+ stripCallbackParams(url);
347
+ const startOver = () => redirectToLogin(url.toString(), options.authUrl);
348
+ if (!state || !expected || state !== expected.state)
349
+ return startOver();
350
+ const token = await exchangeAuthCode(options.auth, code, url.origin, expected.verifier);
351
+ if (!token)
352
+ return startOver();
353
+ const payload = await options.validate(token);
354
+ if (!payload)
355
+ return startOver();
356
+ const scopeOrigin = options.scopeOrigin ?? url.origin;
357
+ if (!hasSiteAccessFor(payload, { audiences: [url.origin, scopeOrigin], scopeOrigin })) {
358
+ return new Response("You don't have access to this site", { status: 403 });
359
+ }
360
+ const headers = new Headers({ Location: url.toString() });
361
+ headers.append("Set-Cookie", makeSessionCookie(options.cookieConfig, token));
362
+ headers.append("Set-Cookie", stateCookie("", secure, 0));
363
+ return new Response(null, { status: 302, headers });
364
+ }
365
+ var confirmedAt = new Map;
366
+ var MAX_CONFIRMED = 5000;
367
+ var DEFAULT_REVALIDATE_SECONDS = 300;
368
+ async function revalidateToken(auth, token, local, audience, intervalSeconds = DEFAULT_REVALIDATE_SECONDS) {
369
+ const last = confirmedAt.get(token);
370
+ if (last !== undefined && Date.now() - last < intervalSeconds * 1000)
371
+ return local;
372
+ const result = await checkTokenRemote(auth, token, audience);
373
+ if (result.status === "unavailable")
374
+ return local;
375
+ confirmedAt.delete(token);
376
+ if (result.status === "invalid")
377
+ return null;
378
+ if (confirmedAt.size >= MAX_CONFIRMED) {
379
+ const oldest = confirmedAt.keys().next();
380
+ if (!oldest.done)
381
+ confirmedAt.delete(oldest.value);
382
+ }
383
+ confirmedAt.set(token, Date.now());
384
+ return result.payload;
385
+ }
386
+ function clearRevalidationCache() {
387
+ confirmedAt.clear();
388
+ }
389
+
390
+ // src/hono.ts
391
+ var DEFAULT_COOKIE = "__Host-uln_auth";
119
392
  var DEFAULT_MAX_AGE = 86400;
120
393
  function isPublicPath(pathname, patterns) {
121
394
  for (const pattern of patterns) {
@@ -129,70 +402,31 @@ function isPublicPath(pathname, patterns) {
129
402
  }
130
403
  return false;
131
404
  }
132
- var cachedKeys = null;
133
- var cachedAt = 0;
134
- var CACHE_TTL = 3600000;
135
- async function getPublicKeys(authUrl) {
136
- if (cachedKeys && Date.now() - cachedAt < CACHE_TTL) {
137
- return cachedKeys;
405
+ var keyCaches = new Map;
406
+ var CACHE_TTL = 10 * 60000;
407
+ var MIN_REFRESH_INTERVAL = 1e4;
408
+ async function getPublicKeys(authUrl, wantedKid) {
409
+ const cached = keyCaches.get(authUrl);
410
+ const age = cached ? Date.now() - cached.fetchedAt : Number.POSITIVE_INFINITY;
411
+ const hasKid = wantedKid === null || cached?.keys.some((k) => k.kid === wantedKid);
412
+ if (cached && age < CACHE_TTL && (hasKid || age < MIN_REFRESH_INTERVAL)) {
413
+ return cached.keys;
138
414
  }
139
- const res = await fetch(`${authUrl}/.well-known/jwk.json`);
140
- if (!res.ok)
141
- throw new Error(`Failed to fetch JWKS from ${authUrl}: ${res.status}`);
142
- const jwks = await res.json();
143
- cachedKeys = await Promise.all(jwks.keys.map(async (jwk) => ({
144
- kid: jwk.kid,
145
- key: await importPublicKey(jwk)
146
- })));
147
- cachedAt = Date.now();
148
- return cachedKeys;
149
- }
150
- async function validateWithPublicKeys(token, authUrl, maxAge) {
151
- const publicKeys = await getPublicKeys(authUrl);
152
- return validateToken({ publicKeys, maxAgeSeconds: maxAge }, token);
153
- }
154
- async function validateWithBinding(token, binding) {
155
- const res = await binding.fetch("https://auth/api/verify-token", {
156
- method: "POST",
157
- headers: { "Content-Type": "application/json" },
158
- body: JSON.stringify({ token })
159
- });
160
- if (!res.ok)
161
- return null;
162
- const data = await res.json();
163
- return {
164
- sub: data.user.id,
165
- email: data.user.email,
166
- name: data.user.name,
167
- admin: data.user.admin,
168
- scopes: data.user.scopes,
169
- flags: data.user.flags,
170
- kid: data.user.kid,
171
- iat: 0,
172
- exp: 0
173
- };
415
+ const keys = await fetchJwks(publicFetcher(authUrl));
416
+ keyCaches.set(authUrl, { keys, fetchedAt: Date.now() });
417
+ return keys;
174
418
  }
175
- async function exchangeCodeWithPublicEndpoint(code, redirectOrigin, authUrl) {
176
- const res = await fetch(`${authUrl}/api/exchange-code`, {
177
- method: "POST",
178
- headers: { "Content-Type": "application/json" },
179
- body: JSON.stringify({ code, redirectOrigin })
180
- });
181
- if (!res.ok)
182
- return null;
183
- const data = await res.json();
184
- return typeof data.token === "string" ? data.token : null;
419
+ function clearPublicKeyCache() {
420
+ keyCaches.clear();
185
421
  }
186
- async function exchangeCodeWithBinding(code, redirectOrigin, binding) {
187
- const res = await binding.fetch("https://auth/api/exchange-code", {
188
- method: "POST",
189
- headers: { "Content-Type": "application/json" },
190
- body: JSON.stringify({ code, redirectOrigin })
191
- });
192
- if (!res.ok)
193
- return null;
194
- const data = await res.json();
195
- return typeof data.token === "string" ? data.token : null;
422
+ function isFetcher(value) {
423
+ return typeof value === "object" && value !== null && "fetch" in value && typeof value.fetch === "function";
424
+ }
425
+ function getBinding(env, name) {
426
+ const binding = typeof env === "object" && env !== null ? Reflect.get(env, name) : undefined;
427
+ if (!isFetcher(binding))
428
+ throw new Error(`Service binding "${name}" not found in env`);
429
+ return bindingFetcher(binding);
196
430
  }
197
431
  function auth(options) {
198
432
  const authUrl = options?.authUrl ?? DEFAULT_AUTH_URL;
@@ -200,55 +434,51 @@ function auth(options) {
200
434
  const maxAge = options?.maxAge ?? DEFAULT_MAX_AGE;
201
435
  const publicPaths = options?.public ?? [];
202
436
  const bindingName = options?.binding;
437
+ const revalidateSeconds = options?.revalidateSeconds ?? DEFAULT_REVALIDATE_SECONDS;
203
438
  const cookieConf = { name: cookieName, maxAgeSeconds: maxAge, secure: true };
204
- async function validate(token, env) {
205
- if (bindingName) {
206
- const binding = env[bindingName];
207
- if (!binding)
208
- throw new Error(`Service binding "${bindingName}" not found in env`);
209
- return validateWithBinding(token, binding);
210
- }
211
- return validateWithPublicKeys(token, authUrl, maxAge);
439
+ function authService(env) {
440
+ return bindingName ? getBinding(env, bindingName) : publicFetcher(authUrl);
212
441
  }
213
- async function exchangeCode(code, redirectOrigin, env) {
442
+ async function validate(token, env, audiences) {
214
443
  if (bindingName) {
215
- const binding = env[bindingName];
216
- if (!binding)
217
- throw new Error(`Service binding "${bindingName}" not found in env`);
218
- return exchangeCodeWithBinding(code, redirectOrigin, binding);
444
+ const audience = peekTokenPayload(token)?.aud;
445
+ if (audience === undefined || !audiences.includes(audience))
446
+ return null;
447
+ return verifyTokenRemote(getBinding(env, bindingName), token, audience);
219
448
  }
220
- return exchangeCodeWithPublicEndpoint(code, redirectOrigin, authUrl);
449
+ const publicKeys = await getPublicKeys(authUrl, peekTokenKid(token));
450
+ const local = await validateToken({ publicKeys, maxAgeSeconds: maxAge }, token);
451
+ if (!local || local.aud === undefined || !audiences.includes(local.aud))
452
+ return null;
453
+ if (revalidateSeconds <= 0)
454
+ return local;
455
+ return revalidateToken(publicFetcher(authUrl), token, local, local.aud, revalidateSeconds);
221
456
  }
222
457
  return async (c, next) => {
223
458
  const url = new URL(c.req.url);
459
+ const scopeOrigin = options?.siteOrigin ?? url.origin;
460
+ const site = { audiences: [url.origin, scopeOrigin], scopeOrigin };
224
461
  if (isPublicPath(url.pathname, publicPaths)) {
225
462
  return next();
226
463
  }
227
- const incomingCode = url.searchParams.get("code");
228
- if (incomingCode) {
229
- const token = await exchangeCode(incomingCode, url.origin, c.env);
230
- if (!token) {
231
- return c.text("Invalid code", 401);
232
- }
233
- const payload = await validate(token, c.env);
234
- if (!payload) {
235
- return c.text("Invalid token", 401);
236
- }
237
- url.searchParams.delete("code");
238
- return new Response(null, {
239
- status: 302,
240
- headers: {
241
- Location: url.toString(),
242
- "Set-Cookie": makeSessionCookie(cookieConf, token)
243
- }
244
- });
245
- }
464
+ const callback = await handleLoginCallback(c.req.raw, {
465
+ auth: authService(c.env),
466
+ authUrl,
467
+ cookieConfig: cookieConf,
468
+ validate: (token) => validate(token, c.env, site.audiences),
469
+ scopeOrigin: site.scopeOrigin
470
+ });
471
+ if (callback)
472
+ return callback;
246
473
  const cookieHeader = c.req.header("Cookie");
247
474
  if (cookieHeader) {
248
475
  const token = extractCookie(cookieHeader, cookieName);
249
476
  if (token) {
250
- const payload = await validate(token, c.env);
477
+ const payload = await validate(token, c.env, site.audiences);
251
478
  if (payload) {
479
+ if (!hasSiteAccessFor(payload, site)) {
480
+ return c.text("You don't have access to this site", 403);
481
+ }
252
482
  c.set("user", payload);
253
483
  return next();
254
484
  }
@@ -257,23 +487,25 @@ function auth(options) {
257
487
  const authHeader = c.req.header("Authorization");
258
488
  if (authHeader?.startsWith("Bearer ")) {
259
489
  const token = authHeader.substring(7);
260
- const payload = await validate(token, c.env);
261
- if (payload) {
262
- c.set("user", payload);
263
- return next();
264
- }
265
- return c.json({ error: "Invalid token" }, 401);
490
+ const payload = await validate(token, c.env, site.audiences);
491
+ if (!payload)
492
+ return c.json({ error: "Invalid token" }, 401);
493
+ if (!hasSiteAccessFor(payload, site))
494
+ return c.json({ error: "Forbidden" }, 403);
495
+ c.set("user", payload);
496
+ return next();
266
497
  }
267
- const loginUrl = new URL("/login", authUrl);
268
- loginUrl.searchParams.set("redirect", c.req.url);
269
- loginUrl.searchParams.set("response_type", "code");
270
- return c.redirect(loginUrl.toString(), 302);
498
+ return redirectToLogin(c.req.url, authUrl);
271
499
  };
272
500
  }
273
501
  function getUser(c) {
274
- return c.get("user");
502
+ const user = c.get("user");
503
+ if (!user)
504
+ throw new Error("getUser() called on a route that is not behind the auth() middleware");
505
+ return user;
275
506
  }
276
507
  export {
277
- getUser,
278
- auth
508
+ auth,
509
+ clearPublicKeyCache,
510
+ getUser
279
511
  };
package/dist/index.d.ts CHANGED
@@ -1,10 +1,11 @@
1
- export type { User, UserStore, RateLimitEntry, RateLimitStore, RateLimitConfig, TokenPayload, SigningConfig, VerifyConfig, PublicKeyEntry, CookieConfig, } from "./types.ts";
2
- export { hashPassword, verifyPassword } from "./password.ts";
3
- export { createToken, validateToken, generateKeyPair, importPrivateKey, importPublicKey } from "./token.ts";
4
- export type { CreateTokenInput } from "./token.ts";
5
- export { makeSessionCookie, makeClearCookie, extractCookie } from "./cookie.ts";
6
- export { checkRateLimit, recordFailedAttempt, clearRateLimit } from "./rate-limit.ts";
1
+ export { extractCookie, makeClearCookie, makeSessionCookie } from "./cookie.ts";
2
+ export { checkBearerToken, getAuthUser, getClientIP, redirectToLogin } from "./middleware.ts";
3
+ export { hashPassword, MAX_PASSWORD_LENGTH, needsRehash, verifyPassword } from "./password.ts";
7
4
  export type { RateLimitResult } from "./rate-limit.ts";
8
- export { getClientIP, checkBearerToken, getAuthUser, redirectToLogin } from "./middleware.ts";
9
- export { createUser, verifyUserCredentials, changePassword, isValidEmail } from "./users.ts";
10
- export { generateTotpSecret, getTotpUri, verifyTotpCode, base32Encode, generateBackupCodes, hashBackupCode, } from "./totp.ts";
5
+ export { checkRateLimit, clearRateLimit, consumeRateLimit, recordFailedAttempt } from "./rate-limit.ts";
6
+ export { hasSiteAccess } from "./remote.ts";
7
+ export type { CreateTokenInput } from "./token.ts";
8
+ export { createToken, generateKeyPair, importPrivateKey, importPublicKey, isTokenPayload, validateToken, } from "./token.ts";
9
+ export { base32Encode, generateBackupCodes, generateTotpSecret, getTotpUri, hashBackupCode, verifyTotpCode, verifyTotpStep, } from "./totp.ts";
10
+ export type { CookieConfig, PublicKeyEntry, RateLimitConfig, RateLimitEntry, RateLimitStore, SigningConfig, TokenPayload, User, UserStore, VerifyConfig, } from "./types.ts";
11
+ export { changePassword, createUser, isValidEmail, verifyUserCredentials } from "./users.ts";