"use strict";
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 __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.AuthService = void 0;
const constants_1 = require("../constants");
const backend_common_1 = require("@n8n/backend-common");
const config_1 = require("@n8n/config");
const constants_2 = require("@n8n/constants");
const db_1 = require("@n8n/db");
const di_1 = require("@n8n/di");
const crypto_1 = require("crypto");
const jsonwebtoken_1 = require("jsonwebtoken");
const auth_error_1 = require("../errors/response-errors/auth.error");
const forbidden_error_1 = require("../errors/response-errors/forbidden.error");
const license_1 = require("../license");
const mfa_service_1 = require("../mfa/mfa.service");
const jwt_service_1 = require("../services/jwt.service");
const url_service_1 = require("../services/url.service");
let AuthService = class AuthService {
    constructor(globalConfig, logger, license, jwtService, urlService, userRepository, invalidAuthTokenRepository, mfaService) {
        this.globalConfig = globalConfig;
        this.logger = logger;
        this.license = license;
        this.jwtService = jwtService;
        this.urlService = urlService;
        this.userRepository = userRepository;
        this.invalidAuthTokenRepository = invalidAuthTokenRepository;
        this.mfaService = mfaService;
        const restEndpoint = globalConfig.endpoints.rest;
        this.skipBrowserIdCheckEndpoints = [
            `/${restEndpoint}/push`,
            `/${restEndpoint}/binary-data/`,
            `/${restEndpoint}/oauth1-credential/callback`,
            `/${restEndpoint}/oauth2-credential/callback`,
            '/types/nodes.json',
            '/types/credentials.json',
            '/types/node-versions.json',
            '/mcp-oauth/authorize/',
            `/${restEndpoint}/chat/conversations/:sessionId/messages/:messageId/attachments/:index`,
            `/${restEndpoint}/instance-ai/events/:threadId`,
        ];
    }
    createAuthMiddleware({ allowSkipMFA, allowSkipPreviewAuth, allowUnauthenticated, }) {
        return async (req, res, next) => {
            const token = req.cookies[constants_1.AUTH_COOKIE_NAME];
            if (token) {
                try {
                    const isInvalid = await this.invalidAuthTokenRepository.existsBy({ token });
                    if (isInvalid)
                        throw new auth_error_1.AuthError('Unauthorized');
                    const [user, { usedMfa }] = await this.resolveJwt(token, req, res);
                    const mfaEnforced = await this.mfaService.isMFAEnforced();
                    if (mfaEnforced && !usedMfa && !allowSkipMFA) {
                        if (user.mfaEnabled) {
                            throw new auth_error_1.AuthError('MFA not used during authentication');
                        }
                        else {
                            if (allowUnauthenticated) {
                                req.authInfo = {
                                    usedMfa,
                                    mfaEnrollmentRequired: true,
                                };
                                return next();
                            }
                            res.status(401).json({ status: 'error', message: 'Unauthorized', mfaRequired: true });
                            return;
                        }
                    }
                    req.user = user;
                    req.authInfo = {
                        usedMfa,
                    };
                }
                catch (error) {
                    if (error instanceof jsonwebtoken_1.JsonWebTokenError || error instanceof auth_error_1.AuthError) {
                        this.clearCookie(res);
                    }
                    else {
                        throw error;
                    }
                }
            }
            const isPreviewMode = process.env.N8N_PREVIEW_MODE === 'true';
            const shouldSkipAuth = (allowSkipPreviewAuth && isPreviewMode) || allowUnauthenticated;
            if (req.user)
                next();
            else if (shouldSkipAuth)
                next();
            else
                res.status(401).json({ status: 'error', message: 'Unauthorized' });
        };
    }
    getCookieToken(req) {
        if (typeof req.cookies === 'object' && req.cookies !== null) {
            const cookies = req.cookies;
            return cookies[constants_1.AUTH_COOKIE_NAME];
        }
        return undefined;
    }
    getBrowserId(req) {
        if ('browserId' in req && typeof req.browserId === 'string') {
            return req.browserId;
        }
        return undefined;
    }
    getMethod(req) {
        return req.method;
    }
    getEndpoint(req) {
        return req.route ? `${req.baseUrl}${req.route.path}` : req.baseUrl;
    }
    clearCookie(res) {
        res.clearCookie(constants_1.AUTH_COOKIE_NAME);
    }
    async invalidateToken(req) {
        const token = req.cookies[constants_1.AUTH_COOKIE_NAME];
        if (!token)
            return;
        try {
            const { exp } = this.jwtService.decode(token);
            if (exp) {
                await this.invalidAuthTokenRepository.insert({
                    token,
                    expiresAt: new Date(exp * 1000),
                });
            }
        }
        catch (e) {
            this.logger.warn('failed to invalidate auth token', { error: e.message });
        }
    }
    issueCookie(res, user, usedMfa, browserId, isEmbed, cookieOverrides) {
        const isWithinUsersLimit = this.license.isWithinUsersLimit();
        if (user.role.slug !== db_1.GLOBAL_OWNER_ROLE.slug && !isWithinUsersLimit) {
            throw new forbidden_error_1.ForbiddenError(constants_1.RESPONSE_ERROR_MESSAGES.USERS_QUOTA_REACHED);
        }
        const token = this.issueJWT(user, usedMfa, browserId, isEmbed);
        const { samesite, secure } = this.globalConfig.auth.cookie;
        res.cookie(constants_1.AUTH_COOKIE_NAME, token, {
            maxAge: this.jwtExpiration * constants_2.Time.seconds.toMilliseconds,
            httpOnly: true,
            sameSite: cookieOverrides?.sameSite ?? samesite,
            secure: cookieOverrides?.secure ?? secure,
        });
    }
    issueJWT(user, usedMfa = false, browserId, isEmbed) {
        const payload = {
            id: user.id,
            hash: this.createJWTHash(user),
            browserId: browserId && this.hash(browserId),
            usedMfa,
            ...(isEmbed && { isEmbed }),
        };
        return this.jwtService.sign(payload, {
            expiresIn: this.jwtExpiration,
        });
    }
    async validateCookieToken(token) {
        const isInvalid = await this.invalidAuthTokenRepository.existsBy({ token });
        if (isInvalid)
            throw new auth_error_1.AuthError('Unauthorized');
        await this.validateToken(token);
    }
    async authenticateUserBasedOnToken(token, method, endpoint, browserId) {
        const isInvalid = await this.invalidAuthTokenRepository.existsBy({ token });
        if (isInvalid)
            throw new auth_error_1.AuthError('Unauthorized');
        const { user, jwtPayload } = await this.validateToken(token);
        this.validateBrowserId(jwtPayload, browserId, endpoint, method);
        const usedMfa = jwtPayload.usedMfa ?? false;
        if (usedMfa) {
            return user;
        }
        const mfaEnforced = await this.mfaService.isMFAEnforced();
        if (!mfaEnforced && !user.mfaEnabled) {
            return user;
        }
        throw new auth_error_1.AuthError('Unauthorized');
    }
    validateBrowserId(jwtPayload, browserId, endpoint, method) {
        if (method === 'GET' && this.skipBrowserIdCheckEndpoints.includes(endpoint)) {
            this.logger.debug(`Skipped browserId check on ${endpoint}`);
        }
        else if (jwtPayload.browserId &&
            (!browserId || jwtPayload.browserId !== this.hash(browserId))) {
            this.logger.warn(`browserId check failed on ${endpoint}`);
            throw new auth_error_1.AuthError('Unauthorized');
        }
    }
    async validateToken(token) {
        const jwtPayload = this.jwtService.verify(token, {
            algorithms: ['HS256'],
        });
        const user = await this.userRepository.findOne({
            where: { id: jwtPayload.id },
            relations: ['role'],
        });
        if (!user ||
            user.disabled ||
            jwtPayload.hash !== this.createJWTHash(user)) {
            throw new auth_error_1.AuthError('Unauthorized');
        }
        return {
            user,
            jwtPayload,
        };
    }
    async resolveJwt(token, req, res) {
        const { user, jwtPayload } = await this.validateToken(token);
        const browserId = this.getBrowserId(req);
        const endpoint = this.getEndpoint(req);
        const method = this.getMethod(req);
        this.validateBrowserId(jwtPayload, browserId, endpoint, method);
        if (jwtPayload.exp * 1000 - Date.now() < this.jwtRefreshTimeout) {
            this.logger.debug('JWT about to expire. Will be refreshed');
            const embedCookieOverrides = jwtPayload.isEmbed
                ? { sameSite: 'none', secure: true }
                : undefined;
            this.issueCookie(res, user, jwtPayload.usedMfa ?? false, browserId, jwtPayload.isEmbed, embedCookieOverrides);
        }
        return [user, { usedMfa: jwtPayload.usedMfa ?? false }];
    }
    generatePasswordResetToken(user, expiresIn = '20m') {
        const payload = { sub: user.id, hash: this.createJWTHash(user) };
        return this.jwtService.sign(payload, { expiresIn });
    }
    generatePasswordResetUrl(user) {
        const instanceBaseUrl = this.urlService.getInstanceBaseUrl();
        const url = new URL(`${instanceBaseUrl}/change-password`);
        url.searchParams.append('token', this.generatePasswordResetToken(user));
        url.searchParams.append('mfaEnabled', user.mfaEnabled.toString());
        return url.toString();
    }
    async resolvePasswordResetToken(token) {
        let decodedToken;
        try {
            decodedToken = this.jwtService.verify(token);
        }
        catch (e) {
            if (e instanceof jsonwebtoken_1.TokenExpiredError) {
                this.logger.debug('Reset password token expired', { token });
            }
            else {
                this.logger.debug('Error verifying token', { token });
            }
            return;
        }
        const user = await this.userRepository.findOne({
            where: { id: decodedToken.sub },
            relations: ['authIdentities', 'role'],
        });
        if (!user) {
            this.logger.debug('Request to resolve password token failed because no user was found for the provided user ID', { userId: decodedToken.sub, token });
            return;
        }
        if (decodedToken.hash !== this.createJWTHash(user)) {
            this.logger.debug('Password updated since this token was generated');
            return;
        }
        return user;
    }
    createJWTHash({ email, password, mfaEnabled, mfaSecret }) {
        const payload = [email, password];
        if (mfaEnabled && mfaSecret) {
            payload.push(mfaSecret.substring(0, 3));
        }
        return this.hash(payload.join(':')).substring(0, 10);
    }
    hash(input) {
        return (0, crypto_1.createHash)('sha256').update(input).digest('base64');
    }
    get jwtRefreshTimeout() {
        const { jwtRefreshTimeoutHours, jwtSessionDurationHours } = this.globalConfig.userManagement;
        if (jwtRefreshTimeoutHours === 0) {
            return Math.floor(jwtSessionDurationHours * 0.25 * constants_2.Time.hours.toMilliseconds);
        }
        else {
            return Math.floor(jwtRefreshTimeoutHours * constants_2.Time.hours.toMilliseconds);
        }
    }
    get jwtExpiration() {
        return this.globalConfig.userManagement.jwtSessionDurationHours * constants_2.Time.hours.toSeconds;
    }
};
exports.AuthService = AuthService;
exports.AuthService = AuthService = __decorate([
    (0, di_1.Service)(),
    __metadata("design:paramtypes", [config_1.GlobalConfig,
        backend_common_1.Logger,
        license_1.License,
        jwt_service_1.JwtService,
        url_service_1.UrlService,
        db_1.UserRepository,
        db_1.InvalidAuthTokenRepository,
        mfa_service_1.MfaService])
], AuthService);
//# sourceMappingURL=auth.service.js.map