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..3053328 100644 --- a/lib/__tests__/sdk/utilities/token-utils.spec.ts +++ b/lib/__tests__/sdk/utilities/token-utils.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 * as mocks from '../../mocks'; import { describe, it, expect, beforeAll, afterEach } from 'vitest'; import { @@ -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/clients/browser/authcode-with-pkce.ts b/lib/sdk/clients/browser/authcode-with-pkce.ts index 98d59a3..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; + } }; /** @@ -114,10 +129,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 +221,7 @@ const createAuthCodeWithPKCEClient = (options: BrowserPKCEClientOptions) => { ); } - return await tokenClaims.getClaimValue( - sessionManager, - claim, - type, - client.tokenValidationDetails - ); + return await tokenClaims.getClaimValue(sessionManager, claim, type); }; /** @@ -233,12 +240,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 +258,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 +271,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 +285,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 +302,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/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; 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/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/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/lib/sdk/utilities/token-claims.ts b/lib/sdk/utilities/token-claims.ts index c1b97d5..3cad665 100644 --- a/lib/sdk/utilities/token-claims.ts +++ b/lib/sdk/utilities/token-claims.ts @@ -1,10 +1,15 @@ 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 * 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 @@ -13,23 +18,22 @@ 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; }; /** * 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 @@ -38,12 +42,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), }; }; @@ -51,71 +54,72 @@ 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 }} */ 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 }; }; /** * 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 }} */ 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, }); /** * 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 }} */ 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, @@ -126,17 +130,17 @@ 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[] }} */ 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..45badca 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, @@ -105,26 +111,26 @@ 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} + * @returns {UserType | 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) } - ); + + // 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'); + } 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 +141,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..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", @@ -70,8 +71,9 @@ }, "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" }, "packageManager": "pnpm@10.6.5+sha512.cdf928fca20832cd59ec53826492b7dc25dc524d4370b6b4adbf65803d32efaa6c1c88147c0ae4e8d579a6c9eec715757b50d4fa35eea179d868eada4ed043af" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3661311..057e609 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,12 +11,15 @@ 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) - jose: - specifier: ^6.0.10 - version: 6.0.11 uncrypto: specifier: ^0.1.3 version: 0.1.3 @@ -63,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 @@ -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