@uln/auth 0.1.0 → 0.2.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/client.d.ts CHANGED
@@ -1,4 +1,6 @@
1
- export type { TokenPayload, TokenConfig, CookieConfig } from "./types.ts";
2
- export { createToken, validateToken } from "./token.ts";
1
+ export type { TokenPayload, VerifyConfig, PublicKeyEntry, CookieConfig } from "./types.ts";
2
+ export { validateToken, importPublicKey } from "./token.ts";
3
3
  export { makeSessionCookie, makeClearCookie, extractCookie } from "./cookie.ts";
4
4
  export { getAuthUser, redirectToLogin, getClientIP, checkBearerToken } from "./middleware.ts";
5
+ import type { PublicKeyEntry } from "./types.ts";
6
+ export declare function fetchPublicKeys(baseUrl: string): Promise<PublicKeyEntry[]>;
package/dist/client.js CHANGED
@@ -41,21 +41,24 @@ function base64urlDecode(str) {
41
41
  }
42
42
  return bytes;
43
43
  }
44
- async function hmacSign(secret, data) {
45
- const key = await crypto.subtle.importKey("raw", encoder.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, [
46
- "sign"
47
- ]);
48
- const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(data));
49
- return base64urlEncode(new Uint8Array(signature));
44
+ var ECDSA_PARAMS = { name: "ECDSA", namedCurve: "P-256" };
45
+ var SIGN_PARAMS = { name: "ECDSA", hash: "SHA-256" };
46
+ async function generateKeyPair() {
47
+ const kid = crypto.randomUUID().slice(0, 8);
48
+ const keyPair = await crypto.subtle.generateKey(ECDSA_PARAMS, true, ["sign", "verify"]);
49
+ if (!("privateKey" in keyPair))
50
+ throw new Error("Expected CryptoKeyPair from ECDSA generateKey");
51
+ const privateExport = await crypto.subtle.exportKey("jwk", keyPair.privateKey);
52
+ const publicExport = await crypto.subtle.exportKey("jwk", keyPair.publicKey);
53
+ if (privateExport instanceof ArrayBuffer || publicExport instanceof ArrayBuffer)
54
+ throw new Error("Expected JWK from exportKey");
55
+ return { kid, privateKeyJwk: privateExport, publicKeyJwk: publicExport };
50
56
  }
