"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 = this.cipher.decrypt(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: this.cipher.encrypt(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