fix(auth0): use jose to decode id_token sub claim for cache key

Replace manual base64 JWT decoding with jose's decodeJwt for
correctness and consistency with other auth modules. Add jose
as a dependency.

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-02 13:43:16 +01:00
parent ca3ef847f5
commit 44a42bf8e7
4 changed files with 30 additions and 11 deletions
@@ -39,6 +39,7 @@
"@backstage/plugin-auth-node": "workspace:^",
"@types/passport": "^1.0.3",
"express": "^4.22.0",
"jose": "^5.0.0",
"passport": "^0.7.0",
"passport-auth0": "^1.4.3",
"passport-oauth2": "^1.6.1"
@@ -63,6 +63,15 @@ describe('createAuth0Authenticator', () => {
},
});
// Create a minimal valid JWT with a sub claim for decodeJwt
function createTestIdToken(sub: string): string {
const header = Buffer.from(JSON.stringify({ alg: 'none' })).toString(
'base64url',
);
const payload = Buffer.from(JSON.stringify({ sub })).toString('base64url');
return `${header}.${payload}.`;
}
const mockProfile = {
provider: 'auth0',
id: 'user-123',
@@ -101,7 +110,7 @@ describe('createAuth0Authenticator', () => {
token_type: 'bearer',
scope: 'openid profile',
expires_in: 3600,
id_token: 'id-token-value',
id_token: createTestIdToken('auth0|user-123'),
},
});
});
@@ -134,7 +143,7 @@ describe('createAuth0Authenticator', () => {
tokenType: 'bearer',
scope: 'openid profile',
expiresInSeconds: 3600,
idToken: 'id-token-value',
idToken: createTestIdToken('auth0|user-123'),
refreshToken: 'new-refresh-token',
});
});
@@ -166,7 +175,7 @@ describe('createAuth0Authenticator', () => {
token_type: 'bearer',
scope: 'openid profile',
expires_in: 3600,
id_token: 'id-token-2',
id_token: createTestIdToken('auth0|user-123'),
},
});
@@ -181,22 +190,21 @@ describe('createAuth0Authenticator', () => {
// Token refresh still happens
expect(mockExecuteRefresh).toHaveBeenCalledTimes(1);
// But profile fetch is skipped
// But profile fetch is skipped because same sub
expect(mockFetchProfile).not.toHaveBeenCalled();
expect(result.fullProfile).toEqual(mockProfile);
// Session uses fresh token data
expect(result.session.accessToken).toBe('another-access-token');
expect(result.session.idToken).toBe('id-token-2');
});
it('should fetch profile again when refresh token changes', async () => {
it('should fetch profile again when sub claim changes', async () => {
const authenticator = createAuth0Authenticator({ cache });
const ctx = authenticator.initialize({
callbackUrl: 'http://localhost/callback',
config: mockConfig,
});
// First call with token A
// First call with user-123
await authenticator.refresh(
{
refreshToken: 'token-a',
@@ -208,7 +216,17 @@ describe('createAuth0Authenticator', () => {
expect(mockFetchProfile).toHaveBeenCalledTimes(1);
// Second call with token B - different session, should miss cache
// Second call with a different sub - different user, should miss cache
mockExecuteRefresh.mockResolvedValue({
accessToken: 'new-access-token',
refreshToken: 'new-refresh-token',
params: {
token_type: 'bearer',
scope: 'openid profile',
expires_in: 3600,
id_token: createTestIdToken('auth0|user-456'),
},
});
const updatedProfile = { ...mockProfile, displayName: 'Updated User' };
mockFetchProfile.mockResolvedValue(updatedProfile);
@@ -16,6 +16,7 @@
import { CacheService } from '@backstage/backend-plugin-api';
import express from 'express';
import { decodeJwt } from 'jose';
import { Strategy } from 'passport';
import {
createOAuthAuthenticator,
@@ -138,9 +139,7 @@ export function createAuth0Authenticator(options?: { cache?: CacheService }) {
input.scope,
);
const sub = JSON.parse(
Buffer.from(result.params.id_token.split('.')[1], 'base64').toString(),
).sub;
const { sub } = decodeJwt(result.params.id_token);
const cacheKey = `auth0-profile:${sub}`;
let fullProfile = (await profileCache?.get(cacheKey)) as
| PassportProfile
+1
View File
@@ -4199,6 +4199,7 @@ __metadata:
"@types/passport-auth0": "npm:^1.0.5"
"@types/passport-oauth2": "npm:^1.4.15"
express: "npm:^4.22.0"
jose: "npm:^5.0.0"
passport: "npm:^0.7.0"
passport-auth0: "npm:^1.4.3"
passport-oauth2: "npm:^1.6.1"