Skip to content

feat: migrate from jose to @kinde/jwt-validator packages #77

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 23 additions & 6 deletions lib/__tests__/sdk/oauth2-flows/AuthCodeWithPKCE.spec.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,25 @@
import { vi } from 'vitest';

// Mock the validateToken function - must be at the top for Vitest hoisting
vi.mock('@kinde/jwt-validator', () => ({
validateToken: vi.fn().mockImplementation(async ({ token }) => {
try {
const payload = JSON.parse(atob(token.split('.')[1]));
const currentTime = Math.floor(Date.now() / 1000);
const isExpired = payload.exp && currentTime >= payload.exp;
return {
valid: !isExpired,
message: isExpired ? 'Token expired' : 'Token valid',
};
} catch (e) {
return {
valid: false,
message: 'Invalid token format',
};
}
}),
}));

import type {
AuthorizationCodeOptions,
SDKHeaderOverrideOptions,
Expand All @@ -6,7 +28,7 @@ import { base64UrlEncode, sha256 } from '../../../sdk/utilities';
import { AuthCodeWithPKCE } from '../../../sdk/oauth2-flows';
import { getSDKHeader } from '../../../sdk/version';
import * as mocks from '../../mocks';
import { describe, it, expect, beforeAll, afterEach } from 'vitest';
import { describe, it, expect, afterEach } from 'vitest';

describe('AuthCodeWitPKCE', () => {
const { sessionManager } = mocks;
Expand All @@ -17,11 +39,6 @@ describe('AuthCodeWitPKCE', () => {
clientId: 'client-id',
};

beforeAll(async () => {
const { publicKey } = await mocks.getKeys();
clientConfig.jwks = { keys: [publicKey] };
});

describe('new AuthCodeWithPKCE', () => {
it('can construct AuthCodeWithPKCE instance', () => {
expect(() => new AuthCodeWithPKCE(clientConfig)).not.toThrowError();
Expand Down
29 changes: 23 additions & 6 deletions lib/__tests__/sdk/oauth2-flows/AuthorizationCode.spec.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,25 @@
import { vi } from 'vitest';

// Mock the validateToken function - must be at the top for Vitest hoisting
vi.mock('@kinde/jwt-validator', () => ({
validateToken: vi.fn().mockImplementation(async ({ token }) => {
try {
const payload = JSON.parse(atob(token.split('.')[1]));
const currentTime = Math.floor(Date.now() / 1000);
const isExpired = payload.exp && currentTime >= payload.exp;
return {
valid: !isExpired,
message: isExpired ? 'Token expired' : 'Token valid',
};
} catch (e) {
return {
valid: false,
message: 'Invalid token format',
};
}
}),
}));

import { getSDKHeader } from '../../../sdk/version';
import * as mocks from '../../mocks';

Expand All @@ -10,7 +32,7 @@ import {

import { KindeSDKError, KindeSDKErrorCode } from '../../../sdk/exceptions';
import { generateRandomString } from '../../../sdk/utilities';
import { describe, it, expect, beforeAll, afterEach } from 'vitest';
import { describe, it, expect, afterEach } from 'vitest';

describe('AuthorizationCode', () => {
const { sessionManager } = mocks;
Expand All @@ -22,11 +44,6 @@ describe('AuthorizationCode', () => {
clientId: 'client-id',
};

beforeAll(async () => {
const { publicKey } = await mocks.getKeys();
clientConfig.jwks = { keys: [publicKey] };
});

describe('new AuthorizationCode', () => {
it('can construct AuthorizationCode instance', () => {
expect(
Expand Down
28 changes: 22 additions & 6 deletions lib/__tests__/sdk/oauth2-flows/ClientCredentials.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,25 @@
import { importJWK } from 'jose';
import { vi } from 'vitest';

// Mock the validateToken function - must be at the top for Vitest hoisting
vi.mock('@kinde/jwt-validator', () => ({
validateToken: vi.fn().mockImplementation(async ({ token }) => {
try {
const payload = JSON.parse(atob(token.split('.')[1]));
const currentTime = Math.floor(Date.now() / 1000);
const isExpired = payload.exp && currentTime >= payload.exp;
return {
valid: !isExpired,
message: isExpired ? 'Token expired' : 'Token valid',
};
} catch (e) {
return {
valid: false,
message: 'Invalid token format',
};
}
}),
}));

import { ClientCredentials } from '../../../sdk/oauth2-flows/ClientCredentials';
import { type ClientCredentialsOptions } from '../../../sdk/oauth2-flows/types';
import {
Expand Down Expand Up @@ -32,14 +53,9 @@ describe('ClientCredentials', () => {
let validationDetails: TokenValidationDetailsType;

beforeAll(async () => {
const { publicKey } = await mocks.getKeys();

validationDetails = {
issuer: clientConfig.authDomain,
keyProvider: async () => await importJWK(publicKey, mocks.mockJwtAlg),
};

clientConfig.jwks = { keys: [publicKey] };
});

const body = new URLSearchParams({
Expand Down
4 changes: 0 additions & 4 deletions lib/__tests__/sdk/utilities/feature-flags.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {
getFlag,
type TokenValidationDetailsType,
} from '../../../sdk/utilities';
import { importJWK } from 'jose';

describe('feature-flags', () => {
let mockAccessToken: Awaited<ReturnType<typeof mocks.getMockAccessToken>>;
Expand All @@ -16,11 +15,8 @@ describe('feature-flags', () => {
let validationDetails: TokenValidationDetailsType;

beforeAll(async () => {
const { publicKey } = await mocks.getKeys();

validationDetails = {
issuer: '',
keyProvider: async () => await importJWK(publicKey, mocks.mockJwtAlg),
};
});

Expand Down
52 changes: 8 additions & 44 deletions lib/__tests__/sdk/utilities/token-claims.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,15 @@ import {
getClaimValue,
getPermission,
getClaim,
type TokenValidationDetailsType,
} from '../../../sdk/utilities';
import { importJWK } from 'jose';
import { describe, it, expect, beforeAll, afterAll } from 'vitest';

describe('token-claims', () => {
let mockAccessToken: Awaited<ReturnType<typeof mocks.getMockAccessToken>>;
let mockIdToken: Awaited<ReturnType<typeof mocks.getMockIdToken>>;
const authDomain = 'https://local-testing@kinde.com';
const { sessionManager } = mocks;

let validationDetails: TokenValidationDetailsType;

beforeAll(async () => {
const { publicKey } = await mocks.getKeys();

validationDetails = {
issuer: authDomain,
keyProvider: async () => await importJWK(publicKey, mocks.mockJwtAlg),
};

mockAccessToken = await mocks.getMockAccessToken();
mockIdToken = await mocks.getMockIdToken();
await sessionManager.setSessionItem('access_token', mockAccessToken.token);
Expand All @@ -40,12 +28,7 @@ describe('token-claims', () => {
describe('getClaimValue', () => {
it('returns value for a token claim if claim exists', () => {
Object.keys(mockAccessToken.payload).forEach(async (name: string) => {
const claimValue = await getClaimValue(
sessionManager,
name,
'access_token',
validationDetails
);
const claimValue = await getClaimValue(sessionManager, name, 'access_token');
const tokenPayload = mockAccessToken.payload as Record<string, unknown>;
expect(claimValue).toStrictEqual(tokenPayload[name]);
});
Expand All @@ -56,8 +39,7 @@ describe('token-claims', () => {
const claimValue = await getClaimValue(
sessionManager,
claimName,
'access_token',
validationDetails
'access_token'
);
expect(claimValue).toBe(null);
});
Expand All @@ -66,25 +48,15 @@ describe('token-claims', () => {
describe('getClaim', () => {
it('returns value for a token claim if claim exists', () => {
Object.keys(mockAccessToken.payload).forEach(async (name: string) => {
const claim = await getClaim(
sessionManager,
name,
'access_token',
validationDetails
);
const claim = await getClaim(sessionManager, name, 'access_token');
const tokenPayload = mockAccessToken.payload as Record<string, unknown>;
expect(claim).toStrictEqual({ name, value: tokenPayload[name] });
});
});

it('return null if claim does not exist', async () => {
const claimName = 'non-existant-claim';
const claim = await getClaim(
sessionManager,
claimName,
'access_token',
validationDetails
);
const claim = await getClaim(sessionManager, claimName, 'access_token');
expect(claim).toStrictEqual({ name: claimName, value: null });
});
});
Expand All @@ -93,9 +65,7 @@ describe('token-claims', () => {
it('return orgCode and isGranted = true if permission is given', () => {
const { permissions } = mockAccessToken.payload;
permissions.forEach(async (permission) => {
expect(
await getPermission(sessionManager, permission, validationDetails)
).toStrictEqual({
expect(await getPermission(sessionManager, permission)).toStrictEqual({
orgCode: mockAccessToken.payload.org_code,
isGranted: true,
});
Expand All @@ -105,9 +75,7 @@ describe('token-claims', () => {
it('return isGranted = false is permission is not given', async () => {
const orgCode = mockAccessToken.payload.org_code;
const permissionName = 'non-existant-permission';
expect(
await getPermission(sessionManager, permissionName, validationDetails)
).toStrictEqual({
expect(await getPermission(sessionManager, permissionName)).toStrictEqual({
orgCode,
isGranted: false,
});
Expand All @@ -116,9 +84,7 @@ describe('token-claims', () => {
describe('getUserOrganizations', () => {
it('lists all user organizations using id token', async () => {
const orgCodes = mockIdToken.payload.org_codes;
expect(
await getUserOrganizations(sessionManager, validationDetails)
).toStrictEqual({
expect(await getUserOrganizations(sessionManager)).toStrictEqual({
orgCodes,
});
});
Expand All @@ -127,9 +93,7 @@ describe('token-claims', () => {
describe('getOrganization', () => {
it('returns organization code using accesss token', async () => {
const orgCode = mockAccessToken.payload.org_code;
expect(await getOrganization(sessionManager, validationDetails)).toStrictEqual(
{ orgCode }
);
expect(await getOrganization(sessionManager)).toStrictEqual({ orgCode });
});
});
});
36 changes: 27 additions & 9 deletions lib/__tests__/sdk/utilities/token-utils.spec.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,25 @@
import { vi } from 'vitest';

// Mock the validateToken function - must be at the top for Vitest hoisting
vi.mock('@kinde/jwt-validator', () => ({
validateToken: vi.fn().mockImplementation(async ({ token }) => {
try {
const payload = JSON.parse(atob(token.split('.')[1]));
const currentTime = Math.floor(Date.now() / 1000);
const isExpired = payload.exp && currentTime >= payload.exp;
return {
valid: !isExpired,
message: isExpired ? 'Token expired' : 'Token valid',
};
} catch (e) {
return {
valid: false,
message: 'Invalid token format',
};
}
}),
}));

import * as mocks from '../../mocks';
import { describe, it, expect, beforeAll, afterEach } from 'vitest';
import {
Expand All @@ -10,19 +32,15 @@ import {
} from '../../../sdk/utilities';

import { KindeSDKError, KindeSDKErrorCode } from '../../../sdk/exceptions';
import { importJWK } from 'jose';

describe('token-utils', () => {
const domain = 'local-testing@kinde.com';
const { sessionManager } = mocks;
let validationDetails: TokenValidationDetailsType;

beforeAll(async () => {
const { publicKey } = await mocks.getKeys();

validationDetails = {
issuer: domain,
keyProvider: async () => await importJWK(publicKey, mocks.mockJwtAlg),
};
});

Expand Down Expand Up @@ -99,7 +117,7 @@ describe('token-utils', () => {
validationDetails
);

const storedUser = await getUserFromSession(sessionManager, validationDetails);
const storedUser = await getUserFromSession(sessionManager);
const expectedUser = {
family_name: idTokenPayload.family_name,
given_name: idTokenPayload.given_name,
Expand All @@ -116,28 +134,28 @@ describe('token-utils', () => {

describe('isTokenExpired()', () => {
it('returns true if null is provided as argument', async () => {
expect(await isTokenExpired(null, validationDetails)).toBe(true);
expect(isTokenExpired(null)).toBe(true);
});

it('returns true if provided token is expired', async () => {
const { token: mockAccessToken } = await mocks.getMockAccessToken(
domain,
true
);
expect(await isTokenExpired(mockAccessToken, validationDetails)).toBe(true);
expect(isTokenExpired(mockAccessToken)).toBe(true);
});

it('returns true if provided token is missing "exp" claim', async () => {
const { token: mockAccessToken } = await mocks.getMockAccessToken(
domain,
true
);
expect(await isTokenExpired(mockAccessToken, validationDetails)).toBe(true);
expect(isTokenExpired(mockAccessToken)).toBe(true);
});

it('returns false if provided token is not expired', async () => {
const { token: mockAccessToken } = await mocks.getMockAccessToken(domain);
expect(await isTokenExpired(mockAccessToken, validationDetails)).toBe(false);
expect(isTokenExpired(mockAccessToken)).toBe(false);
});
});
});
Loading