@geekmidas/auth 0.1.0 → 1.0.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.
Files changed (117) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/{cacheTokenStorage-Ca05vf3T.mjs → cacheTokenStorage-16NE8Cd9.mjs} +3 -3
  3. package/dist/cacheTokenStorage-16NE8Cd9.mjs.map +1 -0
  4. package/dist/{cacheTokenStorage-BzBRJfd-.d.mts → cacheTokenStorage-B7sOoysu.d.cts} +8 -6
  5. package/dist/cacheTokenStorage-B7sOoysu.d.cts.map +1 -0
  6. package/dist/{cacheTokenStorage-CvptztkW.cjs → cacheTokenStorage-CEpMniTi.cjs} +3 -3
  7. package/dist/cacheTokenStorage-CEpMniTi.cjs.map +1 -0
  8. package/dist/{cacheTokenStorage-yNUpDD8W.d.cts → cacheTokenStorage-_tq4Kmhc.d.mts} +8 -6
  9. package/dist/cacheTokenStorage-_tq4Kmhc.d.mts.map +1 -0
  10. package/dist/cacheTokenStorage.cjs +1 -1
  11. package/dist/cacheTokenStorage.d.cts +1 -1
  12. package/dist/cacheTokenStorage.d.mts +1 -1
  13. package/dist/cacheTokenStorage.mjs +1 -1
  14. package/dist/{client-DaiUm0UZ.cjs → client-BPl_mMvb.cjs} +14 -8
  15. package/dist/client-BPl_mMvb.cjs.map +1 -0
  16. package/dist/{client-DjNZS6vZ.mjs → client-eidYNJSH.mjs} +14 -8
  17. package/dist/client-eidYNJSH.mjs.map +1 -0
  18. package/dist/client.cjs +2 -2
  19. package/dist/client.d.cts +1 -1
  20. package/dist/client.d.mts +1 -1
  21. package/dist/client.mjs +2 -2
  22. package/dist/hono/jwt.cjs +1 -1
  23. package/dist/hono/jwt.cjs.map +1 -1
  24. package/dist/hono/jwt.d.cts +2 -1
  25. package/dist/hono/jwt.d.cts.map +1 -0
  26. package/dist/hono/jwt.d.mts +2 -1
  27. package/dist/hono/jwt.d.mts.map +1 -0
  28. package/dist/hono/jwt.mjs +1 -1
  29. package/dist/hono/jwt.mjs.map +1 -1
  30. package/dist/hono/oidc.cjs +1 -1
  31. package/dist/hono/oidc.cjs.map +1 -1
  32. package/dist/hono/oidc.d.cts +3 -2
  33. package/dist/hono/oidc.d.cts.map +1 -0
  34. package/dist/hono/oidc.d.mts +3 -2
  35. package/dist/hono/oidc.d.mts.map +1 -0
  36. package/dist/hono/oidc.mjs +1 -1
  37. package/dist/hono/oidc.mjs.map +1 -1
  38. package/dist/index.cjs +3 -3
  39. package/dist/index.d.cts +2 -2
  40. package/dist/index.d.mts +2 -2
  41. package/dist/index.mjs +3 -3
  42. package/dist/{jwt-DF9kFbmh.d.cts → jwt-3eIuY2dg.d.cts} +2 -1
  43. package/dist/jwt-3eIuY2dg.d.cts.map +1 -0
  44. package/dist/jwt-Bo_eACEi.cjs.map +1 -1
  45. package/dist/{jwt-DH8CJ2xC.d.mts → jwt-BqLlRkw-.d.mts} +2 -1
  46. package/dist/jwt-BqLlRkw-.d.mts.map +1 -0
  47. package/dist/jwt-D88cPJyT.mjs.map +1 -1
  48. package/dist/jwt.d.cts +1 -1
  49. package/dist/jwt.d.mts +1 -1
  50. package/dist/lambda/jwt.cjs +2 -2
  51. package/dist/lambda/jwt.cjs.map +1 -1
  52. package/dist/lambda/jwt.d.cts +2 -1
  53. package/dist/lambda/jwt.d.cts.map +1 -0
  54. package/dist/lambda/jwt.d.mts +2 -1
  55. package/dist/lambda/jwt.d.mts.map +1 -0
  56. package/dist/lambda/jwt.mjs +2 -2
  57. package/dist/lambda/jwt.mjs.map +1 -1
  58. package/dist/lambda/oidc.cjs +2 -2
  59. package/dist/lambda/oidc.cjs.map +1 -1
  60. package/dist/lambda/oidc.d.cts +3 -2
  61. package/dist/lambda/oidc.d.cts.map +1 -0
  62. package/dist/lambda/oidc.d.mts +3 -2
  63. package/dist/lambda/oidc.d.mts.map +1 -0
  64. package/dist/lambda/oidc.mjs +2 -2
  65. package/dist/lambda/oidc.mjs.map +1 -1
  66. package/dist/oidc-C0KyYaAK.cjs.map +1 -1
  67. package/dist/{oidc-CKOGLxjV.d.mts → oidc-ChA17wLB.d.cts} +3 -2
  68. package/dist/oidc-ChA17wLB.d.cts.map +1 -0
  69. package/dist/{oidc-DDLIB5gb.d.cts → oidc-DTRgBBl6.d.mts} +3 -2
  70. package/dist/oidc-DTRgBBl6.d.mts.map +1 -0
  71. package/dist/oidc-KA30gMlx.mjs.map +1 -1
  72. package/dist/oidc.d.cts +2 -2
  73. package/dist/oidc.d.mts +2 -2
  74. package/dist/server.cjs +2 -2
  75. package/dist/server.d.cts +2 -2
  76. package/dist/server.d.mts +2 -2
  77. package/dist/server.mjs +2 -2
  78. package/dist/{tokenManager--Q93eNYr.d.mts → tokenManager-CL3yE06D.d.cts} +2 -1
  79. package/dist/tokenManager-CL3yE06D.d.cts.map +1 -0
  80. package/dist/{tokenManager-BVXxfHRa.mjs → tokenManager-Ci2Tgxp8.mjs} +5 -3
  81. package/dist/tokenManager-Ci2Tgxp8.mjs.map +1 -0
  82. package/dist/{tokenManager-EPmwgai2.cjs → tokenManager-CvEMgxOp.cjs} +5 -3
  83. package/dist/tokenManager-CvEMgxOp.cjs.map +1 -0
  84. package/dist/{tokenManager-H6-lWEah.d.cts → tokenManager-RrwjL0y-.d.mts} +2 -1
  85. package/dist/tokenManager-RrwjL0y-.d.mts.map +1 -0
  86. package/dist/tokenManager.cjs +1 -1
  87. package/dist/tokenManager.d.cts +1 -1
  88. package/dist/tokenManager.d.mts +1 -1
  89. package/dist/tokenManager.mjs +1 -1
  90. package/package.json +3 -3
  91. package/src/__tests__/cacheTokenStorage.spec.ts +148 -148
  92. package/src/__tests__/client.spec.ts +339 -339
  93. package/src/__tests__/hono-jwt.spec.ts +219 -219
  94. package/src/__tests__/hono-oidc.spec.ts +560 -560
  95. package/src/__tests__/jwt.spec.ts +150 -127
  96. package/src/__tests__/lambda-jwt.spec.ts +205 -205
  97. package/src/__tests__/lambda-oidc.spec.ts +580 -579
  98. package/src/__tests__/oidc.spec.ts +365 -365
  99. package/src/__tests__/tokenManager.spec.ts +254 -254
  100. package/src/cacheTokenStorage.ts +32 -32
  101. package/src/client.ts +199 -190
  102. package/src/hono/jwt.ts +111 -111
  103. package/src/hono/oidc.ts +131 -131
  104. package/src/index.ts +2 -2
  105. package/src/jwt.ts +57 -57
  106. package/src/lambda/jwt.ts +170 -170
  107. package/src/lambda/oidc.ts +165 -165
  108. package/src/oidc.ts +121 -121
  109. package/src/server.ts +6 -7
  110. package/src/tokenManager.ts +89 -87
  111. package/tsconfig.json +10 -0
  112. package/dist/cacheTokenStorage-Ca05vf3T.mjs.map +0 -1
  113. package/dist/cacheTokenStorage-CvptztkW.cjs.map +0 -1
  114. package/dist/client-DaiUm0UZ.cjs.map +0 -1
  115. package/dist/client-DjNZS6vZ.mjs.map +0 -1
  116. package/dist/tokenManager-BVXxfHRa.mjs.map +0 -1
  117. package/dist/tokenManager-EPmwgai2.cjs.map +0 -1
