Skip to content

Commit

Permalink
Merge pull request #61 from kinde-oss/feature/extra-jwt-checks
Browse files Browse the repository at this point in the history
Extra JWT checks
  • Loading branch information
DaveOrDead authored Mar 4, 2024
2 parents 0250557 + 4afdf87 commit f187919
Show file tree
Hide file tree
Showing 13 changed files with 422 additions and 65 deletions.
8 changes: 8 additions & 0 deletions src/constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,11 @@ export enum flagDataTypeMap {
i = 'integer',
b = 'boolean'
}

export enum storageMap {
token_bundle = 'kinde_token',
access_token = 'kinde_access_token',
id_token = 'kinde_id_token',
user = 'user',
refresh_token = 'kinde_refresh_token'
}
97 changes: 62 additions & 35 deletions src/createKindeClient.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import {version} from './utils/version';
import {SESSION_PREFIX} from './constants/index';
import {SESSION_PREFIX, storageMap} from './constants/index';
import {jwtDecode} from 'jwt-decode';

import {
type JWT,
isValidJwt,
isJWTActive,
setupChallenge,
getClaim,
getClaimValue,
Expand All @@ -12,6 +13,7 @@ import {
getStringFlag,
getBooleanFlag,
getFlag,
isTokenValid,
isCustomDomain
} from './utils/index';
import {store} from './state/store';
Expand Down Expand Up @@ -108,32 +110,57 @@ const createKindeClient = async (
const setStore = (data: KindeState & {error: string}) => {
if (!data || data.error) return;

const accessToken = jwtDecode(data.access_token);
const idToken = jwtDecode(data.id_token)! as JWT & KindeUser;
store.setItem('kinde_token', data);
store.setItem('kinde_access_token', accessToken);
store.setItem('kinde_id_token', idToken);
store.setItem('user', {
id: idToken.sub,
given_name: idToken.given_name,
family_name: idToken.family_name,
email: idToken.email,
picture: idToken.picture
});
const idTokenHeader = jwtDecode(data.id_token, {header: true});
const accessToken = jwtDecode(data.access_token);
const accessTokenHeader = jwtDecode(data.access_token, {header: true});

if (isUseLocalStorage) {
localStorage.setItem('kinde_refresh_token', data.refresh_token);
} else {
store.setItem('kinde_refresh_token', data.refresh_token);
const validatorOptions = {
iss: domain,
azp: clientId,
aud: audience
};
const isIDValid = isTokenValid(
{
payload: idToken,
header: idTokenHeader
},
{...validatorOptions, aud: clientId}
);
const isAccessValid = isTokenValid(
{
payload: accessToken,
header: accessTokenHeader
},
validatorOptions
);

if (isIDValid && isAccessValid) {
store.setItem(storageMap.token_bundle, data);
store.setItem(storageMap.access_token, accessToken);
store.setItem(storageMap.id_token, idToken);
store.setItem(storageMap.user, {
id: idToken.sub,
given_name: idToken.given_name,
family_name: idToken.family_name,
email: idToken.email,
picture: idToken.picture
});

if (isUseLocalStorage) {
localStorage.setItem(storageMap.refresh_token, data.refresh_token);
} else {
store.setItem(storageMap.refresh_token, data.refresh_token);
}
}
};

const useRefreshToken = async (
{tokenType} = {tokenType: 'kinde_access_token'}
{tokenType} = {tokenType: storageMap.access_token}
) => {
const refresh_token = isUseLocalStorage
? (localStorage.getItem('kinde_refresh_token') as string)
: (store.getItem('kinde_refresh_token') as string);
? (localStorage.getItem(storageMap.refresh_token) as string)
: (store.getItem(storageMap.refresh_token) as string);

if (refresh_token || isUseCookie) {
try {
Expand All @@ -157,7 +184,7 @@ const createKindeClient = async (
const data = await response.json();
setStore(data);

if (tokenType === 'kinde_id_token') {
if (tokenType === storageMap.id_token) {
return data.id_token;
}

Expand All @@ -168,18 +195,18 @@ const createKindeClient = async (
}
};

const getTokenType = async (tokenType: string) => {
const token = store.getItem('kinde_token') as KindeState;
const getTokenType = async (tokenType: storageMap) => {
const token = store.getItem(storageMap.token_bundle) as KindeState;

if (!token) {
return await useRefreshToken({tokenType});
}

const tokenToReturn = store.getItem(tokenType);
const isTokenValid = isValidJwt(tokenToReturn as JWT);
const isTokenActive = isJWTActive(tokenToReturn as JWT);

if (isTokenValid) {
return tokenType === 'kinde_access_token'
if (isTokenActive) {
return tokenType === storageMap.access_token
? token.access_token
: token.id_token;
} else {
Expand All @@ -188,21 +215,21 @@ const createKindeClient = async (
};

const getToken = async () => {
return await getTokenType('kinde_access_token');
return await getTokenType(storageMap.access_token);
};

const getIdToken = async () => {
return await getTokenType('kinde_id_token');
return await getTokenType(storageMap.id_token);
};

const isAuthenticated = async () => {
const accessToken = store.getItem('kinde_access_token');
const accessToken = store.getItem(storageMap.access_token);
if (!accessToken) {
return false;
}

const isTokenValid = isValidJwt(accessToken as JWT);
if (isTokenValid) {
const isTokenActive = isJWTActive(accessToken as JWT);
if (isTokenActive) {
return true;
}

Expand Down Expand Up @@ -369,7 +396,7 @@ const createKindeClient = async (
};

const getUser = (): KindeUser => {
return store.getItem('user') as KindeUser;
return store.getItem(storageMap.user) as KindeUser;
};

const getUserProfile = async () => {
Expand All @@ -385,14 +412,14 @@ const createKindeClient = async (
headers: headers
});
const json = await res.json();
store.setItem('user', {
store.setItem(storageMap.user, {
id: json.sub,
given_name: json.given_name,
family_name: json.family_name,
email: json.email,
picture: json.picture
});
return store.getItem('user') as KindeUser;
return store.getItem(storageMap.user) as KindeUser;
} catch (err) {
console.error(err);
}
Expand All @@ -405,7 +432,7 @@ const createKindeClient = async (
store.reset();

if (isUseLocalStorage) {
localStorage.removeItem('kinde_refresh_token');
localStorage.removeItem(storageMap.refresh_token);
}

const searchParams = new URLSearchParams({
Expand Down
2 changes: 1 addition & 1 deletion src/testData/accessTokenStub.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
const accessTokenStub = {
aud: ['stake:prod-api'],
azp: '1234567890',
exp: 1683779908,
exp: 1772323199,
feature_flags: {
theme: {
t: 's',
Expand Down
2 changes: 1 addition & 1 deletion src/testData/idTokenStub.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ const idTokenStub = {
auth_time: 1683693508,
azp: '123456789',
email: '[email protected]',
exp: 1683697108,
exp: 1772323199,
family_name: 'Lannister',
given_name: 'Jaime',
iat: 1683693508,
Expand Down
5 changes: 3 additions & 2 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export {pkceChallengeFromVerifier} from './pkceChallengeFromVerifier/pkceChallengeFromVerifier';
export {randomString} from './randomString/randomString';
export {isValidJwt} from './isValidJwt/isValidJwt';
export {isJWTActive} from './isJWTActive/isJWTActive';
export {setupChallenge} from './setupChallenge/setupChallenge';
export {getClaim} from './getClaim/getClaim';
export {getClaimValue} from './getClaimValue/getClaimValue';
Expand All @@ -9,6 +9,7 @@ export {getBooleanFlag} from './getBooleanFlag/getBooleanFlag';
export {getIntegerFlag} from './getIntegerFlag/getIntegerFlag';
export {getStringFlag} from './getStringFlag/getStringFlag';
export {getUserOrganizations} from './getUserOrganizations/getUserOrganizations';
export {isTokenValid} from './isTokenValid/isTokenValid';
export {isCustomDomain} from './isCustomDomain/isCustomDomain';
export * from './isValidJwt/isValidJwt.types';
export * from './isJWTActive/isJWTActive.types';
export * from './getFlag/getFlag.types';
19 changes: 19 additions & 0 deletions src/utils/isJWTActive/isJWTActive.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import {initializeStore} from '../../testData/initializeStore';
import {accessTokenStub} from '../../testData/accessTokenStub';
import type {JWT} from './isJWTActive.types';
import {isJWTActive} from './isJWTActive';
import {store} from '../../state/store';

describe('isJWTActive util', () => {
beforeEach(() => initializeStore());

test('returns false if provided token is expired', () => {
const expiredToken = {...accessTokenStub, exp: 949363200};
expect(isJWTActive(expiredToken)).toBe(false);
});

test('return true if provided token is not expired', () => {
const unexpiredToken = store.getItem('kinde_access_token') as JWT;
expect(isJWTActive(unexpiredToken)).toBe(true);
});
});
8 changes: 8 additions & 0 deletions src/utils/isJWTActive/isJWTActive.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import {JWT} from './isJWTActive.types';

const LEEWAY = 60;

export const isJWTActive = (jwtToken: JWT): boolean => {
const unixTime = Math.floor(Date.now() / 1000);
return jwtToken.exp + LEEWAY > unixTime;
};
File renamed without changes.
Loading

0 comments on commit f187919

Please sign in to comment.