From 8949803609bb1a541f7a1dfd887ab94b1b7771a3 Mon Sep 17 00:00:00 2001 From: Jack Palmer Date: Thu, 5 Feb 2026 13:23:57 +0000 Subject: [PATCH 1/5] feat: auth0 strategy - set invitation query parameter as authorizationParam when present Signed-off-by: Jack Palmer --- .changeset/calm-jars-wear.md | 5 +++++ .../src/strategy.ts | 18 ++++++++++++++---- 2 files changed, 19 insertions(+), 4 deletions(-) create mode 100644 .changeset/calm-jars-wear.md diff --git a/.changeset/calm-jars-wear.md b/.changeset/calm-jars-wear.md new file mode 100644 index 0000000000..2b71931600 --- /dev/null +++ b/.changeset/calm-jars-wear.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend-module-auth0-provider': minor +--- + +Set invitation query parameter as authorizationParam when present diff --git a/plugins/auth-backend-module-auth0-provider/src/strategy.ts b/plugins/auth-backend-module-auth0-provider/src/strategy.ts index a8699555a7..c2eb2a283c 100644 --- a/plugins/auth-backend-module-auth0-provider/src/strategy.ts +++ b/plugins/auth-backend-module-auth0-provider/src/strategy.ts @@ -16,6 +16,7 @@ import Auth0InternalStrategy from 'passport-auth0'; import type { StateStore } from 'passport-oauth2'; +import type express from 'express'; /** @public */ export interface Auth0StrategyOptionsWithRequest { @@ -47,15 +48,24 @@ export class Auth0Strategy extends Auth0InternalStrategy { this.organization = options.organization; } + authenticate(req: express.Request, options: Record): void { + super.authenticate(req, { + ...options, + invitation: req.query.invitation?.toString(), + }); + } + authorizationParams(options: Record): Record { const params = super.authorizationParams(options); if (this.organization) { - return { - ...params, - organization: this.organization, - }; + params.organization = this.organization; } + + if (options.invitation) { + params.invitation = options.invitation; + } + return params; } } From 867c905da5de062fb2944e1b5ffa1c072d9c4f86 Mon Sep 17 00:00:00 2001 From: Jack Palmer Date: Thu, 5 Feb 2026 13:32:49 +0000 Subject: [PATCH 2/5] feat: auth0 strategy - add additional organization query params to authorizationParams Signed-off-by: Jack Palmer --- .changeset/{calm-jars-wear.md => brown-grapes-fold.md} | 2 +- plugins/auth-backend-module-auth0-provider/src/strategy.ts | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) rename .changeset/{calm-jars-wear.md => brown-grapes-fold.md} (51%) diff --git a/.changeset/calm-jars-wear.md b/.changeset/brown-grapes-fold.md similarity index 51% rename from .changeset/calm-jars-wear.md rename to .changeset/brown-grapes-fold.md index 2b71931600..6f9c7bcab5 100644 --- a/.changeset/calm-jars-wear.md +++ b/.changeset/brown-grapes-fold.md @@ -2,4 +2,4 @@ '@backstage/plugin-auth-backend-module-auth0-provider': minor --- -Set invitation query parameter as authorizationParam when present +feat: Add support for organizational invites in auth0 strategy diff --git a/plugins/auth-backend-module-auth0-provider/src/strategy.ts b/plugins/auth-backend-module-auth0-provider/src/strategy.ts index c2eb2a283c..f45e672556 100644 --- a/plugins/auth-backend-module-auth0-provider/src/strategy.ts +++ b/plugins/auth-backend-module-auth0-provider/src/strategy.ts @@ -51,15 +51,16 @@ export class Auth0Strategy extends Auth0InternalStrategy { authenticate(req: express.Request, options: Record): void { super.authenticate(req, { ...options, - invitation: req.query.invitation?.toString(), + organization: req.query.organization, + invitation: req.query.invitation, }); } authorizationParams(options: Record): Record { const params = super.authorizationParams(options); - if (this.organization) { - params.organization = this.organization; + if (options.organization || this.organization) { + params.organization = options.organization || this.organization; } if (options.invitation) { From 3458f3a73139128b403922974ec7ad19e4259907 Mon Sep 17 00:00:00 2001 From: Jack Palmer Date: Thu, 5 Feb 2026 14:01:04 +0000 Subject: [PATCH 3/5] tests: organization and invitation params Signed-off-by: Jack Palmer --- .../auth-backend-module-auth0-provider/src/module.test.ts | 6 +++++- plugins/auth-backend-module-auth0-provider/src/strategy.ts | 6 ++++-- 2 files changed, 9 insertions(+), 3 deletions(-) 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 7e7f649831..394eb75693 100644 --- a/plugins/auth-backend-module-auth0-provider/src/module.test.ts +++ b/plugins/auth-backend-module-auth0-provider/src/module.test.ts @@ -54,7 +54,9 @@ describe('authModuleAuth0Provider', () => { const agent = request.agent(server); - const res = await agent.get('/api/auth/auth0/start?env=development'); + const res = await agent.get( + '/api/auth/auth0/start?env=development&organization=foo-organization&invitation=foo-invitation', + ); expect(res.status).toEqual(302); @@ -78,6 +80,8 @@ describe('authModuleAuth0Provider', () => { accessType: 'offline', connection: 'connection', connection_scope: 'connectionScope', + organization: 'foo-organization', + invitation: 'foo-invitation', nonce: expect.any(String), state: expect.any(String), }); diff --git a/plugins/auth-backend-module-auth0-provider/src/strategy.ts b/plugins/auth-backend-module-auth0-provider/src/strategy.ts index f45e672556..d644787bf5 100644 --- a/plugins/auth-backend-module-auth0-provider/src/strategy.ts +++ b/plugins/auth-backend-module-auth0-provider/src/strategy.ts @@ -49,10 +49,12 @@ export class Auth0Strategy extends Auth0InternalStrategy { } authenticate(req: express.Request, options: Record): void { + const { organization, invitation } = req.query; + super.authenticate(req, { ...options, - organization: req.query.organization, - invitation: req.query.invitation, + ...(organization ? { organization } : {}), + ...(invitation ? { invitation } : {}), }); } From bf3861fdb60f6a1db46902fa31268ceec2792387 Mon Sep 17 00:00:00 2001 From: Jack Palmer Date: Thu, 5 Feb 2026 15:20:32 +0000 Subject: [PATCH 4/5] 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:^" From 035a808d5ed2e8bcf4cbccfd51b159797f607a6a Mon Sep 17 00:00:00 2001 From: Jack Palmer Date: Fri, 6 Feb 2026 09:02:24 +0000 Subject: [PATCH 5/5] chore: address pr comments Signed-off-by: Jack Palmer --- .changeset/brown-grapes-fold.md | 4 ++-- plugins/auth-backend-module-auth0-provider/src/module.test.ts | 2 +- plugins/auth-backend-module-auth0-provider/src/strategy.ts | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.changeset/brown-grapes-fold.md b/.changeset/brown-grapes-fold.md index 6f9c7bcab5..d8ab0a23f3 100644 --- a/.changeset/brown-grapes-fold.md +++ b/.changeset/brown-grapes-fold.md @@ -1,5 +1,5 @@ --- -'@backstage/plugin-auth-backend-module-auth0-provider': minor +'@backstage/plugin-auth-backend-module-auth0-provider': patch --- -feat: Add support for organizational invites in auth0 strategy +Add support for organizational invites in auth0 strategy 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 773cb70a85..28ccf90fa4 100644 --- a/plugins/auth-backend-module-auth0-provider/src/module.test.ts +++ b/plugins/auth-backend-module-auth0-provider/src/module.test.ts @@ -175,7 +175,7 @@ describe('authModuleAuth0Provider', () => { '/api/auth/auth0/start?env=development&organization=foo-organization&invitation=foo-invitation', ); - expect(res.status).toEqual(409); + expect(res.status).toEqual(400); 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 a89c9199cf..b6252aa7bd 100644 --- a/plugins/auth-backend-module-auth0-provider/src/strategy.ts +++ b/plugins/auth-backend-module-auth0-provider/src/strategy.ts @@ -17,7 +17,7 @@ import Auth0InternalStrategy from 'passport-auth0'; import type { StateStore } from 'passport-oauth2'; import type express from 'express'; -import { ConflictError } from '@backstage/errors'; +import { InputError } from '@backstage/errors'; /** @public */ export interface Auth0StrategyOptionsWithRequest { @@ -58,7 +58,7 @@ export class Auth0Strategy extends Auth0InternalStrategy { this.organization && organization !== this.organization ) { - throw new ConflictError( + throw new InputError( 'Organization mismatch. The organization provided in the request does not match the organization configured in the strategy.', ); }