Skip to content

Commit 1d55abf

Browse files
committed
feat: add JWT validation on JWT retrieval
1 parent e377c6d commit 1d55abf

16 files changed

+233
-175
lines changed

lib/__tests__/mocks.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,3 +129,24 @@ class ServerSessionManager implements SessionManager {
129129
export const sessionManager = new ServerSessionManager();
130130

131131
global.fetch = fetchClient;
132+
133+
// Mock @kinde/jwt-validator
134+
export const createJwtValidatorMock = () => ({
135+
validateToken: vi.fn().mockImplementation(async ({ token, domain }) => {
136+
if (!token) {
137+
return { valid: false, message: 'Token is required' };
138+
}
139+
140+
if (!domain) {
141+
return { valid: false, message: 'Domain is required' };
142+
}
143+
144+
const jwtParts = token.split('.');
145+
if (jwtParts.length !== 3) {
146+
return { valid: false, message: 'Invalid JWT format' };
147+
}
148+
149+
// If it passes basic validation, return true (simplified for testing)
150+
return { valid: true, message: 'Token is valid' };
151+
}),
152+
});

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

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,3 @@
1-
import { vi } from 'vitest';
2-
3-
// Mock the validateToken function - must be at the top for Vitest hoisting
4-
vi.mock('@kinde/jwt-validator', () => ({
5-
validateToken: vi.fn().mockImplementation(async ({ token }) => {
6-
try {
7-
const payload = JSON.parse(atob(token.split('.')[1]));
8-
const currentTime = Math.floor(Date.now() / 1000);
9-
const isExpired = payload.exp && currentTime >= payload.exp;
10-
return {
11-
valid: !isExpired,
12-
message: isExpired ? 'Token expired' : 'Token valid',
13-
};
14-
} catch (e) {
15-
return {
16-
valid: false,
17-
message: 'Invalid token format',
18-
};
19-
}
20-
}),
21-
}));
22-
231
import type {
242
AuthorizationCodeOptions,
253
SDKHeaderOverrideOptions,

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

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,3 @@
1-
import { vi } from 'vitest';
2-
3-
// Mock the validateToken function - must be at the top for Vitest hoisting
4-
vi.mock('@kinde/jwt-validator', () => ({
5-
validateToken: vi.fn().mockImplementation(async ({ token }) => {
6-
try {
7-
const payload = JSON.parse(atob(token.split('.')[1]));
8-
const currentTime = Math.floor(Date.now() / 1000);
9-
const isExpired = payload.exp && currentTime >= payload.exp;
10-
return {
11-
valid: !isExpired,
12-
message: isExpired ? 'Token expired' : 'Token valid',
13-
};
14-
} catch (e) {
15-
return {
16-
valid: false,
17-
message: 'Invalid token format',
18-
};
19-
}
20-
}),
21-
}));
22-
231
import { getSDKHeader } from '../../../sdk/version';
242
import * as mocks from '../../mocks';
253

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

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,3 @@
1-
import { vi } from 'vitest';
2-
3-
// Mock the validateToken function - must be at the top for Vitest hoisting
4-
vi.mock('@kinde/jwt-validator', () => ({
5-
validateToken: vi.fn().mockImplementation(async ({ token }) => {
6-
try {
7-
const payload = JSON.parse(atob(token.split('.')[1]));
8-
const currentTime = Math.floor(Date.now() / 1000);
9-
const isExpired = payload.exp && currentTime >= payload.exp;
10-
return {
11-
valid: !isExpired,
12-
message: isExpired ? 'Token expired' : 'Token valid',
13-
};
14-
} catch (e) {
15-
return {
16-
valid: false,
17-
message: 'Invalid token format',
18-
};
19-
}
20-
}),
21-
}));
22-
231
import { ClientCredentials } from '../../../sdk/oauth2-flows/ClientCredentials';
242
import { type ClientCredentialsOptions } from '../../../sdk/oauth2-flows/types';
253
import {

lib/__tests__/sdk/utilities/feature-flags.spec.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,13 @@ import {
1111
describe('feature-flags', () => {
1212
let mockAccessToken: Awaited<ReturnType<typeof mocks.getMockAccessToken>>;
1313
const { sessionManager } = mocks;
14+
const authDomain = 'local-testing@kinde.com';
1415

1516
let validationDetails: TokenValidationDetailsType;
1617

1718
beforeAll(async () => {
1819
validationDetails = {
19-
issuer: '',
20+
issuer: authDomain,
2021
};
2122
});
2223

lib/__tests__/sdk/utilities/token-claims.spec.ts

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,22 @@ import {
66
getClaimValue,
77
getPermission,
88
getClaim,
9+
type TokenValidationDetailsType,
910
} from '../../../sdk/utilities';
1011
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
1112

1213
describe('token-claims', () => {
1314
let mockAccessToken: Awaited<ReturnType<typeof mocks.getMockAccessToken>>;
1415
let mockIdToken: Awaited<ReturnType<typeof mocks.getMockIdToken>>;
16+
const authDomain = 'local-testing@kinde.com';
1517
const { sessionManager } = mocks;
1618

19+
let validationDetails: TokenValidationDetailsType;
20+
1721
beforeAll(async () => {
22+
validationDetails = {
23+
issuer: authDomain,
24+
};
1825
mockAccessToken = await mocks.getMockAccessToken();
1926
mockIdToken = await mocks.getMockIdToken();
2027
await sessionManager.setSessionItem('access_token', mockAccessToken.token);
@@ -28,7 +35,12 @@ describe('token-claims', () => {
2835
describe('getClaimValue', () => {
2936
it('returns value for a token claim if claim exists', () => {
3037
Object.keys(mockAccessToken.payload).forEach(async (name: string) => {
31-
const claimValue = await getClaimValue(sessionManager, name, 'access_token');
38+
const claimValue = await getClaimValue(
39+
sessionManager,
40+
name,
41+
'access_token',
42+
validationDetails
43+
);
3244
const tokenPayload = mockAccessToken.payload as Record<string, unknown>;
3345
expect(claimValue).toStrictEqual(tokenPayload[name]);
3446
});
@@ -39,7 +51,8 @@ describe('token-claims', () => {
3951
const claimValue = await getClaimValue(
4052
sessionManager,
4153
claimName,
42-
'access_token'
54+
'access_token',
55+
validationDetails
4356
);
4457
expect(claimValue).toBe(null);
4558
});
@@ -48,15 +61,25 @@ describe('token-claims', () => {
4861
describe('getClaim', () => {
4962
it('returns value for a token claim if claim exists', () => {
5063
Object.keys(mockAccessToken.payload).forEach(async (name: string) => {
51-
const claim = await getClaim(sessionManager, name, 'access_token');
64+
const claim = await getClaim(
65+
sessionManager,
66+
name,
67+
'access_token',
68+
validationDetails
69+
);
5270
const tokenPayload = mockAccessToken.payload as Record<string, unknown>;
5371
expect(claim).toStrictEqual({ name, value: tokenPayload[name] });
5472
});
5573
});
5674

5775
it('return null if claim does not exist', async () => {
5876
const claimName = 'non-existant-claim';
59-
const claim = await getClaim(sessionManager, claimName, 'access_token');
77+
const claim = await getClaim(
78+
sessionManager,
79+
claimName,
80+
'access_token',
81+
validationDetails
82+
);
6083
expect(claim).toStrictEqual({ name: claimName, value: null });
6184
});
6285
});
@@ -65,7 +88,9 @@ describe('token-claims', () => {
6588
it('return orgCode and isGranted = true if permission is given', () => {
6689
const { permissions } = mockAccessToken.payload;
6790
permissions.forEach(async (permission) => {
68-
expect(await getPermission(sessionManager, permission)).toStrictEqual({
91+
expect(
92+
await getPermission(sessionManager, permission, validationDetails)
93+
).toStrictEqual({
6994
orgCode: mockAccessToken.payload.org_code,
7095
isGranted: true,
7196
});
@@ -75,7 +100,9 @@ describe('token-claims', () => {
75100
it('return isGranted = false is permission is not given', async () => {
76101
const orgCode = mockAccessToken.payload.org_code;
77102
const permissionName = 'non-existant-permission';
78-
expect(await getPermission(sessionManager, permissionName)).toStrictEqual({
103+
expect(
104+
await getPermission(sessionManager, permissionName, validationDetails)
105+
).toStrictEqual({
79106
orgCode,
80107
isGranted: false,
81108
});
@@ -84,7 +111,9 @@ describe('token-claims', () => {
84111
describe('getUserOrganizations', () => {
85112
it('lists all user organizations using id token', async () => {
86113
const orgCodes = mockIdToken.payload.org_codes;
87-
expect(await getUserOrganizations(sessionManager)).toStrictEqual({
114+
expect(
115+
await getUserOrganizations(sessionManager, validationDetails)
116+
).toStrictEqual({
88117
orgCodes,
89118
});
90119
});
@@ -93,7 +122,9 @@ describe('token-claims', () => {
93122
describe('getOrganization', () => {
94123
it('returns organization code using accesss token', async () => {
95124
const orgCode = mockAccessToken.payload.org_code;
96-
expect(await getOrganization(sessionManager)).toStrictEqual({ orgCode });
125+
expect(await getOrganization(sessionManager, validationDetails)).toStrictEqual(
126+
{ orgCode }
127+
);
97128
});
98129
});
99130
});

lib/__tests__/sdk/utilities/token-utils.spec.ts

Lines changed: 5 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,3 @@
1-
import { vi } from 'vitest';
2-
3-
// Mock the validateToken function - must be at the top for Vitest hoisting
4-
vi.mock('@kinde/jwt-validator', () => ({
5-
validateToken: vi.fn().mockImplementation(async ({ token }) => {
6-
try {
7-
const payload = JSON.parse(atob(token.split('.')[1]));
8-
const currentTime = Math.floor(Date.now() / 1000);
9-
const isExpired = payload.exp && currentTime >= payload.exp;
10-
return {
11-
valid: !isExpired,
12-
message: isExpired ? 'Token expired' : 'Token valid',
13-
};
14-
} catch (e) {
15-
return {
16-
valid: false,
17-
message: 'Invalid token format',
18-
};
19-
}
20-
}),
21-
}));
22-
231
import * as mocks from '../../mocks';
242
import { describe, it, expect, beforeAll, afterEach } from 'vitest';
253
import {
@@ -117,7 +95,7 @@ describe('token-utils', () => {
11795
validationDetails
11896
);
11997

120-
const storedUser = await getUserFromSession(sessionManager);
98+
const storedUser = await getUserFromSession(sessionManager, validationDetails);
12199
const expectedUser = {
122100
family_name: idTokenPayload.family_name,
123101
given_name: idTokenPayload.given_name,
@@ -134,28 +112,28 @@ describe('token-utils', () => {
134112

135113
describe('isTokenExpired()', () => {
136114
it('returns true if null is provided as argument', async () => {
137-
expect(isTokenExpired(null)).toBe(true);
115+
expect(await isTokenExpired(null, validationDetails)).toBe(true);
138116
});
139117

140118
it('returns true if provided token is expired', async () => {
141119
const { token: mockAccessToken } = await mocks.getMockAccessToken(
142120
domain,
143121
true
144122
);
145-
expect(isTokenExpired(mockAccessToken)).toBe(true);
123+
expect(await isTokenExpired(mockAccessToken, validationDetails)).toBe(true);
146124
});
147125

148126
it('returns true if provided token is missing "exp" claim', async () => {
149127
const { token: mockAccessToken } = await mocks.getMockAccessToken(
150128
domain,
151129
true
152130
);
153-
expect(isTokenExpired(mockAccessToken)).toBe(true);
131+
expect(await isTokenExpired(mockAccessToken, validationDetails)).toBe(true);
154132
});
155133

156134
it('returns false if provided token is not expired', async () => {
157135
const { token: mockAccessToken } = await mocks.getMockAccessToken(domain);
158-
expect(isTokenExpired(mockAccessToken)).toBe(false);
136+
expect(await isTokenExpired(mockAccessToken, validationDetails)).toBe(false);
159137
});
160138
});
161139
});

lib/sdk/clients/browser/authcode-with-pkce.ts

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,10 @@ const createAuthCodeWithPKCEClient = (options: BrowserPKCEClientOptions) => {
114114
if (!(await isAuthenticated())) {
115115
throw new Error('Cannot get user details, no authentication credential found');
116116
}
117-
return (await utilities.getUserFromSession(sessionManager))!;
117+
return (await utilities.getUserFromSession(
118+
sessionManager,
119+
client.tokenValidationDetails
120+
))!;
118121
};
119122

120123
/**
@@ -206,7 +209,12 @@ const createAuthCodeWithPKCEClient = (options: BrowserPKCEClientOptions) => {
206209
);
207210
}
208211

209-
return await tokenClaims.getClaimValue(sessionManager, claim, type);
212+
return await tokenClaims.getClaimValue(
213+
sessionManager,
214+
claim,
215+
type,
216+
client.tokenValidationDetails
217+
);
210218
};
211219

212220
/**
@@ -225,7 +233,12 @@ const createAuthCodeWithPKCEClient = (options: BrowserPKCEClientOptions) => {
225233
`Cannot return claim "${claim}", no authentication credential found`
226234
);
227235
}
228-
return await tokenClaims.getClaim(sessionManager, claim, type);
236+
return await tokenClaims.getClaim(
237+
sessionManager,
238+
claim,
239+
type,
240+
client.tokenValidationDetails
241+
);
229242
};
230243

231244
/**
@@ -243,7 +256,11 @@ const createAuthCodeWithPKCEClient = (options: BrowserPKCEClientOptions) => {
243256
`Cannot return permission "${name}", no authentication credential found`
244257
);
245258
}
246-
return await tokenClaims.getPermission(sessionManager, name);
259+
return await tokenClaims.getPermission(
260+
sessionManager,
261+
name,
262+
client.tokenValidationDetails
263+
);
247264
};
248265

249266
/**
@@ -256,7 +273,10 @@ const createAuthCodeWithPKCEClient = (options: BrowserPKCEClientOptions) => {
256273
'Cannot return user organization, no authentication credential found'
257274
);
258275
}
259-
return await tokenClaims.getOrganization(sessionManager);
276+
return await tokenClaims.getOrganization(
277+
sessionManager,
278+
client.tokenValidationDetails
279+
);
260280
};
261281

262282
/**
@@ -270,7 +290,10 @@ const createAuthCodeWithPKCEClient = (options: BrowserPKCEClientOptions) => {
270290
'Cannot return user organizations, no authentication credential found'
271291
);
272292
}
273-
return await tokenClaims.getUserOrganizations(sessionManager);
293+
return await tokenClaims.getUserOrganizations(
294+
sessionManager,
295+
client.tokenValidationDetails
296+
);
274297
};
275298

276299
/**
@@ -287,7 +310,10 @@ const createAuthCodeWithPKCEClient = (options: BrowserPKCEClientOptions) => {
287310
'Cannot return user permissions, no authentication credential found'
288311
);
289312
}
290-
return await tokenClaims.getPermissions(sessionManager);
313+
return await tokenClaims.getPermissions(
314+
sessionManager,
315+
client.tokenValidationDetails
316+
);
291317
};
292318

293319
/**

0 commit comments

Comments
 (0)