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