mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-18 02:21:13 +00:00
refactor(core): Remove all legacy auth middleware code (no-changelog) (#8755)
This commit is contained in:
committed by
GitHub
parent
2e84684f04
commit
56c8791aff
@@ -613,18 +613,6 @@ export interface ILicensePostResponse extends ILicenseReadResponse {
|
||||
managementToken: string;
|
||||
}
|
||||
|
||||
export interface JwtToken {
|
||||
token: string;
|
||||
/** The amount of seconds after which the JWT will expire. **/
|
||||
expiresIn: number;
|
||||
}
|
||||
|
||||
export interface JwtPayload {
|
||||
id: string;
|
||||
email: string | null;
|
||||
password: string | null;
|
||||
}
|
||||
|
||||
export interface PublicUser {
|
||||
id: string;
|
||||
email?: string;
|
||||
|
||||
@@ -62,7 +62,6 @@ import { EventBusController } from '@/eventbus/eventBus.controller';
|
||||
import { EventBusControllerEE } from '@/eventbus/eventBus.controller.ee';
|
||||
import { LicenseController } from '@/license/license.controller';
|
||||
import { setupPushServer, setupPushHandler } from '@/push';
|
||||
import { setupAuthMiddlewares } from './middlewares';
|
||||
import { isLdapEnabled } from './Ldap/helpers';
|
||||
import { AbstractServer } from './AbstractServer';
|
||||
import { PostHogClient } from './posthog';
|
||||
@@ -129,9 +128,8 @@ export class Server extends AbstractServer {
|
||||
Container.get(CollaborationService);
|
||||
}
|
||||
|
||||
private async registerControllers(ignoredEndpoints: Readonly<string[]>) {
|
||||
private async registerControllers() {
|
||||
const { app } = this;
|
||||
setupAuthMiddlewares(app, ignoredEndpoints, this.restEndpoint);
|
||||
|
||||
const controllers: Array<Class<object>> = [
|
||||
EventBusController,
|
||||
@@ -278,7 +276,7 @@ export class Server extends AbstractServer {
|
||||
|
||||
await handleMfaDisable();
|
||||
|
||||
await this.registerControllers(ignoredEndpoints);
|
||||
await this.registerControllers();
|
||||
|
||||
// ----------------------------------------
|
||||
// SAML
|
||||
|
||||
224
packages/cli/src/auth/auth.service.ts
Normal file
224
packages/cli/src/auth/auth.service.ts
Normal file
@@ -0,0 +1,224 @@
|
||||
import { Service } from 'typedi';
|
||||
import type { Response } from 'express';
|
||||
import { createHash } from 'crypto';
|
||||
import { JsonWebTokenError, TokenExpiredError, type JwtPayload } from 'jsonwebtoken';
|
||||
|
||||
import config from '@/config';
|
||||
import { AUTH_COOKIE_NAME, RESPONSE_ERROR_MESSAGES, Time } from '@/constants';
|
||||
import type { User } from '@db/entities/User';
|
||||
import { UserRepository } from '@db/repositories/user.repository';
|
||||
import type { AuthRole } from '@/decorators/types';
|
||||
import { AuthError } from '@/errors/response-errors/auth.error';
|
||||
import { UnauthorizedError } from '@/errors/response-errors/unauthorized.error';
|
||||
import { License } from '@/License';
|
||||
import { Logger } from '@/Logger';
|
||||
import type { AuthenticatedRequest, AsyncRequestHandler } from '@/requests';
|
||||
import { JwtService } from '@/services/jwt.service';
|
||||
import { UrlService } from '@/services/url.service';
|
||||
|
||||
interface AuthJwtPayload {
|
||||
/** User Id */
|
||||
id: string;
|
||||
/** User's email */
|
||||
email: string | null;
|
||||
/** SHA-256 hash of bcrypt hash of the user's password */
|
||||
password: string | null;
|
||||
}
|
||||
|
||||
interface IssuedJWT extends AuthJwtPayload {
|
||||
exp: number;
|
||||
}
|
||||
|
||||
@Service()
|
||||
export class AuthService {
|
||||
private middlewareCache = new Map<string, AsyncRequestHandler>();
|
||||
|
||||
constructor(
|
||||
private readonly logger: Logger,
|
||||
private readonly license: License,
|
||||
private readonly jwtService: JwtService,
|
||||
private readonly urlService: UrlService,
|
||||
private readonly userRepository: UserRepository,
|
||||
) {}
|
||||
|
||||
createAuthMiddleware(authRole: AuthRole): AsyncRequestHandler {
|
||||
const { middlewareCache: cache } = this;
|
||||
let authMiddleware = cache.get(authRole);
|
||||
if (authMiddleware) return authMiddleware;
|
||||
|
||||
authMiddleware = async (req: AuthenticatedRequest, res, next) => {
|
||||
if (authRole === 'none') {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
const token = req.cookies[AUTH_COOKIE_NAME];
|
||||
if (token) {
|
||||
try {
|
||||
req.user = await this.resolveJwt(token, res);
|
||||
} catch (error) {
|
||||
if (error instanceof JsonWebTokenError || error instanceof AuthError) {
|
||||
this.clearCookie(res);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!req.user) {
|
||||
res.status(401).json({ status: 'error', message: 'Unauthorized' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (authRole === 'any' || authRole === req.user.role) {
|
||||
next();
|
||||
} else {
|
||||
res.status(403).json({ status: 'error', message: 'Forbidden' });
|
||||
}
|
||||
};
|
||||
|
||||
cache.set(authRole, authMiddleware);
|
||||
return authMiddleware;
|
||||
}
|
||||
|
||||
clearCookie(res: Response) {
|
||||
res.clearCookie(AUTH_COOKIE_NAME);
|
||||
}
|
||||
|
||||
issueCookie(res: Response, user: User) {
|
||||
// If the instance has exceeded its user quota, prevent non-owners from logging in
|
||||
const isWithinUsersLimit = this.license.isWithinUsersLimit();
|
||||
if (
|
||||
config.getEnv('userManagement.isInstanceOwnerSetUp') &&
|
||||
!user.isOwner &&
|
||||
!isWithinUsersLimit
|
||||
) {
|
||||
throw new UnauthorizedError(RESPONSE_ERROR_MESSAGES.USERS_QUOTA_REACHED);
|
||||
}
|
||||
|
||||
const token = this.issueJWT(user);
|
||||
res.cookie(AUTH_COOKIE_NAME, token, {
|
||||
maxAge: this.jwtExpiration * Time.seconds.toMilliseconds,
|
||||
httpOnly: true,
|
||||
sameSite: 'lax',
|
||||
});
|
||||
}
|
||||
|
||||
issueJWT(user: User) {
|
||||
const { id, email, password } = user;
|
||||
const payload: AuthJwtPayload = {
|
||||
id,
|
||||
email,
|
||||
password: password ? this.createPasswordSha(user) : null,
|
||||
};
|
||||
return this.jwtService.sign(payload, {
|
||||
expiresIn: this.jwtExpiration,
|
||||
});
|
||||
}
|
||||
|
||||
async resolveJwt(token: string, res: Response): Promise<User> {
|
||||
const jwtPayload: IssuedJWT = this.jwtService.verify(token, {
|
||||
algorithms: ['HS256'],
|
||||
});
|
||||
|
||||
// TODO: Use an in-memory ttl-cache to cache the User object for upto a minute
|
||||
const user = await this.userRepository.findOne({
|
||||
where: { id: jwtPayload.id },
|
||||
});
|
||||
|
||||
// TODO: include these checks in the cache, to avoid computed this over and over again
|
||||
const passwordHash = user?.password ? this.createPasswordSha(user) : null;
|
||||
|
||||
if (
|
||||
// If not user is found
|
||||
!user ||
|
||||
// or, If the user has been deactivated (i.e. LDAP users)
|
||||
user.disabled ||
|
||||
// or, If the password has been updated
|
||||
jwtPayload.password !== passwordHash ||
|
||||
// or, If the email has been updated
|
||||
user.email !== jwtPayload.email
|
||||
) {
|
||||
throw new AuthError('Unauthorized');
|
||||
}
|
||||
|
||||
if (jwtPayload.exp * 1000 - Date.now() < this.jwtRefreshTimeout) {
|
||||
this.logger.debug('JWT about to expire. Will be refreshed');
|
||||
this.issueCookie(res, user);
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
generatePasswordResetToken(user: User, expiresIn = '20m') {
|
||||
return this.jwtService.sign(
|
||||
{ sub: user.id, passwordSha: this.createPasswordSha(user) },
|
||||
{ expiresIn },
|
||||
);
|
||||
}
|
||||
|
||||
generatePasswordResetUrl(user: 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: string): Promise<User | undefined> {
|
||||
let decodedToken: JwtPayload & { passwordSha: string };
|
||||
try {
|
||||
decodedToken = this.jwtService.verify(token);
|
||||
} catch (e) {
|
||||
if (e instanceof TokenExpiredError) {
|
||||
this.logger.debug('Reset password token expired', { token });
|
||||
} else {
|
||||
this.logger.debug('Error verifying token', { token });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const user = await this.userRepository.findOne({
|
||||
where: { id: decodedToken.sub },
|
||||
relations: ['authIdentities'],
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
this.logger.debug(
|
||||
'Request to resolve password token failed because no user was found for the provided user ID',
|
||||
{ userId: decodedToken.sub, token },
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.createPasswordSha(user) !== decodedToken.passwordSha) {
|
||||
this.logger.debug('Password updated since this token was generated');
|
||||
return;
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
private createPasswordSha({ password }: User) {
|
||||
return createHash('sha256')
|
||||
.update(password.slice(password.length / 2))
|
||||
.digest('hex');
|
||||
}
|
||||
|
||||
/** How many **milliseconds** before expiration should a JWT be renewed */
|
||||
get jwtRefreshTimeout() {
|
||||
const { jwtRefreshTimeoutHours, jwtSessionDurationHours } = config.get('userManagement');
|
||||
if (jwtRefreshTimeoutHours === 0) {
|
||||
return Math.floor(jwtSessionDurationHours * 0.25 * Time.hours.toMilliseconds);
|
||||
} else {
|
||||
return Math.floor(jwtRefreshTimeoutHours * Time.hours.toMilliseconds);
|
||||
}
|
||||
}
|
||||
|
||||
/** How many **seconds** is an issued JWT valid for */
|
||||
get jwtExpiration() {
|
||||
return config.get('userManagement.jwtSessionDurationHours') * Time.hours.toSeconds;
|
||||
}
|
||||
}
|
||||
@@ -1,94 +1,12 @@
|
||||
import type { Response } from 'express';
|
||||
import { createHash } from 'crypto';
|
||||
import { AUTH_COOKIE_NAME, RESPONSE_ERROR_MESSAGES, Time } from '@/constants';
|
||||
import type { JwtPayload, JwtToken } from '@/Interfaces';
|
||||
import type { User } from '@db/entities/User';
|
||||
import config from '@/config';
|
||||
import { License } from '@/License';
|
||||
import { Container } from 'typedi';
|
||||
import { UserRepository } from '@db/repositories/user.repository';
|
||||
import { JwtService } from '@/services/jwt.service';
|
||||
import { UnauthorizedError } from '@/errors/response-errors/unauthorized.error';
|
||||
import { AuthError } from '@/errors/response-errors/auth.error';
|
||||
import { ApplicationError } from 'n8n-workflow';
|
||||
import type { Response } from 'express';
|
||||
|
||||
export function issueJWT(user: User): JwtToken {
|
||||
const { id, email, password } = user;
|
||||
const expiresInHours = config.getEnv('userManagement.jwtSessionDurationHours');
|
||||
const expiresInSeconds = expiresInHours * Time.hours.toSeconds;
|
||||
import type { User } from '@db/entities/User';
|
||||
import { AuthService } from './auth.service';
|
||||
|
||||
const isWithinUsersLimit = Container.get(License).isWithinUsersLimit();
|
||||
|
||||
const payload: JwtPayload = {
|
||||
id,
|
||||
email,
|
||||
password: password ?? null,
|
||||
};
|
||||
|
||||
if (
|
||||
config.getEnv('userManagement.isInstanceOwnerSetUp') &&
|
||||
!user.isOwner &&
|
||||
!isWithinUsersLimit
|
||||
) {
|
||||
throw new UnauthorizedError(RESPONSE_ERROR_MESSAGES.USERS_QUOTA_REACHED);
|
||||
}
|
||||
if (password) {
|
||||
payload.password = createHash('sha256')
|
||||
.update(password.slice(password.length / 2))
|
||||
.digest('hex');
|
||||
}
|
||||
|
||||
const signedToken = Container.get(JwtService).sign(payload, {
|
||||
expiresIn: expiresInSeconds,
|
||||
});
|
||||
|
||||
return {
|
||||
token: signedToken,
|
||||
expiresIn: expiresInSeconds,
|
||||
};
|
||||
}
|
||||
|
||||
export const createPasswordSha = (user: User) =>
|
||||
createHash('sha256')
|
||||
.update(user.password.slice(user.password.length / 2))
|
||||
.digest('hex');
|
||||
|
||||
export async function resolveJwtContent(jwtPayload: JwtPayload): Promise<User> {
|
||||
const user = await Container.get(UserRepository).findOne({
|
||||
where: { id: jwtPayload.id },
|
||||
});
|
||||
|
||||
let passwordHash = null;
|
||||
if (user?.password) {
|
||||
passwordHash = createPasswordSha(user);
|
||||
}
|
||||
|
||||
// currently only LDAP users during synchronization
|
||||
// can be set to disabled
|
||||
if (user?.disabled) {
|
||||
throw new AuthError('Unauthorized');
|
||||
}
|
||||
|
||||
if (!user || jwtPayload.password !== passwordHash || user.email !== jwtPayload.email) {
|
||||
// When owner hasn't been set up, the default user
|
||||
// won't have email nor password (both equals null)
|
||||
throw new ApplicationError('Invalid token content');
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
export async function resolveJwt(token: string): Promise<User> {
|
||||
const jwtPayload: JwtPayload = Container.get(JwtService).verify(token, {
|
||||
algorithms: ['HS256'],
|
||||
});
|
||||
return await resolveJwtContent(jwtPayload);
|
||||
}
|
||||
|
||||
export async function issueCookie(res: Response, user: User): Promise<void> {
|
||||
const userData = issueJWT(user);
|
||||
res.cookie(AUTH_COOKIE_NAME, userData.token, {
|
||||
maxAge: userData.expiresIn * Time.seconds.toMilliseconds,
|
||||
httpOnly: true,
|
||||
sameSite: 'lax',
|
||||
});
|
||||
// This method is still used by cloud hooks.
|
||||
// DO NOT DELETE until the hooks have been updated
|
||||
/** @deprecated Use `AuthService` instead */
|
||||
export function issueCookie(res: Response, user: User) {
|
||||
return Container.get(AuthService).issueCookie(res, user);
|
||||
}
|
||||
|
||||
@@ -131,6 +131,7 @@ export const Time = {
|
||||
},
|
||||
days: {
|
||||
toSeconds: 24 * 60 * 60,
|
||||
toMilliseconds: 24 * 60 * 60 * 1000,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import validator from 'validator';
|
||||
|
||||
import { AuthService } from '@/auth/auth.service';
|
||||
import { Authorized, Get, Post, RestController } from '@/decorators';
|
||||
import { issueCookie, resolveJwt } from '@/auth/jwt';
|
||||
import { AUTH_COOKIE_NAME, RESPONSE_ERROR_MESSAGES } from '@/constants';
|
||||
import { RESPONSE_ERROR_MESSAGES } from '@/constants';
|
||||
import { Request, Response } from 'express';
|
||||
import type { User } from '@db/entities/User';
|
||||
import { LoginRequest, UserRequest } from '@/requests';
|
||||
import { AuthenticatedRequest, LoginRequest, UserRequest } from '@/requests';
|
||||
import type { PublicUser } from '@/Interfaces';
|
||||
import config from '@/config';
|
||||
import { handleEmailLogin, handleLdapLogin } from '@/auth';
|
||||
import { PostHogClient } from '@/posthog';
|
||||
import {
|
||||
@@ -20,7 +20,6 @@ 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 { InternalServerError } from '@/errors/response-errors/internal-server.error';
|
||||
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
|
||||
import { UnauthorizedError } from '@/errors/response-errors/unauthorized.error';
|
||||
import { ApplicationError } from 'n8n-workflow';
|
||||
@@ -31,6 +30,7 @@ export class AuthController {
|
||||
constructor(
|
||||
private readonly logger: Logger,
|
||||
private readonly internalHooks: InternalHooks,
|
||||
private readonly authService: AuthService,
|
||||
private readonly mfaService: MfaService,
|
||||
private readonly userService: UserService,
|
||||
private readonly license: License,
|
||||
@@ -96,7 +96,7 @@ export class AuthController {
|
||||
}
|
||||
}
|
||||
|
||||
await issueCookie(res, user);
|
||||
this.authService.issueCookie(res, user);
|
||||
void this.internalHooks.onUserLoginSuccess({
|
||||
user,
|
||||
authenticationMethod: usedAuthenticationMethod,
|
||||
@@ -112,45 +112,14 @@ export class AuthController {
|
||||
throw new AuthError('Wrong username or password. Do you have caps lock on?');
|
||||
}
|
||||
|
||||
/**
|
||||
* Manually check the `n8n-auth` cookie.
|
||||
*/
|
||||
/** Check if the user is already logged in */
|
||||
@Authorized()
|
||||
@Get('/login')
|
||||
async currentUser(req: Request, res: Response): Promise<PublicUser> {
|
||||
// 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);
|
||||
|
||||
return await this.userService.toPublic(user, { posthog: this.postHog, withScopes: true });
|
||||
} catch (error) {
|
||||
res.clearCookie(AUTH_COOKIE_NAME);
|
||||
}
|
||||
}
|
||||
|
||||
if (config.getEnv('userManagement.isInstanceOwnerSetUp')) {
|
||||
throw new AuthError('Not logged in');
|
||||
}
|
||||
|
||||
try {
|
||||
user = await this.userRepository.findOneOrFail({ 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);
|
||||
return await this.userService.toPublic(user, { posthog: this.postHog, withScopes: true });
|
||||
async currentUser(req: AuthenticatedRequest): Promise<PublicUser> {
|
||||
return await this.userService.toPublic(req.user, {
|
||||
posthog: this.postHog,
|
||||
withScopes: true,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -228,8 +197,8 @@ export class AuthController {
|
||||
*/
|
||||
@Authorized()
|
||||
@Post('/logout')
|
||||
logout(req: Request, res: Response) {
|
||||
res.clearCookie(AUTH_COOKIE_NAME);
|
||||
logout(_: Request, res: Response) {
|
||||
this.authService.clearCookie(res);
|
||||
return { loggedOut: true };
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Response } from 'express';
|
||||
import validator from 'validator';
|
||||
|
||||
import { AuthService } from '@/auth/auth.service';
|
||||
import config from '@/config';
|
||||
import { Authorized, NoAuthRequired, Post, RequireGlobalScope, RestController } from '@/decorators';
|
||||
import { issueCookie } from '@/auth/jwt';
|
||||
import { RESPONSE_ERROR_MESSAGES } from '@/constants';
|
||||
import { UserRequest } from '@/requests';
|
||||
import { License } from '@/License';
|
||||
@@ -26,6 +26,7 @@ export class InvitationController {
|
||||
private readonly logger: Logger,
|
||||
private readonly internalHooks: InternalHooks,
|
||||
private readonly externalHooks: ExternalHooks,
|
||||
private readonly authService: AuthService,
|
||||
private readonly userService: UserService,
|
||||
private readonly license: License,
|
||||
private readonly passwordUtility: PasswordUtility,
|
||||
@@ -165,7 +166,7 @@ export class InvitationController {
|
||||
|
||||
const updatedUser = await this.userRepository.save(invitee, { transaction: false });
|
||||
|
||||
await issueCookie(res, updatedUser);
|
||||
this.authService.issueCookie(res, updatedUser);
|
||||
|
||||
void this.internalHooks.onUserSignup(updatedUser, {
|
||||
user_type: 'email',
|
||||
|
||||
@@ -2,10 +2,11 @@ import validator from 'validator';
|
||||
import { plainToInstance } from 'class-transformer';
|
||||
import { Response } from 'express';
|
||||
import { randomBytes } from 'crypto';
|
||||
|
||||
import { AuthService } from '@/auth/auth.service';
|
||||
import { Authorized, Delete, Get, Patch, Post, RestController } from '@/decorators';
|
||||
import { PasswordUtility } from '@/services/password.utility';
|
||||
import { validateEntity } from '@/GenericHelpers';
|
||||
import { issueCookie } from '@/auth/jwt';
|
||||
import type { User } from '@db/entities/User';
|
||||
import {
|
||||
AuthenticatedRequest,
|
||||
@@ -14,7 +15,7 @@ import {
|
||||
UserUpdatePayload,
|
||||
} from '@/requests';
|
||||
import type { PublicUser } from '@/Interfaces';
|
||||
import { isSamlLicensedAndEnabled } from '../sso/saml/samlHelpers';
|
||||
import { isSamlLicensedAndEnabled } from '@/sso/saml/samlHelpers';
|
||||
import { UserService } from '@/services/user.service';
|
||||
import { Logger } from '@/Logger';
|
||||
import { ExternalHooks } from '@/ExternalHooks';
|
||||
@@ -29,6 +30,7 @@ export class MeController {
|
||||
private readonly logger: Logger,
|
||||
private readonly externalHooks: ExternalHooks,
|
||||
private readonly internalHooks: InternalHooks,
|
||||
private readonly authService: AuthService,
|
||||
private readonly userService: UserService,
|
||||
private readonly passwordUtility: PasswordUtility,
|
||||
private readonly userRepository: UserRepository,
|
||||
@@ -84,7 +86,7 @@ export class MeController {
|
||||
|
||||
this.logger.info('User updated successfully', { userId });
|
||||
|
||||
await issueCookie(res, user);
|
||||
this.authService.issueCookie(res, user);
|
||||
|
||||
const updatedKeys = Object.keys(payload);
|
||||
void this.internalHooks.onUserUpdate({
|
||||
@@ -137,7 +139,7 @@ export class MeController {
|
||||
const updatedUser = await this.userRepository.save(user, { transaction: false });
|
||||
this.logger.info('Password updated successfully', { userId: user.id });
|
||||
|
||||
await issueCookie(res, updatedUser);
|
||||
this.authService.issueCookie(res, updatedUser);
|
||||
|
||||
void this.internalHooks.onUserUpdate({
|
||||
user: updatedUser,
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import validator from 'validator';
|
||||
import { Response } from 'express';
|
||||
|
||||
import { AuthService } from '@/auth/auth.service';
|
||||
import config from '@/config';
|
||||
import { validateEntity } from '@/GenericHelpers';
|
||||
import { Authorized, Post, RestController } from '@/decorators';
|
||||
import { PasswordUtility } from '@/services/password.utility';
|
||||
import { issueCookie } from '@/auth/jwt';
|
||||
import { OwnerRequest } from '@/requests';
|
||||
import { SettingsRepository } from '@db/repositories/settings.repository';
|
||||
import { UserRepository } from '@db/repositories/user.repository';
|
||||
import { PostHogClient } from '@/posthog';
|
||||
import { UserService } from '@/services/user.service';
|
||||
import { Logger } from '@/Logger';
|
||||
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
|
||||
import { InternalHooks } from '@/InternalHooks';
|
||||
import { UserRepository } from '@/databases/repositories/user.repository';
|
||||
|
||||
@Authorized('global:owner')
|
||||
@RestController('/owner')
|
||||
@@ -22,6 +22,7 @@ export class OwnerController {
|
||||
private readonly logger: Logger,
|
||||
private readonly internalHooks: InternalHooks,
|
||||
private readonly settingsRepository: SettingsRepository,
|
||||
private readonly authService: AuthService,
|
||||
private readonly userService: UserService,
|
||||
private readonly passwordUtility: PasswordUtility,
|
||||
private readonly postHog: PostHogClient,
|
||||
@@ -89,7 +90,7 @@ export class OwnerController {
|
||||
|
||||
this.logger.debug('Setting isInstanceOwnerSetUp updated successfully', { userId });
|
||||
|
||||
await issueCookie(res, owner);
|
||||
this.authService.issueCookie(res, owner);
|
||||
|
||||
void this.internalHooks.onInstanceOwnerSetup({ user_id: userId });
|
||||
|
||||
|
||||
@@ -2,11 +2,11 @@ import { Response } from 'express';
|
||||
import { rateLimit } from 'express-rate-limit';
|
||||
import validator from 'validator';
|
||||
|
||||
import { AuthService } from '@/auth/auth.service';
|
||||
import { Get, Post, RestController } from '@/decorators';
|
||||
import { PasswordUtility } from '@/services/password.utility';
|
||||
import { UserManagementMailer } from '@/UserManagement/email';
|
||||
import { PasswordResetRequest } from '@/requests';
|
||||
import { issueCookie } from '@/auth/jwt';
|
||||
import { isSamlCurrentAuthenticationMethod } from '@/sso/ssoHelpers';
|
||||
import { UserService } from '@/services/user.service';
|
||||
import { License } from '@/License';
|
||||
@@ -36,6 +36,7 @@ export class PasswordResetController {
|
||||
private readonly externalHooks: ExternalHooks,
|
||||
private readonly internalHooks: InternalHooks,
|
||||
private readonly mailer: UserManagementMailer,
|
||||
private readonly authService: AuthService,
|
||||
private readonly userService: UserService,
|
||||
private readonly mfaService: MfaService,
|
||||
private readonly urlService: UrlService,
|
||||
@@ -114,7 +115,7 @@ export class PasswordResetController {
|
||||
throw new UnprocessableRequestError('forgotPassword.ldapUserPasswordResetUnavailable');
|
||||
}
|
||||
|
||||
const url = this.userService.generatePasswordResetUrl(user);
|
||||
const url = this.authService.generatePasswordResetUrl(user);
|
||||
|
||||
const { id, firstName, lastName } = user;
|
||||
try {
|
||||
@@ -163,7 +164,7 @@ export class PasswordResetController {
|
||||
throw new BadRequestError('');
|
||||
}
|
||||
|
||||
const user = await this.userService.resolvePasswordResetToken(token);
|
||||
const user = await this.authService.resolvePasswordResetToken(token);
|
||||
if (!user) throw new NotFoundError('');
|
||||
|
||||
if (!user?.isOwner && !this.license.isWithinUsersLimit()) {
|
||||
@@ -197,7 +198,7 @@ export class PasswordResetController {
|
||||
|
||||
const validPassword = this.passwordUtility.validate(password);
|
||||
|
||||
const user = await this.userService.resolvePasswordResetToken(token);
|
||||
const user = await this.authService.resolvePasswordResetToken(token);
|
||||
if (!user) throw new NotFoundError('');
|
||||
|
||||
if (user.mfaEnabled) {
|
||||
@@ -216,7 +217,7 @@ export class PasswordResetController {
|
||||
|
||||
this.logger.info('User password updated successfully', { userId: user.id });
|
||||
|
||||
await issueCookie(res, user);
|
||||
this.authService.issueCookie(res, user);
|
||||
|
||||
void this.internalHooks.onUserUpdate({
|
||||
user,
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import { plainToInstance } from 'class-transformer';
|
||||
|
||||
import { AuthService } from '@/auth/auth.service';
|
||||
import { User } from '@db/entities/User';
|
||||
import { SharedCredentials } from '@db/entities/SharedCredentials';
|
||||
import { SharedWorkflow } from '@db/entities/SharedWorkflow';
|
||||
@@ -22,7 +25,6 @@ import { AuthIdentity } from '@db/entities/AuthIdentity';
|
||||
import { SharedCredentialsRepository } from '@db/repositories/sharedCredentials.repository';
|
||||
import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository';
|
||||
import { UserRepository } from '@db/repositories/user.repository';
|
||||
import { plainToInstance } from 'class-transformer';
|
||||
import { UserService } from '@/services/user.service';
|
||||
import { listQueryMiddleware } from '@/middlewares';
|
||||
import { Logger } from '@/Logger';
|
||||
@@ -44,6 +46,7 @@ export class UsersController {
|
||||
private readonly sharedWorkflowRepository: SharedWorkflowRepository,
|
||||
private readonly userRepository: UserRepository,
|
||||
private readonly activeWorkflowRunner: ActiveWorkflowRunner,
|
||||
private readonly authService: AuthService,
|
||||
private readonly userService: UserService,
|
||||
) {}
|
||||
|
||||
@@ -116,7 +119,7 @@ export class UsersController {
|
||||
throw new NotFoundError('User not found');
|
||||
}
|
||||
|
||||
const link = this.userService.generatePasswordResetUrl(user);
|
||||
const link = this.authService.generatePasswordResetUrl(user);
|
||||
return { link };
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import { Container } from 'typedi';
|
||||
import { Router } from 'express';
|
||||
import type { Application, Request, Response, RequestHandler } from 'express';
|
||||
import type { Scope } from '@n8n/permissions';
|
||||
import { ApplicationError } from 'n8n-workflow';
|
||||
import type { Class } from 'n8n-core';
|
||||
|
||||
import { AuthService } from '@/auth/auth.service';
|
||||
import config from '@/config';
|
||||
import type { BooleanLicenseFeature } from '@/Interfaces';
|
||||
import { License } from '@/License';
|
||||
import type { AuthenticatedRequest } from '@/requests';
|
||||
import { send } from '@/ResponseHelper'; // TODO: move `ResponseHelper.send` to this file
|
||||
import {
|
||||
@@ -15,7 +20,6 @@ import {
|
||||
CONTROLLER_ROUTES,
|
||||
} from './constants';
|
||||
import type {
|
||||
AuthRole,
|
||||
AuthRoleMetadata,
|
||||
Controller,
|
||||
LicenseMetadata,
|
||||
@@ -23,23 +27,6 @@ import type {
|
||||
RouteMetadata,
|
||||
ScopeMetadata,
|
||||
} from './types';
|
||||
import type { BooleanLicenseFeature } from '@/Interfaces';
|
||||
|
||||
import { License } from '@/License';
|
||||
import type { Scope } from '@n8n/permissions';
|
||||
import { ApplicationError } from 'n8n-workflow';
|
||||
|
||||
export const createAuthMiddleware =
|
||||
(authRole: AuthRole): RequestHandler =>
|
||||
({ user }: AuthenticatedRequest, res, next) => {
|
||||
if (authRole === 'none') return next();
|
||||
|
||||
if (!user) return res.status(401).json({ status: 'error', message: 'Unauthorized' });
|
||||
|
||||
if (authRole === 'any' || authRole === user.role) return next();
|
||||
|
||||
res.status(403).json({ status: 'error', message: 'Unauthorized' });
|
||||
};
|
||||
|
||||
export const createLicenseMiddleware =
|
||||
(features: BooleanLicenseFeature[]): RequestHandler =>
|
||||
@@ -77,11 +64,6 @@ export const createGlobalScopeMiddleware =
|
||||
return next();
|
||||
};
|
||||
|
||||
const authFreeRoutes: string[] = [];
|
||||
|
||||
export const canSkipAuth = (method: string, path: string): boolean =>
|
||||
authFreeRoutes.includes(`${method.toLowerCase()} ${path}`);
|
||||
|
||||
export const registerController = (app: Application, controllerClass: Class<object>) => {
|
||||
const controller = Container.get(controllerClass as Class<Controller>);
|
||||
const controllerBasePath = Reflect.getMetadata(CONTROLLER_BASE_PATH, controllerClass) as
|
||||
@@ -114,6 +96,8 @@ export const registerController = (app: Application, controllerClass: Class<obje
|
||||
(Reflect.getMetadata(CONTROLLER_MIDDLEWARES, controllerClass) ?? []) as MiddlewareMetadata[]
|
||||
).map(({ handlerName }) => controller[handlerName].bind(controller) as RequestHandler);
|
||||
|
||||
const authService = Container.get(AuthService);
|
||||
|
||||
routes.forEach(
|
||||
({ method, path, middlewares: routeMiddlewares, handlerName, usesTemplates }) => {
|
||||
const authRole = authRoles?.[handlerName] ?? authRoles?.['*'];
|
||||
@@ -123,14 +107,13 @@ export const registerController = (app: Application, controllerClass: Class<obje
|
||||
await controller[handlerName](req, res);
|
||||
router[method](
|
||||
path,
|
||||
...(authRole ? [createAuthMiddleware(authRole)] : []),
|
||||
...(authRole ? [authService.createAuthMiddleware(authRole)] : []),
|
||||
...(features ? [createLicenseMiddleware(features)] : []),
|
||||
...(scopes ? [createGlobalScopeMiddleware(scopes)] : []),
|
||||
...controllerMiddlewares,
|
||||
...routeMiddlewares,
|
||||
usesTemplates ? handler : send(handler),
|
||||
);
|
||||
if (!authRole || authRole === 'none') authFreeRoutes.push(`${method} ${prefix}${path}`);
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
import type { Application, NextFunction, Request, RequestHandler, Response } from 'express';
|
||||
import { Container } from 'typedi';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import passport from 'passport';
|
||||
import { Strategy } from 'passport-jwt';
|
||||
import { sync as globSync } from 'fast-glob';
|
||||
import type { JwtPayload } from '@/Interfaces';
|
||||
import type { AuthenticatedRequest } from '@/requests';
|
||||
import { AUTH_COOKIE_NAME, EDITOR_UI_DIST_DIR } from '@/constants';
|
||||
import { issueCookie, resolveJwtContent } from '@/auth/jwt';
|
||||
import { canSkipAuth } from '@/decorators/registerController';
|
||||
import { Logger } from '@/Logger';
|
||||
import { JwtService } from '@/services/jwt.service';
|
||||
import config from '@/config';
|
||||
|
||||
const jwtFromRequest = (req: Request) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
return (req.cookies?.[AUTH_COOKIE_NAME] as string | undefined) ?? null;
|
||||
};
|
||||
|
||||
const userManagementJwtAuth = (): RequestHandler => {
|
||||
const jwtStrategy = new Strategy(
|
||||
{
|
||||
jwtFromRequest,
|
||||
secretOrKey: Container.get(JwtService).jwtSecret,
|
||||
},
|
||||
async (jwtPayload: JwtPayload, done) => {
|
||||
try {
|
||||
const user = await resolveJwtContent(jwtPayload);
|
||||
return done(null, user);
|
||||
} catch (error) {
|
||||
Container.get(Logger).debug('Failed to extract user from JWT payload', { jwtPayload });
|
||||
return done(null, false, { message: 'User not found' });
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
passport.use(jwtStrategy);
|
||||
return passport.initialize();
|
||||
};
|
||||
|
||||
/**
|
||||
* middleware to refresh cookie before it expires
|
||||
*/
|
||||
export const refreshExpiringCookie = (async (req: AuthenticatedRequest, res, next) => {
|
||||
const jwtRefreshTimeoutHours = config.get('userManagement.jwtRefreshTimeoutHours');
|
||||
|
||||
let jwtRefreshTimeoutMilliSeconds: number;
|
||||
|
||||
if (jwtRefreshTimeoutHours === 0) {
|
||||
const jwtSessionDurationHours = config.get('userManagement.jwtSessionDurationHours');
|
||||
|
||||
jwtRefreshTimeoutMilliSeconds = Math.floor(jwtSessionDurationHours * 0.25 * 60 * 60 * 1000);
|
||||
} else {
|
||||
jwtRefreshTimeoutMilliSeconds = Math.floor(jwtRefreshTimeoutHours * 60 * 60 * 1000);
|
||||
}
|
||||
|
||||
const cookieAuth = jwtFromRequest(req);
|
||||
|
||||
if (cookieAuth && req.user && jwtRefreshTimeoutHours !== -1) {
|
||||
const cookieContents = jwt.decode(cookieAuth) as JwtPayload & { exp: number };
|
||||
if (cookieContents.exp * 1000 - Date.now() < jwtRefreshTimeoutMilliSeconds) {
|
||||
await issueCookie(res, req.user);
|
||||
}
|
||||
}
|
||||
next();
|
||||
}) satisfies RequestHandler;
|
||||
|
||||
const passportMiddleware = passport.authenticate('jwt', { session: false }) as RequestHandler;
|
||||
|
||||
const staticAssets = globSync(['**/*.html', '**/*.svg', '**/*.png', '**/*.ico'], {
|
||||
cwd: EDITOR_UI_DIST_DIR,
|
||||
});
|
||||
|
||||
// TODO: delete this
|
||||
const isPostInvitationAccept = (req: Request, restEndpoint: string): boolean =>
|
||||
req.method === 'POST' &&
|
||||
new RegExp(`/${restEndpoint}/invitations/[\\w\\d-]*`).test(req.url) &&
|
||||
req.url.includes('accept');
|
||||
|
||||
const isAuthExcluded = (url: string, ignoredEndpoints: Readonly<string[]>): boolean =>
|
||||
!!ignoredEndpoints
|
||||
.filter(Boolean) // skip empty paths
|
||||
.find((ignoredEndpoint) => url.startsWith(`/${ignoredEndpoint}`));
|
||||
|
||||
/**
|
||||
* This sets up the auth middlewares in the correct order
|
||||
*/
|
||||
export const setupAuthMiddlewares = (
|
||||
app: Application,
|
||||
ignoredEndpoints: Readonly<string[]>,
|
||||
restEndpoint: string,
|
||||
) => {
|
||||
app.use(userManagementJwtAuth());
|
||||
|
||||
app.use(async (req: Request, res: Response, next: NextFunction) => {
|
||||
if (
|
||||
// TODO: refactor me!!!
|
||||
// skip authentication for preflight requests
|
||||
req.method === 'OPTIONS' ||
|
||||
staticAssets.includes(req.url.slice(1)) ||
|
||||
canSkipAuth(req.method, req.path) ||
|
||||
isAuthExcluded(req.url, ignoredEndpoints) ||
|
||||
req.url.startsWith(`/${restEndpoint}/settings`) ||
|
||||
isPostInvitationAccept(req, restEndpoint)
|
||||
) {
|
||||
return next();
|
||||
}
|
||||
|
||||
return passportMiddleware(req, res, next);
|
||||
});
|
||||
|
||||
app.use(refreshExpiringCookie);
|
||||
};
|
||||
@@ -1,4 +1,3 @@
|
||||
export * from './auth';
|
||||
export * from './bodyParser';
|
||||
export * from './cors';
|
||||
export * from './listQuery';
|
||||
|
||||
@@ -7,14 +7,13 @@ import { Server as WSServer } from 'ws';
|
||||
import { parse as parseUrl } from 'url';
|
||||
import { Container, Service } from 'typedi';
|
||||
import config from '@/config';
|
||||
import { resolveJwt } from '@/auth/jwt';
|
||||
import { AUTH_COOKIE_NAME } from '@/constants';
|
||||
import { SSEPush } from './sse.push';
|
||||
import { WebSocketPush } from './websocket.push';
|
||||
import type { PushResponse, SSEPushRequest, WebSocketPushRequest } from './types';
|
||||
import type { IPushDataType } from '@/Interfaces';
|
||||
import type { User } from '@db/entities/User';
|
||||
import { OnShutdown } from '@/decorators/OnShutdown';
|
||||
import { AuthService } from '@/auth/auth.service';
|
||||
|
||||
const useWebSockets = config.getEnv('push.backend') === 'websocket';
|
||||
|
||||
@@ -120,27 +119,15 @@ export const setupPushHandler = (restEndpoint: string, app: Application) => {
|
||||
}
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
|
||||
const authCookie: string = req.cookies?.[AUTH_COOKIE_NAME] ?? '';
|
||||
const user = await resolveJwt(authCookie);
|
||||
req.userId = user.id;
|
||||
} catch (error) {
|
||||
if (ws) {
|
||||
ws.send(`Unauthorized: ${(error as Error).message}`);
|
||||
ws.close(1008);
|
||||
} else {
|
||||
res.status(401).send('Unauthorized');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
const push = Container.get(Push);
|
||||
const authService = Container.get(AuthService);
|
||||
app.use(
|
||||
endpoint,
|
||||
authService.createAuthMiddleware('any'),
|
||||
pushValidationMiddleware,
|
||||
(req: SSEPushRequest | WebSocketPushRequest, res: PushResponse) => push.handleRequest(req, res),
|
||||
);
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import type { User } from '@db/entities/User';
|
||||
import type { Request, Response } from 'express';
|
||||
import type { Response } from 'express';
|
||||
import type { WebSocket } from 'ws';
|
||||
|
||||
import type { User } from '@db/entities/User';
|
||||
import type { AuthenticatedRequest } from '@/requests';
|
||||
|
||||
// TODO: move all push related types here
|
||||
|
||||
export type PushRequest = Request<{}, {}, {}, { sessionId: string }>;
|
||||
export type PushRequest = AuthenticatedRequest<{}, {}, {}, { sessionId: string }>;
|
||||
|
||||
export type SSEPushRequest = PushRequest & { ws: undefined; userId: User['id'] };
|
||||
export type WebSocketPushRequest = PushRequest & { ws: WebSocket; userId: User['id'] };
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { ParsedQs } from 'qs';
|
||||
import type express from 'express';
|
||||
import type {
|
||||
BannerName,
|
||||
@@ -20,6 +21,17 @@ import type { WorkflowEntity } from '@db/entities/WorkflowEntity';
|
||||
import type { CredentialsEntity } from '@db/entities/CredentialsEntity';
|
||||
import type { WorkflowHistory } from '@db/entities/WorkflowHistory';
|
||||
|
||||
export type AsyncRequestHandler<
|
||||
Params = Record<string, string>,
|
||||
ResBody = unknown,
|
||||
ReqBody = unknown,
|
||||
ReqQuery = ParsedQs,
|
||||
> = (
|
||||
req: express.Request<Params, ResBody, ReqBody, ReqQuery>,
|
||||
res: express.Response<ResBody>,
|
||||
next: () => void,
|
||||
) => Promise<void>;
|
||||
|
||||
export class UserUpdatePayload implements Pick<User, 'email' | 'firstName' | 'lastName'> {
|
||||
@IsEmail()
|
||||
email: string;
|
||||
@@ -62,8 +74,12 @@ export type AuthenticatedRequest<
|
||||
ResponseBody = {},
|
||||
RequestBody = {},
|
||||
RequestQuery = {},
|
||||
> = Omit<express.Request<RouteParams, ResponseBody, RequestBody, RequestQuery>, 'user'> & {
|
||||
> = Omit<
|
||||
express.Request<RouteParams, ResponseBody, RequestBody, RequestQuery>,
|
||||
'user' | 'cookies'
|
||||
> & {
|
||||
user: User;
|
||||
cookies: Record<string, string | undefined>;
|
||||
};
|
||||
|
||||
// ----------------------------------
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
import { Container, Service } from 'typedi';
|
||||
import { type AssignableRole, User } from '@db/entities/User';
|
||||
import type { IUserSettings } from 'n8n-workflow';
|
||||
import { ApplicationError, ErrorReporterProxy as ErrorReporter } from 'n8n-workflow';
|
||||
|
||||
import { type AssignableRole, User } from '@db/entities/User';
|
||||
import { UserRepository } from '@db/repositories/user.repository';
|
||||
import type { PublicUser } from '@/Interfaces';
|
||||
import type { PostHogClient } from '@/posthog';
|
||||
import { type JwtPayload, JwtService } from './jwt.service';
|
||||
import { TokenExpiredError } from 'jsonwebtoken';
|
||||
import { Logger } from '@/Logger';
|
||||
import { createPasswordSha } from '@/auth/jwt';
|
||||
import { UserManagementMailer } from '@/UserManagement/email';
|
||||
import { InternalHooks } from '@/InternalHooks';
|
||||
import { UrlService } from '@/services/url.service';
|
||||
import { ApplicationError, ErrorReporterProxy as ErrorReporter } from 'n8n-workflow';
|
||||
import type { UserRequest } from '@/requests';
|
||||
import { InternalServerError } from '@/errors/response-errors/internal-server.error';
|
||||
|
||||
@@ -20,7 +18,6 @@ export class UserService {
|
||||
constructor(
|
||||
private readonly logger: Logger,
|
||||
private readonly userRepository: UserRepository,
|
||||
private readonly jwtService: JwtService,
|
||||
private readonly mailer: UserManagementMailer,
|
||||
private readonly urlService: UrlService,
|
||||
) {}
|
||||
@@ -39,57 +36,6 @@ export class UserService {
|
||||
return await this.userRepository.update(userId, { settings: { ...settings, ...newSettings } });
|
||||
}
|
||||
|
||||
generatePasswordResetToken(user: User, expiresIn = '20m') {
|
||||
return this.jwtService.sign(
|
||||
{ sub: user.id, passwordSha: createPasswordSha(user) },
|
||||
{ expiresIn },
|
||||
);
|
||||
}
|
||||
|
||||
generatePasswordResetUrl(user: 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: string): Promise<User | undefined> {
|
||||
let decodedToken: JwtPayload & { passwordSha: string };
|
||||
try {
|
||||
decodedToken = this.jwtService.verify(token);
|
||||
} catch (e) {
|
||||
if (e instanceof TokenExpiredError) {
|
||||
this.logger.debug('Reset password token expired', { token });
|
||||
} else {
|
||||
this.logger.debug('Error verifying token', { token });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const user = await this.userRepository.findOne({
|
||||
where: { id: decodedToken.sub },
|
||||
relations: ['authIdentities'],
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
this.logger.debug(
|
||||
'Request to resolve password token failed because no user was found for the provided user ID',
|
||||
{ userId: decodedToken.sub, token },
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (createPasswordSha(user) !== decodedToken.passwordSha) {
|
||||
this.logger.debug('Password updated since this token was generated');
|
||||
return;
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
async toPublic(
|
||||
user: User,
|
||||
options?: {
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import express from 'express';
|
||||
import { validate } from 'class-validator';
|
||||
import type { PostBindingContext } from 'samlify/types/src/entity';
|
||||
import url from 'url';
|
||||
|
||||
import {
|
||||
Authorized,
|
||||
Get,
|
||||
@@ -7,20 +11,16 @@ import {
|
||||
RestController,
|
||||
RequireGlobalScope,
|
||||
} from '@/decorators';
|
||||
import { SamlUrls } from '../constants';
|
||||
import {
|
||||
samlLicensedAndEnabledMiddleware,
|
||||
samlLicensedMiddleware,
|
||||
} from '../middleware/samlEnabledMiddleware';
|
||||
import { SamlService } from '../saml.service.ee';
|
||||
import { SamlConfiguration } from '../types/requests';
|
||||
import { getInitSSOFormView } from '../views/initSsoPost';
|
||||
import { issueCookie } from '@/auth/jwt';
|
||||
import { validate } from 'class-validator';
|
||||
import type { PostBindingContext } from 'samlify/types/src/entity';
|
||||
import { isConnectionTestRequest, isSamlLicensedAndEnabled } from '../samlHelpers';
|
||||
import type { SamlLoginBinding } from '../types';
|
||||
|
||||
import { AuthService } from '@/auth/auth.service';
|
||||
import { AuthenticatedRequest } from '@/requests';
|
||||
import { InternalHooks } from '@/InternalHooks';
|
||||
import querystring from 'querystring';
|
||||
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
|
||||
import { AuthError } from '@/errors/response-errors/auth.error';
|
||||
import { UrlService } from '@/services/url.service';
|
||||
|
||||
import { SamlUrls } from '../constants';
|
||||
import {
|
||||
getServiceProviderConfigTestReturnUrl,
|
||||
getServiceProviderEntityId,
|
||||
@@ -28,17 +28,21 @@ import {
|
||||
} from '../serviceProvider.ee';
|
||||
import { getSamlConnectionTestSuccessView } from '../views/samlConnectionTestSuccess';
|
||||
import { getSamlConnectionTestFailedView } from '../views/samlConnectionTestFailed';
|
||||
import { InternalHooks } from '@/InternalHooks';
|
||||
import url from 'url';
|
||||
import querystring from 'querystring';
|
||||
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
|
||||
import { AuthError } from '@/errors/response-errors/auth.error';
|
||||
import { UrlService } from '@/services/url.service';
|
||||
import { isConnectionTestRequest, isSamlLicensedAndEnabled } from '../samlHelpers';
|
||||
import type { SamlLoginBinding } from '../types';
|
||||
import {
|
||||
samlLicensedAndEnabledMiddleware,
|
||||
samlLicensedMiddleware,
|
||||
} from '../middleware/samlEnabledMiddleware';
|
||||
import { SamlService } from '../saml.service.ee';
|
||||
import { SamlConfiguration } from '../types/requests';
|
||||
import { getInitSSOFormView } from '../views/initSsoPost';
|
||||
|
||||
@Authorized()
|
||||
@RestController('/sso/saml')
|
||||
export class SamlController {
|
||||
constructor(
|
||||
private readonly authService: AuthService,
|
||||
private readonly samlService: SamlService,
|
||||
private readonly urlService: UrlService,
|
||||
private readonly internalHooks: InternalHooks,
|
||||
@@ -46,7 +50,7 @@ export class SamlController {
|
||||
|
||||
@NoAuthRequired()
|
||||
@Get(SamlUrls.metadata)
|
||||
async getServiceProviderMetadata(req: express.Request, res: express.Response) {
|
||||
async getServiceProviderMetadata(_: express.Request, res: express.Response) {
|
||||
return res
|
||||
.header('Content-Type', 'text/xml')
|
||||
.send(this.samlService.getServiceProviderInstance().getMetadata());
|
||||
@@ -147,7 +151,7 @@ export class SamlController {
|
||||
});
|
||||
// Only sign in user if SAML is enabled, otherwise treat as test connection
|
||||
if (isSamlLicensedAndEnabled()) {
|
||||
await issueCookie(res, loginResult.authenticatedUser);
|
||||
this.authService.issueCookie(res, loginResult.authenticatedUser);
|
||||
if (loginResult.onboardingRequired) {
|
||||
return res.redirect(this.urlService.getInstanceBaseUrl() + SamlUrls.samlOnboarding);
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user