diff --git a/.changeset/proud-streets-switch.md b/.changeset/proud-streets-switch.md new file mode 100644 index 0000000000..081a1e7b3a --- /dev/null +++ b/.changeset/proud-streets-switch.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend-module-oidc-provider': minor +--- + +Added support for custom start URL search parameters (with the new `startUrlSearchParams` config property) diff --git a/docs/auth/oidc.md b/docs/auth/oidc.md index 7c4b09d9bf..55827ab1dc 100644 --- a/docs/auth/oidc.md +++ b/docs/auth/oidc.md @@ -241,6 +241,12 @@ These parameters have implicit default values. Don't override them unless you kn - `prompt`: Recommended to use `auto` so the browser will request sign-in to the IDP if the user has no active session. - `sessionDuration`: Lifespan of the user session. +- `startUrlSearchParams`: This is a dictionary of search (query) parameters for the OIDC + authorization start URL. Don't define it unless you want to change the identity + provider's behavior. (For example, you could set the `organization` parameter to guide + users towards a particular sign-in option that your organization prefers.) **Note:** the + start URL is controlled by the browser, so this feature is only for improving the + Backstage user experience. :::note Config Reloading Backstage does not yet support hot reloading of auth provider configuration. Any changes to this YAML file require a restart of Backstage. diff --git a/plugins/auth-backend-module-oidc-provider/config.d.ts b/plugins/auth-backend-module-oidc-provider/config.d.ts index c11b9319ec..f25d13ae2e 100644 --- a/plugins/auth-backend-module-oidc-provider/config.d.ts +++ b/plugins/auth-backend-module-oidc-provider/config.d.ts @@ -33,6 +33,7 @@ export interface Config { additionalScopes?: string | string[]; prompt?: string; timeout?: HumanDuration | string; + startUrlSearchParams?: [string: string]; signIn?: { resolvers: Array< | { diff --git a/plugins/auth-backend-module-oidc-provider/report.api.md b/plugins/auth-backend-module-oidc-provider/report.api.md index 11cde22db1..ab7d2bda49 100644 --- a/plugins/auth-backend-module-oidc-provider/report.api.md +++ b/plugins/auth-backend-module-oidc-provider/report.api.md @@ -25,6 +25,7 @@ export const oidcAuthenticator: OAuthAuthenticator< client: BaseClient; strategy: Strategy; }>; + searchParams: Record; }, OidcAuthResult >; diff --git a/plugins/auth-backend-module-oidc-provider/src/authenticator.test.ts b/plugins/auth-backend-module-oidc-provider/src/authenticator.test.ts index c327d4ecc5..1940a71082 100644 --- a/plugins/auth-backend-module-oidc-provider/src/authenticator.test.ts +++ b/plugins/auth-backend-module-oidc-provider/src/authenticator.test.ts @@ -279,6 +279,62 @@ describe('oidcAuthenticator', () => { expect(searchParams.get('response_type')).toBe('code'); }); + it('passes custom start URL search parameters', async () => { + const customImplementation = oidcAuthenticator.initialize({ + callbackUrl: 'https://backstage.test/callback', + config: new ConfigReader({ + metadataUrl: 'https://oidc.test/.well-known/openid-configuration', + clientId: 'clientId123', + clientSecret: 'clientSecret', + startUrlSearchParams: { + foo: '1', + bar: '2', + }, + }), + }); + + const startResponse = await oidcAuthenticator.start( + startRequest, + customImplementation, + ); + + const { searchParams } = new URL(startResponse.url); + + expect(searchParams.get('foo')).toBe('1'); + expect(searchParams.get('bar')).toBe('2'); + }); + + it('does not override the core start URL search parameters with custom ones', async () => { + const customImplementation = oidcAuthenticator.initialize({ + callbackUrl: 'https://backstage.test/callback', + config: new ConfigReader({ + metadataUrl: 'https://oidc.test/.well-known/openid-configuration', + clientId: 'clientId123', + clientSecret: 'clientSecret', + startUrlSearchParams: { + foo: '1', + prompt: 'customPrompt', + scope: 'customScope', + state: 'customState', + nonce: 'customNonce', + }, + }), + }); + + const startResponse = await oidcAuthenticator.start( + startRequest, + customImplementation, + ); + + const { searchParams } = new URL(startResponse.url); + + expect(searchParams.get('foo')).toBe('1'); + expect(searchParams.get('scope')).not.toBe('customScope'); + expect(searchParams.get('state')).not.toBe('customState'); + expect(searchParams.get('nonce')).not.toBe('customNonce'); + expect(searchParams.get('prompt')).not.toBe('customPrompt'); + }); + it('passes a nonce', async () => { const startResponse = await oidcAuthenticator.start( startRequest, diff --git a/plugins/auth-backend-module-oidc-provider/src/authenticator.ts b/plugins/auth-backend-module-oidc-provider/src/authenticator.ts index 6e7022f7cf..316ead3954 100644 --- a/plugins/auth-backend-module-oidc-provider/src/authenticator.ts +++ b/plugins/auth-backend-module-oidc-provider/src/authenticator.ts @@ -83,6 +83,9 @@ export const oidcAuthenticator = createOAuthAuthenticator({ ); const initializedPrompt = config.getOptionalString('prompt'); + const startUrlSearchParams: Record = + config.getOptional('startUrlSearchParams') || {}; + if (config.has('scope')) { throw new Error( 'The oidc provider no longer supports the "scope" configuration option. Please use the "additionalScopes" option instead.', @@ -143,17 +146,21 @@ export const oidcAuthenticator = createOAuthAuthenticator({ return { helper, client, strategy }; }); - return { initializedPrompt, promise }; + return { initializedPrompt, promise, searchParams: startUrlSearchParams }; }, async start(input, ctx) { - const { initializedPrompt, promise } = ctx; + const { initializedPrompt, promise, searchParams } = ctx; const { helper } = await promise; + + // Merge the custom start URL params, but do not override the standard params (scope, state etc) const options: Record = { + ...searchParams, scope: input.scope, state: input.state, nonce: crypto.randomBytes(16).toString('base64'), }; + const prompt = initializedPrompt || 'none'; if (prompt !== 'auto') { options.prompt = prompt;