Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion backend/src/authorization/auth.middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { Messages } from '../exceptions/text/messages.js';
import { isObjectEmpty } from '../helpers/index.js';
import { Constants } from '../helpers/constants/constants.js';
import Sentry from '@sentry/minimal';
import { JwtScopesEnum } from '../entities/user/enums/jwt-scopes.enum.js';

@Injectable()
export class AuthMiddleware implements NestMiddleware {
Expand Down Expand Up @@ -64,6 +65,19 @@ export class AuthMiddleware implements NestMiddleware {
// HttpStatus.UNAUTHORIZED,
// );
// }

const addedScope: Array<JwtScopesEnum> = data['scope'];
if (addedScope && addedScope.length > 0) {
if (addedScope.includes(JwtScopesEnum.TWO_FA_ENABLE)) {
throw new HttpException(
{
message: Messages.TWO_FA_REQUIRED,
},
HttpStatus.UNAUTHORIZED,
);
}
}

const payload = {
sub: userId,
email: data['email'],
Expand All @@ -79,7 +93,7 @@ export class AuthMiddleware implements NestMiddleware {
Sentry.captureException(e);
throw new HttpException(
{
message: Messages.AUTHORIZATION_REJECTED,
message: e.message === Messages.TWO_FA_REQUIRED ? e.message : Messages.AUTHORIZATION_REJECTED,
},
HttpStatus.UNAUTHORIZED,
);
Expand Down
86 changes: 86 additions & 0 deletions backend/src/authorization/non-scoped-auth.middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { HttpException, HttpStatus, Injectable, NestMiddleware } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Request, Response } from 'express';
import jwt from 'jsonwebtoken';
import { Repository } from 'typeorm';
import { LogOutEntity } from '../entities/log-out/log-out.entity.js';
import { Messages } from '../exceptions/text/messages.js';
import { isObjectEmpty } from '../helpers/index.js';
import { Constants } from '../helpers/constants/constants.js';
import Sentry from '@sentry/minimal';

@Injectable()
export class NonScopedAuthMiddleware implements NestMiddleware {
public constructor(
@InjectRepository(LogOutEntity)
private readonly logOutRepository: Repository<LogOutEntity>,
) {}
async use(req: Request, res: Response, next: (err?: any, res?: any) => void): Promise<void> {
console.log(`auth middleware triggered ->: ${new Date().toISOString()}`);
let token: string;
try {
token = req.cookies[Constants.JWT_COOKIE_KEY_NAME];
} catch (e) {
if (process.env.NODE_ENV !== 'test') {
throw new HttpException(
{
message: 'JWT verification failed',
},
HttpStatus.UNAUTHORIZED,
);
}
}

if (!token) {
throw new HttpException('Token is missing', HttpStatus.UNAUTHORIZED);
}

const isLoggedOut = !!(await this.logOutRepository.findOne({ where: { jwtToken: token } }));
if (isLoggedOut) {
throw new HttpException(
{
message: 'JWT verification failed',
},
HttpStatus.UNAUTHORIZED,
);
}

try {
const jwtSecret = process.env.JWT_SECRET;
const data = jwt.verify(token, jwtSecret);
const userId = data['id'];
if (!userId) {
throw new Error('JWT verification failed');
}
// const foundUser = await this.userRepository.findOne({ where: { id: userId } });
// if (!foundUser) {
// throw new HttpException(
// {
// message: Messages.USER_NOT_FOUND,
// },
// HttpStatus.UNAUTHORIZED,
// );
// }

const payload = {
sub: userId,
email: data['email'],
exp: data['exp'],
iat: data['iat'],
};
if (!payload || isObjectEmpty(payload)) {
throw new Error('JWT verification failed');
}
req['decoded'] = payload;
next();
} catch (e) {
Sentry.captureException(e);
throw new HttpException(
{
message: Messages.AUTHORIZATION_REJECTED,
},
HttpStatus.UNAUTHORIZED,
);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { IVerifyInviteUserInCompanyAndConnectionGroup } from './company-info-use
import { Messages } from '../../../exceptions/text/messages.js';
import { AcceptUserValidationInCompany } from '../application/data-structures/accept-user-invitation-in-company.ds.js';
import { isSaaS } from '../../../helpers/app/is-saas.js';
import { get2FaScope } from '../../user/utils/is-jwt-scope-need.util.js';

@Injectable({ scope: Scope.REQUEST })
export class VerifyInviteUserInCompanyAndConnectionGroupUseCase
Expand Down Expand Up @@ -51,11 +52,11 @@ export class VerifyInviteUserInCompanyAndConnectionGroupUseCase
HttpStatus.BAD_REQUEST,
);
}
if (foundUser && !foundUser.isActive) {
if (foundUser) {
foundUser.isActive = true;
foundUser.role = role;
await this._dbContext.userRepository.saveUserEntity(foundUser);
return generateGwtToken(foundUser);
return generateGwtToken(foundUser, get2FaScope(foundUser, foundInvitation.company));
}
const newUser = await this._dbContext.userRepository.saveRegisteringUser({
email: invitedUserEmail,
Expand Down Expand Up @@ -100,6 +101,6 @@ export class VerifyInviteUserInCompanyAndConnectionGroupUseCase
);
}
}
return generateGwtToken(newUser);
return generateGwtToken(newUser, get2FaScope(newUser, foundInvitation.company));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { UserHelperService } from '../../user/user-helper.service.js';
import { generateGwtToken, IToken } from '../../user/utils/generate-gwt-token.js';
import { VerifyAddUserInGroupDs } from '../application/data-sctructures/verify-add-user-in-group.ds.js';
import { IVerifyAddUserInGroup } from './use-cases.interfaces.js';
import { get2FaScope } from '../../user/utils/is-jwt-scope-need.util.js';

@Injectable()
export class VerifyAddUserInGroupUseCase
Expand Down Expand Up @@ -48,14 +49,15 @@ export class VerifyAddUserInGroupUseCase
HttpStatus.BAD_REQUEST,
);
}
const foundCompany = await this._dbContext.companyInfoRepository.findCompanyInfoByUserId(invitationEntity.ownerId);
if (!invitationEntity.ownerId) {
const foundUser = await this._dbContext.userRepository.findOneUserById(invitationEntity.user.id);
foundUser.isActive = true;
foundUser.password = await Encryptor.hashUserPassword(user_password);
foundUser.name = user_name;
const savedUser = await this._dbContext.userRepository.saveUserEntity(foundUser);
await this._dbContext.userInvitationRepository.removeInvitationEntity(invitationEntity);
return generateGwtToken(savedUser);
return generateGwtToken(savedUser, get2FaScope(savedUser, foundCompany));
}

const foundUser = await this._dbContext.userRepository.findOneUserById(invitationEntity.user.id);
Expand All @@ -64,6 +66,6 @@ export class VerifyAddUserInGroupUseCase
foundUser.name = user_name;
const savedUser = await this._dbContext.userRepository.saveUserEntity(foundUser);
await this._dbContext.userInvitationRepository.removeInvitationEntity(invitationEntity);
return generateGwtToken(savedUser);
return generateGwtToken(savedUser, get2FaScope(savedUser, foundCompany));
}
}
3 changes: 3 additions & 0 deletions backend/src/entities/user/enums/jwt-scopes.enum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export enum JwtScopesEnum {
TWO_FA_ENABLE = '2fa_enable',
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { Encryptor } from '../../../helpers/encryption/encryptor.js';
import { ChangeUsualUserPasswordDs } from '../application/data-structures/change-usual-user-password.ds.js';
import { generateGwtToken, IToken } from '../utils/generate-gwt-token.js';
import { IUsualPasswordChange } from './user-use-cases.interfaces.js';
import { get2FaScope } from '../utils/is-jwt-scope-need.util.js';

@Injectable()
export class ChangeUsualPasswordUseCase
Expand Down Expand Up @@ -41,6 +42,7 @@ export class ChangeUsualPasswordUseCase
}
user.password = await Encryptor.hashUserPassword(userData.newPassword);
const updatedUser = await this._dbContext.userRepository.saveUserEntity(user);
return generateGwtToken(updatedUser);
const foundUserCompany = await this._dbContext.companyInfoRepository.finOneCompanyInfoByUserId(updatedUser.id);
return generateGwtToken(updatedUser, get2FaScope(updatedUser, foundUserCompany));
}
}
5 changes: 3 additions & 2 deletions backend/src/entities/user/use-cases/otp-login-use.case.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { IToken, generateGwtToken } from '../utils/generate-gwt-token.js';
import { Messages } from '../../../exceptions/text/messages.js';
import { VerifyOtpDS } from '../application/data-structures/verify-otp.ds.js';
import { authenticator } from 'otplib';
import { get2FaScope } from '../utils/is-jwt-scope-need.util.js';

