Support custom start URL search parameters in OIDC provider

Signed-off-by: Chris Kilding <56678532+chriskilding-relx@users.noreply.github.com>
This commit is contained in:
Chris Kilding
2025-09-30 15:46:57 +01:00
parent 625148b35f
commit e54fcb2ed5
6 changed files with 78 additions and 2 deletions
+5
View File
@@ -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)
+6
View File
@@ -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.
+1
View File
@@ -33,6 +33,7 @@ export interface Config {
additionalScopes?: string | string[];
prompt?: string;
timeout?: HumanDuration | string;
startUrlSearchParams?: [string: string];
signIn?: {
resolvers: Array<
| {
@@ -25,6 +25,7 @@ export const oidcAuthenticator: OAuthAuthenticator<
client: BaseClient;
strategy: Strategy<OidcAuthResult, BaseClient>;
}>;
searchParams: Record<string, string>;
},
OidcAuthResult
>;
@@ -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,
@@ -83,6 +83,9 @@ export const oidcAuthenticator = createOAuthAuthenticator({
);
const initializedPrompt = config.getOptionalString('prompt');
const startUrlSearchParams: Record<string, string> =
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<string, string> = {
...searchParams,
scope: input.scope,
state: input.state,
nonce: crypto.randomBytes(16).toString('base64'),
};
const prompt = initializedPrompt || 'none';
if (prompt !== 'auto') {
options.prompt = prompt;