@@ -1,623 +1,624 @@
1
1
  import type {
2
- APIGatewayRequestAuthorizerEvent,
3
- APIGatewayTokenAuthorizerEvent,
4
- Context as LambdaContext,
5
- Statement,
2
+ APIGatewayRequestAuthorizerEvent,
3
+ APIGatewayTokenAuthorizerEvent,
4
+ Context as LambdaContext,
5
+ Statement,
6
6
  } from 'aws-lambda';
7
7
 
8
8
  // Helper to access Resource property from Statement union type
9
9
  type StatementWithResource = Statement & { Resource: string | string[] };
10
+
10
11
  import * as jose from 'jose';
11
- import { http, HttpResponse } from 'msw';
12
+ import { HttpResponse, http } from 'msw';
12
13
  import { setupServer } from 'msw/node';
13
14
  import {
14
- afterAll,
15
- afterEach,
16
- beforeAll,
17
- describe,
18
- expect,
19
- it,
20
- vi,
15
+ afterAll,
16
+ afterEach,
17
+ beforeAll,
18
+ describe,
19
+ expect,
20
+ it,
21
+ vi,
21
22
  } from 'vitest';
22
23
  import { OidcAuthorizer } from '../lambda/oidc';
23
24
 
24
25
  // Mock discovery document
25
26
  const mockDiscovery = {
26
- issuer: 'https://auth.example.com',
27
- authorization_endpoint: 'https://auth.example.com/authorize',
28
- token_endpoint: 'https://auth.example.com/token',
29
- userinfo_endpoint: 'https://auth.example.com/userinfo',
30
- jwks_uri: 'https://auth.example.com/.well-known/jwks.json',
31
- scopes_supported: ['openid', 'profile', 'email'],
32
- response_types_supported: ['code', 'token'],
33
- claims_supported: ['sub', 'name', 'email'],
27
+ issuer: 'https://auth.example.com',
28
+ authorization_endpoint: 'https://auth.example.com/authorize',
29
+ token_endpoint: 'https://auth.example.com/token',
30
+ userinfo_endpoint: 'https://auth.example.com/userinfo',
31
+ jwks_uri: 'https://auth.example.com/.well-known/jwks.json',
32
+ scopes_supported: ['openid', 'profile', 'email'],
33
+ response_types_supported: ['code', 'token'],
34
+ claims_supported: ['sub', 'name', 'email'],
34
35
  };
35
36
 
36
37
  // Generate test keys and tokens - stored in object for mock access
37
38
  const testKeys: {
38
- privateKey: jose.KeyLike | null;
39
- publicKey: jose.KeyLike | null;
39
+ privateKey: jose.KeyLike | null;
40
+ publicKey: jose.KeyLike | null;
40
41
  } = {
41
- privateKey: null,
42
- publicKey: null,
42
+ privateKey: null,
43
+ publicKey: null,
43
44
  };
44
45
  let jwks: jose.JSONWebKeySet;
45
46
 
46
47
  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
- };
48
+ const keyPair = await jose.generateKeyPair('RS256');
49
+ testKeys.privateKey = keyPair.privateKey;
50
+ testKeys.publicKey = keyPair.publicKey;
51
+ const publicJwk = await jose.exportJWK(keyPair.publicKey);
52
+ jwks = {
53
+ keys: [{ ...publicJwk, kid: 'test-key-id', use: 'sig', alg: 'RS256' }],
54
+ };
54
55
  }
55
56
 
56
57
  // Mock createRemoteJWKSet to return test keys directly.
57
58
  // MSW intercepts jose's fetch in standalone scripts but fails in vitest for unknown reasons.
58
59
  // TODO: Investigate vitest/MSW/jose interaction and remove this mock if possible.
59
60
  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