51
- function timingSafeEqual(a, b) {
52
- if (a.length !== b.length)
53
- return false;
54
- let diff = 0;
55
- for (let i = 0;i < a.length; i++) {
56
- diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
57
- }
58
- return diff === 0;
57
+ async function importPrivateKey(jwk) {
58
+ return crypto.subtle.importKey("jwk", jwk, ECDSA_PARAMS, false, ["sign"]);
59
+ }
60
+ async function importPublicKey(jwk) {
61
+ return crypto.subtle.importKey("jwk", jwk, ECDSA_PARAMS, false, ["verify"]);
59
62
  }
60
63
  async function createToken(config, user) {
61
64
  const now = Math.floor(Date.now() / 1000);
@@ -65,34 +68,49 @@ async function createToken(config, user) {
65
68
  name: user.name,
66
69
  admin: user.isAdmin,
67
70
  scopes: user.scopes,
71
+ flags: user.flags,
72
+ kid: config.kid,
68
73
  iat: now,
69
74
  exp: now + config.maxAgeSeconds
70
75
  };
71
76
  const payloadB64 = base64urlEncode(encoder.encode(JSON.stringify(payload)));
72
- const sig = await hmacSign(config.secret, payloadB64);
73
- return `${payloadB64}.${sig}`;
77
+ const payloadBytes = encoder.encode(payloadB64);
78
+ const signature = await crypto.subtle.sign(SIGN_PARAMS, config.privateKey, payloadBytes);
79
+ const sigB64 = base64urlEncode(new Uint8Array(signature));
80
+ return `${payloadB64}.${sigB64}`;
74
81
  }
75
82
  async function validateToken(config, token) {
76
83
  const dotIndex = token.indexOf(".");
77
84
  if (dotIndex === -1)
78
85
  return null;
79
86
  const payloadB64 = token.substring(0, dotIndex);
80
- const sig = token.substring(dotIndex + 1);
81
- const expectedSig = await hmacSign(config.secret, payloadB64);
82
- if (!timingSafeEqual(sig, expectedSig))
83
- return null;
87
+ const sigB64 = token.substring(dotIndex + 1);
88
+ let payload;
84
89
  try {
85
90
  const decoded = new TextDecoder().decode(base64urlDecode(payloadB64));
86
- const payload = JSON.parse(decoded);
87
- if (typeof payload.sub !== "string" || typeof payload.exp !== "number")
88
- return null;
89
- const now = Math.floor(Date.now() / 1000);
90
- if (now > payload.exp)
91
+ payload = JSON.parse(decoded);
92
+ } catch {
93
+ return null;
94
+ }
95
+ if (typeof payload.sub !== "string" || typeof payload.exp !== "number" || typeof payload.kid !== "string") {
96
+ return null;
97
+ }
98
+ const now = Math.floor(Date.now() / 1000);
99
+ if (now > payload.exp)
100
+ return null;
101
+ const entry = config.publicKeys.find((k) => k.kid === payload.kid);
102
+ if (!entry)
103
+ return null;
104
+ try {
105
+ const signatureBytes = base64urlDecode(sigB64);
106
+ const payloadBytes = encoder.encode(payloadB64);
107
+ const valid = await crypto.subtle.verify(SIGN_PARAMS, entry.key, signatureBytes, payloadBytes);
108
+ if (!valid)
91
109
  return null;
92
- return payload;
93
110
  } catch {
94
111
  return null;
95
112
  }
113
+ return payload;
96
114
  }
97
115
 
98
116
  // src/middleware.ts
@@ -103,27 +121,39 @@ function checkBearerToken(request, expectedToken) {
103
121
  const header = request.headers.get("Authorization");
104
122
  return header === `Bearer ${expectedToken}`;
105
123
  }
106
- async function getAuthUser(request, tokenConfig, cookieConfig) {
124
+ async function getAuthUser(request, verifyConfig, cookieConfig) {
107
125
  const cookieHeader = request.headers.get("Cookie");
108
126
  if (!cookieHeader)
109
127
  return null;
110
128
  const token = extractCookie(cookieHeader, cookieConfig.name);
111
129
  if (!token)
112
130
  return null;
113
- return validateToken(tokenConfig, token);
131
+ return validateToken(verifyConfig, token);
114
132
  }
115
133
  function redirectToLogin(currentUrl) {
116
134
  const loginUrl = `https://auth.uln.industries/login?redirect=${encodeURIComponent(currentUrl)}`;
117
135
  return Response.redirect(loginUrl, 302);
118
136
  }
137
+ // src/client.ts
138
+ async function fetchPublicKeys(baseUrl) {
139
+ const res = await fetch(`${baseUrl}/.well-known/jwk.json`);
140
+ if (!res.ok)
141
+ throw new Error(`Failed to fetch JWKS from ${baseUrl}: ${res.status}`);
142
+ const jwks = await res.json();
143
+ return Promise.all(jwks.keys.map(async (jwk) => ({
144
+ kid: jwk.kid,
145
+ key: await importPublicKey(jwk)
146
+ })));
147
+ }
119
148
  export {
120
149
  validateToken,
121
150
  redirectToLogin,
122
151
  makeSessionCookie,
123
152
  makeClearCookie,
153
+ importPublicKey,
124
154
  getClientIP,
125
155
  getAuthUser,
156
+ fetchPublicKeys,
126
157
  extractCookie,
127
- createToken,
128
158
  checkBearerToken
129
159
  };
package/dist/hono.d.ts ADDED
@@ -0,0 +1,50 @@
1
+ import type { Context, MiddlewareHandler } from "hono";
2
+ import type { TokenPayload } from "./types.ts";
3
+ interface AuthOptions {
4
+ /**
5
+ * Service binding name (e.g. "AUTH"). If provided, validates tokens
6
+ * via the auth worker service binding instead of fetching public keys.
7
+ */
8
+ binding?: string;
9
+ /**
10
+ * Paths that don't require authentication. Supports exact matches
11
+ * and trailing wildcard (e.g. "/api/public/*").
12
+ */
13
+ public?: string[];
14
+ /**
15
+ * Auth service base URL. Defaults to "https://auth.uln.industries".
16
+ */
17
+ authUrl?: string;
18
+ /** Cookie name. Defaults to "uln_auth". */
19
+ cookie?: string;
20
+ /** Cookie/token max age in seconds. Defaults to 86400 (24h). */
21
+ maxAge?: number;
22
+ }
23
+ /**
24
+ * Hono middleware that handles the full auth flow:
25
+ *
26
+ * 1. If `?token=` in URL → validate, set cookie, redirect to clean URL
27
+ * 2. If session cookie present → validate, set `c.get("user")`
28
+ * 3. If unauthenticated → redirect to auth.uln.industries/login
29
+ *
30
+ * Usage:
31
+ * ```ts
32
+ * import { auth } from "@uln/auth/hono";
33
+ *
34
+ * // Public key validation (default — no config needed):
35
+ * app.use("/*", auth());
36
+ *
37
+ * // Service binding (if on same CF account):
38
+ * app.use("/*", auth({ binding: "AUTH" }));
39
+ *
40
+ * // With public paths:
41
+ * app.use("/*", auth({ public: ["/health", "/api/public/*"] }));
42
+ *
43
+ * // Access user in handlers:
44
+ * app.get("/me", (c) => c.json(c.get("user")));
45
+ * ```
46
+ */
47
+ export declare function auth(options?: AuthOptions): MiddlewareHandler;
48
+ /** Type-safe helper to get the authenticated user from context. */
49
+ export declare function getUser(c: Context): TokenPayload;
50
+ export type { TokenPayload } from "./types.ts";
package/dist/hono.js ADDED
@@ -0,0 +1,242 @@
1
+ // @bun
2
+ // src/cookie.ts
3
+ function makeSessionCookie(config, token) {
4
+ const parts = [`${config.name}=${token}`, `Max-Age=${config.maxAgeSeconds}`, "Path=/", "HttpOnly", "SameSite=Lax"];
5
+ if (config.secure)
6
+ parts.push("Secure");
7
+ return parts.join("; ");
8
+ }
9
+ function makeClearCookie(config) {
10
+ const parts = [`${config.name}=`, "Max-Age=0", "Path=/", "HttpOnly", "SameSite=Lax"];
11
+ if (config.secure)
12
+ parts.push("Secure");
13
+ return parts.join("; ");
14
+ }
15
+ function extractCookie(cookieHeader, name) {
16
+ const prefix = `${name}=`;
17
+ for (const cookie of cookieHeader.split(";")) {
18
+ const trimmed = cookie.trim();
19
+ if (trimmed.startsWith(prefix)) {
20
+ return trimmed.substring(prefix.length);
21
+ }
22
+ }
23
+ return null;
24
+ }
25
+
26
+ // src/token.ts
27
+ var encoder = new TextEncoder;
28
+ function base64urlEncode(data) {
29
+ let binary = "";
30
+ for (const byte of data) {
31
+ binary += String.fromCharCode(byte);
32
+ }
33
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
34
+ }
35
+ function base64urlDecode(str) {
36
+ const padded = str.replace(/-/g, "+").replace(/_/g, "/");
37
+ const binary = atob(padded);
38
+ const bytes = new Uint8Array(binary.length);
39
+ for (let i = 0;i < binary.length; i++) {
40
+ bytes[i] = binary.charCodeAt(i);
41
+ }
42
+ return bytes;
43
+ }
44
+ var ECDSA_PARAMS = { name: "ECDSA", namedCurve: "P-256" };
45
+ var SIGN_PARAMS = { name: "ECDSA", hash: "SHA-256" };
46
+ async function generateKeyPair() {
47
+ const kid = crypto.randomUUID().slice(0, 8);
48
+ const keyPair = await crypto.subtle.generateKey(ECDSA_PARAMS, true, ["sign", "verify"]);
49
+ if (!("privateKey" in keyPair))
50
+ throw new Error("Expected CryptoKeyPair from ECDSA generateKey");
51
+ const privateExport = await crypto.subtle.exportKey("jwk", keyPair.privateKey);
52
+ const publicExport = await crypto.subtle.exportKey("jwk", keyPair.publicKey);
53
+ if (privateExport instanceof ArrayBuffer || publicExport instanceof ArrayBuffer)
54
+ throw new Error("Expected JWK from exportKey");
55
+ return { kid, privateKeyJwk: privateExport, publicKeyJwk: publicExport };
56
+ }
57
+ async function importPrivateKey(jwk) {
58
+ return crypto.subtle.importKey("jwk", jwk, ECDSA_PARAMS, false, ["sign"]);
59
+ }
60
+ async function importPublicKey(jwk) {
61
+ return crypto.subtle.importKey("jwk", jwk, ECDSA_PARAMS, false, ["verify"]);
62
+ }
63
+ async function createToken(config, user) {
64
+ const now = Math.floor(Date.now() / 1000);
65
+ const payload = {
66
+ sub: user.id,
67
+ email: user.email,
68
+ name: user.name,
69
+ admin: user.isAdmin,
70
+ scopes: user.scopes,
71
+ flags: user.flags,
72
+ kid: config.kid,
73
+ iat: now,
74
+ exp: now + config.maxAgeSeconds
75
+ };
76
+ const payloadB64 = base64urlEncode(encoder.encode(JSON.stringify(payload)));
77
+ const payloadBytes = encoder.encode(payloadB64);
78
+ const signature = await crypto.subtle.sign(SIGN_PARAMS, config.privateKey, payloadBytes);
79
+ const sigB64 = base64urlEncode(new Uint8Array(signature));
80
+ return `${payloadB64}.${sigB64}`;
81
+ }
82
+ async function validateToken(config, token) {
83
+ const dotIndex = token.indexOf(".");
84
+ if (dotIndex === -1)
85
+ return null;
86
+ const payloadB64 = token.substring(0, dotIndex);
87
+ const sigB64 = token.substring(dotIndex + 1);
88
+ let payload;
89
+ try {
90
+ const decoded = new TextDecoder().decode(base64urlDecode(payloadB64));
91
+ payload = JSON.parse(decoded);
92
+ } catch {
93
+ return null;
94
+ }
95
+ if (typeof payload.sub !== "string" || typeof payload.exp !== "number" || typeof payload.kid !== "string") {
96
+ return null;
97
+ }
98
+ const now = Math.floor(Date.now() / 1000);
99
+ if (now > payload.exp)
100
+ return null;
101
+ const entry = config.publicKeys.find((k) => k.kid === payload.kid);
102
+ if (!entry)
103
+ return null;
104
+ try {
105
+ const signatureBytes = base64urlDecode(sigB64);
106
+ const payloadBytes = encoder.encode(payloadB64);
107
+ const valid = await crypto.subtle.verify(SIGN_PARAMS, entry.key, signatureBytes, payloadBytes);
108
+ if (!valid)
109
+ return null;
110
+ } catch {
111
+ return null;
112
+ }
113
+ return payload;
114
+ }
115
+
116
+ // src/hono.ts
117
+ var DEFAULT_AUTH_URL = "https://auth.uln.industries";
118
+ var DEFAULT_COOKIE = "uln_auth";
119
+ var DEFAULT_MAX_AGE = 86400;
120
+ function isPublicPath(pathname, patterns) {
121
+ for (const pattern of patterns) {
122
+ if (pattern.endsWith("/*")) {
123
+ const prefix = pattern.slice(0, -1);
124
+ if (pathname === prefix.slice(0, -1) || pathname.startsWith(prefix))
125
+ return true;
126
+ } else if (pathname === pattern) {
127
+ return true;
128
+ }
129
+ }
130
+ return false;
131
+ }
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;
138
+ }
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
+ };
174
+ }
175
+ function auth(options) {
176
+ const authUrl = options?.authUrl ?? DEFAULT_AUTH_URL;
177
+ const cookieName = options?.cookie ?? DEFAULT_COOKIE;
178
+ const maxAge = options?.maxAge ?? DEFAULT_MAX_AGE;
179
+ const publicPaths = options?.public ?? [];
180
+ const bindingName = options?.binding;
181
+ const cookieConf = { name: cookieName, maxAgeSeconds: maxAge, secure: true };
182
+ async function validate(token, env) {
183
+ if (bindingName) {
184
+ const binding = env[bindingName];
185
+ if (!binding)
186
+ throw new Error(`Service binding "${bindingName}" not found in env`);
187
+ return validateWithBinding(token, binding);
188
+ }
189
+ return validateWithPublicKeys(token, authUrl, maxAge);
190
+ }
191
+ return async (c, next) => {
192
+ const url = new URL(c.req.url);
193
+ if (isPublicPath(url.pathname, publicPaths)) {
194
+ return next();
195
+ }
196
+ const incomingToken = url.searchParams.get("token");
197
+ if (incomingToken) {
198
+ const payload = await validate(incomingToken, c.env);
199
+ if (!payload) {
200
+ return c.text("Invalid token", 401);
201
+ }
202
+ url.searchParams.delete("token");
203
+ return new Response(null, {
204
+ status: 302,
205
+ headers: {
206
+ Location: url.toString(),
207
+ "Set-Cookie": makeSessionCookie(cookieConf, incomingToken)
208
+ }
209
+ });
210
+ }
211
+ const cookieHeader = c.req.header("Cookie");
212
+ if (cookieHeader) {
213
+ const token = extractCookie(cookieHeader, cookieName);
214
+ if (token) {
215
+ const payload = await validate(token, c.env);
216
+ if (payload) {
217
+ c.set("user", payload);
218
+ return next();
219
+ }
220
+ }
221
+ }
222
+ const authHeader = c.req.header("Authorization");
223
+ if (authHeader?.startsWith("Bearer ")) {
224
+ const token = authHeader.substring(7);
225
+ const payload = await validate(token, c.env);
226
+ if (payload) {
227
+ c.set("user", payload);
228
+ return next();
229
+ }
230
+ return c.json({ error: "Invalid token" }, 401);
231
+ }
232
+ const loginUrl = `${authUrl}/login?redirect=${encodeURIComponent(c.req.url)}`;
233
+ return c.redirect(loginUrl, 302);
234
+ };
235
+ }
236
+ function getUser(c) {
237
+ return c.get("user");
238
+ }
239
+ export {
240
+ getUser,
241
+ auth
242
+ };
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- export type { User, UserStore, RateLimitEntry, RateLimitStore, RateLimitConfig, TokenPayload, TokenConfig, CookieConfig, } from "./types.ts";
1
+ export type { User, UserStore, RateLimitEntry, RateLimitStore, RateLimitConfig, TokenPayload, SigningConfig, VerifyConfig, PublicKeyEntry, CookieConfig, } from "./types.ts";
2
2
  export { hashPassword, verifyPassword } from "./password.ts";
