diff --git a/packages/auth0-nuxt/src/runtime/server/api/auth/login.get.spec.ts b/packages/auth0-nuxt/src/runtime/server/api/auth/login.get.spec.ts index e22d4ba..4d307e7 100644 --- a/packages/auth0-nuxt/src/runtime/server/api/auth/login.get.spec.ts +++ b/packages/auth0-nuxt/src/runtime/server/api/auth/login.get.spec.ts @@ -50,6 +50,7 @@ describe('login.get handler', () => { expect(mockAuth0Client.startInteractiveLogin).toHaveBeenCalledWith({ appState: { returnTo: 'http://localhost:3000/foo' }, + authorizationParams: undefined, }); }); @@ -60,6 +61,66 @@ describe('login.get handler', () => { expect(mockAuth0Client.startInteractiveLogin).toHaveBeenCalledWith({ appState: { returnTo: undefined }, + authorizationParams: undefined, + }); + }); + + it('should forward additional query params as authorizationParams', async () => { + getQueryMock.mockReturnValue({ + returnTo: 'http://localhost:3000/foo', + organization: 'org_123', + login_hint: 'user@example.com', + }); + + await loginHandler(mockEvent); + + expect(mockAuth0Client.startInteractiveLogin).toHaveBeenCalledWith({ + appState: { returnTo: 'http://localhost:3000/foo' }, + authorizationParams: { + organization: 'org_123', + login_hint: 'user@example.com', + }, + }); + }); + + it('should not forward returnTo or reserved OAuth params as authorizationParams', async () => { + getQueryMock.mockReturnValue({ + returnTo: 'http://localhost:3000/foo', + scope: 'openid evil', + audience: 'https://evil', + redirect_uri: 'https://evil', + client_id: 'evil', + state: 'evil', + nonce: 'evil', + response_type: 'evil', + code_challenge: 'evil', + code_challenge_method: 'evil', + organization: 'org_123', + }); + + await loginHandler(mockEvent); + + expect(mockAuth0Client.startInteractiveLogin).toHaveBeenCalledWith({ + appState: { returnTo: 'http://localhost:3000/foo' }, + authorizationParams: { + organization: 'org_123', + }, + }); + }); + + it('should not forward prototype-polluting keys as authorizationParams', async () => { + getQueryMock.mockReturnValue({ + returnTo: 'http://localhost:3000/foo', + __proto__: 'evil', + constructor: 'evil', + prototype: 'evil', + }); + + await loginHandler(mockEvent); + + expect(mockAuth0Client.startInteractiveLogin).toHaveBeenCalledWith({ + appState: { returnTo: 'http://localhost:3000/foo' }, + authorizationParams: undefined, }); }); diff --git a/packages/auth0-nuxt/src/runtime/server/api/auth/login.get.ts b/packages/auth0-nuxt/src/runtime/server/api/auth/login.get.ts index 7bfbb53..84e124e 100644 --- a/packages/auth0-nuxt/src/runtime/server/api/auth/login.get.ts +++ b/packages/auth0-nuxt/src/runtime/server/api/auth/login.get.ts @@ -6,6 +6,40 @@ interface LoginParams { returnTo?: string; } +const DENIED_KEYS = new Set([ + ...Object.getOwnPropertyNames(Object.prototype), + '__proto__', + 'constructor', + 'prototype', +]); + +const RESERVED_OAUTH_PARAMS = new Set([ + 'response_type', + 'state', + 'code_challenge', + 'code_challenge_method', + 'client_id', + 'redirect_uri', + 'nonce', + 'scope', + 'audience', +]); + +function filterAuthorizationParams( + params: Record, + disallowed: string[] +): Record | undefined { + const filtered: Record = Object.create(null); + + for (const [key, value] of Object.entries(params)) { + if (!DENIED_KEYS.has(key) && !disallowed.includes(key) && !RESERVED_OAUTH_PARAMS.has(key)) { + filtered[key] = value; + } + } + + return Object.keys(filtered).length > 0 ? filtered : undefined; +} + export default defineEventHandler(async (event) => { const auth0Client = useAuth0(event); const auth0ClientOptions = event.context.auth0ClientOptions; @@ -16,6 +50,7 @@ export default defineEventHandler(async (event) => { const authorizationUrl = await auth0Client.startInteractiveLogin({ appState: { returnTo: sanitizedReturnTo }, + authorizationParams: filterAuthorizationParams(query, ['returnTo']), }); sendRedirect(event, authorizationUrl.href);