From b2be3a8e9e7d1bce3279db0efea555ec924a8285 Mon Sep 17 00:00:00 2001 From: Nik Ho Date: Fri, 4 Jul 2025 19:54:48 +1200 Subject: [PATCH 1/8] update token expiry check with 30 sec leeway --- packages/passport/sdk/src/authManager.ts | 8 +++++--- packages/passport/sdk/src/utils/token.ts | 15 +++++++++++++-- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/packages/passport/sdk/src/authManager.ts b/packages/passport/sdk/src/authManager.ts index 4eba9d222c..10203a1b2c 100644 --- a/packages/passport/sdk/src/authManager.ts +++ b/packages/passport/sdk/src/authManager.ts @@ -13,7 +13,7 @@ import { getDetail, Detail } from '@imtbl/metrics'; import localForage from 'localforage'; import DeviceCredentialsManager from './storage/device_credentials_manager'; import logger from './utils/logger'; -import { isTokenExpired } from './utils/token'; +import { isAccessTokenExpiredOrExpiring } from './utils/token'; import { PassportError, PassportErrorType, withPassportError } from './errors/passportError'; import { PassportMetadata, @@ -70,7 +70,7 @@ const getAuthConfiguration = (config: PassportConfiguration): UserManagerSetting end_session_endpoint: endSessionEndpoint.toString(), revocation_endpoint: `${authenticationDomain}/oauth/revoke`, }, - mergeClaimsStrategy: { array: 'merge' }, + // mergeClaimsStrategy: { array: 'merge' }, automaticSilentRenew: false, // Disabled until https://github.com/authts/oidc-client-ts/issues/430 has been resolved scope: oidcConfiguration.scope, userStore, @@ -458,13 +458,15 @@ export default class AuthManager { const oidcUser = await this.userManager.getUser(); if (!oidcUser) return null; - if (!isTokenExpired(oidcUser)) { + // if the token is not expired or expiring in 30 seconds or less, return the user + if (!isAccessTokenExpiredOrExpiring(oidcUser)) { const user = AuthManager.mapOidcUserToDomainModel(oidcUser); if (user && typeAssertion(user)) { return user; } } + // if the token is expired or expiring in 30 seconds or less, refresh the token if (oidcUser.refresh_token) { const user = await this.refreshTokenAndUpdatePromise(); if (user && typeAssertion(user)) { diff --git a/packages/passport/sdk/src/utils/token.ts b/packages/passport/sdk/src/utils/token.ts index 2d39048764..9909a42d6c 100644 --- a/packages/passport/sdk/src/utils/token.ts +++ b/packages/passport/sdk/src/utils/token.ts @@ -14,10 +14,21 @@ export function isIdTokenExpired(idToken: string | undefined): boolean { return decodedToken.exp < now; } -export function isTokenExpired(oidcUser: OidcUser): boolean { - const { id_token: idToken, expired } = oidcUser; +export function isAccessTokenExpiredOrExpiring(oidcUser: OidcUser): boolean { + const { id_token: idToken, expired, expires_in } = oidcUser; if (expired) { return true; } + + // if token will expire in 30 seconds or less, return true + if (expires_in && expires_in <= 30) { + return true; + } + + // Handle missing idToken - assume they need to login again + if (!idToken) { + return true; + } + return isIdTokenExpired(idToken); } From 8e7748252d67ba88ab72ac1491e18973360c8f1e Mon Sep 17 00:00:00 2001 From: Nik Ho Date: Fri, 4 Jul 2025 20:20:34 +1200 Subject: [PATCH 2/8] uncomment config option --- packages/passport/sdk/src/authManager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/passport/sdk/src/authManager.ts b/packages/passport/sdk/src/authManager.ts index 10203a1b2c..910ef4a52c 100644 --- a/packages/passport/sdk/src/authManager.ts +++ b/packages/passport/sdk/src/authManager.ts @@ -70,7 +70,7 @@ const getAuthConfiguration = (config: PassportConfiguration): UserManagerSetting end_session_endpoint: endSessionEndpoint.toString(), revocation_endpoint: `${authenticationDomain}/oauth/revoke`, }, - // mergeClaimsStrategy: { array: 'merge' }, + mergeClaimsStrategy: { array: 'merge' }, automaticSilentRenew: false, // Disabled until https://github.com/authts/oidc-client-ts/issues/430 has been resolved scope: oidcConfiguration.scope, userStore, From eadfb06b870943dfb6396cb877a90c70ae54faf5 Mon Sep 17 00:00:00 2001 From: Nik Ho Date: Fri, 4 Jul 2025 20:20:58 +1200 Subject: [PATCH 3/8] update tests --- packages/passport/sdk/src/authManager.test.ts | 67 ++++++++++++++++--- packages/passport/sdk/src/utils/token.test.ts | 10 +-- 2 files changed, 61 insertions(+), 16 deletions(-) diff --git a/packages/passport/sdk/src/authManager.test.ts b/packages/passport/sdk/src/authManager.test.ts index 209fccdaa0..19eb5eab1d 100644 --- a/packages/passport/sdk/src/authManager.test.ts +++ b/packages/passport/sdk/src/authManager.test.ts @@ -6,7 +6,7 @@ import Overlay from './overlay'; import { PassportError, PassportErrorType } from './errors/passportError'; import { PassportConfiguration } from './config'; import { mockUser, mockUserImx, mockUserZkEvm } from './test/mocks'; -import { isTokenExpired } from './utils/token'; +import { isAccessTokenExpiredOrExpiring } from './utils/token'; import { isUserZkEvm, PassportModuleConfiguration } from './types'; jest.mock('jwt-decode'); @@ -352,7 +352,7 @@ describe('AuthManager', () => { describe('when getUser returns a user', () => { it('should return the user', async () => { mockGetUser.mockReturnValue(mockOidcUser); - (isTokenExpired as jest.Mock).mockReturnValue(false); + (isAccessTokenExpiredOrExpiring as jest.Mock).mockReturnValue(false); const result = await authManager.getUserOrLogin(); @@ -364,7 +364,7 @@ describe('AuthManager', () => { it('calls attempts to sign in the user using signinPopup', async () => { mockGetUser.mockRejectedValue(new Error(mockErrorMsg)); mockSigninPopup.mockReturnValue(mockOidcUser); - (isTokenExpired as jest.Mock).mockReturnValue(false); + (isAccessTokenExpiredOrExpiring as jest.Mock).mockReturnValue(false); const result = await authManager.getUserOrLogin(); @@ -510,16 +510,61 @@ describe('AuthManager', () => { describe('getUser', () => { it('should retrieve the user from the userManager and return the domain model', async () => { mockGetUser.mockReturnValue(mockOidcUser); - (isTokenExpired as jest.Mock).mockReturnValue(false); + (isAccessTokenExpiredOrExpiring as jest.Mock).mockReturnValue(false); const result = await authManager.getUser(); expect(result).toEqual(mockUser); }); + it('should return null when user has no idToken and isAccessTokenExpiredOrExpiring returns true', async () => { + const userWithoutIdToken = { ...mockOidcUser, id_token: undefined }; + mockGetUser.mockReturnValue(userWithoutIdToken); + (isAccessTokenExpiredOrExpiring as jest.Mock).mockReturnValue(true); + + const result = await authManager.getUser(); + + expect(result).toBeNull(); + expect(isAccessTokenExpiredOrExpiring).toHaveBeenCalledWith(userWithoutIdToken); + }); + + it('should refresh token when expires_in is 30 seconds or less', async () => { + const expiringUser = { ...mockOidcUser, expires_in: 25 }; + mockGetUser.mockReturnValue(expiringUser); + (isAccessTokenExpiredOrExpiring as jest.Mock).mockReturnValue(true); + mockSigninSilent.mockResolvedValue(mockOidcUser); + + const result = await authManager.getUser(); + + expect(mockSigninSilent).toBeCalledTimes(1); + expect(result).toEqual(mockUser); + }); + + it('should handle user with null expires_in', async () => { + const userWithNullExpiresIn = { ...mockOidcUser, expires_in: null }; + mockGetUser.mockReturnValue(userWithNullExpiresIn); + (isAccessTokenExpiredOrExpiring as jest.Mock).mockReturnValue(false); + + const result = await authManager.getUser(); + + expect(mockSigninSilent).not.toHaveBeenCalled(); + expect(result).toEqual(mockUser); + }); + + it('should return user directly when token is not expired or expiring', async () => { + const freshUser = { ...mockOidcUser, expires_in: 3600 }; // 1 hour + mockGetUser.mockReturnValue(freshUser); + (isAccessTokenExpiredOrExpiring as jest.Mock).mockReturnValue(false); + + const result = await authManager.getUser(); + + expect(mockSigninSilent).not.toHaveBeenCalled(); + expect(result).toEqual(mockUser); + }); + it('should call signinSilent and returns user when user token is expired with the refresh token', async () => { mockGetUser.mockReturnValue(mockOidcExpiredUser); - (isTokenExpired as jest.Mock).mockReturnValue(true); + (isAccessTokenExpiredOrExpiring as jest.Mock).mockReturnValue(true); mockSigninSilent.mockResolvedValue(mockOidcUser); const result = await authManager.getUser(); @@ -530,7 +575,7 @@ describe('AuthManager', () => { it('should reject with an error when signinSilent throws a string', async () => { mockGetUser.mockReturnValue(mockOidcExpiredUser); - (isTokenExpired as jest.Mock).mockReturnValue(true); + (isAccessTokenExpiredOrExpiring as jest.Mock).mockReturnValue(true); mockSigninSilent.mockRejectedValue('oops'); await expect(() => authManager.getUser()).rejects.toThrow( @@ -543,7 +588,7 @@ describe('AuthManager', () => { it('should return null when the user token is expired without refresh token', async () => { mockGetUser.mockReturnValue(mockOidcExpiredNoRefreshTokenUser); - (isTokenExpired as jest.Mock).mockReturnValue(true); + (isAccessTokenExpiredOrExpiring as jest.Mock).mockReturnValue(true); const result = await authManager.getUser(); @@ -553,7 +598,7 @@ describe('AuthManager', () => { it('should return null when the user token is expired with the refresh token, but signinSilent returns null', async () => { mockGetUser.mockReturnValue(mockOidcExpiredUser); - (isTokenExpired as jest.Mock).mockReturnValue(true); + (isAccessTokenExpiredOrExpiring as jest.Mock).mockReturnValue(true); mockSigninSilent.mockResolvedValue(null); const result = await authManager.getUser(); @@ -584,7 +629,7 @@ describe('AuthManager', () => { describe('when the user is expired', () => { it('should only call refresh the token once', async () => { mockGetUser.mockReturnValue(mockOidcExpiredUser); - (isTokenExpired as jest.Mock).mockReturnValue(true); + (isAccessTokenExpiredOrExpiring as jest.Mock).mockReturnValue(true); mockSigninSilent.mockReturnValue(mockOidcUser); await Promise.allSettled([ @@ -600,7 +645,7 @@ describe('AuthManager', () => { describe('when the user does not meet the type assertion', () => { it('should return null', async () => { mockGetUser.mockReturnValue(mockOidcUser); - (isTokenExpired as jest.Mock).mockReturnValue(false); + (isAccessTokenExpiredOrExpiring as jest.Mock).mockReturnValue(false); const result = await authManager.getUser(isUserZkEvm); @@ -617,7 +662,7 @@ describe('AuthManager', () => { zkevm_user_admin_address: mockUserZkEvm.zkEvm.userAdminAddress, }, }); - (isTokenExpired as jest.Mock).mockReturnValue(false); + (isAccessTokenExpiredOrExpiring as jest.Mock).mockReturnValue(false); const result = await authManager.getUser(isUserZkEvm); diff --git a/packages/passport/sdk/src/utils/token.test.ts b/packages/passport/sdk/src/utils/token.test.ts index 876265c567..79c5864c86 100644 --- a/packages/passport/sdk/src/utils/token.test.ts +++ b/packages/passport/sdk/src/utils/token.test.ts @@ -2,7 +2,7 @@ import encode from 'jwt-encode'; import { User as OidcUser, } from 'oidc-client-ts'; -import { isIdTokenExpired, isTokenExpired } from './token'; +import { isIdTokenExpired, isAccessTokenExpiredOrExpiring } from './token'; const now = Math.floor(Date.now() / 1000); const oneHourLater = now + 3600; @@ -31,13 +31,13 @@ describe('isIdTokenExpired', () => { }); }); -describe('isTokenExpired', () => { +describe('isAccessTokenExpiredOrExpiring', () => { it('should return true if expired is true', () => { const user = { id_token: mockValidIdToken, expired: true, } as unknown as OidcUser; - expect(isTokenExpired(user)).toBe(true); + expect(isAccessTokenExpiredOrExpiring(user)).toBe(true); }); it('should return false if idToken is valid', () => { @@ -45,7 +45,7 @@ describe('isTokenExpired', () => { id_token: mockValidIdToken, expired: false, } as unknown as OidcUser; - expect(isTokenExpired(user)).toBe(false); + expect(isAccessTokenExpiredOrExpiring(user)).toBe(false); }); it('should return true idToken is expired', () => { @@ -53,6 +53,6 @@ describe('isTokenExpired', () => { id_token: mockExpiredIdToken, expired: false, } as unknown as OidcUser; - expect(isTokenExpired(user)).toBe(true); + expect(isAccessTokenExpiredOrExpiring(user)).toBe(true); }); }); From bd4c8d0d8f93594af6572339815073fa6c8626ab Mon Sep 17 00:00:00 2001 From: Nik Ho Date: Mon, 14 Jul 2025 16:10:34 +1200 Subject: [PATCH 4/8] update access token expiry logic --- packages/passport/sdk/src/utils/token.test.ts | 78 +++++++++++++++++-- packages/passport/sdk/src/utils/token.ts | 29 ++++--- 2 files changed, 91 insertions(+), 16 deletions(-) diff --git a/packages/passport/sdk/src/utils/token.test.ts b/packages/passport/sdk/src/utils/token.test.ts index 79c5864c86..139f78902d 100644 --- a/packages/passport/sdk/src/utils/token.test.ts +++ b/packages/passport/sdk/src/utils/token.test.ts @@ -7,16 +7,23 @@ import { isIdTokenExpired, isAccessTokenExpiredOrExpiring } from './token'; const now = Math.floor(Date.now() / 1000); const oneHourLater = now + 3600; const oneHourBefore = now - 3600; +const fifteenSecondsLater = now + 15; +const fortyFiveSecondsLater = now + 45; const mockExpiredIdToken = encode({ iat: oneHourBefore, exp: oneHourBefore, }, 'secret'); + export const mockValidIdToken = encode({ iat: now, exp: oneHourLater, }, 'secret'); +const mockFreshAccessToken = encode({ + exp: fortyFiveSecondsLater, // Expires in 45 seconds (outside 30-second buffer) +}, 'secret'); + describe('isIdTokenExpired', () => { it('should return false if idToken is undefined', () => { expect(isIdTokenExpired(undefined)).toBe(false); @@ -32,26 +39,83 @@ describe('isIdTokenExpired', () => { }); describe('isAccessTokenExpiredOrExpiring', () => { - it('should return true if expired is true', () => { + it('should return true if access token is missing', () => { const user = { id_token: mockValidIdToken, - expired: true, + access_token: undefined, + } as unknown as OidcUser; + expect(isAccessTokenExpiredOrExpiring(user)).toBe(true); + }); + + it('should return true if id token is missing', () => { + const mockValidAccessToken = encode({ + exp: oneHourLater, + }, 'secret'); + + const user = { + id_token: undefined, + access_token: mockValidAccessToken, } as unknown as OidcUser; expect(isAccessTokenExpiredOrExpiring(user)).toBe(true); }); - it('should return false if idToken is valid', () => { + it('should return true if access token is expired', () => { + const mockExpiredAccessToken = encode({ + exp: oneHourBefore, + }, 'secret'); + const user = { id_token: mockValidIdToken, - expired: false, + access_token: mockExpiredAccessToken, } as unknown as OidcUser; - expect(isAccessTokenExpiredOrExpiring(user)).toBe(false); + expect(isAccessTokenExpiredOrExpiring(user)).toBe(true); + }); + + it('should return true if access token is expiring within 30 seconds', () => { + const mockExpiringAccessToken = encode({ + exp: fifteenSecondsLater, // Expires in 15 seconds (within 30-second buffer) + }, 'secret'); + + const user = { + id_token: mockValidIdToken, + access_token: mockExpiringAccessToken, + } as unknown as OidcUser; + expect(isAccessTokenExpiredOrExpiring(user)).toBe(true); }); - it('should return true idToken is expired', () => { + it('should return true if access token is valid but id token is expired', () => { const user = { id_token: mockExpiredIdToken, - expired: false, + access_token: mockFreshAccessToken, + } as unknown as OidcUser; + expect(isAccessTokenExpiredOrExpiring(user)).toBe(true); + }); + + it('should return false if both tokens are valid and not expiring', () => { + const user = { + id_token: mockValidIdToken, + access_token: mockFreshAccessToken, + } as unknown as OidcUser; + expect(isAccessTokenExpiredOrExpiring(user)).toBe(false); + }); + + it('should return true if access token is malformed', () => { + const user = { + id_token: mockValidIdToken, + access_token: 'invalid-jwt-token', + } as unknown as OidcUser; + expect(isAccessTokenExpiredOrExpiring(user)).toBe(true); + }); + + it('should return true if access token has no exp claim (security vulnerability)', () => { + const accessTokenWithoutExp = encode({ + iat: now, + sub: 'user123', + }, 'secret'); + + const user = { + id_token: mockValidIdToken, + access_token: accessTokenWithoutExp, } as unknown as OidcUser; expect(isAccessTokenExpiredOrExpiring(user)).toBe(true); }); diff --git a/packages/passport/sdk/src/utils/token.ts b/packages/passport/sdk/src/utils/token.ts index 9909a42d6c..e5798bca63 100644 --- a/packages/passport/sdk/src/utils/token.ts +++ b/packages/passport/sdk/src/utils/token.ts @@ -2,7 +2,7 @@ import jwt_decode from 'jwt-decode'; import { User as OidcUser, } from 'oidc-client-ts'; -import { IdTokenPayload } from '../types'; +import { IdTokenPayload, TokenPayload } from '../types'; export function isIdTokenExpired(idToken: string | undefined): boolean { if (!idToken) { @@ -15,18 +15,29 @@ export function isIdTokenExpired(idToken: string | undefined): boolean { } export function isAccessTokenExpiredOrExpiring(oidcUser: OidcUser): boolean { - const { id_token: idToken, expired, expires_in } = oidcUser; - if (expired) { - return true; - } + const { id_token: idToken, access_token: accessToken } = oidcUser; - // if token will expire in 30 seconds or less, return true - if (expires_in && expires_in <= 30) { + // Handle missing tokens - assume they need to login again + if (!accessToken || !idToken) { return true; } - // Handle missing idToken - assume they need to login again - if (!idToken) { + // Decode the access token to check its expiration + try { + const decodedAccessToken = jwt_decode(accessToken); + const now = Math.floor(Date.now() / 1000); + + // Access tokens without expiration claims are invalid (security vulnerability) + if (!decodedAccessToken.exp) { + return true; + } + + // Check if access token is expired or expiring in 30 seconds or less + if (decodedAccessToken.exp <= now + 30) { + return true; + } + } catch (error) { + // If we can't decode the access token, assume it's invalid return true; } From 2b58e8f75520d9eb1196d1321c84f410f974f508 Mon Sep 17 00:00:00 2001 From: Nik Ho Date: Mon, 14 Jul 2025 16:56:45 +1200 Subject: [PATCH 5/8] update auth manager tests --- packages/passport/sdk/src/authManager.test.ts | 34 +++++++++++-------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/packages/passport/sdk/src/authManager.test.ts b/packages/passport/sdk/src/authManager.test.ts index 19eb5eab1d..b85b8c4311 100644 --- a/packages/passport/sdk/src/authManager.test.ts +++ b/packages/passport/sdk/src/authManager.test.ts @@ -517,20 +517,22 @@ describe('AuthManager', () => { expect(result).toEqual(mockUser); }); - it('should return null when user has no idToken and isAccessTokenExpiredOrExpiring returns true', async () => { - const userWithoutIdToken = { ...mockOidcUser, id_token: undefined }; + it('should return null when user has no idToken', async () => { + const userWithoutIdToken = { ...mockOidcUser, id_token: undefined, refresh_token: undefined }; mockGetUser.mockReturnValue(userWithoutIdToken); - (isAccessTokenExpiredOrExpiring as jest.Mock).mockReturnValue(true); + // Restore real function behavior for this test + (isAccessTokenExpiredOrExpiring as jest.Mock).mockImplementation( + jest.requireActual('./utils/token').isAccessTokenExpiredOrExpiring, + ); const result = await authManager.getUser(); expect(result).toBeNull(); - expect(isAccessTokenExpiredOrExpiring).toHaveBeenCalledWith(userWithoutIdToken); }); - it('should refresh token when expires_in is 30 seconds or less', async () => { - const expiringUser = { ...mockOidcUser, expires_in: 25 }; - mockGetUser.mockReturnValue(expiringUser); + it('should refresh token when access token is expired or expiring', async () => { + const userWithExpiringAccessToken = { ...mockOidcUser }; + mockGetUser.mockReturnValue(userWithExpiringAccessToken); (isAccessTokenExpiredOrExpiring as jest.Mock).mockReturnValue(true); mockSigninSilent.mockResolvedValue(mockOidcUser); @@ -540,19 +542,23 @@ describe('AuthManager', () => { expect(result).toEqual(mockUser); }); - it('should handle user with null expires_in', async () => { - const userWithNullExpiresIn = { ...mockOidcUser, expires_in: null }; - mockGetUser.mockReturnValue(userWithNullExpiresIn); - (isAccessTokenExpiredOrExpiring as jest.Mock).mockReturnValue(false); + it('should handle user with missing access token', async () => { + const userWithoutAccessToken = { ...mockOidcUser, access_token: undefined }; + mockGetUser.mockReturnValue(userWithoutAccessToken); + // Restore real function behavior for this test + (isAccessTokenExpiredOrExpiring as jest.Mock).mockImplementation( + jest.requireActual('./utils/token').isAccessTokenExpiredOrExpiring, + ); + mockSigninSilent.mockResolvedValue(mockOidcUser); const result = await authManager.getUser(); - expect(mockSigninSilent).not.toHaveBeenCalled(); + expect(mockSigninSilent).toBeCalledTimes(1); expect(result).toEqual(mockUser); }); - it('should return user directly when token is not expired or expiring', async () => { - const freshUser = { ...mockOidcUser, expires_in: 3600 }; // 1 hour + it('should return user directly when access token is not expired or expiring', async () => { + const freshUser = { ...mockOidcUser }; mockGetUser.mockReturnValue(freshUser); (isAccessTokenExpiredOrExpiring as jest.Mock).mockReturnValue(false); From 867c9a19ffbfd5fcc3c81075c355d5a26defc1e1 Mon Sep 17 00:00:00 2001 From: Nik Ho Date: Mon, 14 Jul 2025 17:09:24 +1200 Subject: [PATCH 6/8] fix passport init test mocks --- .../passport/sdk/src/Passport.int.test.ts | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/packages/passport/sdk/src/Passport.int.test.ts b/packages/passport/sdk/src/Passport.int.test.ts index f50d3b57a7..4ad38106d5 100644 --- a/packages/passport/sdk/src/Passport.int.test.ts +++ b/packages/passport/sdk/src/Passport.int.test.ts @@ -29,6 +29,17 @@ const redirectUri = 'example.com'; const popupRedirectUri = 'example.com'; const logoutRedirectUri = 'example.com'; const clientId = 'clientId123'; +const now = Math.floor(Date.now() / 1000); +const oneHourLater = now + 3600; + +const mockValidAccessToken = encode({ + iss: 'https://example.auth0.com/', + aud: 'https://api.example.com/', + sub: 'sub123', + iat: now, + exp: oneHourLater, +}, 'secret'); + const mockOidcUser = { profile: { sub: 'sub123', @@ -37,13 +48,20 @@ const mockOidcUser = { }, expired: false, id_token: mockValidIdToken, - access_token: 'accessToken123', + access_token: mockValidAccessToken, refresh_token: 'refreshToken123', }; const mockOidcUserZkevm = { ...mockOidcUser, id_token: encode({ + iss: 'https://example.auth0.com/', + aud: 'clientId123', + sub: 'sub123', + iat: now, + exp: oneHourLater, + email: 'test@example.com', + nickname: 'test', passport: { zkevm_eth_address: mockUserZkEvm.zkEvm.ethAddress, zkevm_user_admin_address: mockUserZkEvm.zkEvm.userAdminAddress, From f26ab10e9666e52c94eb0440e0bcc369b40089e4 Mon Sep 17 00:00:00 2001 From: Nik Ho Date: Tue, 15 Jul 2025 13:53:44 +1200 Subject: [PATCH 7/8] check id token expiring within 30 seconds --- packages/passport/sdk/src/utils/token.test.ts | 29 ++++++------ packages/passport/sdk/src/utils/token.ts | 44 +++++++++---------- 2 files changed, 34 insertions(+), 39 deletions(-) diff --git a/packages/passport/sdk/src/utils/token.test.ts b/packages/passport/sdk/src/utils/token.test.ts index 139f78902d..e42bee942b 100644 --- a/packages/passport/sdk/src/utils/token.test.ts +++ b/packages/passport/sdk/src/utils/token.test.ts @@ -2,7 +2,7 @@ import encode from 'jwt-encode'; import { User as OidcUser, } from 'oidc-client-ts'; -import { isIdTokenExpired, isAccessTokenExpiredOrExpiring } from './token'; +import { isAccessTokenExpiredOrExpiring } from './token'; const now = Math.floor(Date.now() / 1000); const oneHourLater = now + 3600; @@ -24,20 +24,6 @@ const mockFreshAccessToken = encode({ exp: fortyFiveSecondsLater, // Expires in 45 seconds (outside 30-second buffer) }, 'secret'); -describe('isIdTokenExpired', () => { - it('should return false if idToken is undefined', () => { - expect(isIdTokenExpired(undefined)).toBe(false); - }); - - it('should return true if idToken is expired', () => { - expect(isIdTokenExpired(mockExpiredIdToken)).toBe(true); - }); - - it('should return false if idToken is not expired', () => { - expect(isIdTokenExpired(mockValidIdToken)).toBe(false); - }); -}); - describe('isAccessTokenExpiredOrExpiring', () => { it('should return true if access token is missing', () => { const user = { @@ -91,6 +77,19 @@ describe('isAccessTokenExpiredOrExpiring', () => { expect(isAccessTokenExpiredOrExpiring(user)).toBe(true); }); + it('should return true if access token is valid but id token is expiring within 30 seconds', () => { + const expiringIdToken = encode({ + iat: now, + exp: now + 15, // Expires in 15 seconds + }, 'secret'); + + const user = { + id_token: expiringIdToken, + access_token: mockFreshAccessToken, + } as unknown as OidcUser; + expect(isAccessTokenExpiredOrExpiring(user)).toBe(true); + }); + it('should return false if both tokens are valid and not expiring', () => { const user = { id_token: mockValidIdToken, diff --git a/packages/passport/sdk/src/utils/token.ts b/packages/passport/sdk/src/utils/token.ts index e5798bca63..332a9c6b31 100644 --- a/packages/passport/sdk/src/utils/token.ts +++ b/packages/passport/sdk/src/utils/token.ts @@ -4,14 +4,23 @@ import { } from 'oidc-client-ts'; import { IdTokenPayload, TokenPayload } from '../types'; -export function isIdTokenExpired(idToken: string | undefined): boolean { - if (!idToken) { - return false; - } +function isTokenExpiredOrExpiring(token: string): boolean { + try { + // try to decode the token as access token payload or id token payload + const decodedToken = jwt_decode(token); + const now = Math.floor(Date.now() / 1000); - const decodedToken = jwt_decode(idToken); - const now = Math.floor(Date.now() / 1000); - return decodedToken.exp < now; + // Tokens without expiration claims are invalid (security vulnerability) + if (!decodedToken.exp) { + return true; + } + + // Check if token is expired or expiring in 30 seconds or less + return decodedToken.exp <= now + 30; + } catch (error) { + // If we can't decode the token, assume it's invalid + return true; + } } export function isAccessTokenExpiredOrExpiring(oidcUser: OidcUser): boolean { @@ -22,24 +31,11 @@ export function isAccessTokenExpiredOrExpiring(oidcUser: OidcUser): boolean { return true; } - // Decode the access token to check its expiration - try { - const decodedAccessToken = jwt_decode(accessToken); - const now = Math.floor(Date.now() / 1000); - - // Access tokens without expiration claims are invalid (security vulnerability) - if (!decodedAccessToken.exp) { - return true; - } - - // Check if access token is expired or expiring in 30 seconds or less - if (decodedAccessToken.exp <= now + 30) { - return true; - } - } catch (error) { - // If we can't decode the access token, assume it's invalid + // Check if access token is expired or expiring + if (isTokenExpiredOrExpiring(accessToken)) { return true; } - return isIdTokenExpired(idToken); + // Check if ID token is expired or expiring + return isTokenExpiredOrExpiring(idToken); } From 9ad2e5e83ca6f14632317cd424ce6834ddfea681 Mon Sep 17 00:00:00 2001 From: Nik Ho Date: Fri, 18 Jul 2025 13:20:54 +1200 Subject: [PATCH 8/8] simplify token logic --- packages/passport/sdk/src/utils/token.ts | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/packages/passport/sdk/src/utils/token.ts b/packages/passport/sdk/src/utils/token.ts index 332a9c6b31..659674a2fe 100644 --- a/packages/passport/sdk/src/utils/token.ts +++ b/packages/passport/sdk/src/utils/token.ts @@ -26,16 +26,10 @@ function isTokenExpiredOrExpiring(token: string): boolean { export function isAccessTokenExpiredOrExpiring(oidcUser: OidcUser): boolean { const { id_token: idToken, access_token: accessToken } = oidcUser; - // Handle missing tokens - assume they need to login again if (!accessToken || !idToken) { return true; } - // Check if access token is expired or expiring - if (isTokenExpiredOrExpiring(accessToken)) { - return true; - } - - // Check if ID token is expired or expiring - return isTokenExpiredOrExpiring(idToken); + // Check if either token is expired or expiring + return isTokenExpiredOrExpiring(accessToken) || isTokenExpiredOrExpiring(idToken); }