3
- export { createToken, validateToken } from "./token.ts";
3
+ export { createToken, validateToken, generateKeyPair, importPrivateKey, importPublicKey } from "./token.ts";
4
4
  export type { CreateTokenInput } from "./token.ts";
5
5
  export { makeSessionCookie, makeClearCookie, extractCookie } from "./cookie.ts";
6
6
  export { checkRateLimit, recordFailedAttempt, clearRateLimit } from "./rate-limit.ts";
package/dist/index.js CHANGED
@@ -41,21 +41,24 @@ function base64urlDecode(str) {
41
41
  }
42
42
  return bytes;
43
43
  }
44
- async function hmacSign(secret, data) {
45
- const key = await crypto.subtle.importKey("raw", encoder.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, [
46
- "sign"
47
- ]);
48
- const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(data));
49
- return base64urlEncode(new Uint8Array(signature));
50
- }
51
- function timingSafeEqual(a, b) {
52
- if (a.length !== b.length)
53
- return false;
54
- let diff = 0;
55
- for (let i = 0;i < a.length; i++) {
56
- diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
57
- }
58
- return diff === 0;
44
+ var ECDSA_PARAMS = { name: "ECDSA", namedCurve: "P-256" };
45
+ var SIGN_PARAMS = { name: "ECDSA", hash: "SHA-256" };
46
+ async function generateKeyPair() {
47
+ const kid = crypto.randomUUID().slice(0, 8);
48
+ const keyPair = await crypto.subtle.generateKey(ECDSA_PARAMS, true, ["sign", "verify"]);
49
+ if (!("privateKey" in keyPair))
50
+ throw new Error("Expected CryptoKeyPair from ECDSA generateKey");
51
+ const privateExport = await crypto.subtle.exportKey("jwk", keyPair.privateKey);
52
+ const publicExport = await crypto.subtle.exportKey("jwk", keyPair.publicKey);
53
+ if (privateExport instanceof ArrayBuffer || publicExport instanceof ArrayBuffer)
54
+ throw new Error("Expected JWK from exportKey");
55
+ return { kid, privateKeyJwk: privateExport, publicKeyJwk: publicExport };
56
+ }
57
+ async function importPrivateKey(jwk) {
58
+ return crypto.subtle.importKey("jwk", jwk, ECDSA_PARAMS, false, ["sign"]);
59
+ }
60
+ async function importPublicKey(jwk) {
61
+ return crypto.subtle.importKey("jwk", jwk, ECDSA_PARAMS, false, ["verify"]);
59
62
  }
60
63
  async function createToken(config, user) {
61
64
  const now = Math.floor(Date.now() / 1000);
@@ -65,34 +68,49 @@ async function createToken(config, user) {
65
68
  name: user.name,
66
69
  admin: user.isAdmin,
67
70
  scopes: user.scopes,
71
+ flags: user.flags,
72
+ kid: config.kid,
68
73
  iat: now,
69
74
  exp: now + config.maxAgeSeconds
70
75
  };
71
76
  const payloadB64 = base64urlEncode(encoder.encode(JSON.stringify(payload)));
72
- const sig = await hmacSign(config.secret, payloadB64);
73
- return `${payloadB64}.${sig}`;
77
+ const payloadBytes = encoder.encode(payloadB64);
78
+ const signature = await crypto.subtle.sign(SIGN_PARAMS, config.privateKey, payloadBytes);
79
+ const sigB64 = base64urlEncode(new Uint8Array(signature));
80
+ return `${payloadB64}.${sigB64}`;
74
81
  }
75
82
  async function validateToken(config, token) {
76
83
  const dotIndex = token.indexOf(".");
77
84
  if (dotIndex === -1)
78
85
  return null;
79
86
  const payloadB64 = token.substring(0, dotIndex);
80
- const sig = token.substring(dotIndex + 1);
81
- const expectedSig = await hmacSign(config.secret, payloadB64);
82
- if (!timingSafeEqual(sig, expectedSig))
83
- return null;
87
+ const sigB64 = token.substring(dotIndex + 1);
88
+ let payload;
84
89
  try {
85
90
  const decoded = new TextDecoder().decode(base64urlDecode(payloadB64));
86
- const payload = JSON.parse(decoded);
87
- if (typeof payload.sub !== "string" || typeof payload.exp !== "number")
88
- return null;
89
- const now = Math.floor(Date.now() / 1000);
90
- if (now > payload.exp)
91
+ payload = JSON.parse(decoded);
92
+ } catch {
93
+ return null;
94
+ }
95
+ if (typeof payload.sub !== "string" || typeof payload.exp !== "number" || typeof payload.kid !== "string") {
96
+ return null;
97
+ }
98
+ const now = Math.floor(Date.now() / 1000);
99
+ if (now > payload.exp)
100
+ return null;
101
+ const entry = config.publicKeys.find((k) => k.kid === payload.kid);
102
+ if (!entry)
103
+ return null;
104
+ try {
105
+ const signatureBytes = base64urlDecode(sigB64);
106
+ const payloadBytes = encoder.encode(payloadB64);
107
+ const valid = await crypto.subtle.verify(SIGN_PARAMS, entry.key, signatureBytes, payloadBytes);
108
+ if (!valid)
91
109
  return null;
92
- return payload;
93
110
  } catch {
94
111
  return null;
95
112
  }
113
+ return payload;
96
114
  }
97
115
 
98
116
  // src/middleware.ts
@@ -103,14 +121,14 @@ function checkBearerToken(request, expectedToken) {
103
121
  const header = request.headers.get("Authorization");
104
122
  return header === `Bearer ${expectedToken}`;
105
123
  }
106
- async function getAuthUser(request, tokenConfig, cookieConfig) {
124
+ async function getAuthUser(request, verifyConfig, cookieConfig) {
107
125
  const cookieHeader = request.headers.get("Cookie");
108
126
  if (!cookieHeader)
109
127
  return null;
110
128
  const token = extractCookie(cookieHeader, cookieConfig.name);
111
129
  if (!token)
112
130
  return null;
113
- return validateToken(tokenConfig, token);
131
+ return validateToken(verifyConfig, token);
114
132
  }
115
133
  function redirectToLogin(currentUrl) {
116
134
  const loginUrl = `https://auth.uln.industries/login?redirect=${encodeURIComponent(currentUrl)}`;
@@ -237,9 +255,12 @@ export {
237
255
  recordFailedAttempt,
238
256
  makeSessionCookie,
239
257
  makeClearCookie,
258
+ importPublicKey,
259
+ importPrivateKey,
240
260
  hashPassword,
241
261
  getClientIP,
242
262
  getAuthUser,
263
+ generateKeyPair,
243
264
  extractCookie,
244
265
  createUser,
245
266
  createToken,
@@ -1,5 +1,5 @@
1
- import type { CookieConfig, TokenConfig, TokenPayload } from "./types.ts";
1
+ import type { CookieConfig, TokenPayload, VerifyConfig } from "./types.ts";
2
2
  export declare function getClientIP(request: Request): string;
3
3
  export declare function checkBearerToken(request: Request, expectedToken: string): boolean;
4
- export declare function getAuthUser(request: Request, tokenConfig: TokenConfig, cookieConfig: CookieConfig): Promise<TokenPayload | null>;
4
+ export declare function getAuthUser(request: Request, verifyConfig: VerifyConfig, cookieConfig: CookieConfig): Promise<TokenPayload | null>;
5
5
  export declare function redirectToLogin(currentUrl: string): Response;