UNPKG

n8n

Version:

n8n Workflow Automation Tool

305 lines 13.2 kB
"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'); } else { this.logger.debug('Error verifying 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 }); 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