From bf3861fdb60f6a1db46902fa31268ceec2792387 Mon Sep 17 00:00:00 2001 From: Jack Palmer Date: Thu, 5 Feb 2026 15:20:32 +0000 Subject: [PATCH] chore: Throw error when organization declared in strategy does not match organization in request Signed-off-by: Jack Palmer --- .../package.json | 1 + .../src/module.test.ts | 99 ++++++++++++++++++- .../src/strategy.ts | 12 +++ yarn.lock | 1 + 4 files changed, 108 insertions(+), 5 deletions(-) diff --git a/plugins/auth-backend-module-auth0-provider/package.json b/plugins/auth-backend-module-auth0-provider/package.json index 5bd7e342ee..edb5b457b4 100644 --- a/plugins/auth-backend-module-auth0-provider/package.json +++ b/plugins/auth-backend-module-auth0-provider/package.json @@ -35,6 +35,7 @@ }, "dependencies": { "@backstage/backend-plugin-api": "workspace:^", + "@backstage/errors": "workspace:^", "@backstage/plugin-auth-node": "workspace:^", "express": "^4.22.0", "passport": "^0.7.0", diff --git a/plugins/auth-backend-module-auth0-provider/src/module.test.ts b/plugins/auth-backend-module-auth0-provider/src/module.test.ts index 394eb75693..773cb70a85 100644 --- a/plugins/auth-backend-module-auth0-provider/src/module.test.ts +++ b/plugins/auth-backend-module-auth0-provider/src/module.test.ts @@ -54,9 +54,7 @@ describe('authModuleAuth0Provider', () => { const agent = request.agent(server); - const res = await agent.get( - '/api/auth/auth0/start?env=development&organization=foo-organization&invitation=foo-invitation', - ); + const res = await agent.get('/api/auth/auth0/start?env=development'); expect(res.status).toEqual(302); @@ -80,8 +78,6 @@ describe('authModuleAuth0Provider', () => { accessType: 'offline', connection: 'connection', connection_scope: 'connectionScope', - organization: 'foo-organization', - invitation: 'foo-invitation', nonce: expect.any(String), state: expect.any(String), }); @@ -91,4 +87,97 @@ describe('authModuleAuth0Provider', () => { nonce: decodeURIComponent(nonceCookie.value), }); }); + + it('should pass through organization and invitation parameters to the authorization URL', async () => { + const { server } = await startTestBackend({ + features: [ + authPlugin, + authModuleAuth0Provider, + mockServices.rootConfig.factory({ + data: { + app: { + baseUrl: 'http://localhost:3000', + }, + auth: { + providers: { + auth0: { + development: { + clientId: 'clientId', + clientSecret: 'clientSecret', + domain: 'domain', + connection: 'connection', + connectionScope: 'connectionScope', + organization: 'foo-organization', + }, + }, + }, + session: { + secret: 'secret', + }, + }, + }, + }), + ], + }); + + const agent = request.agent(server); + + const res = await agent.get( + '/api/auth/auth0/start?env=development&organization=foo-organization&invitation=foo-invitation', + ); + + const startUrl = new URL(res.get('location')); + expect(startUrl.origin).toBe('https://domain'); + expect(startUrl.pathname).toBe('/authorize'); + expect(Object.fromEntries(startUrl.searchParams)).toEqual( + expect.objectContaining({ + organization: 'foo-organization', + invitation: 'foo-invitation', + }), + ); + }); + + it('should throw an error if the organization in the request does not match the organization configured in the strategy', async () => { + const { server } = await startTestBackend({ + features: [ + authPlugin, + authModuleAuth0Provider, + mockServices.rootConfig.factory({ + data: { + app: { + baseUrl: 'http://localhost:3000', + }, + auth: { + providers: { + auth0: { + development: { + clientId: 'clientId', + clientSecret: 'clientSecret', + domain: 'domain', + connection: 'connection', + connectionScope: 'connectionScope', + organization: 'bar-organization', + }, + }, + }, + session: { + secret: 'secret', + }, + }, + }, + }), + ], + }); + + const agent = request.agent(server); + + const res = await agent.get( + '/api/auth/auth0/start?env=development&organization=foo-organization&invitation=foo-invitation', + ); + + expect(res.status).toEqual(409); + expect(res.text).toContain( + 'Organization mismatch. The organization provided in the request does not match the organization configured in the strategy.', + ); + }); }); diff --git a/plugins/auth-backend-module-auth0-provider/src/strategy.ts b/plugins/auth-backend-module-auth0-provider/src/strategy.ts index d644787bf5..a89c9199cf 100644 --- a/plugins/auth-backend-module-auth0-provider/src/strategy.ts +++ b/plugins/auth-backend-module-auth0-provider/src/strategy.ts @@ -17,6 +17,7 @@ import Auth0InternalStrategy from 'passport-auth0'; import type { StateStore } from 'passport-oauth2'; import type express from 'express'; +import { ConflictError } from '@backstage/errors'; /** @public */ export interface Auth0StrategyOptionsWithRequest { @@ -51,6 +52,17 @@ export class Auth0Strategy extends Auth0InternalStrategy { authenticate(req: express.Request, options: Record): void { const { organization, invitation } = req.query; + // Throw an error if the organization in the request does not match the organization configured in the strategy + if ( + organization && + this.organization && + organization !== this.organization + ) { + throw new ConflictError( + 'Organization mismatch. The organization provided in the request does not match the organization configured in the strategy.', + ); + } + super.authenticate(req, { ...options, ...(organization ? { organization } : {}), diff --git a/yarn.lock b/yarn.lock index 7d27834ec3..d1e744a8b2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4284,6 +4284,7 @@ __metadata: "@backstage/backend-plugin-api": "workspace:^" "@backstage/backend-test-utils": "workspace:^" "@backstage/cli": "workspace:^" + "@backstage/errors": "workspace:^" "@backstage/plugin-auth-backend": "workspace:^" "@backstage/plugin-auth-node": "workspace:^" "@backstage/types": "workspace:^"