UNPKG

n8n

Version:

n8n Workflow Automation Tool

561 lines 26.3 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.OidcService = void 0; const api_types_1 = require("@n8n/api-types"); const backend_common_1 = require("@n8n/backend-common"); const config_1 = require("@n8n/config"); const db_1 = require("@n8n/db"); const decorators_1 = require("@n8n/decorators"); const di_1 = require("@n8n/di"); const crypto_1 = require("crypto"); const n8n_core_1 = require("n8n-core"); const n8n_workflow_1 = require("n8n-workflow"); const undici_1 = require("undici"); const bad_request_error_1 = require("../../errors/response-errors/bad-request.error"); const forbidden_error_1 = require("../../errors/response-errors/forbidden.error"); const internal_server_error_1 = require("../../errors/response-errors/internal-server.error"); const claims_context_builder_1 = require("../../modules/provisioning.ee/claims-context.builder"); const provisioning_service_ee_1 = require("../../modules/provisioning.ee/provisioning.service.ee"); const jwt_service_1 = require("../../services/jwt.service"); const url_service_1 = require("../../services/url.service"); const sso_helpers_1 = require("../../sso.ee/sso-helpers"); const constants_1 = require("./constants"); const DEFAULT_OIDC_CONFIG = { clientId: '', clientSecret: '', discoveryEndpoint: '', loginEnabled: false, prompt: 'select_account', authenticationContextClassReference: [], }; const DEFAULT_OIDC_RUNTIME_CONFIG = { ...DEFAULT_OIDC_CONFIG, discoveryEndpoint: new URL('http://n8n.io/not-set'), }; let OidcService = class OidcService { constructor(settingsRepository, authIdentityRepository, urlService, globalConfig, userRepository, cipher, logger, jwtService, instanceSettings, provisioningService) { this.settingsRepository = settingsRepository; this.authIdentityRepository = authIdentityRepository; this.urlService = urlService; this.globalConfig = globalConfig; this.userRepository = userRepository; this.cipher = cipher; this.logger = logger; this.jwtService = jwtService; this.instanceSettings = instanceSettings; this.provisioningService = provisioningService; this.oidcConfig = DEFAULT_OIDC_RUNTIME_CONFIG; this.isReloading = false; } async init() { this.oidcConfig = await this.loadConfig(true); this.logger.debug(`OIDC login is ${this.oidcConfig.loginEnabled ? 'enabled' : 'disabled'}.`); await this.setOidcLoginEnabled(this.oidcConfig.loginEnabled); if (this.oidcConfig.loginEnabled) { await this.loadOpenIdClient(); } } async loadOpenIdClient() { if (!this.openidClient) { this.openidClient = await Promise.resolve().then(() => __importStar(require('openid-client'))); } } getCallbackUrl() { return `${this.urlService.getInstanceBaseUrl()}/${this.globalConfig.endpoints.rest}/sso/oidc/callback`; } getRedactedConfig() { return { ...this.oidcConfig, discoveryEndpoint: this.oidcConfig.discoveryEndpoint.toString(), clientSecret: constants_1.OIDC_CLIENT_SECRET_REDACTED_VALUE, }; } generateState(testMode = false) { const state = `n8n_state:${(0, crypto_1.randomUUID)()}`; const payload = { state }; if (testMode) { payload.testMode = true; } return { signed: this.jwtService.sign(payload, { expiresIn: '15m' }), plaintext: state, }; } verifyState(signedState) { let state; let testMode; try { const decodedState = this.jwtService.verify(signedState); state = decodedState?.state; testMode = decodedState?.testMode; } catch (error) { this.logger.error('Failed to verify state', { error }); throw new bad_request_error_1.BadRequestError('Invalid state'); } if (typeof state !== 'string') { this.logger.error('Provided state has an invalid format'); throw new bad_request_error_1.BadRequestError('Invalid state'); } const splitState = state.split(':'); if (splitState.length !== 2 || splitState[0] !== 'n8n_state') { this.logger.error('Provided state is missing the well-known prefix'); throw new bad_request_error_1.BadRequestError('Invalid state'); } if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(splitState[1])) { this.logger.error('Provided state is not formatted correctly'); throw new bad_request_error_1.BadRequestError('Invalid state'); } return { state, testMode }; } generateNonce() { const nonce = `n8n_nonce:${(0, crypto_1.randomUUID)()}`; return { signed: this.jwtService.sign({ nonce }, { expiresIn: '15m' }), plaintext: nonce, }; } verifyNonce(signedNonce) { let nonce; try { const decodedNonce = this.jwtService.verify(signedNonce); nonce = decodedNonce?.nonce; } catch (error) { this.logger.error('Failed to verify nonce', { error }); throw new bad_request_error_1.BadRequestError('Invalid nonce'); } if (typeof nonce !== 'string') { this.logger.error('Provided nonce has an invalid format'); throw new bad_request_error_1.BadRequestError('Invalid nonce'); } const splitNonce = nonce.split(':'); if (splitNonce.length !== 2 || splitNonce[0] !== 'n8n_nonce') { this.logger.error('Provided nonce is missing the well-known prefix'); throw new bad_request_error_1.BadRequestError('Invalid nonce'); } if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(splitNonce[1])) { this.logger.error('Provided nonce is not formatted correctly'); throw new bad_request_error_1.BadRequestError('Invalid nonce'); } return nonce; } async generateLoginUrl() { await this.loadOpenIdClient(); const configuration = await this.getOidcConfiguration(); const state = this.generateState(); const nonce = this.generateNonce(); const prompt = this.oidcConfig.prompt; const authenticationContextClassReference = this.oidcConfig.authenticationContextClassReference; const provisioningConfig = await this.provisioningService.getConfig(); const provisioningEnabled = provisioningConfig.scopesProvisionInstanceRole || provisioningConfig.scopesProvisionProjectRoles; const scope = provisioningEnabled ? `openid email profile ${provisioningConfig.scopesName}` : 'openid email profile'; const authorizationURL = this.openidClient.buildAuthorizationUrl(configuration, { redirect_uri: this.getCallbackUrl(), response_type: 'code', scope, prompt, state: state.plaintext, nonce: nonce.plaintext, ...(authenticationContextClassReference.length > 0 && { acr_values: authenticationContextClassReference.join(' '), }), }); return { url: authorizationURL, state: state.signed, nonce: nonce.signed }; } async loginUser(callbackUrl, storedState, storedNonce) { await this.loadOpenIdClient(); const configuration = await this.getOidcConfiguration(); const { state: expectedState } = this.verifyState(storedState); const expectedNonce = this.verifyNonce(storedNonce); let tokens; try { tokens = await this.openidClient.authorizationCodeGrant(configuration, callbackUrl, { expectedState, expectedNonce, }); } catch (error) { this.logger.error('Failed to exchange authorization code for tokens', { error }); throw new bad_request_error_1.BadRequestError('Invalid authorization code'); } let claims; try { claims = tokens.claims(); } catch (error) { this.logger.error('Failed to extract claims from tokens', { error }); throw new bad_request_error_1.BadRequestError('Invalid token'); } if (!claims) { throw new forbidden_error_1.ForbiddenError('No claims found in the OIDC token'); } let userInfo; try { userInfo = await this.openidClient.fetchUserInfo(configuration, tokens.access_token, claims.sub); } catch (error) { this.logger.error('Failed to fetch user info', { error }); throw new bad_request_error_1.BadRequestError('Invalid token'); } if (!userInfo.email) { throw new bad_request_error_1.BadRequestError('An email is required'); } if (!(0, db_1.isValidEmail)(userInfo.email)) { throw new bad_request_error_1.BadRequestError('Invalid email format'); } const openidUser = await this.authIdentityRepository.findOne({ where: { providerId: claims.sub, providerType: 'oidc' }, relations: { user: { role: true, }, }, }); if (openidUser) { await this.applySsoProvisioning(openidUser.user, claims, userInfo); return openidUser.user; } const foundUser = await this.userRepository.findOne({ where: { email: userInfo.email }, relations: ['authIdentities', 'role'], }); if (foundUser) { this.logger.debug(`OIDC login: User with email ${userInfo.email} already exists, linking OIDC identity.`); const id = this.authIdentityRepository.create({ providerId: claims.sub, providerType: 'oidc', userId: foundUser.id, }); await this.authIdentityRepository.save(id); await this.applySsoProvisioning(foundUser, claims, userInfo); return foundUser; } const user = await this.userRepository.manager.transaction(async (trx) => { const { user: newUser } = await this.userRepository.createUserWithProject({ firstName: userInfo.given_name, lastName: userInfo.family_name, email: userInfo.email, authIdentities: [], role: db_1.GLOBAL_MEMBER_ROLE, password: 'no password set', }, trx); await trx.save(trx.create(db_1.AuthIdentity, { providerId: claims.sub, providerType: 'oidc', userId: newUser.id, })); return newUser; }); await this.applySsoProvisioning(user, claims, userInfo); return user; } async generateTestLoginUrl() { await this.loadOpenIdClient(); const config = await this.loadConfig(true); const configuration = await this.createProxyAwareConfiguration(config.discoveryEndpoint, config.clientId, config.clientSecret); const state = this.generateState(true); const nonce = this.generateNonce(); const provisioningConfig = await this.provisioningService.getConfig(); const provisioningEnabled = provisioningConfig.scopesProvisionInstanceRole || provisioningConfig.scopesProvisionProjectRoles; const scope = provisioningEnabled ? `openid email profile ${provisioningConfig.scopesName}` : 'openid email profile'; const authorizationURL = this.openidClient.buildAuthorizationUrl(configuration, { redirect_uri: this.getCallbackUrl(), response_type: 'code', scope, prompt: config.prompt, state: state.plaintext, nonce: nonce.plaintext, ...(config.authenticationContextClassReference.length > 0 && { acr_values: config.authenticationContextClassReference.join(' '), }), }); return { url: authorizationURL, state: state.signed, nonce: nonce.signed }; } async processTestCallback(callbackUrl, storedState, storedNonce) { await this.loadOpenIdClient(); const config = await this.loadConfig(true); const configuration = await this.createProxyAwareConfiguration(config.discoveryEndpoint, config.clientId, config.clientSecret); const { state: expectedState } = this.verifyState(storedState); const expectedNonce = this.verifyNonce(storedNonce); let tokens; try { tokens = await this.openidClient.authorizationCodeGrant(configuration, callbackUrl, { expectedState, expectedNonce, }); } catch (error) { this.logger.error('Failed to exchange authorization code for tokens', { error }); throw new bad_request_error_1.BadRequestError('Invalid authorization code'); } let claims; try { claims = tokens.claims(); } catch (error) { this.logger.error('Failed to extract claims from tokens', { error }); throw new bad_request_error_1.BadRequestError('Invalid token'); } if (!claims) { throw new forbidden_error_1.ForbiddenError('No claims found in the OIDC token'); } let userInfo; try { userInfo = await this.openidClient.fetchUserInfo(configuration, tokens.access_token, claims.sub); } catch (error) { this.logger.error('Failed to fetch user info', { error }); throw new bad_request_error_1.BadRequestError('Invalid token'); } return { claims: { ...claims }, userInfo: { ...userInfo }, }; } async applySsoProvisioning(user, claims, userInfo) { if (await this.provisioningService.isExpressionMappingEnabled()) { const context = (0, claims_context_builder_1.buildOidcClaimsContext)(claims, userInfo); await this.provisioningService.provisionExpressionMappedRolesForUser(user, context); return; } const provisioningConfig = await this.provisioningService.getConfig(); const projectRoleMapping = claims[provisioningConfig.scopesProjectsRolesClaimName]; const instanceRole = claims[provisioningConfig.scopesInstanceRoleClaimName]; if (instanceRole) { await this.provisioningService.provisionInstanceRoleForUser(user, instanceRole); } if (projectRoleMapping) { await this.provisioningService.provisionProjectRolesForUser(user.id, projectRoleMapping); } } async broadcastReloadOIDCConfigurationCommand() { if (this.instanceSettings.isMultiMain) { const { Publisher } = await Promise.resolve().then(() => __importStar(require('../../scaling/pubsub/publisher.service'))); await di_1.Container.get(Publisher).publishCommand({ command: 'reload-oidc-config' }); } } async reload() { if (this.isReloading) { this.logger.warn('OIDC configuration reload already in progress'); return; } this.isReloading = true; try { this.logger.debug('OIDC configuration changed, starting to load it from the database'); const configFromDB = await this.loadConfigurationFromDatabase(true); if (configFromDB) { this.oidcConfig = configFromDB; this.cachedOidcConfiguration = undefined; } else { this.logger.warn('OIDC configuration not found in database, ignoring reload message'); } await (0, sso_helpers_1.reloadAuthenticationMethod)(); const isOidcLoginEnabled = (0, sso_helpers_1.isOidcCurrentAuthenticationMethod)(); this.logger.debug(`OIDC login is now ${isOidcLoginEnabled ? 'enabled' : 'disabled'}.`); di_1.Container.get(config_1.GlobalConfig).sso.oidc.loginEnabled = isOidcLoginEnabled; } catch (error) { this.logger.error('OIDC configuration changed, failed to reload OIDC configuration', { error, }); } finally { this.isReloading = false; } } async loadConfigurationFromDatabase(decryptSecret = false) { const configFromDB = await this.settingsRepository.findByKey(constants_1.OIDC_PREFERENCES_DB_KEY); if (configFromDB) { try { const configValue = (0, n8n_workflow_1.jsonParse)(configFromDB.value); if (configValue.discoveryEndpoint === '') return undefined; const oidcConfig = api_types_1.OidcConfigDto.parse(configValue); const discoveryUrl = new URL(oidcConfig.discoveryEndpoint); if (oidcConfig.clientSecret && decryptSecret) { oidcConfig.clientSecret = await this.cipher.decryptV2(oidcConfig.clientSecret); } return { ...oidcConfig, discoveryEndpoint: discoveryUrl, }; } catch (error) { this.logger.warn('Failed to load OIDC configuration from database, falling back to default configuration.', { error }); } } return undefined; } async loadConfig(decryptSecret = false) { const currentConfig = await this.loadConfigurationFromDatabase(decryptSecret); if (currentConfig) { return currentConfig; } return DEFAULT_OIDC_RUNTIME_CONFIG; } async updateConfig(newConfig) { const isEnablingOidcWhileOtherSsoProtocolIsAlreadyEnabled = newConfig.loginEnabled && !(0, sso_helpers_1.isEmailCurrentAuthenticationMethod)() && !(0, sso_helpers_1.isOidcCurrentAuthenticationMethod)(); if (isEnablingOidcWhileOtherSsoProtocolIsAlreadyEnabled) { throw new internal_server_error_1.InternalServerError(`Cannot switch OIDC login enabled state when an authentication method other than email or OIDC is active (current: ${(0, sso_helpers_1.getCurrentAuthenticationMethod)()})`); } let discoveryEndpoint; try { discoveryEndpoint = new URL(newConfig.discoveryEndpoint); } catch (error) { this.logger.error(`The provided endpoint is not a valid URL: ${newConfig.discoveryEndpoint}`); throw new n8n_workflow_1.UserError('Provided discovery endpoint is not a valid URL'); } if (newConfig.clientSecret === constants_1.OIDC_CLIENT_SECRET_REDACTED_VALUE) { newConfig.clientSecret = this.oidcConfig.clientSecret; } try { const discoveredMetadata = await this.createProxyAwareConfiguration(discoveryEndpoint, newConfig.clientId, newConfig.clientSecret); this.logger.debug(`Discovered OIDC metadata: ${JSON.stringify(discoveredMetadata)}`); } catch (error) { this.logger.error('Failed to discover OIDC metadata', { error }); throw new n8n_workflow_1.UserError('Failed to discover OIDC metadata, based on the provided configuration'); } await this.settingsRepository.save({ key: constants_1.OIDC_PREFERENCES_DB_KEY, value: JSON.stringify({ ...newConfig, clientSecret: await this.cipher.encryptV2(newConfig.clientSecret), }), loadOnStartup: true, }); this.oidcConfig = { ...newConfig, discoveryEndpoint, }; this.cachedOidcConfiguration = undefined; this.logger.debug(`OIDC login is now ${this.oidcConfig.loginEnabled ? 'enabled' : 'disabled'}.`); await this.setOidcLoginEnabled(this.oidcConfig.loginEnabled); await this.broadcastReloadOIDCConfigurationCommand(); } async setOidcLoginEnabled(enabled) { const currentAuthenticationMethod = (0, sso_helpers_1.getCurrentAuthenticationMethod)(); const isEnablingOidcWhileOtherSsoProtocolIsAlreadyEnabled = enabled && !(0, sso_helpers_1.isEmailCurrentAuthenticationMethod)() && !(0, sso_helpers_1.isOidcCurrentAuthenticationMethod)(); if (isEnablingOidcWhileOtherSsoProtocolIsAlreadyEnabled) { throw new internal_server_error_1.InternalServerError(`Cannot switch OIDC login enabled state when an authentication method other than email or OIDC is active (current: ${currentAuthenticationMethod})`); } const targetAuthenticationMethod = !enabled && currentAuthenticationMethod === 'oidc' ? 'email' : currentAuthenticationMethod; di_1.Container.get(config_1.GlobalConfig).sso.oidc.loginEnabled = enabled; await (0, sso_helpers_1.setCurrentAuthenticationMethod)(enabled ? 'oidc' : targetAuthenticationMethod); } async createProxyAwareConfiguration(discoveryUrl, clientId, clientSecret) { await this.loadOpenIdClient(); const hasProxyConfig = process.env.HTTP_PROXY ?? process.env.HTTPS_PROXY ?? process.env.ALL_PROXY; if (hasProxyConfig) { this.logger.debug('Configuring OIDC client with proxy support', { HTTP_PROXY: process.env.HTTP_PROXY, HTTPS_PROXY: process.env.HTTPS_PROXY, NO_PROXY: process.env.NO_PROXY, ALL_PROXY: process.env.ALL_PROXY, }); const proxyAgent = new undici_1.EnvHttpProxyAgent(); const proxyFetch = async (url, options) => { return await fetch(url, { ...options, dispatcher: proxyAgent, }); }; const configuration = await this.openidClient.discovery(discoveryUrl, clientId, clientSecret, undefined, { [this.openidClient.customFetch]: proxyFetch, }); configuration[this.openidClient.customFetch] = proxyFetch; return configuration; } return await this.openidClient.discovery(discoveryUrl, clientId, clientSecret); } async getOidcConfiguration() { const now = Date.now(); if (this.cachedOidcConfiguration === undefined || now >= this.cachedOidcConfiguration.validTill.getTime() || this.oidcConfig.discoveryEndpoint.toString() !== this.cachedOidcConfiguration.discoveryEndpoint.toString() || this.oidcConfig.clientId !== this.cachedOidcConfiguration.clientId || this.oidcConfig.clientSecret !== this.cachedOidcConfiguration.clientSecret) { this.cachedOidcConfiguration = { ...this.oidcConfig, configuration: this.createProxyAwareConfiguration(this.oidcConfig.discoveryEndpoint, this.oidcConfig.clientId, this.oidcConfig.clientSecret), validTill: new Date(Date.now() + 60 * 60 * 1000), }; } return await this.cachedOidcConfiguration.configuration; } }; exports.OidcService = OidcService; __decorate([ (0, decorators_1.OnPubSubEvent)('reload-oidc-config'), __metadata("design:type", Function), __metadata("design:paramtypes", []), __metadata("design:returntype", Promise) ], OidcService.prototype, "reload", null); exports.OidcService = OidcService = __decorate([ (0, di_1.Service)(), __metadata("design:paramtypes", [db_1.SettingsRepository, db_1.AuthIdentityRepository, url_service_1.UrlService, config_1.GlobalConfig, db_1.UserRepository, n8n_core_1.Cipher, backend_common_1.Logger, jwt_service_1.JwtService, n8n_core_1.InstanceSettings, provisioning_service_ee_1.ProvisioningService]) ], OidcService); //# sourceMappingURL=oidc.service.ee.js.map