@Injectable()
export class OtpLoginUseCase extends AbstractUseCase<VerifyOtpDS, IToken> implements IOtpLogin {
Expand Down Expand Up @@ -37,7 +38,7 @@ export class OtpLoginUseCase extends AbstractUseCase<VerifyOtpDS, IToken> implem
HttpStatus.UNAUTHORIZED,
);
}

return generateGwtToken(foundUser);
const foundUserCompany = await this._dbContext.companyInfoRepository.finOneCompanyInfoByUserId(foundUser.id);
return generateGwtToken(foundUser, get2FaScope(foundUser, foundUserCompany));
}
}
4 changes: 3 additions & 1 deletion backend/src/entities/user/use-cases/usual-login-use.case.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { UsualLoginDs } from '../application/data-structures/usual-login.ds.js';
import { generateGwtToken, generateTemporaryJwtToken, IToken } from '../utils/generate-gwt-token.js';
import { IUsualLogin } from './user-use-cases.interfaces.js';
import { UserEntity } from '../user.entity.js';
import { get2FaScope } from '../utils/is-jwt-scope-need.util.js';

@Injectable()
export class UsualLoginUseCase extends AbstractUseCase<UsualLoginDs, IToken> implements IUsualLogin {
Expand Down Expand Up @@ -73,6 +74,7 @@ export class UsualLoginUseCase extends AbstractUseCase<UsualLoginDs, IToken> imp
if (user.isOTPEnabled) {
return generateTemporaryJwtToken(user);
}
return generateGwtToken(user);
const foundUserCompany = await this._dbContext.companyInfoRepository.finOneCompanyInfoByUserId(user.id);
return generateGwtToken(user, get2FaScope(user, foundUserCompany));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { RegisteredUserDs } from '../application/data-structures/registered-user
import { ResetUsualUserPasswordDs } from '../application/data-structures/reset-usual-user-password.ds.js';
import { generateGwtToken } from '../utils/generate-gwt-token.js';
import { IVerifyPasswordReset } from './user-use-cases.interfaces.js';
import { get2FaScope } from '../utils/is-jwt-scope-need.util.js';

@Injectable()
export class VerifyResetUserPasswordUseCase
Expand All @@ -25,9 +26,8 @@ export class VerifyResetUserPasswordUseCase
protected async implementation(inputData: ResetUsualUserPasswordDs): Promise<RegisteredUserDs> {
const { verificationString, newUserPassword } = inputData;
ValidationHelper.isPasswordStrongOrThrowError(newUserPassword);
const verificationEntity = await this._dbContext.passwordResetRepository.findPasswordResetWidthVerificationString(
verificationString,
);
const verificationEntity =
await this._dbContext.passwordResetRepository.findPasswordResetWidthVerificationString(verificationString);
if (!verificationEntity || !verificationEntity.user) {
throw new HttpException(
{
Expand All @@ -48,10 +48,11 @@ export class VerifyResetUserPasswordUseCase
foundUser.password = await Encryptor.hashUserPassword(newUserPassword);
await this._dbContext.userRepository.saveUserEntity(foundUser);
await this._dbContext.passwordResetRepository.removePasswordResetEntity(verificationEntity);
const foundUserCompany = await this._dbContext.companyInfoRepository.finOneCompanyInfoByUserId(foundUser.id);
return {
id: foundUser.id,
email: foundUser.email,
token: generateGwtToken(foundUser),
token: generateGwtToken(foundUser, get2FaScope(foundUser, foundUserCompany)),
name: foundUser.name,
};
}
Expand Down
12 changes: 8 additions & 4 deletions backend/src/entities/user/user.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import { DisableOtpUseCase } from './use-cases/disable-otp.use.case.js';
import { GetUserSessionSettingsUseCase } from './use-cases/get-user-session-settings.use.case.js';
import { SaveUserSettingsUseCase } from './use-cases/save-user-session-settings.use.case.js';
import { CompanyInfoEntity } from '../company-info/company-info.entity.js';
import { NonScopedAuthMiddleware } from '../../authorization/non-scoped-auth.middleware.js';

@Module({
imports: [
Expand Down Expand Up @@ -149,13 +150,16 @@ export class UserModule implements NestModule {
{ path: 'user/email/verify/request', method: RequestMethod.GET },
{ path: 'user/delete/', method: RequestMethod.PUT },
{ path: 'user/email/change/request', method: RequestMethod.GET },
{ path: 'user/otp/generate', method: RequestMethod.POST },
{ path: 'user/otp/verify', method: RequestMethod.POST },
{ path: 'user/otp/disable', method: RequestMethod.POST },
{ path: 'user/settings', method: RequestMethod.POST },
{ path: 'user/settings', method: RequestMethod.GET },
)
.apply(TemporaryAuthMiddleware)
.forRoutes({ path: 'user/otp/login', method: RequestMethod.POST });
.forRoutes({ path: 'user/otp/login', method: RequestMethod.POST })
.apply(NonScopedAuthMiddleware)
.forRoutes(
{ path: 'user/otp/generate', method: RequestMethod.POST },
{ path: 'user/otp/verify', method: RequestMethod.POST },
{ path: 'user/otp/disable', method: RequestMethod.POST },
);
}
}
13 changes: 0 additions & 13 deletions backend/src/entities/user/utils/build-registered-user.ds.ts

This file was deleted.

4 changes: 3 additions & 1 deletion backend/src/entities/user/utils/generate-gwt-token.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import jwt from 'jsonwebtoken';
import { UserEntity } from '../user.entity.js';
import { ApiProperty } from '@nestjs/swagger';
import { JwtScopesEnum } from '../enums/jwt-scopes.enum.js';

export function generateGwtToken(user: UserEntity): IToken {
export function generateGwtToken(user: UserEntity, scope: Array<JwtScopesEnum>): IToken {
const today = new Date();
const exp = new Date(today);
exp.setTime(today.getTime() + 60 * 60 * 1000 * 24 * 7);
Expand All @@ -12,6 +13,7 @@ export function generateGwtToken(user: UserEntity): IToken {
id: user.id,
email: user.email,
exp: Math.floor(exp.getTime() / 1000),
scope: scope ? scope : undefined,
},
jwtSecret,
);
Expand Down
11 changes: 11 additions & 0 deletions backend/src/entities/user/utils/is-jwt-scope-need.util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { CompanyInfoEntity } from '../../company-info/company-info.entity.js';
import { JwtScopesEnum } from '../enums/jwt-scopes.enum.js';
import { UserEntity } from '../user.entity.js';

export function isJwt2faScopeNeed(user: UserEntity, userCompany: CompanyInfoEntity): boolean {
return userCompany?.is2faEnabled && !user.isOTPEnabled;
}

export function get2FaScope(user: UserEntity, userCompany: CompanyInfoEntity): Array<JwtScopesEnum | null> {
return isJwt2faScopeNeed(user, userCompany) ? [JwtScopesEnum.TWO_FA_ENABLE] : null;
}
1 change: 1 addition & 0 deletions backend/src/exceptions/text/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@ export const Messages = {
TRY_VERIFY_ADD_USER_WHEN_LOGGED_IN: `You can't join a group when you are logged in as another user. Please log out and try again.`,
TYPE_MISSING: 'Type is missing',
TOKEN_MISSING: 'Token is missing',
TWO_FA_REQUIRED: `Two factor authentication required in this company according to company settings. Please enable 2fa in your profile settings.`,
UNABLE_FIND_PORT: `Unable to find a free port. Please try again later. If the problem persists, please contact our support team`,
UPDATE_ROW_FAILED: 'Row updating failed',
USER_ALREADY_ADDED: 'User has already been added in this group',
Expand Down
Loading