File size: 6,325 Bytes
aec3094 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 | import { LoginRequestDto, ResolveSignupTokenQueryDto } from '@n8n/api-types';
import { Logger } from '@n8n/backend-common';
import type { User, PublicUser } from '@n8n/db';
import { UserRepository } from '@n8n/db';
import { Body, Get, Post, Query, RestController } from '@n8n/decorators';
import { Container } from '@n8n/di';
import { isEmail } from 'class-validator';
import { Response } from 'express';
import { handleEmailLogin } from '@/auth';
import { AuthService } from '@/auth/auth.service';
import { RESPONSE_ERROR_MESSAGES } from '@/constants';
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 { EventService } from '@/events/event.service';
import { License } from '@/license';
import { MfaService } from '@/mfa/mfa.service';
import { PostHogClient } from '@/posthog';
import { AuthenticatedRequest, AuthlessRequest } from '@/requests';
import { UserService } from '@/services/user.service';
import {
getCurrentAuthenticationMethod,
isLdapCurrentAuthenticationMethod,
isSamlCurrentAuthenticationMethod,
} from '@/sso.ee/sso-helpers';
@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: AuthlessRequest,
res: Response,
@Body payload: LoginRequestDto,
): Promise<PublicUser | undefined> {
const { emailOrLdapLoginId, password, mfaCode, mfaRecoveryCode } = payload;
let user: User | undefined;
let usedAuthenticationMethod = getCurrentAuthenticationMethod();
if (usedAuthenticationMethod === 'email' && !isEmail(emailOrLdapLoginId)) {
throw new BadRequestError('Invalid email address');
}
if (isSamlCurrentAuthenticationMethod()) {
// attempt to fetch user data with the credentials, but don't log in yet
const preliminaryUser = await handleEmailLogin(emailOrLdapLoginId, 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(emailOrLdapLoginId, password);
if (preliminaryUser?.role === 'global:owner') {
user = preliminaryUser;
usedAuthenticationMethod = 'email';
} else {
const { LdapService } = await import('@/ldap.ee/ldap.service.ee');
user = await Container.get(LdapService).handleLdapLogin(emailOrLdapLoginId, password);
}
} else {
user = await handleEmailLogin(emailOrLdapLoginId, password);
}
if (user) {
if (user.mfaEnabled) {
if (!mfaCode && !mfaRecoveryCode) {
throw new AuthError('MFA Error', 998);
}
const isMfaCodeOrMfaRecoveryCodeValid = await this.mfaService.validateMfa(
user.id,
mfaCode,
mfaRecoveryCode,
);
if (!isMfaCodeOrMfaRecoveryCodeValid) {
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: emailOrLdapLoginId,
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<PublicUser> {
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: AuthlessRequest,
_res: Response,
@Query payload: ResolveSignupTokenQueryDto,
) {
const { inviterId, inviteeId } = payload;
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);
}
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 };
}
}
|