From de777103282e4e4ec20346de54fa401312140972 Mon Sep 17 00:00:00 2001 From: flow-gijs Date: Tue, 14 Jul 2026 15:07:19 +0200 Subject: [PATCH] fix: forward extra parameters in refreshToken to the token endpoint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RefreshTokenParameters extends RequestOptions, whose index signature accepts arbitrary extra keys — but refreshToken() built the request body from a fixed set of fields, so any extra parameter was silently dropped. Custom token-endpoint parameters are commonly consumed by Auth0 Actions to customize issued tokens, and expo-auth-session (a frequent migration source) forwards them via extraParams. Spread the extra parameters into the body ahead of the fixed fields so they are sent to /oauth/token but can never override the OAuth-mandated grant_type, client_id, refresh_token, scope, or audience. Co-Authored-By: Claude Fable 5 --- .../services/AuthenticationOrchestrator.ts | 14 +++++-- .../AuthenticationOrchestrator.spec.ts | 42 +++++++++++++++++++ 2 files changed, 52 insertions(+), 4 deletions(-) diff --git a/src/core/services/AuthenticationOrchestrator.ts b/src/core/services/AuthenticationOrchestrator.ts index 2376142f4..c3e240fe6 100644 --- a/src/core/services/AuthenticationOrchestrator.ts +++ b/src/core/services/AuthenticationOrchestrator.ts @@ -190,13 +190,19 @@ export class AuthenticationOrchestrator implements IAuthenticationProvider { async refreshToken(parameters: RefreshTokenParameters): Promise { validateParameters(parameters, ['refreshToken']); - const { headers, ...payload } = parameters; + // Forward any extra parameters to the token endpoint. `RefreshTokenParameters` + // accepts arbitrary keys (via RequestOptions), and custom parameters are commonly + // consumed by Auth0 Actions to customize the issued tokens. Extras are spread + // first so they can never override the OAuth-mandated fields. + const { headers, refreshToken, scope, audience, ...extraParameters } = + parameters; const body = { + ...extraParameters, grant_type: 'refresh_token', client_id: this.clientId, - refresh_token: payload.refreshToken, - scope: includeRequiredScope(payload.scope), - audience: payload.audience, + refresh_token: refreshToken, + scope: includeRequiredScope(scope), + audience: audience, }; const { json, response } = await this.client.post( diff --git a/src/core/services/__tests__/AuthenticationOrchestrator.spec.ts b/src/core/services/__tests__/AuthenticationOrchestrator.spec.ts index 12be2f237..f9bf68a0a 100644 --- a/src/core/services/__tests__/AuthenticationOrchestrator.spec.ts +++ b/src/core/services/__tests__/AuthenticationOrchestrator.spec.ts @@ -299,6 +299,48 @@ describe('AuthenticationOrchestrator', () => { undefined ); }); + + it('should forward extra parameters to the token endpoint', async () => { + mockHttpClientInstance.post.mockResolvedValueOnce({ + json: tokensResponse, + response: new Response(null, { status: 200 }), + }); + await orchestrator.refreshToken({ + ...parameters, + organizationId: 'org_123', + }); + + expect(mockHttpClientInstance.post).toHaveBeenCalledWith( + '/oauth/token', + expect.objectContaining({ + grant_type: 'refresh_token', + refresh_token: parameters.refreshToken, + organizationId: 'org_123', + }), + undefined + ); + }); + + it('should not let extra parameters override the OAuth fields', async () => { + mockHttpClientInstance.post.mockResolvedValueOnce({ + json: tokensResponse, + response: new Response(null, { status: 200 }), + }); + await orchestrator.refreshToken({ + ...parameters, + grant_type: 'password', + client_id: 'spoofed-client', + }); + + expect(mockHttpClientInstance.post).toHaveBeenCalledWith( + '/oauth/token', + expect.objectContaining({ + grant_type: 'refresh_token', + client_id: clientId, + }), + undefined + ); + }); }); describe('revoke token', () => {