Skip to content

Commit 5823160

Browse files
authored
Merge pull request #46 from kinde-oss/leo/verify_jwt
feat: verify JWTs before accessing and storing
2 parents 6cc94ab + 543626e commit 5823160

23 files changed

+627
-239
lines changed

lib/__tests__/mocks.ts

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,27 @@
1+
import { type JWK, SignJWT, exportJWK, generateKeyPair, importJWK } from 'jose';
12
import { type SessionManager } from '../sdk/session-managers';
23

4+
let mockPrivateKey: JWK | undefined;
5+
let mockPublicKey: JWK | undefined;
6+
7+
export const mockJwtAlg = 'RS256';
8+
9+
export const getKeys = async (): Promise<{ privateKey: JWK; publicKey: JWK }> => {
10+
if (mockPrivateKey !== undefined && mockPublicKey !== undefined) {
11+
return { privateKey: mockPrivateKey, publicKey: mockPublicKey };
12+
}
13+
const { publicKey: generatedPublicKey, privateKey: generatedPrivateKey } =
14+
await generateKeyPair(mockJwtAlg);
15+
16+
const generatedPrivateJwk = await exportJWK(generatedPrivateKey);
17+
const generatedPublicJwk = await exportJWK(generatedPublicKey);
18+
19+
mockPrivateKey = generatedPrivateJwk;
20+
mockPublicKey = generatedPublicJwk;
21+
22+
return { privateKey: mockPrivateKey, publicKey: mockPublicKey };
23+
};
24+
325
export const fetchClient = jest.fn().mockImplementation(
426
async () =>
527
await Promise.resolve({
@@ -9,7 +31,7 @@ export const fetchClient = jest.fn().mockImplementation(
931
})
1032
);
1133

12-
export const getMockAccessToken = (
34+
export const getMockAccessToken = async (
1335
domain: string = 'local-testing@kinde.com',
1436
isExpired: boolean = false,
1537
isExpClaimMissing: boolean = false
@@ -35,13 +57,19 @@ export const getMockAccessToken = (
3557
},
3658
};
3759

60+
const { privateKey } = await getKeys();
61+
const key = await importJWK(privateKey, mockJwtAlg);
62+
const jwt = await new SignJWT(tokenPayload)
63+
.setProtectedHeader({ alg: mockJwtAlg })
64+
.sign(key);
65+
3866
return {
39-
token: `.${btoa(JSON.stringify(tokenPayload))}.`,
67+
token: jwt,
4068
payload: tokenPayload,
4169
};
4270
};
4371

44-
export const getMockIdToken = (
72+
export const getMockIdToken = async (
4573
domain: string = 'local-testing@kinde.com',
4674
isExpired: boolean = false
4775
) => {
@@ -65,8 +93,14 @@ export const getMockIdToken = (
6593
updated_at: iat,
6694
};
6795

96+
const { privateKey } = await getKeys();
97+
const key = await importJWK(privateKey, mockJwtAlg);
98+
const jwt = await new SignJWT(tokenPayload)
99+
.setProtectedHeader({ alg: mockJwtAlg })
100+
.sign(key);
101+
68102
return {
69-
token: `.${btoa(JSON.stringify(tokenPayload))}.`,
103+
token: jwt,
70104
payload: tokenPayload,
71105
};
72106
};

lib/__tests__/sdk/oauth2-flows/AuthCodeWithPKCE.spec.ts

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ describe('AuthCodeWitPKCE', () => {
1616
clientId: 'client-id',
1717
};
1818

19+
beforeAll(async () => {
20+
const { publicKey } = await mocks.getKeys();
21+
clientConfig.jwks = { keys: [publicKey] };
22+
});
23+
1924
describe('new AuthCodeWithPKCE', () => {
2025
it('can construct AuthCodeWithPKCE instance', () => {
2126
expect(() => new AuthCodeWithPKCE(clientConfig)).not.toThrowError();
@@ -119,8 +124,10 @@ describe('AuthCodeWitPKCE', () => {
119124
});
120125

121126
it('saves tokens to memory store after exchanging auth code for tokens', async () => {
122-
const mockAccessToken = mocks.getMockAccessToken(clientConfig.authDomain);
123-
const mockIdToken = mocks.getMockIdToken(clientConfig.authDomain);
127+
const mockAccessToken = await mocks.getMockAccessToken(
128+
clientConfig.authDomain
129+
);
130+
const mockIdToken = await mocks.getMockIdToken(clientConfig.authDomain);
124131
mocks.fetchClient.mockResolvedValue({
125132
json: () => ({
126133
access_token: mockAccessToken.token,
@@ -159,7 +166,9 @@ describe('AuthCodeWitPKCE', () => {
159166
});
160167

161168
it('return an existing token if an unexpired token is available', async () => {
162-
const mockAccessToken = mocks.getMockAccessToken(clientConfig.authDomain);
169+
const mockAccessToken = await mocks.getMockAccessToken(
170+
clientConfig.authDomain
171+
);
163172
await sessionManager.setSessionItem('access_token', mockAccessToken.token);
164173
const client = new AuthCodeWithPKCE(clientConfig);
165174
const token = await client.getToken(sessionManager);
@@ -168,7 +177,7 @@ describe('AuthCodeWitPKCE', () => {
168177
});
169178

170179
it('throws an error if no refresh token is found in memory', async () => {
171-
const mockAccessToken = mocks.getMockAccessToken(
180+
const mockAccessToken = await mocks.getMockAccessToken(
172181
clientConfig.authDomain,
173182
true
174183
);
@@ -180,8 +189,8 @@ describe('AuthCodeWitPKCE', () => {
180189
});
181190

182191
it('fetches new tokens if access token is expired and refresh token is available', async () => {
183-
const newAccessToken = mocks.getMockAccessToken(clientConfig.authDomain);
184-
const newIdToken = mocks.getMockIdToken(clientConfig.authDomain);
192+
const newAccessToken = await mocks.getMockAccessToken(clientConfig.authDomain);
193+
const newIdToken = await mocks.getMockIdToken(clientConfig.authDomain);
185194
mocks.fetchClient.mockResolvedValue({
186195
json: () => ({
187196
access_token: newAccessToken.token,
@@ -190,7 +199,7 @@ describe('AuthCodeWitPKCE', () => {
190199
}),
191200
});
192201

193-
const expiredAccessToken = mocks.getMockAccessToken(
202+
const expiredAccessToken = await mocks.getMockAccessToken(
194203
clientConfig.authDomain,
195204
true
196205
);
@@ -219,8 +228,8 @@ describe('AuthCodeWitPKCE', () => {
219228
});
220229

221230
it('overrides SDK version header if options are provided to client constructor', async () => {
222-
const newAccessToken = mocks.getMockAccessToken(clientConfig.authDomain);
223-
const newIdToken = mocks.getMockIdToken(clientConfig.authDomain);
231+
const newAccessToken = await mocks.getMockAccessToken(clientConfig.authDomain);
232+
const newIdToken = await mocks.getMockIdToken(clientConfig.authDomain);
224233
mocks.fetchClient.mockResolvedValue({
225234
json: () => ({
226235
access_token: newAccessToken.token,
@@ -229,7 +238,7 @@ describe('AuthCodeWitPKCE', () => {
229238
}),
230239
});
231240

232-
const expiredAccessToken = mocks.getMockAccessToken(
241+
const expiredAccessToken = await mocks.getMockAccessToken(
233242
clientConfig.authDomain,
234243
true
235244
);
@@ -260,8 +269,8 @@ describe('AuthCodeWitPKCE', () => {
260269
});
261270

262271
it('commits new tokens to memory if new tokens are fetched', async () => {
263-
const newAccessToken = mocks.getMockAccessToken(clientConfig.authDomain);
264-
const newIdToken = mocks.getMockIdToken(clientConfig.authDomain);
272+
const newAccessToken = await mocks.getMockAccessToken(clientConfig.authDomain);
273+
const newIdToken = await mocks.getMockIdToken(clientConfig.authDomain);
265274
const newRefreshToken = 'new_refresh_token';
266275

267276
mocks.fetchClient.mockResolvedValue({
@@ -272,7 +281,7 @@ describe('AuthCodeWitPKCE', () => {
272281
}),
273282
});
274283

275-
const expiredAccessToken = mocks.getMockAccessToken(
284+
const expiredAccessToken = await mocks.getMockAccessToken(
276285
clientConfig.authDomain,
277286
true
278287
);

lib/__tests__/sdk/oauth2-flows/AuthorizationCode.spec.ts

Lines changed: 29 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ describe('AuthorizationCode', () => {
2121
clientId: 'client-id',
2222
};
2323

24+
beforeAll(async () => {
25+
const { publicKey } = await mocks.getKeys();
26+
clientConfig.jwks = { keys: [publicKey] };
27+
});
28+
2429
describe('new AuthorizationCode', () => {
2530
it('can construct AuthorizationCode instance', () => {
2631
expect(
@@ -164,8 +169,10 @@ describe('AuthorizationCode', () => {
164169
});
165170

166171
it('saves tokens to memory store after exchanging auth code for tokens', async () => {
167-
const mockAccessToken = mocks.getMockAccessToken(clientConfig.authDomain);
168-
const mockIdToken = mocks.getMockIdToken(clientConfig.authDomain);
172+
const mockAccessToken = await mocks.getMockAccessToken(
173+
clientConfig.authDomain
174+
);
175+
const mockIdToken = await mocks.getMockIdToken(clientConfig.authDomain);
169176
mocks.fetchClient.mockResolvedValue({
170177
json: () => ({
171178
access_token: mockAccessToken.token,
@@ -200,7 +207,9 @@ describe('AuthorizationCode', () => {
200207
});
201208

202209
it('return an existing token if an unexpired token is available', async () => {
203-
const mockAccessToken = mocks.getMockAccessToken(clientConfig.authDomain);
210+
const mockAccessToken = await mocks.getMockAccessToken(
211+
clientConfig.authDomain
212+
);
204213
await sessionManager.setSessionItem('access_token', mockAccessToken.token);
205214
const client = new AuthorizationCode(clientConfig, clientSecret);
206215
const token = await client.getToken(sessionManager);
@@ -216,7 +225,7 @@ describe('AuthorizationCode', () => {
216225
});
217226

218227
it('throws an error if no refresh token is found in memory', async () => {
219-
const mockAccessToken = mocks.getMockAccessToken(
228+
const mockAccessToken = await mocks.getMockAccessToken(
220229
clientConfig.authDomain,
221230
true
222231
);
@@ -236,7 +245,7 @@ describe('AuthorizationCode', () => {
236245
}),
237246
});
238247

239-
const expiredAccessToken = mocks.getMockAccessToken(
248+
const expiredAccessToken = await mocks.getMockAccessToken(
240249
clientConfig.authDomain,
241250
true
242251
);
@@ -251,8 +260,8 @@ describe('AuthorizationCode', () => {
251260
});
252261

253262
it('fetches new tokens if access token is expired and refresh token is available', async () => {
254-
const newAccessToken = mocks.getMockAccessToken(clientConfig.authDomain);
255-
const newIdToken = mocks.getMockIdToken(clientConfig.authDomain);
263+
const newAccessToken = await mocks.getMockAccessToken(clientConfig.authDomain);
264+
const newIdToken = await mocks.getMockIdToken(clientConfig.authDomain);
256265
mocks.fetchClient.mockResolvedValue({
257266
json: () => ({
258267
access_token: newAccessToken.token,
@@ -261,7 +270,7 @@ describe('AuthorizationCode', () => {
261270
}),
262271
});
263272

264-
const expiredAccessToken = mocks.getMockAccessToken(
273+
const expiredAccessToken = await mocks.getMockAccessToken(
265274
clientConfig.authDomain,
266275
true
267276
);
@@ -291,8 +300,8 @@ describe('AuthorizationCode', () => {
291300
});
292301

293302
it('overrides SDK version header if options are provided to client constructor', async () => {
294-
const newAccessToken = mocks.getMockAccessToken(clientConfig.authDomain);
295-
const newIdToken = mocks.getMockIdToken(clientConfig.authDomain);
303+
const newAccessToken = await mocks.getMockAccessToken(clientConfig.authDomain);
304+
const newIdToken = await mocks.getMockIdToken(clientConfig.authDomain);
296305
mocks.fetchClient.mockResolvedValue({
297306
json: () => ({
298307
access_token: newAccessToken.token,
@@ -301,7 +310,7 @@ describe('AuthorizationCode', () => {
301310
}),
302311
});
303312

304-
const expiredAccessToken = mocks.getMockAccessToken(
313+
const expiredAccessToken = await mocks.getMockAccessToken(
305314
clientConfig.authDomain,
306315
true
307316
);
@@ -335,8 +344,8 @@ describe('AuthorizationCode', () => {
335344
});
336345

337346
it('commits new tokens to memory if new tokens are fetched', async () => {
338-
const newAccessToken = mocks.getMockAccessToken(clientConfig.authDomain);
339-
const newIdToken = mocks.getMockIdToken(clientConfig.authDomain);
347+
const newAccessToken = await mocks.getMockAccessToken(clientConfig.authDomain);
348+
const newIdToken = await mocks.getMockIdToken(clientConfig.authDomain);
340349
const newRefreshToken = 'new_refresh_token';
341350

342351
mocks.fetchClient.mockResolvedValue({
@@ -347,7 +356,7 @@ describe('AuthorizationCode', () => {
347356
}),
348357
});
349358

350-
const expiredAccessToken = mocks.getMockAccessToken(
359+
const expiredAccessToken = await mocks.getMockAccessToken(
351360
clientConfig.authDomain,
352361
true
353362
);
@@ -372,7 +381,9 @@ describe('AuthorizationCode', () => {
372381
await sessionManager.setSessionItem('refresh_token', 'mines are here');
373382
await sessionManager.setSessionItem(
374383
'access_token',
375-
mocks.getMockAccessToken(clientConfig.authDomain, true).token
384+
(
385+
await mocks.getMockAccessToken(clientConfig.authDomain, true)
386+
).token
376387
);
377388

378389
const client = new AuthorizationCode(clientConfig, clientSecret);
@@ -392,7 +403,9 @@ describe('AuthorizationCode', () => {
392403
});
393404

394405
it('fetches user profile using the available access token', async () => {
395-
const mockAccessToken = mocks.getMockAccessToken(clientConfig.authDomain);
406+
const mockAccessToken = await mocks.getMockAccessToken(
407+
clientConfig.authDomain
408+
);
396409
await sessionManager.setSessionItem('access_token', mockAccessToken.token);
397410

398411
const headers = new Headers();
@@ -416,26 +429,5 @@ describe('AuthorizationCode', () => {
416429
{ method: 'GET', headers }
417430
);
418431
});
419-
420-
it('commits fetched user details to memory store', async () => {
421-
const mockAccessToken = mocks.getMockAccessToken(clientConfig.authDomain);
422-
await sessionManager.setSessionItem('access_token', mockAccessToken.token);
423-
const userDetails = {
424-
family_name: 'family_name',
425-
given_name: 'give_name',
426-
email: 'test@test.com',
427-
picture: null,
428-
id: 'id',
429-
};
430-
431-
mocks.fetchClient.mockResolvedValue({
432-
json: () => userDetails,
433-
});
434-
435-
const client = new AuthorizationCode(clientConfig, clientSecret);
436-
await client.getUserProfile(sessionManager);
437-
expect(mocks.fetchClient).toHaveBeenCalledTimes(1);
438-
expect(await sessionManager.getSessionItem('user')).toStrictEqual(userDetails);
439-
});
440432
});
441433
});

0 commit comments

Comments
 (0)