- };
61
+ const actual = await importOriginal<typeof jose>();
62
+ return {
63
+ ...actual,
64
+ createRemoteJWKSet: () => {
65
+ return async (protectedHeader: jose.JWSHeaderParameters) => {
66
+ if (protectedHeader.kid === 'test-key-id' && testKeys.publicKey) {
67
+ return testKeys.publicKey;
68
+ }
69
+ throw new Error(`Unknown key ID: ${protectedHeader.kid}`);
70
+ };
71
+ },
72
+ };
72
73
  });
73
74
 
74
75
  async function createTestToken(
75
- claims: Record<string, unknown> = {},
76
- options: { expiresIn?: string } = {},
76
+ claims: Record<string, unknown> = {},
77
+ options: { expiresIn?: string } = {},
77
78
  ) {
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);
79
+ if (!testKeys.privateKey) {
80
+ throw new Error('Keys not initialized');
81
+ }
82
+ return await new jose.SignJWT({ sub: 'user-123', ...claims })
83
+ .setProtectedHeader({ alg: 'RS256', kid: 'test-key-id' })
84
+ .setIssuedAt()
85
+ .setIssuer('https://auth.example.com')
86
+ .setAudience('my-api')
87
+ .setExpirationTime(options.expiresIn ?? '1h')
88
+ .sign(testKeys.privateKey);
88
89
  }
89
90
 
90
91
  // MSW server setup
91
92
  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
- }),
93
+ http.get('https://auth.example.com/.well-known/openid-configuration', () => {
94
+ return HttpResponse.json(mockDiscovery);
95
+ }),
96
+ http.get('https://auth.example.com/.well-known/jwks.json', () => {
97
+ return HttpResponse.json(jwks);
98
+ }),
98
99
  );
99
100
 
100
101
  // Mock Lambda context
101
102
  const mockContext: LambdaContext = {
102
- callbackWaitsForEmptyEventLoop: false,
103
- functionName: 'test-authorizer',
104
- functionVersion: '1',
105
- invokedFunctionArn: 'arn:aws:lambda:us-east-1:123456789:function:test',
106
- memoryLimitInMB: '128',
107
- awsRequestId: 'test-request-id',
108
- logGroupName: '/aws/lambda/test',
109
- logStreamName: 'test-stream',
110
- getRemainingTimeInMillis: () => 5000,
111
- done: () => {},
112
- fail: () => {},
113
- succeed: () => {},
103
+ callbackWaitsForEmptyEventLoop: false,
104
+ functionName: 'test-authorizer',
105
+ functionVersion: '1',
106
+ invokedFunctionArn: 'arn:aws:lambda:us-east-1:123456789:function:test',
107
+ memoryLimitInMB: '128',
108
+ awsRequestId: 'test-request-id',
109
+ logGroupName: '/aws/lambda/test',
110
+ logStreamName: 'test-stream',
111
+ getRemainingTimeInMillis: () => 5000,
112
+ done: () => {},
113
+ fail: () => {},
114
+ succeed: () => {},
114
115
  };
115
116
 
