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) <noreply@anthropic.com>
Signed-off-by: Jack Palmer <jackpalmer@spotify.com>
This commit is contained in:
Jack Palmer
2026-04-07 14:01:45 +01:00
parent 44a42bf8e7
commit dfde6becee
2 changed files with 54 additions and 8 deletions
@@ -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();
});
});
@@ -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 {