chore(core): Send custom n8n scope in OIDC for provisioning if configured (#20757)

This commit is contained in:
Andreas Fitzek 2025-10-14 13:31:49 +02:00 committed by GitHub
parent f0a3978b62
commit db00101980
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 107 additions and 1 deletions

View File

@ -176,10 +176,19 @@ export class OidcService {
const prompt = this.oidcConfig.prompt;
const provisioning = this.globalConfig.sso.provisioning;
const provisioningEnabled =
provisioning.scopesProvisionInstanceRole || provisioning.scopesProvisionProjectRoles;
// Include the custom n8n scope if provisioning is enabled
const scope = provisioningEnabled
? `openid email profile ${provisioning.scopesName}`
: 'openid email profile';
const authorizationURL = client.buildAuthorizationUrl(configuration, {
redirect_uri: this.getCallbackUrl(),
response_type: 'code',
scope: 'openid email profile',
scope,
prompt,
state: state.plaintext,
nonce: nonce.plaintext,

View File

@ -23,6 +23,7 @@ import { OidcService } from '@/sso.ee/oidc/oidc.service.ee';
import { createUser } from '@test-integration/db/users';
import { UserError } from 'n8n-workflow';
import { JwtService } from '@/services/jwt.service';
import { GlobalConfig } from '@n8n/config';
beforeAll(async () => {
await testDb.init();
@ -276,6 +277,102 @@ describe('OIDC service', () => {
expect(authUrl.nonce).toBeDefined();
});
describe('SSO provisioning', () => {
beforeAll(async () => {
const mockConfiguration = new real_odic_client.Configuration(
{
issuer: 'https://example.com/auth/realms/n8n',
client_id: 'test-client-id',
redirect_uris: ['http://n8n.io/sso/oidc/callback'],
response_types: ['code'],
scopes: ['openid', 'profile', 'email'],
authorization_endpoint: 'https://example.com/auth',
},
'test-client-id',
);
discoveryMock.mockResolvedValue(mockConfiguration);
const initialConfig: OidcConfigDto = {
clientId: 'test-client-id',
clientSecret: 'test-client-secret',
discoveryEndpoint: 'https://example.com/.well-known/openid-configuration',
loginEnabled: true,
prompt: 'consent',
};
await oidcService.updateConfig(initialConfig);
});
let provisioningConfig: GlobalConfig['sso']['provisioning'];
beforeEach(() => {
// safe original provisioning config, by making a copy
provisioningConfig = {
...Container.get(GlobalConfig).sso.provisioning,
};
});
afterEach(() => {
// restore original provisioning config
Container.get(GlobalConfig).sso.provisioning = provisioningConfig;
});
const validateUrl = (authUrl: Awaited<ReturnType<OidcService['generateLoginUrl']>>) => {
expect(authUrl.url.pathname).toEqual('/auth');
expect(authUrl.url.searchParams.get('client_id')).toEqual('test-client-id');
expect(authUrl.url.searchParams.get('redirect_uri')).toEqual(
'http://localhost:5678/rest/sso/oidc/callback',
);
expect(authUrl.url.searchParams.get('response_type')).toEqual('code');
expect(authUrl.url.searchParams.get('prompt')).toBeDefined();
expect(authUrl.url.searchParams.get('prompt')).toEqual('consent');
expect(authUrl.url.searchParams.get('state')).toBeDefined();
expect(authUrl.url.searchParams.get('state')?.startsWith('n8n_state:')).toBe(true);
expect(authUrl.state).toBeDefined();
expect(authUrl.nonce).toBeDefined();
};
it('should not include the provisioning scope if no provisioning is enabled', async () => {
Container.get(GlobalConfig).sso.provisioning.scopesProvisionProjectRoles = false;
Container.get(GlobalConfig).sso.provisioning.scopesProvisionInstanceRole = false;
const authUrl = await oidcService.generateLoginUrl();
validateUrl(authUrl);
expect(authUrl.url.searchParams.get('scope')).toEqual('openid email profile');
});
it('should include the provisioning scope if project provisioning is enabled', async () => {
Container.get(GlobalConfig).sso.provisioning.scopesProvisionProjectRoles = true;
Container.get(GlobalConfig).sso.provisioning.scopesProvisionInstanceRole = false;
Container.get(GlobalConfig).sso.provisioning.scopesName = 'n8n_test_scope';
const authUrl = await oidcService.generateLoginUrl();
validateUrl(authUrl);
expect(authUrl.url.searchParams.get('scope')).toEqual('openid email profile n8n_test_scope');
});
it('should include the provisioning scope if instance provisioning is enabled', async () => {
Container.get(GlobalConfig).sso.provisioning.scopesProvisionProjectRoles = false;
Container.get(GlobalConfig).sso.provisioning.scopesProvisionInstanceRole = true;
Container.get(GlobalConfig).sso.provisioning.scopesName = 'n8n_test_scope';
const authUrl = await oidcService.generateLoginUrl();
validateUrl(authUrl);
expect(authUrl.url.searchParams.get('scope')).toEqual('openid email profile n8n_test_scope');
});
it('should include the provisioning scope if project and instance provisioning is enabled', async () => {
Container.get(GlobalConfig).sso.provisioning.scopesProvisionProjectRoles = true;
Container.get(GlobalConfig).sso.provisioning.scopesProvisionInstanceRole = true;
Container.get(GlobalConfig).sso.provisioning.scopesName = 'n8n_test_scope';
const authUrl = await oidcService.generateLoginUrl();
validateUrl(authUrl);
expect(authUrl.url.searchParams.get('scope')).toEqual('openid email profile n8n_test_scope');
});
});
describe('loginUser', () => {
it('should handle new user login with valid callback URL', async () => {
const state = oidcService.generateState();