116
117
  describe('OidcAuthorizer', () => {
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('tokenHandler', () => {
131
- it('should allow valid token', async () => {
132
- const authorizer = new OidcAuthorizer({
133
- config: {
134
- issuer: 'https://auth.example.com',
135
- audience: 'my-api',
136
- },
137
- });
138
-
139
- const token = await createTestToken();
140
- const event: APIGatewayTokenAuthorizerEvent = {
141
- type: 'TOKEN',
142
- authorizationToken: `Bearer ${token}`,
143
- methodArn:
144
- 'arn:aws:execute-api:us-east-1:123456789:api-id/stage/GET/resource',
145
- };
146
-
147
- const handler = authorizer.tokenHandler();
148
- const result = await handler(event, mockContext);
149
-
150
- expect(result.principalId).toBe('user-123');
151
- expect(result.policyDocument.Statement[0].Effect).toBe('Allow');
152
- });
153
-
154
- it('should deny invalid token', async () => {
155
- const authorizer = new OidcAuthorizer({
156
- config: {
157
- issuer: 'https://auth.example.com',
158
- audience: 'my-api',
159
- },
160
- });
161
-
162
- const event: APIGatewayTokenAuthorizerEvent = {
163
- type: 'TOKEN',
164
- authorizationToken: 'Bearer invalid-token',
165
- methodArn:
166
- 'arn:aws:execute-api:us-east-1:123456789:api-id/stage/GET/resource',
167
- };
168
-
169
- const handler = authorizer.tokenHandler();
170
- const result = await handler(event, mockContext);
171
-
172
- expect(result.principalId).toBe('unauthorized');
173
- expect(result.policyDocument.Statement[0].Effect).toBe('Deny');
174
- });
175
-
176
- it('should handle token without Bearer prefix', async () => {
177
- const authorizer = new OidcAuthorizer({
178
- config: {
179
- issuer: 'https://auth.example.com',
180
- audience: 'my-api',
181
- },
182
- });
183
-
184
- const token = await createTestToken();
185
- const event: APIGatewayTokenAuthorizerEvent = {
186
- type: 'TOKEN',
187
- authorizationToken: token,
188
- methodArn:
189
- 'arn:aws:execute-api:us-east-1:123456789:api-id/stage/GET/resource',
190
- };
191
-
192
- const handler = authorizer.tokenHandler();
193
- const result = await handler(event, mockContext);
194
-
195
- expect(result.principalId).toBe('user-123');
196
- expect(result.policyDocument.Statement[0].Effect).toBe('Allow');
197
- });
198
-
199
- it('should use wildcard resource by default', async () => {
200
- const authorizer = new OidcAuthorizer({
201
- config: {
202
- issuer: 'https://auth.example.com',
203
- audience: 'my-api',
204
- },
205
- });
206
-
207
- const token = await createTestToken();
208
- const event: APIGatewayTokenAuthorizerEvent = {
209
- type: 'TOKEN',
210
- authorizationToken: `Bearer ${token}`,
211
- methodArn:
212
- 'arn:aws:execute-api:us-east-1:123456789:api-id/stage/GET/resource',
213
- };
214
-
215
- const handler = authorizer.tokenHandler();
216
- const result = await handler(event, mockContext);
217
-
218
- const statement = result.policyDocument
219
- .Statement[0] as StatementWithResource;
220
- expect(statement.Resource).toBe(
221
- 'arn:aws:execute-api:us-east-1:123456789:api-id/stage/*',
222
- );
223
- });
224
-
225
- it('should use exact resource when wildcardResource is false', async () => {
226
- const authorizer = new OidcAuthorizer({
227
- config: {
228
- issuer: 'https://auth.example.com',
229
- audience: 'my-api',
230
- },
231
- wildcardResource: false,
232
- });
233
-
234
- const token = await createTestToken();
235
- const event: APIGatewayTokenAuthorizerEvent = {
236
- type: 'TOKEN',
237
- authorizationToken: `Bearer ${token}`,
238
- methodArn:
239
- 'arn:aws:execute-api:us-east-1:123456789:api-id/stage/GET/resource',
240
- };
241
-
242
- const handler = authorizer.tokenHandler();
243
- const result = await handler(event, mockContext);
244
-
245
- const statement = result.policyDocument
246
- .Statement[0] as StatementWithResource;
247
- expect(statement.Resource).toBe(
248
- 'arn:aws:execute-api:us-east-1:123456789:api-id/stage/GET/resource',
249
- );
250
- });
251
-
252
- it('should use custom getPrincipalId', async () => {
253
- const authorizer = new OidcAuthorizer({
254
- config: {
255
- issuer: 'https://auth.example.com',
256
- audience: 'my-api',
257
- },
258
- getPrincipalId: (claims) => `custom-${claims.sub}`,
259
- });
260
-
261
- const token = await createTestToken();
262
- const event: APIGatewayTokenAuthorizerEvent = {
263
- type: 'TOKEN',
264
- authorizationToken: `Bearer ${token}`,
265
- methodArn:
266
- 'arn:aws:execute-api:us-east-1:123456789:api-id/stage/GET/resource',
267
- };
268
-
269
- const handler = authorizer.tokenHandler();
270
- const result = await handler(event, mockContext);
271
-
272
- expect(result.principalId).toBe('custom-user-123');
273
- });
274
-
275
- it('should include context from getContext', async () => {
276
- const authorizer = new OidcAuthorizer({
277
- config: {
278
- issuer: 'https://auth.example.com',
279
- audience: 'my-api',
280
- },
281
- getContext: (claims) => ({
282
- userId: claims.sub!,
283
- role: 'admin',
284
- }),
285
- });
286
-
287
- const token = await createTestToken();
288
- const event: APIGatewayTokenAuthorizerEvent = {
289
- type: 'TOKEN',
290
- authorizationToken: `Bearer ${token}`,
291
- methodArn:
292
- 'arn:aws:execute-api:us-east-1:123456789:api-id/stage/GET/resource',
293
- };
294
-
295
- const handler = authorizer.tokenHandler();
296
- const result = await handler(event, mockContext);
297
-
298
- expect(result.context).toEqual({
299
- userId: 'user-123',
300
- role: 'admin',
301
- });
302
- });
303
-
304
- it('should deny when authorize callback returns false', async () => {
305
- const authorizer = new OidcAuthorizer({
306
- config: {
307
- issuer: 'https://auth.example.com',
308
- audience: 'my-api',
309
- },
310
- authorize: () => false,
311
- });
312
-
313
- const token = await createTestToken();
314
- const event: APIGatewayTokenAuthorizerEvent = {
315
- type: 'TOKEN',
316
- authorizationToken: `Bearer ${token}`,
317
- methodArn:
318
- 'arn:aws:execute-api:us-east-1:123456789:api-id/stage/GET/resource',
319
- };
320
-
321
- const handler = authorizer.tokenHandler();
322
- const result = await handler(event, mockContext);
323
-
324
- expect(result.principalId).toBe('unauthorized');
325
- expect(result.policyDocument.Statement[0].Effect).toBe('Deny');
326
- });
327
-
328
- it('should allow when authorize callback returns true', async () => {
329
- const authorizer = new OidcAuthorizer({
330
- config: {
331
- issuer: 'https://auth.example.com',
332
- audience: 'my-api',
333
- },
334
- authorize: (claims) => claims.sub === 'user-123',
335
- });
336
-
337
- const token = await createTestToken();
338
- const event: APIGatewayTokenAuthorizerEvent = {
339
- type: 'TOKEN',
340
- authorizationToken: `Bearer ${token}`,
341
- methodArn:
342
- 'arn:aws:execute-api:us-east-1:123456789:api-id/stage/GET/resource',
343
- };
344
-
345
- const handler = authorizer.tokenHandler();
346
- const result = await handler(event, mockContext);
347
-
348
- expect(result.principalId).toBe('user-123');
349
- expect(result.policyDocument.Statement[0].Effect).toBe('Allow');
350
- });
351
-
352
- it('should support async authorize callback', async () => {
353
- const authorizer = new OidcAuthorizer({
354
- config: {
355
- issuer: 'https://auth.example.com',
356
- audience: 'my-api',
357
- },
358
- authorize: async (claims) => {
359
- await Promise.resolve();
360
- return claims.sub === 'user-123';
361
- },
362
- });
363
-
364
- const token = await createTestToken();
365
- const event: APIGatewayTokenAuthorizerEvent = {
366
- type: 'TOKEN',
367
- authorizationToken: `Bearer ${token}`,
368
- methodArn:
369
- 'arn:aws:execute-api:us-east-1:123456789:api-id/stage/GET/resource',
370
- };
371
-
372
- const handler = authorizer.tokenHandler();
373
- const result = await handler(event, mockContext);
374
-
375
- expect(result.principalId).toBe('user-123');
376
- expect(result.policyDocument.Statement[0].Effect).toBe('Allow');
377
- });
378
- });
379
-
380
- describe('requestHandler', () => {
381
- function createRequestEvent(
382
- headers: Record<string, string | undefined>,
383
- ): APIGatewayRequestAuthorizerEvent {
384
- return {
385
- type: 'REQUEST',
386
- methodArn:
387
- 'arn:aws:execute-api:us-east-1:123456789:api-id/stage/GET/resource',
388
- headers,
389
- multiValueHeaders: null,
390
- pathParameters: null,
391
- queryStringParameters: null,
392
- multiValueQueryStringParameters: null,
393
- stageVariables: null,
394
- requestContext: {} as any,
395
- resource: '/resource',
396
- path: '/resource',
397
- httpMethod: 'GET',
398
- };
399
- }
400
-
401
- it('should allow valid token from authorization header', async () => {
402
- const authorizer = new OidcAuthorizer({
403
- config: {
404
- issuer: 'https://auth.example.com',
405
- audience: 'my-api',
406
- },
407
- });
408
-
409
- const token = await createTestToken();
410
- const event = createRequestEvent({
411
- authorization: `Bearer ${token}`,
412
- });
413
-
414
- const handler = authorizer.requestHandler();
415
- const result = await handler(event, mockContext);
416
-
417
- expect(result.principalId).toBe('user-123');
418
- expect(result.policyDocument.Statement[0].Effect).toBe('Allow');
419
- });
420
-
421
- it('should deny when no token is provided', async () => {
422
- const authorizer = new OidcAuthorizer({
423
- config: {
424
- issuer: 'https://auth.example.com',
425
- audience: 'my-api',
426
- },
427
- });
428
-
429
- const event = createRequestEvent({});
430
-
431
- const handler = authorizer.requestHandler();
432
- const result = await handler(event, mockContext);
433
-
434
- expect(result.principalId).toBe('unauthorized');
435
- expect(result.policyDocument.Statement[0].Effect).toBe('Deny');
436
- });
437
-
438
- it('should extract token from cookie', async () => {
439
- const authorizer = new OidcAuthorizer({
440
- config: {
441
- issuer: 'https://auth.example.com',
442
- audience: 'my-api',
443
- },
444
- extraction: {
445
- cookieName: 'auth_token',
446
- },
447
- });
448
-
449
- const token = await createTestToken();
450
- const event = createRequestEvent({
451
- cookie: `auth_token=${token}; other=value`,
452
- });
453
-
454
- const handler = authorizer.requestHandler();
455
- const result = await handler(event, mockContext);
456
-
457
- expect(result.principalId).toBe('user-123');
458
- expect(result.policyDocument.Statement[0].Effect).toBe('Allow');
459
- });
460
-
461
- it('should handle case-insensitive header names', async () => {
462
- const authorizer = new OidcAuthorizer({
463
- config: {
464
- issuer: 'https://auth.example.com',
465
- audience: 'my-api',
466
- },
467
- });
468
-
469
- const token = await createTestToken();
470
- const event = createRequestEvent({
471
- Authorization: `Bearer ${token}`,
472
- });
473
-
474
- const handler = authorizer.requestHandler();
475
- const result = await handler(event, mockContext);
476
-
477
- expect(result.principalId).toBe('user-123');
478
- expect(result.policyDocument.Statement[0].Effect).toBe('Allow');
479
- });
480
-
481
- it('should extract token without prefix when configured', async () => {
482
- const authorizer = new OidcAuthorizer({
483
- config: {
484
- issuer: 'https://auth.example.com',
485
- audience: 'my-api',
486
- },
487
- extraction: {
488
- headerName: 'x-auth-token',
489
- tokenPrefix: '',
490
- },
491
- });
492
-
493
- const token = await createTestToken();
494
- const event = createRequestEvent({
495
- 'x-auth-token': token,
496
- });
497
-
498
- const handler = authorizer.requestHandler();
499
- const result = await handler(event, mockContext);
500
-
501
- expect(result.principalId).toBe('user-123');
502
- expect(result.policyDocument.Statement[0].Effect).toBe('Allow');
503
- });
504
-
505
- it('should deny invalid token', async () => {
506
- const authorizer = new OidcAuthorizer({
507
- config: {
508
- issuer: 'https://auth.example.com',
509
- audience: 'my-api',
510
- },
511
- });
512
-
513
- const event = createRequestEvent({
514
- authorization: 'Bearer invalid-token',
515
- });
516
-
517
- const handler = authorizer.requestHandler();
518
- const result = await handler(event, mockContext);
519
-
520
- expect(result.principalId).toBe('unauthorized');
521
- expect(result.policyDocument.Statement[0].Effect).toBe('Deny');
522
- });
523
-
524
- it('should use wildcard resource by default', async () => {
525
- const authorizer = new OidcAuthorizer({
526
- config: {
527
- issuer: 'https://auth.example.com',
528
- audience: 'my-api',
529
- },
530
- });
531
-
532
- const token = await createTestToken();
533
- const event = createRequestEvent({
534
- authorization: `Bearer ${token}`,
535
- });
536
-
537
- const handler = authorizer.requestHandler();
538
- const result = await handler(event, mockContext);
539
-
540
- const statement = result.policyDocument
541
- .Statement[0] as StatementWithResource;
542
- expect(statement.Resource).toBe(
543
- 'arn:aws:execute-api:us-east-1:123456789:api-id/stage/*',
544
- );
545
- });
546
-
547
- it('should include context from getContext', async () => {
548
- const authorizer = new OidcAuthorizer({
549
- config: {
550
- issuer: 'https://auth.example.com',
551
- audience: 'my-api',
552
- },
553
- getContext: (claims) => ({
554
- userId: claims.sub!,
555
- }),
556
- });
557
-
558
- const token = await createTestToken();
559
- const event = createRequestEvent({
560
- authorization: `Bearer ${token}`,
561
- });
562
-
563
- const handler = authorizer.requestHandler();
564
- const result = await handler(event, mockContext);
565
-
566
- expect(result.context).toEqual({
567
- userId: 'user-123',
568
- });
569
- });
570
-
571
- it('should deny when authorize callback returns false', async () => {
572
- const authorizer = new OidcAuthorizer({
573
- config: {
574
- issuer: 'https://auth.example.com',
575
- audience: 'my-api',
576
- },
577
- authorize: () => false,
578
- });
579
-
580
- const token = await createTestToken();
581
- const event = createRequestEvent({
582
- authorization: `Bearer ${token}`,
583
- });
584
-
585
- const handler = authorizer.requestHandler();
586
- const result = await handler(event, mockContext);
587
-
588
- expect(result.principalId).toBe('unauthorized');
589
- expect(result.policyDocument.Statement[0].Effect).toBe('Deny');
590
- });
591
-
592
- it('should handle null headers gracefully', async () => {
593
- const authorizer = new OidcAuthorizer({
594
- config: {
595
- issuer: 'https://auth.example.com',
596
- audience: 'my-api',
597
- },
598
- });
599
-
600
- const event: APIGatewayRequestAuthorizerEvent = {
601
- type: 'REQUEST',
602
- methodArn:
603
- 'arn:aws:execute-api:us-east-1:123456789:api-id/stage/GET/resource',
604
- headers: null,
605
- multiValueHeaders: null,
606
- pathParameters: null,
607
- queryStringParameters: null,
608
- multiValueQueryStringParameters: null,
609
- stageVariables: null,
610
- requestContext: {} as any,
611
- resource: '/resource',
612
- path: '/resource',
613
- httpMethod: 'GET',
614
- };
615
-
616
- const handler = authorizer.requestHandler();
617
- const result = await handler(event, mockContext);
618
-
619
- expect(result.principalId).toBe('unauthorized');
620
- expect(result.policyDocument.Statement[0].Effect).toBe('Deny');
621
- });
622
- });
118
+ beforeAll(async () => {
119
+ await setupKeys();
120
+ server.listen({ onUnhandledRequest: 'error' });
121
+ });
122
+
123
+ afterEach(() => {
124
+ server.resetHandlers();
125
+ });
126
+
127
+ afterAll(() => {
128
+ server.close();
129
+ });
130
+
131
+ describe('tokenHandler', () => {
132
+ it('should allow valid token', async () => {
133
+ const authorizer = new OidcAuthorizer({
134
+ config: {
135
+ issuer: 'https://auth.example.com',
136
+ audience: 'my-api',
137
+ },
138
+ });
139
+
140
+ const token = await createTestToken();
141
+ const event: APIGatewayTokenAuthorizerEvent = {
142
+ type: 'TOKEN',
143
+ authorizationToken: `Bearer ${token}`,
144
+ methodArn:
145
+ 'arn:aws:execute-api:us-east-1:123456789:api-id/stage/GET/resource',
146
+ };
147
+
148
+ const handler = authorizer.tokenHandler();
149
+ const result = await handler(event, mockContext);
150
+
151
+ expect(result.principalId).toBe('user-123');
152
+ expect(result.policyDocument.Statement[0].Effect).toBe('Allow');
153
+ });
154
+
155
+ it('should deny invalid token', async () => {
156
+ const authorizer = new OidcAuthorizer({
157
+ config: {
158
+ issuer: 'https://auth.example.com',
159
+ audience: 'my-api',
160
+ },
161
+ });
162
+
163
+ const event: APIGatewayTokenAuthorizerEvent = {
164
+ type: 'TOKEN',
165
+ authorizationToken: 'Bearer invalid-token',
166
+ methodArn:
167
+ 'arn:aws:execute-api:us-east-1:123456789:api-id/stage/GET/resource',
168
+ };
169
+
170
+ const handler = authorizer.tokenHandler();
171
+ const result = await handler(event, mockContext);
172
+
173
+ expect(result.principalId).toBe('unauthorized');
174
+ expect(result.policyDocument.Statement[0].Effect).toBe('Deny');
175
+ });
176
+
177
+ it('should handle token without Bearer prefix', async () => {
178
+ const authorizer = new OidcAuthorizer({
179
+ config: {
180
+ issuer: 'https://auth.example.com',
181
+ audience: 'my-api',
182
+ },
183
+ });
184
+
185
+ const token = await createTestToken();
186
+ const event: APIGatewayTokenAuthorizerEvent = {
187
+ type: 'TOKEN',
188
+ authorizationToken: token,
189
+ methodArn:
190
+ 'arn:aws:execute-api:us-east-1:123456789:api-id/stage/GET/resource',
191
+ };
192
+
193
+ const handler = authorizer.tokenHandler();
194
+ const result = await handler(event, mockContext);
195
+
196
+ expect(result.principalId).toBe('user-123');
197
+ expect(result.policyDocument.Statement[0].Effect).toBe('Allow');
198
+ });
199
+
200
+ it('should use wildcard resource by default', async () => {
201
+ const authorizer = new OidcAuthorizer({
202
+ config: {
203
+ issuer: 'https://auth.example.com',
204
+ audience: 'my-api',
205
+ },
206
+ });
207
+
208
+ const token = await createTestToken();
209
+ const event: APIGatewayTokenAuthorizerEvent = {
210
+ type: 'TOKEN',
211
+ authorizationToken: `Bearer ${token}`,
212
+ methodArn:
213
+ 'arn:aws:execute-api:us-east-1:123456789:api-id/stage/GET/resource',
214
+ };
215
+
216
+ const handler = authorizer.tokenHandler();
217
+ const result = await handler(event, mockContext);
218
+
219
+ const statement = result.policyDocument
220
+ .Statement[0] as StatementWithResource;
221
+ expect(statement.Resource).toBe(
222
+ 'arn:aws:execute-api:us-east-1:123456789:api-id/stage/*',
223
+ );
224
+ });
225
+
226
+ it('should use exact resource when wildcardResource is false', async () => {
227
+ const authorizer = new OidcAuthorizer({
228
+ config: {
229
+ issuer: 'https://auth.example.com',
230
+ audience: 'my-api',
231
+ },
232
+ wildcardResource: false,
233
+ });
234
+
235
+ const token = await createTestToken();
236
+ const event: APIGatewayTokenAuthorizerEvent = {
237
+ type: 'TOKEN',
238
+ authorizationToken: `Bearer ${token}`,
239
+ methodArn:
240
+ 'arn:aws:execute-api:us-east-1:123456789:api-id/stage/GET/resource',
241
+ };
242
+
243
+ const handler = authorizer.tokenHandler();
244
+ const result = await handler(event, mockContext);
245
+
246
+ const statement = result.policyDocument
247
+ .Statement[0] as StatementWithResource;
248
+ expect(statement.Resource).toBe(
249
+ 'arn:aws:execute-api:us-east-1:123456789:api-id/stage/GET/resource',
250
+ );
251
+ });
252
+
253
+ it('should use custom getPrincipalId', async () => {
254
+ const authorizer = new OidcAuthorizer({
255
+ config: {
256
+ issuer: 'https://auth.example.com',
257
+ audience: 'my-api',
258
+ },
259
+ getPrincipalId: (claims) => `custom-${claims.sub}`,
260
+ });
261
+
262
+ const token = await createTestToken();
263
+ const event: APIGatewayTokenAuthorizerEvent = {
264
+ type: 'TOKEN',
265
+ authorizationToken: `Bearer ${token}`,
266
+ methodArn:
267
+ 'arn:aws:execute-api:us-east-1:123456789:api-id/stage/GET/resource',
268
+ };
269
+
270
+ const handler = authorizer.tokenHandler();
271
+ const result = await handler(event, mockContext);
272
+
273
+ expect(result.principalId).toBe('custom-user-123');
274
+ });
275
+
276
+ it('should include context from getContext', async () => {
277
+ const authorizer = new OidcAuthorizer({
278
+ config: {
279
+ issuer: 'https://auth.example.com',
280
+ audience: 'my-api',
281
+ },
282
+ getContext: (claims) => ({
283
+ userId: claims.sub!,
284
+ role: 'admin',
285
+ }),
286
+ });
287
+
288
+ const token = await createTestToken();
289
+ const event: APIGatewayTokenAuthorizerEvent = {
290
+ type: 'TOKEN',
291
+ authorizationToken: `Bearer ${token}`,
292
+ methodArn:
293
+ 'arn:aws:execute-api:us-east-1:123456789:api-id/stage/GET/resource',
294
+ };
295
+
296
+ const handler = authorizer.tokenHandler();
297
+ const result = await handler(event, mockContext);
298
+
299
+ expect(result.context).toEqual({
300
+ userId: 'user-123',
301
+ role: 'admin',
302
+ });
303
+ });
304
+
305
+ it('should deny when authorize callback returns false', async () => {
306
+ const authorizer = new OidcAuthorizer({
307
+ config: {
308
+ issuer: 'https://auth.example.com',
309
+ audience: 'my-api',
310
+ },
311
+ authorize: () => false,
312
+ });
313
+
314
+ const token = await createTestToken();
315
+ const event: APIGatewayTokenAuthorizerEvent = {
316
+ type: 'TOKEN',
317
+ authorizationToken: `Bearer ${token}`,
318
+ methodArn:
319
+ 'arn:aws:execute-api:us-east-1:123456789:api-id/stage/GET/resource',
320
+ };
321
+
322
+ const handler = authorizer.tokenHandler();
323
+ const result = await handler(event, mockContext);
324
+
325
+ expect(result.principalId).toBe('unauthorized');
326
+ expect(result.policyDocument.Statement[0].Effect).toBe('Deny');
327
+ });
328
+
329
+ it('should allow when authorize callback returns true', async () => {
330
+ const authorizer = new OidcAuthorizer({
331
+ config: {
332
+ issuer: 'https://auth.example.com',
333
+ audience: 'my-api',
334
+ },
335
+ authorize: (claims) => claims.sub === 'user-123',
336
+ });
337
+
338
+ const token = await createTestToken();
339
+ const event: APIGatewayTokenAuthorizerEvent = {
340
+ type: 'TOKEN',
341
+ authorizationToken: `Bearer ${token}`,
342
+ methodArn:
343
+ 'arn:aws:execute-api:us-east-1:123456789:api-id/stage/GET/resource',
344
+ };
345
+
346
+ const handler = authorizer.tokenHandler();
347
+ const result = await handler(event, mockContext);
348
+
349
+ expect(result.principalId).toBe('user-123');
350
+ expect(result.policyDocument.Statement[0].Effect).toBe('Allow');
351
+ });
352
+
353
+ it('should support async authorize callback', async () => {
354
+ const authorizer = new OidcAuthorizer({
355
+ config: {
356
+ issuer: 'https://auth.example.com',
357
+ audience: 'my-api',
358
+ },
359
+ authorize: async (claims) => {
360
+ await Promise.resolve();
361
+ return claims.sub === 'user-123';
362
+ },
363
+ });
364
+
365
+ const token = await createTestToken();
366
+ const event: APIGatewayTokenAuthorizerEvent = {
367
+ type: 'TOKEN',
368
+ authorizationToken: `Bearer ${token}`,
369
+ methodArn:
370
+ 'arn:aws:execute-api:us-east-1:123456789:api-id/stage/GET/resource',
371
+ };
372
+
373
+ const handler = authorizer.tokenHandler();
374
+ const result = await handler(event, mockContext);
375
+
376
+ expect(result.principalId).toBe('user-123');
377
+ expect(result.policyDocument.Statement[0].Effect).toBe('Allow');
378
+ });
379
+ });
380
+
381
+ describe('requestHandler', () => {
382
+ function createRequestEvent(
383
+ headers: Record<string, string | undefined>,
384
+ ): APIGatewayRequestAuthorizerEvent {
385
+ return {
386
+ type: 'REQUEST',
387
+ methodArn:
388
+ 'arn:aws:execute-api:us-east-1:123456789:api-id/stage/GET/resource',
389
+ headers,
390
+ multiValueHeaders: null,
391
+ pathParameters: null,
392
+ queryStringParameters: null,
393
+ multiValueQueryStringParameters: null,
394
+ stageVariables: null,
395
+ requestContext: {} as any,
396
+ resource: '/resource',
397
+ path: '/resource',
398
+ httpMethod: 'GET',
399
+ };
400
+ }
401
+
402
+ it('should allow valid token from authorization header', async () => {
403
+ const authorizer = new OidcAuthorizer({
404
+ config: {
405
+ issuer: 'https://auth.example.com',
406
+ audience: 'my-api',
407
+ },
408
+ });
409
+
410
+ const token = await createTestToken();
411
+ const event = createRequestEvent({
412
+ authorization: `Bearer ${token}`,
413
+ });
414
+
415
+ const handler = authorizer.requestHandler();
416
+ const result = await handler(event, mockContext);
417
+
418
+ expect(result.principalId).toBe('user-123');
419
+ expect(result.policyDocument.Statement[0].Effect).toBe('Allow');
420
+ });
421
+
422
+ it('should deny when no token is provided', async () => {
423
+ const authorizer = new OidcAuthorizer({
424
+ config: {
425
+ issuer: 'https://auth.example.com',
426
+ audience: 'my-api',
427
+ },
428
+ });
429
+
430
+ const event = createRequestEvent({});
431
+
432
+ const handler = authorizer.requestHandler();
433
+ const result = await handler(event, mockContext);
434
+
435
+ expect(result.principalId).toBe('unauthorized');
436
+ expect(result.policyDocument.Statement[0].Effect).toBe('Deny');
437
+ });
438
+
439
+ it('should extract token from cookie', async () => {
440
+ const authorizer = new OidcAuthorizer({
441
+ config: {
442
+ issuer: 'https://auth.example.com',
443
+ audience: 'my-api',
444
+ },
445
+ extraction: {
446
+ cookieName: 'auth_token',
447
+ },
448
+ });
449
+
450
+ const token = await createTestToken();
451
+ const event = createRequestEvent({
452
+ cookie: `auth_token=${token}; other=value`,
453
+ });
454
+
455
+ const handler = authorizer.requestHandler();
456
+ const result = await handler(event, mockContext);
457
+
458
+ expect(result.principalId).toBe('user-123');
459
+ expect(result.policyDocument.Statement[0].Effect).toBe('Allow');
460
+ });
461
+
462
+ it('should handle case-insensitive header names', async () => {
463
+ const authorizer = new OidcAuthorizer({
464
+ config: {
465
+ issuer: 'https://auth.example.com',
466
+ audience: 'my-api',
467
+ },
468
+ });
469
+
470
+ const token = await createTestToken();
471
+ const event = createRequestEvent({
472
+ Authorization: `Bearer ${token}`,
473
+ });
474
+
475
+ const handler = authorizer.requestHandler();
476
+ const result = await handler(event, mockContext);
477
+
478
+ expect(result.principalId).toBe('user-123');
479
+ expect(result.policyDocument.Statement[0].Effect).toBe('Allow');
480
+ });
481
+
482
+ it('should extract token without prefix when configured', async () => {
483
+ const authorizer = new OidcAuthorizer({
484
+ config: {
485
+ issuer: 'https://auth.example.com',
486
+ audience: 'my-api',
487
+ },
488
+ extraction: {
489
+ headerName: 'x-auth-token',
490
+ tokenPrefix: '',
491
+ },
492
+ });
493
+
494
+ const token = await createTestToken();
495
+ const event = createRequestEvent({
496
+ 'x-auth-token': token,
497
+ });
498
+
499
+ const handler = authorizer.requestHandler();
500
+ const result = await handler(event, mockContext);
501
+
502
+ expect(result.principalId).toBe('user-123');
503
+ expect(result.policyDocument.Statement[0].Effect).toBe('Allow');
504
+ });
505
+
506
+ it('should deny invalid token', async () => {
507
+ const authorizer = new OidcAuthorizer({
508
+ config: {
509
+ issuer: 'https://auth.example.com',
510
+ audience: 'my-api',
511
+ },
512
+ });
513
+
514
+ const event = createRequestEvent({
515
+ authorization: 'Bearer invalid-token',
516
+ });
517
+
518
+ const handler = authorizer.requestHandler();
519
+ const result = await handler(event, mockContext);
520
+
521
+ expect(result.principalId).toBe('unauthorized');
522
+ expect(result.policyDocument.Statement[0].Effect).toBe('Deny');
523
+ });
524
+
525
+ it('should use wildcard resource by default', async () => {
526
+ const authorizer = new OidcAuthorizer({
527
+ config: {
528
+ issuer: 'https://auth.example.com',
529
+ audience: 'my-api',
530
+ },
531
+ });
532
+
533
+ const token = await createTestToken();
534
+ const event = createRequestEvent({
535
+ authorization: `Bearer ${token}`,
536
+ });
537
+
538
+ const handler = authorizer.requestHandler();
539
+ const result = await handler(event, mockContext);
540
+
541
+ const statement = result.policyDocument
542
+ .Statement[0] as StatementWithResource;
543
+ expect(statement.Resource).toBe(
544
+ 'arn:aws:execute-api:us-east-1:123456789:api-id/stage/*',
545
+ );
546
+ });
547
+
548
+ it('should include context from getContext', async () => {
549
+ const authorizer = new OidcAuthorizer({
550
+ config: {
551
+ issuer: 'https://auth.example.com',
552
+ audience: 'my-api',
553
+ },
554
+ getContext: (claims) => ({
555
+ userId: claims.sub!,
556
+ }),
557
+ });
558
+
559
+ const token = await createTestToken();
560
+ const event = createRequestEvent({
561
+ authorization: `Bearer ${token}`,
562
+ });
563
+
564
+ const handler = authorizer.requestHandler();
565
+ const result = await handler(event, mockContext);
566
+
567
+ expect(result.context).toEqual({
568
+ userId: 'user-123',
569
+ });
570
+ });
571
+
572
+ it('should deny when authorize callback returns false', async () => {
573
+ const authorizer = new OidcAuthorizer({
574
+ config: {
575
+ issuer: 'https://auth.example.com',
576
+ audience: 'my-api',
577
+ },
578
+ authorize: () => false,
579
+ });
580
+
581
+ const token = await createTestToken();
582
+ const event = createRequestEvent({
583
+ authorization: `Bearer ${token}`,
584
+ });
585
+
586
+ const handler = authorizer.requestHandler();
587
+ const result = await handler(event, mockContext);
588
+
589
+ expect(result.principalId).toBe('unauthorized');
590
+ expect(result.policyDocument.Statement[0].Effect).toBe('Deny');
591
+ });
592
+
593
+ it('should handle null headers gracefully', async () => {
594
+ const authorizer = new OidcAuthorizer({
595
+ config: {
596
+ issuer: 'https://auth.example.com',
597
+ audience: 'my-api',
598
+ },
599
+ });
600
+
601
+ const event: APIGatewayRequestAuthorizerEvent = {
602
+ type: 'REQUEST',
603
+ methodArn:
604
+ 'arn:aws:execute-api:us-east-1:123456789:api-id/stage/GET/resource',
605
+ headers: null,
606
+ multiValueHeaders: null,
607
+ pathParameters: null,
608
+ queryStringParameters: null,
609
+ multiValueQueryStringParameters: null,
610
+ stageVariables: null,
611
+ requestContext: {} as any,
612
+ resource: '/resource',
613
+ path: '/resource',
614
+ httpMethod: 'GET',
615
+ };
616
+
617
+ const handler = authorizer.requestHandler();
618
+ const result = await handler(event, mockContext);
619
+
620
+ expect(result.principalId).toBe('unauthorized');
621
+ expect(result.policyDocument.Statement[0].Effect).toBe('Deny');
622
+ });
623
+ });
623
624
  });