@geekmidas/auth 0.0.4 → 0.0.5

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.
Files changed (102) hide show
  1. package/dist/{cacheTokenStorage-BlpcZwef.d.mts → cacheTokenStorage-BzBRJfd-.d.mts} +2 -1
  2. package/dist/cacheTokenStorage-Ca05vf3T.mjs +2 -1
  3. package/dist/cacheTokenStorage-Ca05vf3T.mjs.map +1 -0
  4. package/dist/cacheTokenStorage-CvptztkW.cjs +2 -1
  5. package/dist/cacheTokenStorage-CvptztkW.cjs.map +1 -0
  6. package/dist/cacheTokenStorage-yNUpDD8W.d.cts +2 -1
  7. package/dist/cacheTokenStorage.d.mts +1 -1
  8. package/dist/{chunk-DWy1uDak.cjs → chunk-CUT6urMc.cjs} +0 -9
  9. package/dist/client-DaiUm0UZ.cjs +2 -1
  10. package/dist/client-DaiUm0UZ.cjs.map +1 -0
  11. package/dist/client-DjNZS6vZ.mjs +2 -1
  12. package/dist/client-DjNZS6vZ.mjs.map +1 -0
  13. package/dist/client.d.mts +1 -1
  14. package/dist/hono/jwt.cjs +92 -0
  15. package/dist/hono/jwt.cjs.map +1 -0
  16. package/dist/hono/jwt.d.cts +45 -0
  17. package/dist/hono/jwt.d.mts +45 -0
  18. package/dist/hono/jwt.mjs +91 -0
  19. package/dist/hono/jwt.mjs.map +1 -0
  20. package/dist/hono/oidc.cjs +104 -0
  21. package/dist/hono/oidc.cjs.map +1 -0
  22. package/dist/hono/oidc.d.cts +48 -0
  23. package/dist/hono/oidc.d.mts +48 -0
  24. package/dist/hono/oidc.mjs +103 -0
  25. package/dist/hono/oidc.mjs.map +1 -0
  26. package/dist/index.cjs +1 -1
  27. package/dist/index.d.cts +1 -1
  28. package/dist/index.d.mts +2 -2
  29. package/dist/jwt-Bo_eACEi.cjs +70 -0
  30. package/dist/jwt-Bo_eACEi.cjs.map +1 -0
  31. package/dist/jwt-D88cPJyT.mjs +52 -0
  32. package/dist/jwt-D88cPJyT.mjs.map +1 -0
  33. package/dist/jwt-DF9kFbmh.d.cts +61 -0
  34. package/dist/jwt-DH8CJ2xC.d.mts +61 -0
  35. package/dist/jwt.cjs +5 -0
  36. package/dist/jwt.d.cts +2 -0
  37. package/dist/jwt.d.mts +2 -0
  38. package/dist/jwt.mjs +3 -0
  39. package/dist/lambda/jwt.cjs +126 -0
  40. package/dist/lambda/jwt.cjs.map +1 -0
  41. package/dist/lambda/jwt.d.cts +52 -0
  42. package/dist/lambda/jwt.d.mts +52 -0
  43. package/dist/lambda/jwt.mjs +125 -0
  44. package/dist/lambda/jwt.mjs.map +1 -0
  45. package/dist/lambda/oidc.cjs +117 -0
  46. package/dist/lambda/oidc.cjs.map +1 -0
  47. package/dist/lambda/oidc.d.cts +44 -0
  48. package/dist/lambda/oidc.d.mts +44 -0
  49. package/dist/lambda/oidc.mjs +116 -0
  50. package/dist/lambda/oidc.mjs.map +1 -0
  51. package/dist/oidc-C0KyYaAK.cjs +68 -0
  52. package/dist/oidc-C0KyYaAK.cjs.map +1 -0
  53. package/dist/oidc-CKOGLxjV.d.mts +86 -0
  54. package/dist/oidc-DDLIB5gb.d.cts +86 -0
  55. package/dist/oidc-KA30gMlx.mjs +62 -0
  56. package/dist/oidc-KA30gMlx.mjs.map +1 -0
  57. package/dist/oidc.cjs +3 -0
  58. package/dist/oidc.d.cts +3 -0
  59. package/dist/oidc.d.mts +3 -0
  60. package/dist/oidc.mjs +3 -0
  61. package/dist/server.cjs +1 -1
  62. package/dist/server.d.cts +1 -1
  63. package/dist/server.d.mts +2 -2
  64. package/dist/{tokenManager-CCcFk2Py.d.mts → tokenManager--Q93eNYr.d.mts} +2 -1
  65. package/dist/tokenManager-BVXxfHRa.mjs +2 -1
  66. package/dist/tokenManager-BVXxfHRa.mjs.map +1 -0
  67. package/dist/{tokenManager-B60gFnEF.cjs → tokenManager-EPmwgai2.cjs} +3 -2
  68. package/dist/tokenManager-EPmwgai2.cjs.map +1 -0
  69. package/dist/{tokenManager-mpzJKLvX.d.cts → tokenManager-H6-lWEah.d.cts} +2 -1
  70. package/dist/tokenManager.cjs +1 -1
  71. package/dist/tokenManager.d.cts +1 -1
  72. package/dist/tokenManager.d.mts +1 -1
  73. package/package.json +48 -1
  74. package/src/__tests__/hono-jwt.spec.ts +232 -0
  75. package/src/__tests__/hono-oidc.spec.ts +605 -0
  76. package/src/__tests__/jwt.spec.ts +174 -0
  77. package/src/__tests__/lambda-jwt.spec.ts +278 -0
  78. package/src/__tests__/lambda-oidc.spec.ts +623 -0
  79. package/src/__tests__/oidc.spec.ts +405 -0
  80. package/src/hono/jwt.ts +142 -0
  81. package/src/hono/oidc.ts +170 -0
  82. package/src/jwt.ts +106 -0
  83. package/src/lambda/jwt.ts +210 -0
  84. package/src/lambda/oidc.ts +203 -0
  85. package/src/oidc.ts +156 -0
  86. package/dist/__tests__/cacheTokenStorage.spec.cjs +0 -146
  87. package/dist/__tests__/cacheTokenStorage.spec.d.cts +0 -1
  88. package/dist/__tests__/cacheTokenStorage.spec.d.mts +0 -1
  89. package/dist/__tests__/cacheTokenStorage.spec.mjs +0 -145
  90. package/dist/__tests__/client.spec.cjs +0 -17281
  91. package/dist/__tests__/client.spec.d.cts +0 -1
  92. package/dist/__tests__/client.spec.d.mts +0 -1
  93. package/dist/__tests__/client.spec.mjs +0 -17308
  94. package/dist/__tests__/tokenManager.spec.cjs +0 -197
  95. package/dist/__tests__/tokenManager.spec.d.cts +0 -1
  96. package/dist/__tests__/tokenManager.spec.d.mts +0 -1
  97. package/dist/__tests__/tokenManager.spec.mjs +0 -196
  98. package/dist/playground.cjs +0 -0
  99. package/dist/playground.d.cts +0 -1
  100. package/dist/playground.d.mts +0 -1
  101. package/dist/playground.mjs +0 -0
  102. package/src/playground.ts +0 -0
