From dfde6becee383b07cebd5890f485b512c17b7418 Mon Sep 17 00:00:00 2001 From: Jack Palmer Date: Tue, 7 Apr 2026 14:01:45 +0100 Subject: [PATCH] fix(auth0): skip profile cache when id_token has no sub claim When the JWT id_token lacks a sub claim, the cache key would be auth0-profile:undefined, causing all users without a sub to share the same cached profile. Now skips caching entirely when sub is missing and fetches the profile directly. Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Jack Palmer --- .../src/authenticator.test.ts | 43 +++++++++++++++++++ .../src/authenticator.ts | 19 ++++---- 2 files changed, 54 insertions(+), 8 deletions(-) diff --git a/plugins/auth-backend-module-auth0-provider/src/authenticator.test.ts b/plugins/auth-backend-module-auth0-provider/src/authenticator.test.ts index 0fdeb87cdb..aebb73812c 100644 --- a/plugins/auth-backend-module-auth0-provider/src/authenticator.test.ts +++ b/plugins/auth-backend-module-auth0-provider/src/authenticator.test.ts @@ -242,4 +242,47 @@ describe('createAuth0Authenticator', () => { expect(mockFetchProfile).toHaveBeenCalledTimes(2); expect(result.fullProfile).toEqual(updatedProfile); }); + + it('should skip cache when id_token has no sub claim', async () => { + const header = Buffer.from(JSON.stringify({ alg: 'none' })).toString( + 'base64url', + ); + const payload = Buffer.from(JSON.stringify({ aud: 'test' })).toString( + 'base64url', + ); + const idTokenWithoutSub = `${header}.${payload}.`; + + mockExecuteRefresh.mockResolvedValue({ + accessToken: 'new-access-token', + refreshToken: 'new-refresh-token', + params: { + token_type: 'bearer', + scope: 'openid profile', + expires_in: 3600, + id_token: idTokenWithoutSub, + }, + }); + + const authenticator = createAuth0Authenticator({ cache }); + const ctx = authenticator.initialize({ + callbackUrl: 'http://localhost/callback', + config: mockConfig, + }); + + const result = await authenticator.refresh( + { + refreshToken: 'my-refresh-token', + scope: 'openid profile', + req: {} as express.Request, + }, + ctx, + ); + + // Profile is fetched directly + expect(mockFetchProfile).toHaveBeenCalledWith('new-access-token'); + expect(result.fullProfile).toEqual(mockProfile); + // Cache is never read or written + expect(cache.get).not.toHaveBeenCalled(); + expect(cache.set).not.toHaveBeenCalled(); + }); }); diff --git a/plugins/auth-backend-module-auth0-provider/src/authenticator.ts b/plugins/auth-backend-module-auth0-provider/src/authenticator.ts index 6390324869..c22705a165 100644 --- a/plugins/auth-backend-module-auth0-provider/src/authenticator.ts +++ b/plugins/auth-backend-module-auth0-provider/src/authenticator.ts @@ -140,17 +140,20 @@ export function createAuth0Authenticator(options?: { cache?: CacheService }) { ); const { sub } = decodeJwt(result.params.id_token); - const cacheKey = `auth0-profile:${sub}`; - let fullProfile = (await profileCache?.get(cacheKey)) as - | PassportProfile - | undefined; + const cacheKey = sub ? `auth0-profile:${sub}` : undefined; + + let fullProfile = cacheKey + ? ((await profileCache?.get(cacheKey)) as PassportProfile | undefined) + : undefined; if (!fullProfile) { fullProfile = await helper.fetchProfile(result.accessToken); - await profileCache?.set( - cacheKey, - JSON.parse(JSON.stringify(fullProfile)), - ); + if (cacheKey) { + await profileCache?.set( + cacheKey, + JSON.parse(JSON.stringify(fullProfile)), + ); + } } return {