2023-01-27 02:19:47 -08:00
|
|
|
import validator from 'validator';
|
2023-04-24 02:45:31 -07:00
|
|
|
import { Authorized, Get, Post, RestController } from '@/decorators';
|
2023-01-27 02:19:47 -08:00
|
|
|
import { AuthError, BadRequestError, InternalServerError } from '@/ResponseHelper';
|
2023-02-21 00:35:35 -08:00
|
|
|
import { sanitizeUser, withFeatureFlags } from '@/UserManagement/UserManagementHelper';
|
2023-01-27 02:19:47 -08:00
|
|
|
import { issueCookie, resolveJwt } from '@/auth/jwt';
|
|
|
|
import { AUTH_COOKIE_NAME } from '@/constants';
|
2023-01-27 05:56:56 -08:00
|
|
|
import { Request, Response } from 'express';
|
2023-01-27 02:19:47 -08:00
|
|
|
import type { ILogger } from 'n8n-workflow';
|
|
|
|
import type { User } from '@db/entities/User';
|
2023-01-27 05:56:56 -08:00
|
|
|
import { LoginRequest, UserRequest } from '@/requests';
|
|
|
|
import { In } from 'typeorm';
|
2023-01-27 02:19:47 -08:00
|
|
|
import type { Config } from '@/config';
|
2023-02-21 00:35:35 -08:00
|
|
|
import type {
|
|
|
|
PublicUser,
|
|
|
|
IDatabaseCollections,
|
|
|
|
IInternalHooksClass,
|
|
|
|
CurrentUser,
|
|
|
|
} from '@/Interfaces';
|
2023-01-27 02:19:47 -08:00
|
|
|
import { handleEmailLogin, handleLdapLogin } from '@/auth';
|
2023-02-21 00:35:35 -08:00
|
|
|
import type { PostHogClient } from '@/posthog';
|
2023-03-24 09:46:06 -07:00
|
|
|
import {
|
2023-04-28 09:11:33 -07:00
|
|
|
getCurrentAuthenticationMethod,
|
2023-03-24 09:46:06 -07:00
|
|
|
isLdapCurrentAuthenticationMethod,
|
|
|
|
isSamlCurrentAuthenticationMethod,
|
|
|
|
} from '@/sso/ssoHelpers';
|
2023-04-12 01:59:14 -07:00
|
|
|
import type { UserRepository } from '@db/repositories';
|
2023-04-28 09:11:33 -07:00
|
|
|
import { InternalHooks } from '../InternalHooks';
|
|
|
|
import Container from 'typedi';
|
2023-01-27 02:19:47 -08:00
|
|
|
|
|
|
|
@RestController()
|
|
|
|
export class AuthController {
|
|
|
|
private readonly config: Config;
|
|
|
|
|
|
|
|
private readonly logger: ILogger;
|
|
|
|
|
|
|
|
private readonly internalHooks: IInternalHooksClass;
|
|
|
|
|
2023-04-12 01:59:14 -07:00
|
|
|
private readonly userRepository: UserRepository;
|
2023-01-27 02:19:47 -08:00
|
|
|
|
2023-02-21 00:35:35 -08:00
|
|
|
private readonly postHog?: PostHogClient;
|
|
|
|
|
2023-01-27 02:19:47 -08:00
|
|
|
constructor({
|
|
|
|
config,
|
|
|
|
logger,
|
|
|
|
internalHooks,
|
|
|
|
repositories,
|
2023-02-21 00:35:35 -08:00
|
|
|
postHog,
|
2023-01-27 02:19:47 -08:00
|
|
|
}: {
|
|
|
|
config: Config;
|
|
|
|
logger: ILogger;
|
|
|
|
internalHooks: IInternalHooksClass;
|
|
|
|
repositories: Pick<IDatabaseCollections, 'User'>;
|
2023-02-21 00:35:35 -08:00
|
|
|
postHog?: PostHogClient;
|
2023-01-27 02:19:47 -08:00
|
|
|
}) {
|
|
|
|
this.config = config;
|
|
|
|
this.logger = logger;
|
|
|
|
this.internalHooks = internalHooks;
|
|
|
|
this.userRepository = repositories.User;
|
2023-02-21 00:35:35 -08:00
|
|
|
this.postHog = postHog;
|
2023-01-27 02:19:47 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Log in a user.
|
|
|
|
*/
|
|
|
|
@Post('/login')
|
2023-02-24 11:37:19 -08:00
|
|
|
async login(req: LoginRequest, res: Response): Promise<PublicUser | undefined> {
|
2023-01-27 02:19:47 -08:00
|
|
|
const { email, password } = req.body;
|
|
|
|
if (!email) throw new Error('Email is required to log in');
|
|
|
|
if (!password) throw new Error('Password is required to log in');
|
|
|
|
|
2023-02-24 11:37:19 -08:00
|
|
|
let user: User | undefined;
|
2023-01-27 02:19:47 -08:00
|
|
|
|
2023-04-28 09:11:33 -07:00
|
|
|
let usedAuthenticationMethod = getCurrentAuthenticationMethod();
|
|
|
|
|
2023-02-24 11:37:19 -08:00
|
|
|
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?.globalRole?.name === 'owner') {
|
|
|
|
user = preliminaryUser;
|
2023-04-28 09:11:33 -07:00
|
|
|
usedAuthenticationMethod = 'email';
|
2023-02-24 11:37:19 -08:00
|
|
|
} else {
|
2023-03-24 09:46:06 -07:00
|
|
|
throw new AuthError('SAML is enabled, please log in with SAML');
|
2023-02-24 11:37:19 -08:00
|
|
|
}
|
2023-03-24 09:46:06 -07:00
|
|
|
} else if (isLdapCurrentAuthenticationMethod()) {
|
|
|
|
user = await handleLdapLogin(email, password);
|
2023-02-24 11:37:19 -08:00
|
|
|
} else {
|
2023-03-24 09:46:06 -07:00
|
|
|
user = await handleEmailLogin(email, password);
|
2023-02-24 11:37:19 -08:00
|
|
|
}
|
2023-01-27 02:19:47 -08:00
|
|
|
if (user) {
|
|
|
|
await issueCookie(res, user);
|
2023-04-28 09:11:33 -07:00
|
|
|
void Container.get(InternalHooks).onUserLoginSuccess({
|
|
|
|
user,
|
|
|
|
authenticationMethod: usedAuthenticationMethod,
|
|
|
|
});
|
2023-02-21 00:35:35 -08:00
|
|
|
return withFeatureFlags(this.postHog, sanitizeUser(user));
|
2023-01-27 02:19:47 -08:00
|
|
|
}
|
2023-04-28 09:11:33 -07:00
|
|
|
void Container.get(InternalHooks).onUserLoginFailed({
|
|
|
|
user: email,
|
|
|
|
authenticationMethod: usedAuthenticationMethod,
|
|
|
|
reason: 'wrong credentials',
|
|
|
|
});
|
2023-01-27 02:19:47 -08:00
|
|
|
throw new AuthError('Wrong username or password. Do you have caps lock on?');
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Manually check the `n8n-auth` cookie.
|
|
|
|
*/
|
|
|
|
@Get('/login')
|
2023-02-21 00:35:35 -08:00
|
|
|
async currentUser(req: Request, res: Response): Promise<CurrentUser> {
|
2023-01-27 02:19:47 -08:00
|
|
|
// Manually check the existing cookie.
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
|
|
const cookieContents = req.cookies?.[AUTH_COOKIE_NAME] as string | undefined;
|
|
|
|
|
|
|
|
let user: User;
|
|
|
|
if (cookieContents) {
|
|
|
|
// If logged in, return user
|
|
|
|
try {
|
|
|
|
user = await resolveJwt(cookieContents);
|
2023-02-21 00:35:35 -08:00
|
|
|
return await withFeatureFlags(this.postHog, sanitizeUser(user));
|
2023-01-27 02:19:47 -08:00
|
|
|
} catch (error) {
|
|
|
|
res.clearCookie(AUTH_COOKIE_NAME);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this.config.getEnv('userManagement.isInstanceOwnerSetUp')) {
|
|
|
|
throw new AuthError('Not logged in');
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
|
|
user = await this.userRepository.findOneOrFail({
|
|
|
|
relations: ['globalRole'],
|
|
|
|
where: {},
|
|
|
|
});
|
|
|
|
} catch (error) {
|
|
|
|
throw new InternalServerError(
|
|
|
|
'No users found in database - did you wipe the users table? Create at least one user.',
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (user.email || user.password) {
|
|
|
|
throw new InternalServerError('Invalid database state - user has password set.');
|
|
|
|
}
|
|
|
|
|
|
|
|
await issueCookie(res, user);
|
2023-02-21 00:35:35 -08:00
|
|
|
return withFeatureFlags(this.postHog, sanitizeUser(user));
|
2023-01-27 02:19:47 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Validate invite token to enable invitee to set up their account.
|
|
|
|
*/
|
|
|
|
@Get('/resolve-signup-token')
|
|
|
|
async resolveSignupToken(req: UserRequest.ResolveSignUp) {
|
|
|
|
const { inviterId, inviteeId } = req.query;
|
|
|
|
|
|
|
|
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.find({ where: { id: In([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');
|
|
|
|
}
|
|
|
|
|
|
|
|
void this.internalHooks.onUserInviteEmailClick({ inviter, invitee });
|
|
|
|
|
|
|
|
const { firstName, lastName } = inviter;
|
|
|
|
return { inviter: { firstName, lastName } };
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Log out a user.
|
|
|
|
*/
|
2023-04-24 02:45:31 -07:00
|
|
|
@Authorized()
|
2023-01-27 02:19:47 -08:00
|
|
|
@Post('/logout')
|
|
|
|
logout(req: Request, res: Response) {
|
|
|
|
res.clearCookie(AUTH_COOKIE_NAME);
|
|
|
|
return { loggedOut: true };
|
|
|
|
}
|
|
|
|
}
|