194 lines
6.4 KiB
TypeScript
194 lines
6.4 KiB
TypeScript
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<PublicUser | undefined> {
|
|
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<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: 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 };
|
|
}
|
|
}
|