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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user