@@ -0,0 +1,405 @@
1
+ // @vitest-environment node
2
+ import * as jose from 'jose';
3
+ import { http, HttpResponse } from 'msw';
4
+ import { setupServer } from 'msw/node';
5
+ import {
6
+ afterAll,
7
+ afterEach,
8
+ beforeAll,
9
+ describe,
10
+ expect,
11
+ it,
12
+ vi,
13
+ } from 'vitest';
14
+ import { OidcVerifier } from '../oidc';
15
+
16
+ // Mock discovery document
17
+ const mockDiscovery = {
18
+ issuer: 'https://auth.example.com',
19
+ authorization_endpoint: 'https://auth.example.com/authorize',
20
+ token_endpoint: 'https://auth.example.com/token',
21
+ userinfo_endpoint: 'https://auth.example.com/userinfo',
22
+ jwks_uri: 'https://auth.example.com/.well-known/jwks.json',
23
+ scopes_supported: ['openid', 'profile', 'email'],
24
+ response_types_supported: ['code', 'token'],
25
+ claims_supported: ['sub', 'name', 'email'],
26
+ };
27
+
28
+ // Mock user info
29
+ const mockUserInfo = {
30
+ sub: 'user-123',
31
+ name: 'Test User',
32
+ email: 'test@example.com',
33
+ email_verified: true,
34
+ };
35
+
36
+ // Test keys - stored in object so the mock can access after initialization
37
+ const testKeys: {
38
+ privateKey: jose.KeyLike | null;
39
+ publicKey: jose.KeyLike | null;
40
+ } = {
41
+ privateKey: null,
42
+ publicKey: null,
43
+ };
44
+ let jwks: jose.JSONWebKeySet;
45
+
46
+ async function setupKeys() {
47
+ const keyPair = await jose.generateKeyPair('RS256');
48
+ testKeys.privateKey = keyPair.privateKey;
49
+ testKeys.publicKey = keyPair.publicKey;
50
+ const publicJwk = await jose.exportJWK(keyPair.publicKey);
51
+ jwks = {
52
+ keys: [{ ...publicJwk, kid: 'test-key-id', use: 'sig', alg: 'RS256' }],
53
+ };
54
+ }
55
+
56
+ // Mock createRemoteJWKSet to return test keys directly.
57
+ // MSW intercepts jose's fetch in standalone scripts but fails in vitest for unknown reasons.
58
+ // TODO: Investigate vitest/MSW/jose interaction and remove this mock if possible.
59
+ vi.mock('jose', async (importOriginal) => {
60
+ const actual = await importOriginal<typeof jose>();
61
+ return {
62
+ ...actual,
63
+ createRemoteJWKSet: () => {
64
+ return async (protectedHeader: jose.JWSHeaderParameters) => {
65
+ if (protectedHeader.kid === 'test-key-id' && testKeys.publicKey) {
66
+ return testKeys.publicKey;
67
+ }
68
+ throw new Error(`Unknown key ID: ${protectedHeader.kid}`);
69
+ };
70
+ },
71
+ };
72
+ });
73
+
74
+ async function createTestToken(
75
+ claims: Record<string, unknown> = {},
76
+ options: { expiresIn?: string } = {},
77
+ ) {
78
+ if (!testKeys.privateKey) {
79
+ throw new Error('Keys not initialized');
80
+ }
81
+ return await new jose.SignJWT({ sub: 'user-123', ...claims })
82
+ .setProtectedHeader({ alg: 'RS256', kid: 'test-key-id' })
83
+ .setIssuedAt()
84
+ .setIssuer('https://auth.example.com')
85
+ .setAudience('my-api')
86
+ .setExpirationTime(options.expiresIn ?? '1h')
87
+ .sign(testKeys.privateKey);
88
+ }
89
+
90
+ // MSW server for discovery, userinfo, and other HTTP endpoints
91
+ const server = setupServer(
92
+ http.get('https://auth.example.com/.well-known/openid-configuration', () => {
93
+ return HttpResponse.json(mockDiscovery);
94
+ }),
95
+ http.get('https://auth.example.com/.well-known/jwks.json', () => {
96
+ return HttpResponse.json(jwks);
97
+ }),
98
+ http.get('https://auth.example.com/userinfo', () => {
99
+ return HttpResponse.json(mockUserInfo);
100
+ }),
101
+ http.get(
102
+ 'https://other-auth.example.com/.well-known/openid-configuration',
103
+ () => {
104
+ return HttpResponse.json({
105
+ ...mockDiscovery,
106
+ issuer: 'https://other-auth.example.com',
107
+ jwks_uri: 'https://other-auth.example.com/.well-known/jwks.json',
108
+ });
109
+ },
110
+ ),
111
+ http.get('https://other-auth.example.com/.well-known/jwks.json', () => {
112
+ return HttpResponse.json(jwks);
113
+ }),
114
+ );
115
+
116
+ describe('OidcVerifier', () => {
117
+ beforeAll(async () => {
118
+ await setupKeys();
119
+ server.listen({ onUnhandledRequest: 'error' });
120
+ });
121
+
122
+ afterEach(() => {
123
+ server.resetHandlers();
124
+ });
125
+
126
+ afterAll(() => {
127
+ server.close();
128
+ });
129
+
130
+ describe('getDiscovery', () => {
131
+ it('should fetch OIDC discovery document', async () => {
132
+ const verifier = new OidcVerifier({
133
+ issuer: 'https://auth.example.com',
134
+ audience: 'my-api',
135
+ });
136
+
137
+ const discovery = await verifier.getDiscovery();
138
+
139
+ expect(discovery.issuer).toBe('https://auth.example.com');
140
+ expect(discovery.jwks_uri).toBe(
141
+ 'https://auth.example.com/.well-known/jwks.json',
142
+ );
143
+ });
144
+
145
+ it('should cache discovery document by default', async () => {
146
+ let callCount = 0;
147
+ server.use(
148
+ http.get(
149
+ 'https://auth.example.com/.well-known/openid-configuration',
150
+ () => {
151
+ callCount++;
152
+ return HttpResponse.json(mockDiscovery);
153
+ },
154
+ ),
155
+ );
156
+
157
+ const verifier = new OidcVerifier({
158
+ issuer: 'https://auth.example.com',
159
+ audience: 'my-api',
160
+ });
161
+
162
+ await verifier.getDiscovery();
163
+ await verifier.getDiscovery();
164
+
165
+ expect(callCount).toBe(1);
166
+ });
167
+
168
+ it('should handle issuer with trailing slash', async () => {
169
+ let requestedUrl = '';
170
+ server.use(
171
+ http.get(
172
+ 'https://auth.example.com/.well-known/openid-configuration',
173
+ ({ request }) => {
174
+ requestedUrl = request.url;
175
+ return HttpResponse.json(mockDiscovery);
176
+ },
177
+ ),
178
+ );
179
+
180
+ const verifier = new OidcVerifier({
181
+ issuer: 'https://auth.example.com/',
182
+ audience: 'my-api',
183
+ });
184
+
185
+ await verifier.getDiscovery();
186
+
187
+ expect(requestedUrl).toBe(
188
+ 'https://auth.example.com/.well-known/openid-configuration',
189
+ );
190
+ });
191
+
192
+ it('should throw on failed discovery fetch', async () => {
193
+ server.use(
194
+ http.get(
195
+ 'https://auth.example.com/.well-known/openid-configuration',
196
+ () => {
197
+ return new HttpResponse(null, {
198
+ status: 500,
199
+ statusText: 'Internal Server Error',
200
+ });
201
+ },
202
+ ),
203
+ );
204
+
205
+ const verifier = new OidcVerifier({
206
+ issuer: 'https://auth.example.com',
207
+ audience: 'my-api',
208
+ });
209
+
210
+ await expect(verifier.getDiscovery()).rejects.toThrow(
211
+ 'Failed to fetch OIDC discovery',
212
+ );
213
+ });
214
+ });
215
+
216
+ describe('verify', () => {
217
+ it('should verify a valid token', async () => {
218
+ const verifier = new OidcVerifier({
219
+ issuer: 'https://auth.example.com',
220
+ audience: 'my-api',
221
+ });
222
+ const token = await createTestToken();
223
+
224
+ const claims = await verifier.verify(token);
225
+
226
+ expect(claims.sub).toBe('user-123');
227
+ });
228
+
229
+ it('should throw for invalid token', async () => {
230
+ const verifier = new OidcVerifier({
231
+ issuer: 'https://auth.example.com',
232
+ audience: 'my-api',
233
+ });
234
+
235
+ await expect(verifier.verify('invalid-token')).rejects.toThrow();
236
+ });
237
+
238
+ it('should throw for expired token', async () => {
239
+ const verifier = new OidcVerifier({
240
+ issuer: 'https://auth.example.com',
241
+ audience: 'my-api',
242
+ });
243
+ const token = await createTestToken({}, { expiresIn: '-1h' });
244
+
245
+ await expect(verifier.verify(token)).rejects.toThrow();
246
+ });
247
+
248
+ it('should throw for wrong issuer', async () => {
249
+ const verifier = new OidcVerifier({
250
+ issuer: 'https://other-auth.example.com',
251
+ audience: 'my-api',
252
+ });
253
+ const token = await createTestToken();
254
+
255
+ await expect(verifier.verify(token)).rejects.toThrow();
256
+ });
257
+
258
+ it('should throw for wrong audience', async () => {
259
+ const verifier = new OidcVerifier({
260
+ issuer: 'https://auth.example.com',
261
+ audience: 'other-api',
262
+ });
263
+ const token = await createTestToken();
264
+
265
+ await expect(verifier.verify(token)).rejects.toThrow();
266
+ });
267
+ });
268
+
269
+ describe('verifyOrNull', () => {
270
+ it('should return claims for valid token', async () => {
271
+ const verifier = new OidcVerifier({
272
+ issuer: 'https://auth.example.com',
273
+ audience: 'my-api',
274
+ });
275
+ const token = await createTestToken();
276
+
277
+ const claims = await verifier.verifyOrNull(token);
278
+
279
+ expect(claims).not.toBeNull();
280
+ expect(claims?.sub).toBe('user-123');
281
+ });
282
+
283
+ it('should return null for invalid token', async () => {
284
+ const verifier = new OidcVerifier({
285
+ issuer: 'https://auth.example.com',
286
+ audience: 'my-api',
287
+ });
288
+
289
+ const claims = await verifier.verifyOrNull('invalid-token');
290
+
291
+ expect(claims).toBeNull();
292
+ });
293
+ });
294
+
295
+ describe('fetchUserInfo', () => {
296
+ it('should fetch user info with valid token', async () => {
297
+ const verifier = new OidcVerifier({
298
+ issuer: 'https://auth.example.com',
299
+ audience: 'my-api',
300
+ });
301
+ const token = await createTestToken();
302
+
303
+ const userInfo = await verifier.fetchUserInfo(token);
304
+
305
+ expect(userInfo).not.toBeNull();
306
+ expect(userInfo?.sub).toBe('user-123');
307
+ expect(userInfo?.name).toBe('Test User');
308
+ expect(userInfo?.email).toBe('test@example.com');
309
+ });
310
+
311
+ it('should return null when userinfo endpoint is not available', async () => {
312
+ server.use(
313
+ http.get(
314
+ 'https://auth.example.com/.well-known/openid-configuration',
315
+ () => {
316
+ return HttpResponse.json({
317
+ ...mockDiscovery,
318
+ userinfo_endpoint: undefined,
319
+ });
320
+ },
321
+ ),
322
+ );
323
+
324
+ const verifier = new OidcVerifier({
325
+ issuer: 'https://auth.example.com',
326
+ audience: 'my-api',
327
+ });
328
+
329
+ const userInfo = await verifier.fetchUserInfo('some-token');
330
+
331
+ expect(userInfo).toBeNull();
332
+ });
333
+
334
+ it('should return null on userinfo fetch error', async () => {
335
+ server.use(
336
+ http.get('https://auth.example.com/userinfo', () => {
337
+ return new HttpResponse(null, { status: 401 });
338
+ }),
339
+ );
340
+
341
+ const verifier = new OidcVerifier({
342
+ issuer: 'https://auth.example.com',
343
+ audience: 'my-api',
344
+ });
345
+
346
+ const userInfo = await verifier.fetchUserInfo('invalid-token');
347
+
348
+ expect(userInfo).toBeNull();
349
+ });
350
+ });
351
+
352
+ describe('clearCache', () => {
353
+ it('should clear cached discovery and jwks', async () => {
354
+ let callCount = 0;
355
+ server.use(
356
+ http.get(
357
+ 'https://auth.example.com/.well-known/openid-configuration',
358
+ () => {
359
+ callCount++;
360
+ return HttpResponse.json(mockDiscovery);
361
+ },
362
+ ),
363
+ );
364
+
365
+ const verifier = new OidcVerifier({
366
+ issuer: 'https://auth.example.com',
367
+ audience: 'my-api',
368
+ });
369
+
370
+ await verifier.getDiscovery();
371
+ expect(callCount).toBe(1);
372
+
373
+ verifier.clearCache();
374
+
375
+ await verifier.getDiscovery();
376
+ expect(callCount).toBe(2);
377
+ });
378
+ });
379
+
380
+ describe('cacheDiscovery option', () => {
381
+ it('should not cache when cacheDiscovery is false', async () => {
382
+ let callCount = 0;
383
+ server.use(
384
+ http.get(
385
+ 'https://auth.example.com/.well-known/openid-configuration',
386
+ () => {
387
+ callCount++;
388
+ return HttpResponse.json(mockDiscovery);
389
+ },
390
+ ),
391
+ );
392
+
393
+ const verifier = new OidcVerifier({
394
+ issuer: 'https://auth.example.com',
395
+ audience: 'my-api',
396
+ cacheDiscovery: false,
397
+ });
398
+
399
+ await verifier.getDiscovery();
400
+ await verifier.getDiscovery();
401
+
402
+ expect(callCount).toBe(2);
403
+ });
404
+ });
405
+ });
@@ -0,0 +1,142 @@
1
+ import type { Context, MiddlewareHandler, Next } from 'hono';
2
+ import {
3
+ type JwtClaims,
4
+ type JwtConfig,
5
+ JwtVerifier,
6
+ type TokenExtractionOptions,
7
+ } from '../jwt';
8
+
9
+ export {
10
+ JwtVerifier,
11
+ type JwtClaims,
12
+ type JwtConfig,
13
+ type TokenExtractionOptions,
14
+ };
15
+
16
+ function extractToken(
17
+ c: Context,
18
+ options: TokenExtractionOptions = {},
19
+ ): string | null {
20
+ const {
21
+ headerName = 'authorization',
22
+ cookieName,
23
+ tokenPrefix = 'Bearer ',
24
+ } = options;
25
+
26
+ const headerValue = c.req.header(headerName);
27
+ if (headerValue) {
28
+ if (tokenPrefix && headerValue.startsWith(tokenPrefix)) {
29
+ return headerValue.slice(tokenPrefix.length);
30
+ }
31
+ return headerValue;
32
+ }
33
+
34
+ if (cookieName) {
35
+ const cookieHeader = c.req.header('cookie');
36
+ if (cookieHeader) {
37
+ const match = cookieHeader.match(new RegExp(`${cookieName}=([^;]+)`));
38
+ if (match) {
39
+ return match[1];
40
+ }
41
+ }
42
+ }
43
+
44
+ return null;
45
+ }
46
+
47
+ export interface JwtMiddlewareOptions<TClaims extends JwtClaims = JwtClaims> {
48
+ config: JwtConfig;
49
+ extraction?: TokenExtractionOptions;
50
+ contextKey?: string;
51
+ onError?: (c: Context, error: Error) => Response | Promise<Response>;
52
+ transformClaims?: (claims: JwtClaims) => TClaims;
53
+ }
54
+
55
+ /**
56
+ * JWT Middleware for Hono
57
+ *
58
+ * @example
59
+ * ```typescript
60
+ * const jwt = new JwtMiddleware({
61
+ * config: { secret: process.env.JWT_SECRET! },
62
+ * });
63
+ *
64
+ * app.use('/api/*', jwt.handler());
65
+ *
66
+ * // Or for optional auth
67
+ * app.use('/public/*', jwt.optional());
68
+ * ```
69
+ */
70
+ export class JwtMiddleware<TClaims extends JwtClaims = JwtClaims> {
71
+ private readonly verifier: JwtVerifier<TClaims>;
72
+ private readonly extraction: TokenExtractionOptions;
73
+ private readonly contextKey: string;
74
+ private readonly onError?: (
75
+ c: Context,
76
+ error: Error,
77
+ ) => Response | Promise<Response>;
78
+ private readonly transformClaims?: (claims: JwtClaims) => TClaims;
79
+
80
+ constructor(options: JwtMiddlewareOptions<TClaims>) {
81
+ this.verifier = new JwtVerifier(options.config);
82
+ this.extraction = options.extraction ?? {};
83
+ this.contextKey = options.contextKey ?? 'jwtClaims';
84
+ this.onError = options.onError;
85
+ this.transformClaims = options.transformClaims;
86
+ }
87
+
88
+ /**
89
+ * Returns middleware that requires valid JWT
90
+ */
91
+ handler(): MiddlewareHandler {
92
+ return async (c: Context, next: Next) => {
93
+ const token = extractToken(c, this.extraction);
94
+
95
+ if (!token) {
96
+ if (this.onError) {
97
+ return this.onError(c, new Error('No token provided'));
98
+ }
99
+ return c.json({ error: 'Unauthorized' }, 401);
100
+ }
101
+
102
+ try {
103
+ const payload = await this.verifier.verify(token);
104
+ const claims = this.transformClaims
105
+ ? this.transformClaims(payload)
106
+ : payload;
107
+
108
+ c.set(this.contextKey, claims);
109
+ c.set('jwtToken', token);
110
+
111
+ await next();
112
+ } catch (error) {
113
+ if (this.onError) {
114
+ return this.onError(c, error as Error);
115
+ }
116
+ return c.json({ error: 'Invalid token' }, 401);
117
+ }
118
+ };
119
+ }
120
+
121
+ /**
122
+ * Returns middleware that allows unauthenticated requests
123
+ */
124
+ optional(): MiddlewareHandler {
125
+ return async (c: Context, next: Next) => {
126
+ const token = extractToken(c, this.extraction);
127
+
128
+ if (token) {
129
+ const claims = await this.verifier.verifyOrNull(token);
130
+ if (claims) {
131
+ const transformed = this.transformClaims
132
+ ? this.transformClaims(claims)
133
+ : claims;
134
+ c.set(this.contextKey, transformed);
135
+ c.set('jwtToken', token);
136
+ }
137
+ }
138
+
139
+ await next();
140
+ };
141
+ }
142
+ }
@@ -0,0 +1,170 @@
1
+ import type { Context, MiddlewareHandler, Next } from 'hono';
2
+ import {
3
+ type OidcClaims,
4
+ type OidcConfig,
5
+ type OidcUserInfo,
6
+ OidcVerifier,
7
+ type TokenExtractionOptions,
8
+ } from '../oidc';
9
+
10
+ export {
11
+ OidcVerifier,
12
+ type OidcClaims,
13
+ type OidcConfig,
14
+ type OidcUserInfo,
15
+ type TokenExtractionOptions,
16
+ };
17
+
18
+ function extractToken(
19
+ c: Context,
20
+ options: TokenExtractionOptions = {},
21
+ ): string | null {
22
+ const {
23
+ headerName = 'authorization',
24
+ cookieName,
25
+ tokenPrefix = 'Bearer ',
26
+ } = options;
27
+
28
+ const headerValue = c.req.header(headerName);
29
+ if (headerValue) {
30
+ if (tokenPrefix && headerValue.startsWith(tokenPrefix)) {
31
+ return headerValue.slice(tokenPrefix.length);
32
+ }
33
+ return headerValue;
34
+ }
35
+
36
+ if (cookieName) {
37
+ const cookieHeader = c.req.header('cookie');
38
+ if (cookieHeader) {
39
+ const match = cookieHeader.match(new RegExp(`${cookieName}=([^;]+)`));
40
+ if (match) {
41
+ return match[1];
42
+ }
43
+ }
44
+ }
45
+
46
+ return null;
47
+ }
48
+
49
+ export interface OidcMiddlewareOptions<
50
+ TClaims extends OidcClaims = OidcClaims,
51
+ TUserInfo extends OidcUserInfo = OidcUserInfo,
52
+ > {
53
+ config: OidcConfig;
54
+ extraction?: TokenExtractionOptions;
55
+ contextKey?: string;
56
+ fetchUserInfo?: boolean;
57
+ onError?: (c: Context, error: Error) => Response | Promise<Response>;
58
+ transformClaims?: (claims: OidcClaims, userInfo?: TUserInfo) => TClaims;
59
+ }
60
+
61
+ /**
62
+ * OIDC Middleware for Hono
63
+ *
64
+ * @example
65
+ * ```typescript
66
+ * const oidc = new OidcMiddleware({
67
+ * config: {
68
+ * issuer: 'https://auth.example.com',
69
+ * audience: 'my-client-id',
70
+ * },
71
+ * });
72
+ *
73
+ * app.use('/api/*', oidc.handler());
74
+ *
75
+ * // With user info
76
+ * const oidcWithUserInfo = new OidcMiddleware({
77
+ * config: { issuer: '...', audience: '...' },
78
+ * fetchUserInfo: true,
79
+ * });
80
+ * ```
81
+ */
82
+ export class OidcMiddleware<
83
+ TClaims extends OidcClaims = OidcClaims,
84
+ TUserInfo extends OidcUserInfo = OidcUserInfo,
85
+ > {
86
+ private readonly verifier: OidcVerifier<TClaims, TUserInfo>;
87
+ private readonly extraction: TokenExtractionOptions;
88
+ private readonly contextKey: string;
89
+ private readonly fetchUserInfo: boolean;
90
+ private readonly onError?: (
91
+ c: Context,
92
+ error: Error,
93
+ ) => Response | Promise<Response>;
94
+ private readonly transformClaims?: (
95
+ claims: OidcClaims,
96
+ userInfo?: TUserInfo,
97
+ ) => TClaims;
98
+
99
+ constructor(options: OidcMiddlewareOptions<TClaims, TUserInfo>) {
100
+ this.verifier = new OidcVerifier(options.config);
101
+ this.extraction = options.extraction ?? {};
102
+ this.contextKey = options.contextKey ?? 'oidcClaims';
103
+ this.fetchUserInfo = options.fetchUserInfo ?? false;
104
+ this.onError = options.onError;
105
+ this.transformClaims = options.transformClaims;
106
+ }
107
+
108
+ handler(): MiddlewareHandler {
109
+ return async (c: Context, next: Next) => {
110
+ const token = extractToken(c, this.extraction);
111
+
112
+ if (!token) {
113
+ if (this.onError) {
114
+ return this.onError(c, new Error('No token provided'));
115
+ }
116
+ return c.json({ error: 'Unauthorized' }, 401);
117
+ }
118
+
119
+ try {
120
+ const payload = await this.verifier.verify(token);
121
+
122
+ let userInfo: TUserInfo | undefined;
123
+ if (this.fetchUserInfo) {
124
+ userInfo = (await this.verifier.fetchUserInfo(token)) ?? undefined;
125
+ c.set('oidcUserInfo', userInfo);
126
+ }
127
+
128
+ const claims = this.transformClaims
129
+ ? this.transformClaims(payload, userInfo)
130
+ : payload;
131
+
132
+ c.set(this.contextKey, claims);
133
+ c.set('oidcToken', token);
134
+
135
+ await next();
136
+ } catch (error) {
137
+ if (this.onError) {
138
+ return this.onError(c, error as Error);
139
+ }
140
+ return c.json({ error: 'Invalid token' }, 401);
141
+ }
142
+ };
143
+ }
144
+
145
+ optional(): MiddlewareHandler {
146
+ return async (c: Context, next: Next) => {
147
+ const token = extractToken(c, this.extraction);
148
+
149
+ if (token) {
150
+ const payload = await this.verifier.verifyOrNull(token);
151
+ if (payload) {
152
+ let userInfo: TUserInfo | undefined;
153
+ if (this.fetchUserInfo) {
154
+ userInfo = (await this.verifier.fetchUserInfo(token)) ?? undefined;
155
+ c.set('oidcUserInfo', userInfo);
156
+ }
157
+
158
+ const claims = this.transformClaims
159
+ ? this.transformClaims(payload, userInfo)
160
+ : payload;
161
+
162
+ c.set(this.contextKey, claims);
163
+ c.set('oidcToken', token);
164
+ }
165
+ }
166
+
167
+ await next();
168
+ };
169
+ }
170
+ }