import validator from 'validator'; import { Response } from 'express'; import { AuthService } from '@/auth/auth.service'; import { Get, Post, RestController } from '@/decorators'; import { RESPONSE_ERROR_MESSAGES } from '@/constants'; import type { User } from '@/databases/entities/user'; import { AuthenticatedRequest, LoginRequest, UserRequest } from '@/requests'; import type { PublicUser } from '@/interfaces'; import { handleEmailLogin, handleLdapLogin } from '@/auth'; import { PostHogClient } from '@/posthog'; import { getCurrentAuthenticationMethod, isLdapCurrentAuthenticationMethod, isSamlCurrentAuthenticationMethod, } from '@/sso/sso-helpers'; import { License } from '@/license'; import { UserService } from '@/services/user.service'; import { MfaService } from '@/mfa/mfa.service'; import { Logger } from '@/logger'; import { AuthError } from '@/errors/response-errors/auth.error'; import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { ForbiddenError } from '@/errors/response-errors/forbidden.error'; import { ApplicationError } from 'n8n-workflow'; import { UserRepository } from '@/databases/repositories/user.repository'; import { EventService } from '@/events/event.service'; @RestController() export class AuthController { constructor( private readonly logger: Logger, private readonly authService: AuthService, private readonly mfaService: MfaService, private readonly userService: UserService, private readonly license: License, private readonly userRepository: UserRepository, private readonly eventService: EventService, private readonly postHog?: PostHogClient, ) {} /** Log in a user */ @Post('/login', { skipAuth: true, rateLimit: true }) async login(req: LoginRequest, res: Response): Promise { const { email, password, mfaToken, mfaRecoveryCode } = req.body; if (!email) throw new ApplicationError('Email is required to log in'); if (!password) throw new ApplicationError('Password is required to log in'); let user: User | undefined; let usedAuthenticationMethod = getCurrentAuthenticationMethod(); if (isSamlCurrentAuthenticationMethod()) { // attempt to fetch user data with the credentials, but don't log in yet const preliminaryUser = await handleEmailLogin(email, password); // if the user is an owner, continue with the login if ( preliminaryUser?.role === 'global:owner' || preliminaryUser?.settings?.allowSSOManualLogin ) { user = preliminaryUser; usedAuthenticationMethod = 'email'; } else { throw new AuthError('SSO is enabled, please log in with SSO'); } } else if (isLdapCurrentAuthenticationMethod()) { const preliminaryUser = await handleEmailLogin(email, password); if (preliminaryUser?.role === 'global:owner') { user = preliminaryUser; usedAuthenticationMethod = 'email'; } else { user = await handleLdapLogin(email, password); } } else { user = await handleEmailLogin(email, password); } if (user) { if (user.mfaEnabled) { if (!mfaToken && !mfaRecoveryCode) { throw new AuthError('MFA Error', 998); } const isMFATokenValid = await this.mfaService.validateMfa( user.id, mfaToken, mfaRecoveryCode, ); if (!isMFATokenValid) { throw new AuthError('Invalid mfa token or recovery code'); } } this.authService.issueCookie(res, user, req.browserId); this.eventService.emit('user-logged-in', { user, authenticationMethod: usedAuthenticationMethod, }); return await this.userService.toPublic(user, { posthog: this.postHog, withScopes: true }); } this.eventService.emit('user-login-failed', { authenticationMethod: usedAuthenticationMethod, userEmail: email, reason: 'wrong credentials', }); throw new AuthError('Wrong username or password. Do you have caps lock on?'); } /** Check if the user is already logged in */ @Get('/login') async currentUser(req: AuthenticatedRequest): Promise { return await this.userService.toPublic(req.user, { posthog: this.postHog, withScopes: true, }); } /** Validate invite token to enable invitee to set up their account */ @Get('/resolve-signup-token', { skipAuth: true }) async resolveSignupToken(req: UserRequest.ResolveSignUp) { const { inviterId, inviteeId } = req.query; const isWithinUsersLimit = this.license.isWithinUsersLimit(); if (!isWithinUsersLimit) { this.logger.debug('Request to resolve signup token failed because of users quota reached', { inviterId, inviteeId, }); throw new ForbiddenError(RESPONSE_ERROR_MESSAGES.USERS_QUOTA_REACHED); } if (!inviterId || !inviteeId) { this.logger.debug( 'Request to resolve signup token failed because of missing user IDs in query string', { inviterId, inviteeId }, ); throw new BadRequestError('Invalid payload'); } // Postgres validates UUID format for (const userId of [inviterId, inviteeId]) { if (!validator.isUUID(userId)) { this.logger.debug('Request to resolve signup token failed because of invalid user ID', { userId, }); throw new BadRequestError('Invalid userId'); } } const users = await this.userRepository.findManyByIds([inviterId, inviteeId]); if (users.length !== 2) { this.logger.debug( 'Request to resolve signup token failed because the ID of the inviter and/or the ID of the invitee were not found in database', { inviterId, inviteeId }, ); throw new BadRequestError('Invalid invite URL'); } const invitee = users.find((user) => user.id === inviteeId); if (!invitee || invitee.password) { this.logger.error('Invalid invite URL - invitee already setup', { inviterId, inviteeId, }); throw new BadRequestError('The invitation was likely either deleted or already claimed'); } const inviter = users.find((user) => user.id === inviterId); if (!inviter?.email || !inviter?.firstName) { this.logger.error( 'Request to resolve signup token failed because inviter does not exist or is not set up', { inviterId: inviter?.id, }, ); throw new BadRequestError('Invalid request'); } this.eventService.emit('user-invite-email-click', { inviter, invitee }); const { firstName, lastName } = inviter; return { inviter: { firstName, lastName } }; } /** Log out a user */ @Post('/logout') async logout(req: AuthenticatedRequest, res: Response) { await this.authService.invalidateToken(req); this.authService.clearCookie(res); return { loggedOut: true }; } }