From b21411e166f0f31f4f718a0d8ea2a57d7ac08070 Mon Sep 17 00:00:00 2001 From: Koosha Owji Date: Tue, 1 Jul 2025 06:05:29 +1000 Subject: [PATCH 1/7] feat: migrate from jose to @kinde/jwt-validator packages Replace jose library with @kinde/jwt-validator@0.4.0 and @kinde/jwt-decoder@0.2.0 Add validateToken mocks to test files --- .../sdk/oauth2-flows/AuthCodeWithPKCE.spec.ts | 29 ++++++-- .../oauth2-flows/AuthorizationCode.spec.ts | 29 ++++++-- .../oauth2-flows/ClientCredentials.spec.ts | 28 +++++-- .../sdk/utilities/feature-flags.spec.ts | 4 - .../sdk/utilities/token-claims.spec.ts | 52 ++----------- .../sdk/utilities/token-utils.spec.ts | 38 +++++++--- lib/sdk/oauth2-flows/AuthCodeAbstract.ts | 15 +--- lib/sdk/oauth2-flows/ClientCredentials.ts | 15 +--- lib/sdk/oauth2-flows/types.ts | 3 - lib/sdk/utilities/token-claims.ts | 74 ++++++------------- lib/sdk/utilities/token-utils.ts | 50 ++++++------- lib/sdk/utilities/types.ts | 2 - package.json | 2 + pnpm-lock.yaml | 19 +++++ 14 files changed, 175 insertions(+), 185 deletions(-) diff --git a/lib/__tests__/sdk/oauth2-flows/AuthCodeWithPKCE.spec.ts b/lib/__tests__/sdk/oauth2-flows/AuthCodeWithPKCE.spec.ts index 8e867ac..076f101 100644 --- a/lib/__tests__/sdk/oauth2-flows/AuthCodeWithPKCE.spec.ts +++ b/lib/__tests__/sdk/oauth2-flows/AuthCodeWithPKCE.spec.ts @@ -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, @@ -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; @@ -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(); diff --git a/lib/__tests__/sdk/oauth2-flows/AuthorizationCode.spec.ts b/lib/__tests__/sdk/oauth2-flows/AuthorizationCode.spec.ts index f8f2cad..3e2cff2 100644 --- a/lib/__tests__/sdk/oauth2-flows/AuthorizationCode.spec.ts +++ b/lib/__tests__/sdk/oauth2-flows/AuthorizationCode.spec.ts @@ -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'; @@ -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; @@ -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( diff --git a/lib/__tests__/sdk/oauth2-flows/ClientCredentials.spec.ts b/lib/__tests__/sdk/oauth2-flows/ClientCredentials.spec.ts index e8b99eb..3ae6b97 100644 --- a/lib/__tests__/sdk/oauth2-flows/ClientCredentials.spec.ts +++ b/lib/__tests__/sdk/oauth2-flows/ClientCredentials.spec.ts @@ -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 { @@ -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({ diff --git a/lib/__tests__/sdk/utilities/feature-flags.spec.ts b/lib/__tests__/sdk/utilities/feature-flags.spec.ts index 4b3b47d..d175c0e 100644 --- a/lib/__tests__/sdk/utilities/feature-flags.spec.ts +++ b/lib/__tests__/sdk/utilities/feature-flags.spec.ts @@ -7,7 +7,6 @@ import { getFlag, type TokenValidationDetailsType, } from '../../../sdk/utilities'; -import { importJWK } from 'jose'; describe('feature-flags', () => { let mockAccessToken: Awaited>; @@ -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), }; }); diff --git a/lib/__tests__/sdk/utilities/token-claims.spec.ts b/lib/__tests__/sdk/utilities/token-claims.spec.ts index 840a2aa..fb50ecd 100644 --- a/lib/__tests__/sdk/utilities/token-claims.spec.ts +++ b/lib/__tests__/sdk/utilities/token-claims.spec.ts @@ -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>; let mockIdToken: Awaited>; - 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); @@ -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; expect(claimValue).toStrictEqual(tokenPayload[name]); }); @@ -56,8 +39,7 @@ describe('token-claims', () => { const claimValue = await getClaimValue( sessionManager, claimName, - 'access_token', - validationDetails + 'access_token' ); expect(claimValue).toBe(null); }); @@ -66,12 +48,7 @@ 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; expect(claim).toStrictEqual({ name, value: tokenPayload[name] }); }); @@ -79,12 +56,7 @@ describe('token-claims', () => { 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 }); }); }); @@ -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, }); @@ -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, }); @@ -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, }); }); @@ -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 }); }); }); }); diff --git a/lib/__tests__/sdk/utilities/token-utils.spec.ts b/lib/__tests__/sdk/utilities/token-utils.spec.ts index 0a9aebe..e9542eb 100644 --- a/lib/__tests__/sdk/utilities/token-utils.spec.ts +++ b/lib/__tests__/sdk/utilities/token-utils.spec.ts @@ -1,5 +1,27 @@ +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 { describe, it, expect, beforeAll, afterEach, vi } from 'vitest'; import { type TokenCollection, commitTokensToSession, @@ -10,7 +32,6 @@ import { } from '../../../sdk/utilities'; import { KindeSDKError, KindeSDKErrorCode } from '../../../sdk/exceptions'; -import { importJWK } from 'jose'; describe('token-utils', () => { const domain = 'local-testing@kinde.com'; @@ -18,11 +39,8 @@ describe('token-utils', () => { let validationDetails: TokenValidationDetailsType; beforeAll(async () => { - const { publicKey } = await mocks.getKeys(); - validationDetails = { issuer: domain, - keyProvider: async () => await importJWK(publicKey, mocks.mockJwtAlg), }; }); @@ -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, @@ -116,7 +134,7 @@ 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 () => { @@ -124,7 +142,7 @@ describe('token-utils', () => { 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 () => { @@ -132,12 +150,12 @@ describe('token-utils', () => { 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); }); }); }); diff --git a/lib/sdk/oauth2-flows/AuthCodeAbstract.ts b/lib/sdk/oauth2-flows/AuthCodeAbstract.ts index 34e7f98..2fa3017 100644 --- a/lib/sdk/oauth2-flows/AuthCodeAbstract.ts +++ b/lib/sdk/oauth2-flows/AuthCodeAbstract.ts @@ -11,8 +11,6 @@ import type { AuthorizationCodeOptions, AuthURLOptions, } from './types.js'; -import { createLocalJWKSet } from 'jose'; -import { getRemoteJwks } from '../utilities/remote-jwks-cache.js'; import type { GeneratePortalUrlParams } from '@kinde/js-utils'; /** @@ -37,17 +35,9 @@ export abstract class AuthCodeAbstract { this.userProfileEndpoint = `${authDomain}/oauth2/v2/user_profile`; this.authorizationEndpoint = `${authDomain}/oauth2/auth`; this.tokenEndpoint = `${authDomain}/oauth2/token`; - const keyProvider = async () => { - const func = - config.jwks !== undefined - ? createLocalJWKSet(config.jwks) - : await getRemoteJwks(authDomain); - return await func({ alg: 'RS256' }); - }; this.tokenValidationDetails = { issuer: config.authDomain, audience: config.audience, - keyProvider, }; } @@ -142,10 +132,7 @@ export abstract class AuthCodeAbstract { throw new Error('No authentication credential found'); } - const isAccessTokenExpired = await utilities.isTokenExpired( - accessToken, - this.tokenValidationDetails - ); + const isAccessTokenExpired = utilities.isTokenExpired(accessToken); if (!isAccessTokenExpired) { return accessToken; } diff --git a/lib/sdk/oauth2-flows/ClientCredentials.ts b/lib/sdk/oauth2-flows/ClientCredentials.ts index a6dfde6..1a83983 100644 --- a/lib/sdk/oauth2-flows/ClientCredentials.ts +++ b/lib/sdk/oauth2-flows/ClientCredentials.ts @@ -1,4 +1,3 @@ -import { createLocalJWKSet } from 'jose'; import { type SessionManager } from '../session-managers/index.js'; import * as utilities from '../utilities/index.js'; import { getSDKHeader } from '../version.js'; @@ -8,7 +7,6 @@ import type { ClientCredentialsOptions, OAuth2CCTokenResponse, } from './types.js'; -import { getRemoteJwks } from '../utilities/remote-jwks-cache.js'; /** * Class provides implementation for the client credentials OAuth2.0 flow. @@ -24,17 +22,9 @@ export class ClientCredentials { this.logoutEndpoint = `${authDomain}/logout?redirect=${logoutRedirectURL ?? ''}`; this.tokenEndpoint = `${authDomain}/oauth2/token`; this.config = config; - const keyProvider = async () => { - const func = - config.jwks !== undefined - ? createLocalJWKSet(config.jwks) - : await getRemoteJwks(authDomain); - return await func({ alg: 'RS256' }); - }; this.tokenValidationDetails = { issuer: config.authDomain, audience: config.audience, - keyProvider, }; } @@ -47,10 +37,7 @@ export class ClientCredentials { */ async getToken(sessionManager: SessionManager): Promise { const accessToken = await utilities.getAccessToken(sessionManager); - const isTokenExpired = await utilities.isTokenExpired( - accessToken, - this.tokenValidationDetails - ); + const isTokenExpired = utilities.isTokenExpired(accessToken); if (accessToken && !isTokenExpired) { return accessToken; } diff --git a/lib/sdk/oauth2-flows/types.ts b/lib/sdk/oauth2-flows/types.ts index 61b8b91..36a7469 100644 --- a/lib/sdk/oauth2-flows/types.ts +++ b/lib/sdk/oauth2-flows/types.ts @@ -1,5 +1,3 @@ -import { type JSONWebKeySet } from 'jose'; - export enum GrantType { AUTHORIZATION_CODE = 'AUTHORIZATION_CODE', CLIENT_CREDENTIALS = 'CLIENT_CREDENTIALS', @@ -36,7 +34,6 @@ export interface OAuth2FlowOptions { authDomain: string; audience?: string | string[]; scope?: string; - jwks?: JSONWebKeySet; } export interface SDKHeaderOverrideOptions { diff --git a/lib/sdk/utilities/token-claims.ts b/lib/sdk/utilities/token-claims.ts index c1b97d5..4f1f0b3 100644 --- a/lib/sdk/utilities/token-claims.ts +++ b/lib/sdk/utilities/token-claims.ts @@ -1,6 +1,6 @@ import { type SessionManager } from '../session-managers/index.js'; -import { type TokenValidationDetailsType, type ClaimTokenType } from './types.js'; -import { jwtVerify } from 'jose'; +import { type ClaimTokenType } from './types.js'; +import { jwtDecoder } from '@kinde/jwt-decoder'; /** * Method extracts the provided claim from the provided token type in the @@ -13,17 +13,11 @@ import { jwtVerify } from 'jose'; export const getClaimValue = async ( sessionManager: SessionManager, claim: string, - type: ClaimTokenType = 'access_token', - validationDetails: TokenValidationDetailsType + type: ClaimTokenType = 'access_token' ): Promise => { const token = (await sessionManager.getSessionItem(`${type}`)) as string; - const key = await validationDetails.keyProvider(); - const decodedToken = await jwtVerify( - token, - key, - type === 'id_token' ? { currentDate: new Date(0) } : {} - ); - const tokenPayload: Record = decodedToken.payload; + // Decode the token payload using the Kinde JWT decoder + const tokenPayload: Record = jwtDecoder(token) ?? {}; return tokenPayload[claim] ?? null; }; @@ -38,12 +32,11 @@ export const getClaimValue = async ( export const getClaim = async ( sessionManager: SessionManager, claim: string, - type: ClaimTokenType, - validationDetails: TokenValidationDetailsType + type: ClaimTokenType ): Promise<{ name: string; value: unknown | null }> => { return { name: claim, - value: await getClaimValue(sessionManager, claim, type, validationDetails), + value: await getClaimValue(sessionManager, claim, type), }; }; @@ -57,21 +50,18 @@ export const getClaim = async ( */ export const getPermission = async ( sessionManager: SessionManager, - name: string, - validationDetails: TokenValidationDetailsType + name: string ): Promise<{ orgCode: string | null; isGranted: boolean }> => { const permissions = ((await getClaimValue( sessionManager, 'permissions', - 'access_token', - validationDetails + 'access_token' )) ?? []) as string[]; const isGranted = permissions.some((p) => p === name); const orgCode = (await getClaimValue( sessionManager, 'org_code', - 'access_token', - validationDetails + 'access_token' )) as string | null; return { orgCode, isGranted }; }; @@ -82,15 +72,11 @@ export const getPermission = async ( * @returns {{ orgCode: string | null }} */ export const getOrganization = async ( - sessionManager: SessionManager, - validationDetails: TokenValidationDetailsType + sessionManager: SessionManager ): Promise<{ orgCode: string | null }> => ({ - orgCode: (await getClaimValue( - sessionManager, - 'org_code', - 'access_token', - validationDetails - )) as string | null, + orgCode: (await getClaimValue(sessionManager, 'org_code', 'access_token')) as + | string + | null, }); /** @@ -100,22 +86,15 @@ export const getOrganization = async ( * @returns {{ permissions: string[], orgCode: string | null }} */ export const getPermissions = async ( - sessionManager: SessionManager, - validationDetails: TokenValidationDetailsType + sessionManager: SessionManager ): Promise<{ permissions: string[]; orgCode: string | null }> => { const [permissions, orgCode] = await Promise.all([ - (getClaimValue( - sessionManager, - 'permissions', - 'access_token', - validationDetails - ) ?? []) as Promise, - getClaimValue( - sessionManager, - 'org_code', - 'access_token', - validationDetails - ) as Promise, + (getClaimValue(sessionManager, 'permissions', 'access_token') ?? []) as Promise< + string[] + >, + getClaimValue(sessionManager, 'org_code', 'access_token') as Promise< + string | null + >, ]); return { permissions, @@ -130,13 +109,8 @@ export const getPermissions = async ( * @returns {{ orgCodes: string[] }} */ export const getUserOrganizations = async ( - sessionManager: SessionManager, - validationDetails: TokenValidationDetailsType + sessionManager: SessionManager ): Promise<{ orgCodes: string[] }> => ({ - orgCodes: ((await getClaimValue( - sessionManager, - 'org_codes', - 'id_token', - validationDetails - )) ?? []) as string[], + orgCodes: ((await getClaimValue(sessionManager, 'org_codes', 'id_token')) ?? + []) as string[], }); diff --git a/lib/sdk/utilities/token-utils.ts b/lib/sdk/utilities/token-utils.ts index f842307..8336add 100644 --- a/lib/sdk/utilities/token-utils.ts +++ b/lib/sdk/utilities/token-utils.ts @@ -6,7 +6,8 @@ import type { } from './types.js'; import { type SessionManager } from '../session-managers/index.js'; import { KindeSDKError, KindeSDKErrorCode } from '../exceptions.js'; -import { jwtVerify } from 'jose'; +import { validateToken } from '@kinde/jwt-validator'; +import { jwtDecoder } from '@kinde/jwt-decoder'; /** * Saves the provided token to the current session. @@ -27,8 +28,13 @@ export const commitTokenToSession = async ( if (type === 'access_token' || type === 'id_token') { try { - const key = await validationDetails.keyProvider(); - await jwtVerify(token, key); + const validation = await validateToken({ + token, + domain: validationDetails.issuer, + }); + if (!validation.valid) { + throw new Error(validation.message); + } } catch (e) { throw new KindeSDKError( KindeSDKErrorCode.INVALID_TOKEN_MEMORY_COMMIT, @@ -108,23 +114,21 @@ export const getAccessToken = async ( * @returns {string | null} */ export const getUserFromSession = async ( - sessionManager: SessionManager, - validationDetails: TokenValidationDetailsType + sessionManager: SessionManager ): Promise => { const idTokenString = (await sessionManager.getSessionItem('id_token')) as string; - const idToken = await jwtVerify( - idTokenString, - await validationDetails.keyProvider(), - { currentDate: new Date(0) } - ); + const payload: Record = jwtDecoder(idTokenString) ?? {}; + if (Object.keys(payload).length === 0) { + throw new Error('Invalid ID token'); + } const user: UserType = { - family_name: idToken.payload.family_name as string, - given_name: idToken.payload.given_name as string, - picture: (idToken.payload.picture as string) ?? null, - email: idToken.payload.email as string, - phone: idToken.payload.phone as string, - id: idToken.payload.sub!, + family_name: payload.family_name as string, + given_name: payload.given_name as string, + picture: (payload.picture as string) ?? null, + email: payload.email as string, + phone: payload.phone as string, + id: payload.sub as string, }; return user; @@ -135,19 +139,13 @@ export const getUserFromSession = async ( * @param {string | null} token * @returns {boolean} is expired or not */ -export const isTokenExpired = async ( - token: string | null, - validationDetails: TokenValidationDetailsType -): Promise => { +export const isTokenExpired = (token: string | null): boolean => { if (!token) return true; try { const currentUnixTime = Math.floor(Date.now() / 1000); - const tokenPayload = await jwtVerify( - token, - await validationDetails.keyProvider() - ); - if (tokenPayload.payload.exp === undefined) return true; - return currentUnixTime >= tokenPayload.payload.exp; + const payload = jwtDecoder(token); + if (!payload || payload.exp === undefined) return true; + return currentUnixTime >= payload.exp; } catch (e) { return true; } diff --git a/lib/sdk/utilities/types.ts b/lib/sdk/utilities/types.ts index 767557a..cd7e9b1 100644 --- a/lib/sdk/utilities/types.ts +++ b/lib/sdk/utilities/types.ts @@ -1,4 +1,3 @@ -import { CryptoKey } from 'jose'; export interface TokenCollection { refresh_token: string; access_token: string; @@ -46,5 +45,4 @@ export interface GetFlagType { export interface TokenValidationDetailsType { issuer: string; audience?: string | string[]; - keyProvider: () => Promise; } diff --git a/package.json b/package.json index a60f4db..2098a64 100644 --- a/package.json +++ b/package.json @@ -70,6 +70,8 @@ }, "dependencies": { "@kinde/js-utils": "0.19.0", + "@kinde/jwt-decoder": "^0.2.0", + "@kinde/jwt-validator": "^0.4.0", "@typescript-eslint/parser": "^8.30.1", "jose": "^6.0.10", "uncrypto": "^0.1.3" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3661311..b48d447 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,12 @@ importers: '@kinde/js-utils': specifier: 0.19.0 version: 0.19.0 + '@kinde/jwt-decoder': + specifier: ^0.2.0 + version: 0.2.0 + '@kinde/jwt-validator': + specifier: ^0.4.0 + version: 0.4.0 '@typescript-eslint/parser': specifier: ^8.30.1 version: 8.33.1(eslint@9.28.0)(typescript@5.8.3) @@ -352,6 +358,9 @@ packages: '@kinde/jwt-decoder@0.2.0': resolution: {integrity: sha512-dqtwCmAvywOVLkkUfp4UbqdvVLsK0cvHsJhU3gDY9rgjAdZhGw0vCreBW6j3MFLxbi6cZm7pMU7/O5SJgvN5Rw==} + '@kinde/jwt-validator@0.4.0': + resolution: {integrity: sha512-aseXLTD/rh/rZ2v85Xy493CEtuC49MA4Hbt6ObccqSJfIGLAeMrAtBh2m9DleigVkMuZ/99/U4PqLnaVDLt5OQ==} + '@lukeed/csprng@1.1.0': resolution: {integrity: sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==} engines: {node: '>=8'} @@ -1769,6 +1778,9 @@ packages: jsonfile@6.1.0: resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} + jsrsasign@11.1.0: + resolution: {integrity: sha512-Ov74K9GihaK9/9WncTe1mPmvrO7Py665TUfUKvraXBpu+xcTWitrtuOwcjf4KMU9maPaYn0OuaWy0HOzy/GBXg==} + keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -2935,6 +2947,11 @@ snapshots: '@kinde/jwt-decoder@0.2.0': {} + '@kinde/jwt-validator@0.4.0': + dependencies: + '@kinde/jwt-decoder': 0.2.0 + jsrsasign: 11.1.0 + '@lukeed/csprng@1.1.0': {} '@nestjs/axios@4.0.0(@nestjs/common@11.1.1(reflect-metadata@0.2.2)(rxjs@7.8.2))(axios@1.9.0)(rxjs@7.8.2)': @@ -4590,6 +4607,8 @@ snapshots: optionalDependencies: graceful-fs: 4.2.11 + jsrsasign@11.1.0: {} + keyv@4.5.4: dependencies: json-buffer: 3.0.1 From d7cae1765186b7ef0d84a464939e405c0055c857 Mon Sep 17 00:00:00 2001 From: Koosha Owji Date: Tue, 1 Jul 2025 06:24:02 +1000 Subject: [PATCH 2/7] fix: update function signatures and move jose to devDependencies and Delete remote-jwks-cache.ts --- .../sdk/utilities/token-utils.spec.ts | 2 +- lib/sdk/clients/browser/authcode-with-pkce.ts | 40 ++++--------------- lib/sdk/clients/server/authorization-code.ts | 5 +-- lib/sdk/clients/server/with-auth-utilities.ts | 22 +++------- lib/sdk/utilities/feature-flags.ts | 3 +- lib/sdk/utilities/remote-jwks-cache.ts | 17 -------- package.json | 2 +- pnpm-lock.yaml | 6 +-- 8 files changed, 20 insertions(+), 77 deletions(-) delete mode 100644 lib/sdk/utilities/remote-jwks-cache.ts diff --git a/lib/__tests__/sdk/utilities/token-utils.spec.ts b/lib/__tests__/sdk/utilities/token-utils.spec.ts index e9542eb..3053328 100644 --- a/lib/__tests__/sdk/utilities/token-utils.spec.ts +++ b/lib/__tests__/sdk/utilities/token-utils.spec.ts @@ -21,7 +21,7 @@ vi.mock('@kinde/jwt-validator', () => ({ })); import * as mocks from '../../mocks'; -import { describe, it, expect, beforeAll, afterEach, vi } from 'vitest'; +import { describe, it, expect, beforeAll, afterEach } from 'vitest'; import { type TokenCollection, commitTokensToSession, diff --git a/lib/sdk/clients/browser/authcode-with-pkce.ts b/lib/sdk/clients/browser/authcode-with-pkce.ts index 98d59a3..ab3377a 100644 --- a/lib/sdk/clients/browser/authcode-with-pkce.ts +++ b/lib/sdk/clients/browser/authcode-with-pkce.ts @@ -114,10 +114,7 @@ const createAuthCodeWithPKCEClient = (options: BrowserPKCEClientOptions) => { if (!(await isAuthenticated())) { throw new Error('Cannot get user details, no authentication credential found'); } - return (await utilities.getUserFromSession( - sessionManager, - client.tokenValidationDetails - ))!; + return (await utilities.getUserFromSession(sessionManager))!; }; /** @@ -209,12 +206,7 @@ const createAuthCodeWithPKCEClient = (options: BrowserPKCEClientOptions) => { ); } - return await tokenClaims.getClaimValue( - sessionManager, - claim, - type, - client.tokenValidationDetails - ); + return await tokenClaims.getClaimValue(sessionManager, claim, type); }; /** @@ -233,12 +225,7 @@ const createAuthCodeWithPKCEClient = (options: BrowserPKCEClientOptions) => { `Cannot return claim "${claim}", no authentication credential found` ); } - return await tokenClaims.getClaim( - sessionManager, - claim, - type, - client.tokenValidationDetails - ); + return await tokenClaims.getClaim(sessionManager, claim, type); }; /** @@ -256,11 +243,7 @@ const createAuthCodeWithPKCEClient = (options: BrowserPKCEClientOptions) => { `Cannot return permission "${name}", no authentication credential found` ); } - return await tokenClaims.getPermission( - sessionManager, - name, - client.tokenValidationDetails - ); + return await tokenClaims.getPermission(sessionManager, name); }; /** @@ -273,10 +256,7 @@ const createAuthCodeWithPKCEClient = (options: BrowserPKCEClientOptions) => { 'Cannot return user organization, no authentication credential found' ); } - return await tokenClaims.getOrganization( - sessionManager, - client.tokenValidationDetails - ); + return await tokenClaims.getOrganization(sessionManager); }; /** @@ -290,10 +270,7 @@ const createAuthCodeWithPKCEClient = (options: BrowserPKCEClientOptions) => { 'Cannot return user organizations, no authentication credential found' ); } - return await tokenClaims.getUserOrganizations( - sessionManager, - client.tokenValidationDetails - ); + return await tokenClaims.getUserOrganizations(sessionManager); }; /** @@ -310,10 +287,7 @@ const createAuthCodeWithPKCEClient = (options: BrowserPKCEClientOptions) => { 'Cannot return user permissions, no authentication credential found' ); } - return await tokenClaims.getPermissions( - sessionManager, - client.tokenValidationDetails - ); + return await tokenClaims.getPermissions(sessionManager); }; /** diff --git a/lib/sdk/clients/server/authorization-code.ts b/lib/sdk/clients/server/authorization-code.ts index 3dbe8c3..b043fb2 100644 --- a/lib/sdk/clients/server/authorization-code.ts +++ b/lib/sdk/clients/server/authorization-code.ts @@ -139,10 +139,7 @@ const createAuthorizationCodeClient = ( if (!(await isAuthenticated(sessionManager))) { throw new Error('Cannot get user details, no authentication credential found'); } - return (await utilities.getUserFromSession( - sessionManager, - client.tokenValidationDetails - ))!; + return (await utilities.getUserFromSession(sessionManager))!; }; /** diff --git a/lib/sdk/clients/server/with-auth-utilities.ts b/lib/sdk/clients/server/with-auth-utilities.ts index edf3762..1daf292 100644 --- a/lib/sdk/clients/server/with-auth-utilities.ts +++ b/lib/sdk/clients/server/with-auth-utilities.ts @@ -110,12 +110,7 @@ const withAuthUtilities = ( ); } - return await tokenClaims.getClaimValue( - sessionManager, - claim, - type, - validationDetails - ); + return await tokenClaims.getClaimValue(sessionManager, claim, type); }; /** @@ -136,12 +131,7 @@ const withAuthUtilities = ( `Cannot return claim "${claim}", no authentication credential found` ); } - return await tokenClaims.getClaim( - sessionManager, - claim, - type, - validationDetails - ); + return await tokenClaims.getClaim(sessionManager, claim, type); }; /** @@ -161,7 +151,7 @@ const withAuthUtilities = ( `Cannot return permission "${name}", no authentication credential found` ); } - return await tokenClaims.getPermission(sessionManager, name, validationDetails); + return await tokenClaims.getPermission(sessionManager, name); }; /** @@ -177,7 +167,7 @@ const withAuthUtilities = ( 'Cannot return user organization, no authentication credential found' ); } - return await tokenClaims.getOrganization(sessionManager, validationDetails); + return await tokenClaims.getOrganization(sessionManager); }; /** @@ -194,7 +184,7 @@ const withAuthUtilities = ( 'Cannot return user organizations, no authentication credential found' ); } - return await tokenClaims.getUserOrganizations(sessionManager, validationDetails); + return await tokenClaims.getUserOrganizations(sessionManager); }; /** @@ -214,7 +204,7 @@ const withAuthUtilities = ( 'Cannot return user permissions, no authentication credential found' ); } - return await tokenClaims.getPermissions(sessionManager, validationDetails); + return await tokenClaims.getPermissions(sessionManager); }; /** diff --git a/lib/sdk/utilities/feature-flags.ts b/lib/sdk/utilities/feature-flags.ts index 55d9e31..20d4631 100644 --- a/lib/sdk/utilities/feature-flags.ts +++ b/lib/sdk/utilities/feature-flags.ts @@ -29,8 +29,7 @@ export const getFlag = async ( ((await getClaimValue( sessionManager, 'feature_flags', - 'access_token', - validationDetails + 'access_token' )) as FeatureFlags) ?? {}; const flag = featureFlags[code]; diff --git a/lib/sdk/utilities/remote-jwks-cache.ts b/lib/sdk/utilities/remote-jwks-cache.ts deleted file mode 100644 index d1b5e63..0000000 --- a/lib/sdk/utilities/remote-jwks-cache.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { createRemoteJWKSet } from 'jose'; - -const remoteJwksCache: Record> = {}; - -export const getRemoteJwks = async (domain: string) => { - if (remoteJwksCache[domain] !== undefined) { - return remoteJwksCache[domain]; - } - - const func = createRemoteJWKSet(new URL(`${domain}/.well-known/jwks.json`), { - cacheMaxAge: 1000 * 60 * 60 * 24, - }); - - remoteJwksCache[domain] = func; - - return func; -}; diff --git a/package.json b/package.json index 2098a64..6fd199c 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,7 @@ "eslint-plugin-prettier": "^5.2.6", "eslint-plugin-promise": "^7.2.1", "husky": "^8.0.3", + "jose": "^6.0.10", "jsdom": "^22.0.0", "lint-staged": "^13.2.2", "ncp": "^2.0.0", @@ -73,7 +74,6 @@ "@kinde/jwt-decoder": "^0.2.0", "@kinde/jwt-validator": "^0.4.0", "@typescript-eslint/parser": "^8.30.1", - "jose": "^6.0.10", "uncrypto": "^0.1.3" }, "packageManager": "pnpm@10.6.5+sha512.cdf928fca20832cd59ec53826492b7dc25dc524d4370b6b4adbf65803d32efaa6c1c88147c0ae4e8d579a6c9eec715757b50d4fa35eea179d868eada4ed043af" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b48d447..057e609 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,9 +20,6 @@ importers: '@typescript-eslint/parser': specifier: ^8.30.1 version: 8.33.1(eslint@9.28.0)(typescript@5.8.3) - jose: - specifier: ^6.0.10 - version: 6.0.11 uncrypto: specifier: ^0.1.3 version: 0.1.3 @@ -69,6 +66,9 @@ importers: husky: specifier: ^8.0.3 version: 8.0.3 + jose: + specifier: ^6.0.10 + version: 6.0.11 jsdom: specifier: ^22.0.0 version: 22.1.0 From a25450f784eabbc9264ad98d3a27f4b6ed7d2dff Mon Sep 17 00:00:00 2001 From: Koosha Owji Date: Tue, 1 Jul 2025 06:49:11 +1000 Subject: [PATCH 3/7] fix: add cryptographic validation to getUserFromSession for security and update JSDocs --- .../sdk/utilities/token-utils.spec.ts | 2 +- lib/sdk/clients/browser/authcode-with-pkce.ts | 2 +- lib/sdk/clients/server/authorization-code.ts | 2 +- lib/sdk/utilities/token-claims.ts | 30 +++++++++++++++++++ lib/sdk/utilities/token-utils.ts | 14 +++++++-- 5 files changed, 45 insertions(+), 5 deletions(-) diff --git a/lib/__tests__/sdk/utilities/token-utils.spec.ts b/lib/__tests__/sdk/utilities/token-utils.spec.ts index 3053328..741da5f 100644 --- a/lib/__tests__/sdk/utilities/token-utils.spec.ts +++ b/lib/__tests__/sdk/utilities/token-utils.spec.ts @@ -117,7 +117,7 @@ describe('token-utils', () => { validationDetails ); - const storedUser = await getUserFromSession(sessionManager); + const storedUser = await getUserFromSession(sessionManager, validationDetails); const expectedUser = { family_name: idTokenPayload.family_name, given_name: idTokenPayload.given_name, diff --git a/lib/sdk/clients/browser/authcode-with-pkce.ts b/lib/sdk/clients/browser/authcode-with-pkce.ts index ab3377a..0cd2eba 100644 --- a/lib/sdk/clients/browser/authcode-with-pkce.ts +++ b/lib/sdk/clients/browser/authcode-with-pkce.ts @@ -114,7 +114,7 @@ const createAuthCodeWithPKCEClient = (options: BrowserPKCEClientOptions) => { if (!(await isAuthenticated())) { throw new Error('Cannot get user details, no authentication credential found'); } - return (await utilities.getUserFromSession(sessionManager))!; + return (await utilities.getUserFromSession(sessionManager, client.tokenValidationDetails))!; }; /** diff --git a/lib/sdk/clients/server/authorization-code.ts b/lib/sdk/clients/server/authorization-code.ts index b043fb2..42b6b93 100644 --- a/lib/sdk/clients/server/authorization-code.ts +++ b/lib/sdk/clients/server/authorization-code.ts @@ -139,7 +139,7 @@ const createAuthorizationCodeClient = ( if (!(await isAuthenticated(sessionManager))) { throw new Error('Cannot get user details, no authentication credential found'); } - return (await utilities.getUserFromSession(sessionManager))!; + return (await utilities.getUserFromSession(sessionManager, client.tokenValidationDetails))!; }; /** diff --git a/lib/sdk/utilities/token-claims.ts b/lib/sdk/utilities/token-claims.ts index 4f1f0b3..9105554 100644 --- a/lib/sdk/utilities/token-claims.ts +++ b/lib/sdk/utilities/token-claims.ts @@ -5,6 +5,11 @@ import { jwtDecoder } from '@kinde/jwt-decoder'; /** * Method extracts the provided claim from the provided token type in the * current session. + * + * Security Model: This function assumes tokens have been cryptographically + * validated during session commit via commitTokensToSession. It performs + * decoding only on pre-validated tokens without re-validation by design. + * * @param {SessionManager} sessionManager * @param {string} claim * @param {ClaimTokenType} type @@ -24,6 +29,11 @@ export const getClaimValue = async ( /** * Method extracts the provided claim from the provided token type in the * current session, the returned object includes the provided claim. + * + * Security Model: This function assumes tokens have been cryptographically + * validated during session commit via commitTokensToSession. It performs + * decoding only on pre-validated tokens without re-validation by design. + * * @param {SessionManager} sessionManager * @param {string} claim * @param {ClaimTokenType} type @@ -44,6 +54,11 @@ export const getClaim = async ( * Method returns the organization code from the current session and returns * a boolean in the returned object indicating if the provided permission is * present in the session. + * + * Security Model: This function assumes tokens have been cryptographically + * validated during session commit via commitTokensToSession. It performs + * decoding only on pre-validated tokens without re-validation by design. + * * @param {SessionManager} sessionManager * @param {string} name * @returns {{ orgCode: string | null, isGranted: boolean }} @@ -68,6 +83,11 @@ export const getPermission = async ( /** * Method extracts the organization code from the current session. + * + * Security Model: This function assumes tokens have been cryptographically + * validated during session commit via commitTokensToSession. It performs + * decoding only on pre-validated tokens without re-validation by design. + * * @param {SessionManager} sessionManager * @returns {{ orgCode: string | null }} */ @@ -82,6 +102,11 @@ export const getOrganization = async ( /** * Method extracts all the permission and the organization code in the access * token in the current session. + * + * Security Model: This function assumes tokens have been cryptographically + * validated during session commit via commitTokensToSession. It performs + * decoding only on pre-validated tokens without re-validation by design. + * * @param {SessionManager} sessionManager * @returns {{ permissions: string[], orgCode: string | null }} */ @@ -105,6 +130,11 @@ export const getPermissions = async ( /** * Method extracts all organization codes from the id token in the current * session. + * + * Security Model: This function assumes tokens have been cryptographically + * validated during session commit via commitTokensToSession. It performs + * decoding only on pre-validated tokens without re-validation by design. + * * @param {SessionManager} sessionManager * @returns {{ orgCodes: string[] }} */ diff --git a/lib/sdk/utilities/token-utils.ts b/lib/sdk/utilities/token-utils.ts index 8336add..088e4b4 100644 --- a/lib/sdk/utilities/token-utils.ts +++ b/lib/sdk/utilities/token-utils.ts @@ -111,12 +111,22 @@ export const getAccessToken = async ( * Extracts the user information from the current session returns null if * the token is not found. * @param {SessionManager} sessionManager - * @returns {string | null} + * @param {TokenValidationDetailsType} validationDetails + * @returns {UserType | null} */ export const getUserFromSession = async ( - sessionManager: SessionManager + sessionManager: SessionManager, + validationDetails: TokenValidationDetailsType ): Promise => { const idTokenString = (await sessionManager.getSessionItem('id_token')) as string; + const validation = await validateToken({ + token: idTokenString, + domain: validationDetails.issuer, + }); + if (!validation.valid) { + throw new Error('Invalid ID token'); + } + const payload: Record = jwtDecoder(idTokenString) ?? {}; if (Object.keys(payload).length === 0) { throw new Error('Invalid ID token'); From f43ef3074b0c718e558152e197435a67db249093 Mon Sep 17 00:00:00 2001 From: Koosha Owji Date: Tue, 1 Jul 2025 16:38:19 +1000 Subject: [PATCH 4/7] Clean up Node environment condition --- lib/sdk/clients/server/index.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/lib/sdk/clients/server/index.ts b/lib/sdk/clients/server/index.ts index 4a4624f..82caa11 100644 --- a/lib/sdk/clients/server/index.ts +++ b/lib/sdk/clients/server/index.ts @@ -1,5 +1,4 @@ import createAuthCodeClient from './authorization-code.js'; -import { isNodeEnvironment } from '../../environment.js'; import createCCClient from './client-credentials.js'; import { GrantType } from '../../oauth2-flows/index.js'; @@ -30,10 +29,6 @@ export const createKindeServerClient = ( grantType: G, options: Options ) => { - if (!isNodeEnvironment()) { - throw new Error('this method must be invoked in a node.js environment'); - } - switch (grantType) { case GrantType.AUTHORIZATION_CODE: { const clientOptions = options as ACClientOptions; From bc68621b038d8d52e322d194371365be16c99581 Mon Sep 17 00:00:00 2001 From: Daniel Rivers Date: Wed, 2 Jul 2025 01:10:52 +0100 Subject: [PATCH 5/7] chore: lint fixes --- lib/sdk/clients/browser/authcode-with-pkce.ts | 5 +++- lib/sdk/clients/server/authorization-code.ts | 5 +++- lib/sdk/utilities/token-claims.ts | 24 +++++++++---------- 3 files changed, 20 insertions(+), 14 deletions(-) diff --git a/lib/sdk/clients/browser/authcode-with-pkce.ts b/lib/sdk/clients/browser/authcode-with-pkce.ts index 0cd2eba..3f88650 100644 --- a/lib/sdk/clients/browser/authcode-with-pkce.ts +++ b/lib/sdk/clients/browser/authcode-with-pkce.ts @@ -114,7 +114,10 @@ const createAuthCodeWithPKCEClient = (options: BrowserPKCEClientOptions) => { if (!(await isAuthenticated())) { throw new Error('Cannot get user details, no authentication credential found'); } - return (await utilities.getUserFromSession(sessionManager, client.tokenValidationDetails))!; + return (await utilities.getUserFromSession( + sessionManager, + client.tokenValidationDetails + ))!; }; /** diff --git a/lib/sdk/clients/server/authorization-code.ts b/lib/sdk/clients/server/authorization-code.ts index 42b6b93..3dbe8c3 100644 --- a/lib/sdk/clients/server/authorization-code.ts +++ b/lib/sdk/clients/server/authorization-code.ts @@ -139,7 +139,10 @@ const createAuthorizationCodeClient = ( if (!(await isAuthenticated(sessionManager))) { throw new Error('Cannot get user details, no authentication credential found'); } - return (await utilities.getUserFromSession(sessionManager, client.tokenValidationDetails))!; + return (await utilities.getUserFromSession( + sessionManager, + client.tokenValidationDetails + ))!; }; /** diff --git a/lib/sdk/utilities/token-claims.ts b/lib/sdk/utilities/token-claims.ts index 9105554..3cad665 100644 --- a/lib/sdk/utilities/token-claims.ts +++ b/lib/sdk/utilities/token-claims.ts @@ -5,11 +5,11 @@ import { jwtDecoder } from '@kinde/jwt-decoder'; /** * Method extracts the provided claim from the provided token type in the * current session. - * + * * Security Model: This function assumes tokens have been cryptographically * validated during session commit via commitTokensToSession. It performs * decoding only on pre-validated tokens without re-validation by design. - * + * * @param {SessionManager} sessionManager * @param {string} claim * @param {ClaimTokenType} type @@ -29,11 +29,11 @@ export const getClaimValue = async ( /** * Method extracts the provided claim from the provided token type in the * current session, the returned object includes the provided claim. - * + * * Security Model: This function assumes tokens have been cryptographically * validated during session commit via commitTokensToSession. It performs * decoding only on pre-validated tokens without re-validation by design. - * + * * @param {SessionManager} sessionManager * @param {string} claim * @param {ClaimTokenType} type @@ -54,11 +54,11 @@ export const getClaim = async ( * Method returns the organization code from the current session and returns * a boolean in the returned object indicating if the provided permission is * present in the session. - * + * * Security Model: This function assumes tokens have been cryptographically * validated during session commit via commitTokensToSession. It performs * decoding only on pre-validated tokens without re-validation by design. - * + * * @param {SessionManager} sessionManager * @param {string} name * @returns {{ orgCode: string | null, isGranted: boolean }} @@ -83,11 +83,11 @@ export const getPermission = async ( /** * Method extracts the organization code from the current session. - * + * * Security Model: This function assumes tokens have been cryptographically * validated during session commit via commitTokensToSession. It performs * decoding only on pre-validated tokens without re-validation by design. - * + * * @param {SessionManager} sessionManager * @returns {{ orgCode: string | null }} */ @@ -102,11 +102,11 @@ export const getOrganization = async ( /** * Method extracts all the permission and the organization code in the access * token in the current session. - * + * * Security Model: This function assumes tokens have been cryptographically * validated during session commit via commitTokensToSession. It performs * decoding only on pre-validated tokens without re-validation by design. - * + * * @param {SessionManager} sessionManager * @returns {{ permissions: string[], orgCode: string | null }} */ @@ -130,11 +130,11 @@ export const getPermissions = async ( /** * Method extracts all organization codes from the id token in the current * session. - * + * * Security Model: This function assumes tokens have been cryptographically * validated during session commit via commitTokensToSession. It performs * decoding only on pre-validated tokens without re-validation by design. - * + * * @param {SessionManager} sessionManager * @returns {{ orgCodes: string[] }} */ From e377c6deebb50e7441ecfe4379664212a9f6e64a Mon Sep 17 00:00:00 2001 From: Koosha Owji Date: Wed, 2 Jul 2025 13:09:14 +1000 Subject: [PATCH 6/7] fix: use decoder-only approach for ID tokens in getUserFromSession Removes validation to accept expired ID tokens for user display info. Creates cleaner separation of concerns - validate when storing, decode when reading. --- lib/__tests__/sdk/utilities/token-utils.spec.ts | 2 +- lib/sdk/clients/browser/authcode-with-pkce.ts | 5 +---- lib/sdk/clients/server/authorization-code.ts | 5 +---- lib/sdk/utilities/token-utils.ts | 12 ++---------- 4 files changed, 5 insertions(+), 19 deletions(-) diff --git a/lib/__tests__/sdk/utilities/token-utils.spec.ts b/lib/__tests__/sdk/utilities/token-utils.spec.ts index 741da5f..3053328 100644 --- a/lib/__tests__/sdk/utilities/token-utils.spec.ts +++ b/lib/__tests__/sdk/utilities/token-utils.spec.ts @@ -117,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, diff --git a/lib/sdk/clients/browser/authcode-with-pkce.ts b/lib/sdk/clients/browser/authcode-with-pkce.ts index 3f88650..ab3377a 100644 --- a/lib/sdk/clients/browser/authcode-with-pkce.ts +++ b/lib/sdk/clients/browser/authcode-with-pkce.ts @@ -114,10 +114,7 @@ const createAuthCodeWithPKCEClient = (options: BrowserPKCEClientOptions) => { if (!(await isAuthenticated())) { throw new Error('Cannot get user details, no authentication credential found'); } - return (await utilities.getUserFromSession( - sessionManager, - client.tokenValidationDetails - ))!; + return (await utilities.getUserFromSession(sessionManager))!; }; /** diff --git a/lib/sdk/clients/server/authorization-code.ts b/lib/sdk/clients/server/authorization-code.ts index 3dbe8c3..b043fb2 100644 --- a/lib/sdk/clients/server/authorization-code.ts +++ b/lib/sdk/clients/server/authorization-code.ts @@ -139,10 +139,7 @@ const createAuthorizationCodeClient = ( if (!(await isAuthenticated(sessionManager))) { throw new Error('Cannot get user details, no authentication credential found'); } - return (await utilities.getUserFromSession( - sessionManager, - client.tokenValidationDetails - ))!; + return (await utilities.getUserFromSession(sessionManager))!; }; /** diff --git a/lib/sdk/utilities/token-utils.ts b/lib/sdk/utilities/token-utils.ts index 088e4b4..45badca 100644 --- a/lib/sdk/utilities/token-utils.ts +++ b/lib/sdk/utilities/token-utils.ts @@ -111,22 +111,14 @@ export const getAccessToken = async ( * Extracts the user information from the current session returns null if * the token is not found. * @param {SessionManager} sessionManager - * @param {TokenValidationDetailsType} validationDetails * @returns {UserType | null} */ export const getUserFromSession = async ( - sessionManager: SessionManager, - validationDetails: TokenValidationDetailsType + sessionManager: SessionManager ): Promise => { const idTokenString = (await sessionManager.getSessionItem('id_token')) as string; - const validation = await validateToken({ - token: idTokenString, - domain: validationDetails.issuer, - }); - if (!validation.valid) { - throw new Error('Invalid ID token'); - } + // Simply decode the ID token without validation to accept old tokens const payload: Record = jwtDecoder(idTokenString) ?? {}; if (Object.keys(payload).length === 0) { throw new Error('Invalid ID token'); From 51200ac51ee2ee59ba862434131a041e4473e4b0 Mon Sep 17 00:00:00 2001 From: Daniel Rivers Date: Wed, 2 Jul 2025 23:56:14 +0100 Subject: [PATCH 7/7] feat: validate token when checking if authorised. --- lib/sdk/clients/browser/authcode-with-pkce.ts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/lib/sdk/clients/browser/authcode-with-pkce.ts b/lib/sdk/clients/browser/authcode-with-pkce.ts index ab3377a..d94d721 100644 --- a/lib/sdk/clients/browser/authcode-with-pkce.ts +++ b/lib/sdk/clients/browser/authcode-with-pkce.ts @@ -2,6 +2,7 @@ import { BrowserSessionManager } from '../../session-managers/index.js'; import { AuthCodeWithPKCE } from '../../oauth2-flows/index.js'; import * as utilities from '../../utilities/index.js'; import type { GeneratePortalUrlParams } from '@kinde/js-utils'; +import { validateToken } from '@kinde/jwt-validator'; import type { UserType, @@ -23,6 +24,7 @@ const createAuthCodeWithPKCEClient = (options: BrowserPKCEClientOptions) => { const { featureFlags, tokenClaims } = utilities; const sessionManager = options.sessionManager ?? new BrowserSessionManager(); const client = new AuthCodeWithPKCE(options); + const domain = options.authDomain; /** * Method makes use of the `createAuthorizationURL` method of the AuthCodeWithPKCE @@ -92,7 +94,20 @@ const createAuthCodeWithPKCEClient = (options: BrowserPKCEClientOptions) => { * @returns {Promise} */ const isAuthenticated = async (): Promise => { - return await client.isAuthenticated(sessionManager); + // First, check if the token is valid before other checks + const token = await client.getToken(sessionManager); + if (!token || typeof token !== 'string' || token.trim() === '') { + return false; + } + try { + const isValid = await validateToken({ token, domain }); + if (!isValid) { + return false; + } + return await client.isAuthenticated(sessionManager); + } catch (e) { + return false; + } }; /**