diff --git a/packages/twenty-server/src/engine/core-modules/auth/auth.exception.ts b/packages/twenty-server/src/engine/core-modules/auth/auth.exception.ts new file mode 100644 index 000000000000..c245db9717fe --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/auth/auth.exception.ts @@ -0,0 +1,17 @@ +import { CustomException } from 'src/utils/custom-exception'; + +export class AuthException extends CustomException { + code: AuthExceptionCode; + constructor(message: string, code: AuthExceptionCode) { + super(message, code); + } +} + +export enum AuthExceptionCode { + USER_NOT_FOUND = 'USER_NOT_FOUND', + CLIENT_NOT_FOUND = 'CLIENT_NOT_FOUND', + INVALID_INPUT = 'INVALID_INPUT', + FORBIDDEN_EXCEPTION = 'FORBIDDEN_EXCEPTION', + INVALID_DATA = 'INVALID_DATA', + INTERNAL_SERVER_ERROR = 'INTERNAL_SERVER_ERROR', +} diff --git a/packages/twenty-server/src/engine/core-modules/auth/auth.resolver.ts b/packages/twenty-server/src/engine/core-modules/auth/auth.resolver.ts index 69f93c019e6f..2987054701a3 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/auth.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/auth.resolver.ts @@ -1,14 +1,5 @@ -import { - BadRequestException, - ForbiddenException, - InternalServerErrorException, - NotFoundException, - UseGuards, -} from '@nestjs/common'; +import { UseFilters, UseGuards } from '@nestjs/common'; import { Args, Mutation, Query, Resolver } from '@nestjs/graphql'; -import { InjectRepository } from '@nestjs/typeorm'; - -import { Repository } from 'typeorm'; import { ApiKeyTokenInput } from 'src/engine/core-modules/auth/dto/api-key-token.input'; import { AppTokenInput } from 'src/engine/core-modules/auth/dto/app-token.input'; @@ -24,6 +15,7 @@ import { TransientToken } from 'src/engine/core-modules/auth/dto/transient-token import { UpdatePasswordViaResetTokenInput } from 'src/engine/core-modules/auth/dto/update-password-via-reset-token.input'; import { ValidatePasswordResetToken } from 'src/engine/core-modules/auth/dto/validate-password-reset-token.entity'; import { ValidatePasswordResetTokenInput } from 'src/engine/core-modules/auth/dto/validate-password-reset-token.input'; +import { AuthGraphqlApiExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-graphql-api-exception.filter'; import { UserService } from 'src/engine/core-modules/user/services/user.service'; import { User } from 'src/engine/core-modules/user/user.entity'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; @@ -31,7 +23,6 @@ import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator'; import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator'; import { JwtAuthGuard } from 'src/engine/guards/jwt.auth.guard'; import { CaptchaGuard } from 'src/engine/integrations/captcha/captcha.guard'; -import { assert } from 'src/utils/assert'; import { ChallengeInput } from './dto/challenge.input'; import { ImpersonateInput } from './dto/impersonate.input'; @@ -48,10 +39,9 @@ import { AuthService } from './services/auth.service'; import { TokenService } from './services/token.service'; @Resolver() +@UseFilters(AuthGraphqlApiExceptionFilter) export class AuthResolver { constructor( - @InjectRepository(Workspace, 'core') - private readonly workspaceRepository: Repository, private authService: AuthService, private tokenService: TokenService, private userService: UserService, @@ -81,16 +71,10 @@ export class AuthResolver { @Query(() => Workspace) async findWorkspaceFromInviteHash( @Args() workspaceInviteHashValidInput: WorkspaceInviteHashValidInput, - ) { - const workspace = await this.workspaceRepository.findOneBy({ - inviteHash: workspaceInviteHashValidInput.inviteHash, - }); - - if (!workspace) { - throw new BadRequestException('Workspace does not exist'); - } - - return workspace; + ): Promise { + return await this.authService.findWorkspaceFromInviteHashOrFail( + workspaceInviteHashValidInput.inviteHash, + ); } @UseGuards(CaptchaGuard) @@ -151,8 +135,6 @@ export class AuthResolver { verifyInput.loginToken, ); - assert(email, 'Invalid token', ForbiddenException); - const result = await this.authService.verify(email); return result; @@ -188,10 +170,6 @@ export class AuthResolver { @Mutation(() => AuthTokens) async renewToken(@Args() args: AppTokenInput): Promise { - if (!args.appToken) { - throw new BadRequestException('Refresh token is mendatory'); - } - const tokens = await this.tokenService.generateTokensFromRefreshToken( args.appToken, ); @@ -205,10 +183,7 @@ export class AuthResolver { @Args() impersonateInput: ImpersonateInput, @AuthUser() user: User, ): Promise { - // Check if user can impersonate - assert(user.canImpersonate, 'User cannot impersonate', ForbiddenException); - - return this.authService.impersonate(impersonateInput.userId); + return await this.authService.impersonate(impersonateInput.userId, user); } @UseGuards(JwtAuthGuard) @@ -240,20 +215,13 @@ export class AuthResolver { @Mutation(() => InvalidatePassword) async updatePasswordViaResetToken( - @Args() args: UpdatePasswordViaResetTokenInput, + @Args() + { passwordResetToken, newPassword }: UpdatePasswordViaResetTokenInput, ): Promise { - const { id } = await this.tokenService.validatePasswordResetToken( - args.passwordResetToken, - ); - - assert(id, 'User not found', NotFoundException); - - const { success } = await this.authService.updatePassword( - id, - args.newPassword, - ); + const { id } = + await this.tokenService.validatePasswordResetToken(passwordResetToken); - assert(success, 'Password update failed', InternalServerErrorException); + await this.authService.updatePassword(id, newPassword); return await this.tokenService.invalidatePasswordResetToken(id); } diff --git a/packages/twenty-server/src/engine/core-modules/auth/controllers/google-apis-auth.controller.ts b/packages/twenty-server/src/engine/core-modules/auth/controllers/google-apis-auth.controller.ts index 2dff706292d9..de2fe47504b4 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/controllers/google-apis-auth.controller.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/controllers/google-apis-auth.controller.ts @@ -3,12 +3,17 @@ import { Get, Req, Res, - UnauthorizedException, + UseFilters, UseGuards, } from '@nestjs/common'; import { Response } from 'express'; +import { + AuthException, + AuthExceptionCode, +} from 'src/engine/core-modules/auth/auth.exception'; +import { AuthRestApiExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-rest-api-exception.filter'; import { GoogleAPIsOauthExchangeCodeForTokenGuard } from 'src/engine/core-modules/auth/guards/google-apis-oauth-exchange-code-for-token.guard'; import { GoogleAPIsOauthRequestCodeGuard } from 'src/engine/core-modules/auth/guards/google-apis-oauth-request-code.guard'; import { GoogleAPIsService } from 'src/engine/core-modules/auth/services/google-apis.service'; @@ -19,6 +24,7 @@ import { EnvironmentService } from 'src/engine/integrations/environment/environm import { LoadServiceWithWorkspaceContext } from 'src/engine/twenty-orm/context/load-service-with-workspace.context'; @Controller('auth/google-apis') +@UseFilters(AuthRestApiExceptionFilter) export class GoogleAPIsAuthController { constructor( private readonly googleAPIsService: GoogleAPIsService, @@ -59,13 +65,17 @@ export class GoogleAPIsAuthController { const demoWorkspaceIds = this.environmentService.get('DEMO_WORKSPACE_IDS'); if (demoWorkspaceIds.includes(workspaceId)) { - throw new UnauthorizedException( + throw new AuthException( 'Cannot connect Google account to demo workspace', + AuthExceptionCode.FORBIDDEN_EXCEPTION, ); } if (!workspaceId) { - throw new Error('Workspace not found'); + throw new AuthException( + 'Workspace not found', + AuthExceptionCode.INVALID_INPUT, + ); } const handle = emails[0].value; diff --git a/packages/twenty-server/src/engine/core-modules/auth/controllers/google-auth.controller.ts b/packages/twenty-server/src/engine/core-modules/auth/controllers/google-auth.controller.ts index 28943d2e75db..42674953ed31 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/controllers/google-auth.controller.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/controllers/google-auth.controller.ts @@ -1,14 +1,23 @@ -import { Controller, Get, Req, Res, UseGuards } from '@nestjs/common'; +import { + Controller, + Get, + Req, + Res, + UseFilters, + UseGuards, +} from '@nestjs/common'; import { Response } from 'express'; -import { GoogleRequest } from 'src/engine/core-modules/auth/strategies/google.auth.strategy'; -import { TokenService } from 'src/engine/core-modules/auth/services/token.service'; -import { GoogleProviderEnabledGuard } from 'src/engine/core-modules/auth/guards/google-provider-enabled.guard'; +import { AuthRestApiExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-rest-api-exception.filter'; import { GoogleOauthGuard } from 'src/engine/core-modules/auth/guards/google-oauth.guard'; +import { GoogleProviderEnabledGuard } from 'src/engine/core-modules/auth/guards/google-provider-enabled.guard'; import { AuthService } from 'src/engine/core-modules/auth/services/auth.service'; +import { TokenService } from 'src/engine/core-modules/auth/services/token.service'; +import { GoogleRequest } from 'src/engine/core-modules/auth/strategies/google.auth.strategy'; @Controller('auth/google') +@UseFilters(AuthRestApiExceptionFilter) export class GoogleAuthController { constructor( private readonly tokenService: TokenService, diff --git a/packages/twenty-server/src/engine/core-modules/auth/controllers/microsoft-auth.controller.ts b/packages/twenty-server/src/engine/core-modules/auth/controllers/microsoft-auth.controller.ts index 798f8ed18419..62f3364b6bca 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/controllers/microsoft-auth.controller.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/controllers/microsoft-auth.controller.ts @@ -1,8 +1,16 @@ -import { Controller, Get, Req, Res, UseGuards } from '@nestjs/common'; +import { + Controller, + Get, + Req, + Res, + UseFilters, + UseGuards, +} from '@nestjs/common'; import { Response } from 'express'; import { TypeORMService } from 'src/database/typeorm/typeorm.service'; +import { AuthRestApiExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-rest-api-exception.filter'; import { MicrosoftOAuthGuard } from 'src/engine/core-modules/auth/guards/microsoft-oauth.guard'; import { MicrosoftProviderEnabledGuard } from 'src/engine/core-modules/auth/guards/microsoft-provider-enabled.guard'; import { AuthService } from 'src/engine/core-modules/auth/services/auth.service'; @@ -10,6 +18,7 @@ import { TokenService } from 'src/engine/core-modules/auth/services/token.servic import { MicrosoftRequest } from 'src/engine/core-modules/auth/strategies/microsoft.auth.strategy'; @Controller('auth/microsoft') +@UseFilters(AuthRestApiExceptionFilter) export class MicrosoftAuthController { constructor( private readonly tokenService: TokenService, diff --git a/packages/twenty-server/src/engine/core-modules/auth/controllers/verify-auth.controller.ts b/packages/twenty-server/src/engine/core-modules/auth/controllers/verify-auth.controller.ts index 68d680845011..40869c5f7db4 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/controllers/verify-auth.controller.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/controllers/verify-auth.controller.ts @@ -1,11 +1,13 @@ -import { Body, Controller, Post } from '@nestjs/common'; +import { Body, Controller, Post, UseFilters } from '@nestjs/common'; -import { AuthService } from 'src/engine/core-modules/auth/services/auth.service'; -import { VerifyInput } from 'src/engine/core-modules/auth/dto/verify.input'; import { Verify } from 'src/engine/core-modules/auth/dto/verify.entity'; +import { VerifyInput } from 'src/engine/core-modules/auth/dto/verify.input'; +import { AuthRestApiExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-rest-api-exception.filter'; +import { AuthService } from 'src/engine/core-modules/auth/services/auth.service'; import { TokenService } from 'src/engine/core-modules/auth/services/token.service'; @Controller('auth/verify') +@UseFilters(AuthRestApiExceptionFilter) export class VerifyAuthController { constructor( private readonly authService: AuthService, diff --git a/packages/twenty-server/src/engine/core-modules/auth/filters/auth-graphql-api-exception.filter.ts b/packages/twenty-server/src/engine/core-modules/auth/filters/auth-graphql-api-exception.filter.ts new file mode 100644 index 000000000000..ebabba88f127 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/auth/filters/auth-graphql-api-exception.filter.ts @@ -0,0 +1,31 @@ +import { Catch } from '@nestjs/common'; + +import { + AuthException, + AuthExceptionCode, +} from 'src/engine/core-modules/auth/auth.exception'; +import { + ForbiddenError, + InternalServerError, + NotFoundError, + UserInputError, +} from 'src/engine/core-modules/graphql/utils/graphql-errors.util'; + +@Catch(AuthException) +export class AuthGraphqlApiExceptionFilter { + catch(exception: AuthException) { + switch (exception.code) { + case AuthExceptionCode.USER_NOT_FOUND: + case AuthExceptionCode.CLIENT_NOT_FOUND: + throw new NotFoundError(exception.message); + case AuthExceptionCode.INVALID_INPUT: + throw new UserInputError(exception.message); + case AuthExceptionCode.FORBIDDEN_EXCEPTION: + throw new ForbiddenError(exception.message); + case AuthExceptionCode.INVALID_DATA: + case AuthExceptionCode.INTERNAL_SERVER_ERROR: + default: + throw new InternalServerError(exception.message); + } + } +} diff --git a/packages/twenty-server/src/engine/core-modules/auth/filters/auth-rest-api-exception.filter.ts b/packages/twenty-server/src/engine/core-modules/auth/filters/auth-rest-api-exception.filter.ts new file mode 100644 index 000000000000..6d29e997bda8 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/auth/filters/auth-rest-api-exception.filter.ts @@ -0,0 +1,33 @@ +import { + ArgumentsHost, + BadRequestException, + Catch, + ExceptionFilter, + InternalServerErrorException, + NotFoundException, + UnauthorizedException, +} from '@nestjs/common'; + +import { + AuthException, + AuthExceptionCode, +} from 'src/engine/core-modules/auth/auth.exception'; + +@Catch(AuthException) +export class AuthRestApiExceptionFilter implements ExceptionFilter { + catch(exception: AuthException, _: ArgumentsHost) { + switch (exception.code) { + case AuthExceptionCode.USER_NOT_FOUND: + case AuthExceptionCode.CLIENT_NOT_FOUND: + throw new NotFoundException(exception.message); + case AuthExceptionCode.INVALID_INPUT: + throw new BadRequestException(exception.message); + case AuthExceptionCode.FORBIDDEN_EXCEPTION: + throw new UnauthorizedException(exception.message); + case AuthExceptionCode.INVALID_DATA: + case AuthExceptionCode.INTERNAL_SERVER_ERROR: + default: + throw new InternalServerErrorException(exception.message); + } + } +} diff --git a/packages/twenty-server/src/engine/core-modules/auth/guards/google-apis-oauth-exchange-code-for-token.guard.ts b/packages/twenty-server/src/engine/core-modules/auth/guards/google-apis-oauth-exchange-code-for-token.guard.ts index 21b8927402e2..e60c41b64dce 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/guards/google-apis-oauth-exchange-code-for-token.guard.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/guards/google-apis-oauth-exchange-code-for-token.guard.ts @@ -1,13 +1,13 @@ -import { - ExecutionContext, - Injectable, - NotFoundException, -} from '@nestjs/common'; +import { ExecutionContext, Injectable } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; +import { + AuthException, + AuthExceptionCode, +} from 'src/engine/core-modules/auth/auth.exception'; import { TokenService } from 'src/engine/core-modules/auth/services/token.service'; import { GoogleAPIScopeConfig, @@ -39,7 +39,10 @@ export class GoogleAPIsOauthExchangeCodeForTokenGuard extends AuthGuard( !this.environmentService.get('MESSAGING_PROVIDER_GMAIL_ENABLED') && !this.environmentService.get('CALENDAR_PROVIDER_GOOGLE_ENABLED') ) { - throw new NotFoundException('Google apis auth is not enabled'); + throw new AuthException( + 'Google apis auth is not enabled', + AuthExceptionCode.FORBIDDEN_EXCEPTION, + ); } const { workspaceId } = await this.tokenService.verifyTransientToken( diff --git a/packages/twenty-server/src/engine/core-modules/auth/guards/google-apis-oauth-request-code.guard.ts b/packages/twenty-server/src/engine/core-modules/auth/guards/google-apis-oauth-request-code.guard.ts index 51ec671a0451..1d70446b1111 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/guards/google-apis-oauth-request-code.guard.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/guards/google-apis-oauth-request-code.guard.ts @@ -1,13 +1,13 @@ -import { - ExecutionContext, - Injectable, - NotFoundException, -} from '@nestjs/common'; +import { ExecutionContext, Injectable } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; +import { + AuthException, + AuthExceptionCode, +} from 'src/engine/core-modules/auth/auth.exception'; import { TokenService } from 'src/engine/core-modules/auth/services/token.service'; import { GoogleAPIScopeConfig } from 'src/engine/core-modules/auth/strategies/google-apis-oauth-exchange-code-for-token.auth.strategy'; import { GoogleAPIsOauthRequestCodeStrategy } from 'src/engine/core-modules/auth/strategies/google-apis-oauth-request-code.auth.strategy'; @@ -36,7 +36,10 @@ export class GoogleAPIsOauthRequestCodeGuard extends AuthGuard('google-apis') { !this.environmentService.get('MESSAGING_PROVIDER_GMAIL_ENABLED') && !this.environmentService.get('CALENDAR_PROVIDER_GOOGLE_ENABLED') ) { - throw new NotFoundException('Google apis auth is not enabled'); + throw new AuthException( + 'Google apis auth is not enabled', + AuthExceptionCode.FORBIDDEN_EXCEPTION, + ); } const { workspaceId } = await this.tokenService.verifyTransientToken( diff --git a/packages/twenty-server/src/engine/core-modules/auth/guards/google-provider-enabled.guard.ts b/packages/twenty-server/src/engine/core-modules/auth/guards/google-provider-enabled.guard.ts index f1a7eaf55620..ed1db3ac9ddb 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/guards/google-provider-enabled.guard.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/guards/google-provider-enabled.guard.ts @@ -1,9 +1,13 @@ -import { Injectable, CanActivate, NotFoundException } from '@nestjs/common'; +import { CanActivate, Injectable } from '@nestjs/common'; import { Observable } from 'rxjs'; -import { EnvironmentService } from 'src/engine/integrations/environment/environment.service'; +import { + AuthException, + AuthExceptionCode, +} from 'src/engine/core-modules/auth/auth.exception'; import { GoogleStrategy } from 'src/engine/core-modules/auth/strategies/google.auth.strategy'; +import { EnvironmentService } from 'src/engine/integrations/environment/environment.service'; @Injectable() export class GoogleProviderEnabledGuard implements CanActivate { @@ -11,7 +15,10 @@ export class GoogleProviderEnabledGuard implements CanActivate { canActivate(): boolean | Promise | Observable { if (!this.environmentService.get('AUTH_GOOGLE_ENABLED')) { - throw new NotFoundException('Google auth is not enabled'); + throw new AuthException( + 'Google auth is not enabled', + AuthExceptionCode.FORBIDDEN_EXCEPTION, + ); } new GoogleStrategy(this.environmentService); diff --git a/packages/twenty-server/src/engine/core-modules/auth/guards/microsoft-provider-enabled.guard.ts b/packages/twenty-server/src/engine/core-modules/auth/guards/microsoft-provider-enabled.guard.ts index 2cd65f35931f..435c0bb04599 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/guards/microsoft-provider-enabled.guard.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/guards/microsoft-provider-enabled.guard.ts @@ -1,9 +1,13 @@ -import { Injectable, CanActivate, NotFoundException } from '@nestjs/common'; +import { CanActivate, Injectable } from '@nestjs/common'; import { Observable } from 'rxjs'; -import { EnvironmentService } from 'src/engine/integrations/environment/environment.service'; +import { + AuthException, + AuthExceptionCode, +} from 'src/engine/core-modules/auth/auth.exception'; import { MicrosoftStrategy } from 'src/engine/core-modules/auth/strategies/microsoft.auth.strategy'; +import { EnvironmentService } from 'src/engine/integrations/environment/environment.service'; @Injectable() export class MicrosoftProviderEnabledGuard implements CanActivate { @@ -11,7 +15,10 @@ export class MicrosoftProviderEnabledGuard implements CanActivate { canActivate(): boolean | Promise | Observable { if (!this.environmentService.get('AUTH_MICROSOFT_ENABLED')) { - throw new NotFoundException('Microsoft auth is not enabled'); + throw new AuthException( + 'Microsoft auth is not enabled', + AuthExceptionCode.FORBIDDEN_EXCEPTION, + ); } new MicrosoftStrategy(this.environmentService); diff --git a/packages/twenty-server/src/engine/core-modules/auth/services/auth.service.ts b/packages/twenty-server/src/engine/core-modules/auth/services/auth.service.ts index 95a0b597bc8b..932716780752 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/services/auth.service.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/services/auth.service.ts @@ -1,45 +1,43 @@ -import { - BadRequestException, - ForbiddenException, - Injectable, - NotFoundException, -} from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import crypto from 'node:crypto'; -import { Repository } from 'typeorm'; import { render } from '@react-email/components'; -import { PasswordUpdateNotifyEmail } from 'twenty-emails'; import { addMilliseconds } from 'date-fns'; import ms from 'ms'; +import { PasswordUpdateNotifyEmail } from 'twenty-emails'; +import { Repository } from 'typeorm'; import { NodeEnvironment } from 'src/engine/integrations/environment/interfaces/node-environment.interface'; -import { ChallengeInput } from 'src/engine/core-modules/auth/dto/challenge.input'; -import { assert } from 'src/utils/assert'; +import { + AppToken, + AppTokenType, +} from 'src/engine/core-modules/app-token/app-token.entity'; +import { + AuthException, + AuthExceptionCode, +} from 'src/engine/core-modules/auth/auth.exception'; import { PASSWORD_REGEX, compareHash, hashPassword, } from 'src/engine/core-modules/auth/auth.util'; -import { Verify } from 'src/engine/core-modules/auth/dto/verify.entity'; +import { AuthorizeApp } from 'src/engine/core-modules/auth/dto/authorize-app.entity'; +import { AuthorizeAppInput } from 'src/engine/core-modules/auth/dto/authorize-app.input'; +import { ChallengeInput } from 'src/engine/core-modules/auth/dto/challenge.input'; +import { UpdatePassword } from 'src/engine/core-modules/auth/dto/update-password.entity'; import { UserExists } from 'src/engine/core-modules/auth/dto/user-exists.entity'; +import { Verify } from 'src/engine/core-modules/auth/dto/verify.entity'; import { WorkspaceInviteHashValid } from 'src/engine/core-modules/auth/dto/workspace-invite-hash-valid.entity'; +import { SignInUpService } from 'src/engine/core-modules/auth/services/sign-in-up.service'; +import { WorkspaceMember } from 'src/engine/core-modules/user/dtos/workspace-member.dto'; +import { UserService } from 'src/engine/core-modules/user/services/user.service'; import { User } from 'src/engine/core-modules/user/user.entity'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; -import { UserService } from 'src/engine/core-modules/user/services/user.service'; -import { EnvironmentService } from 'src/engine/integrations/environment/environment.service'; import { EmailService } from 'src/engine/integrations/email/email.service'; -import { UpdatePassword } from 'src/engine/core-modules/auth/dto/update-password.entity'; -import { SignInUpService } from 'src/engine/core-modules/auth/services/sign-in-up.service'; -import { AuthorizeAppInput } from 'src/engine/core-modules/auth/dto/authorize-app.input'; -import { AuthorizeApp } from 'src/engine/core-modules/auth/dto/authorize-app.entity'; -import { - AppToken, - AppTokenType, -} from 'src/engine/core-modules/app-token/app-token.entity'; -import { WorkspaceMember } from 'src/engine/core-modules/user/dtos/workspace-member.dto'; +import { EnvironmentService } from 'src/engine/integrations/environment/environment.service'; import { TokenService } from './token.service'; @@ -70,15 +68,31 @@ export class AuthService { email: challengeInput.email, }); - assert(user, "This user doesn't exist", NotFoundException); - assert(user.passwordHash, 'Incorrect login method', ForbiddenException); + if (!user) { + throw new AuthException( + 'User not found', + AuthExceptionCode.USER_NOT_FOUND, + ); + } + + if (!user.passwordHash) { + throw new AuthException( + 'Incorrect login method', + AuthExceptionCode.INVALID_INPUT, + ); + } const isValid = await compareHash( challengeInput.password, user.passwordHash, ); - assert(isValid, 'Wrong password', ForbiddenException); + if (!isValid) { + throw new AuthException( + 'Wrong password', + AuthExceptionCode.FORBIDDEN_EXCEPTION, + ); + } return user; } @@ -112,6 +126,13 @@ export class AuthService { } async verify(email: string): Promise { + if (!email) { + throw new AuthException( + 'Email is required', + AuthExceptionCode.INVALID_INPUT, + ); + } + const user = await this.userRepository.findOne({ where: { email, @@ -119,13 +140,19 @@ export class AuthService { relations: ['defaultWorkspace', 'workspaces', 'workspaces.workspace'], }); - assert(user, "This user doesn't exist", NotFoundException); + if (!user) { + throw new AuthException( + 'User not found', + AuthExceptionCode.USER_NOT_FOUND, + ); + } - assert( - user.defaultWorkspace, - 'User has no default workspace', - NotFoundException, - ); + if (!user.defaultWorkspace) { + throw new AuthException( + 'User has no default workspace', + AuthExceptionCode.INVALID_DATA, + ); + } // passwordHash is hidden for security reasons user.passwordHash = ''; @@ -165,18 +192,33 @@ export class AuthService { return { isValid: !!workspace }; } - async impersonate(userId: string) { + async impersonate(userIdToImpersonate: string, userImpersonating: User) { + if (!userImpersonating.canImpersonate) { + throw new AuthException( + 'User cannot impersonate', + AuthExceptionCode.FORBIDDEN_EXCEPTION, + ); + } + const user = await this.userRepository.findOne({ where: { - id: userId, + id: userIdToImpersonate, }, relations: ['defaultWorkspace', 'workspaces', 'workspaces.workspace'], }); - assert(user, "This user doesn't exist", NotFoundException); + if (!user) { + throw new AuthException( + 'User not found', + AuthExceptionCode.USER_NOT_FOUND, + ); + } if (!user.defaultWorkspace.allowImpersonation) { - throw new ForbiddenException('Impersonation not allowed'); + throw new AuthException( + 'Impersonation not allowed', + AuthExceptionCode.FORBIDDEN_EXCEPTION, + ); } const accessToken = await this.tokenService.generateAccessToken(user.id); @@ -215,15 +257,24 @@ export class AuthService { const client = apps.find((app) => app.id === clientId); if (!client) { - throw new NotFoundException(`Invalid client '${clientId}'`); + throw new AuthException( + `Client not found for '${clientId}'`, + AuthExceptionCode.CLIENT_NOT_FOUND, + ); } if (!client.redirectUrl || !authorizeAppInput.redirectUrl) { - throw new NotFoundException(`redirectUrl not found for '${clientId}'`); + throw new AuthException( + `redirectUrl not found for '${clientId}'`, + AuthExceptionCode.FORBIDDEN_EXCEPTION, + ); } if (client.redirectUrl !== authorizeAppInput.redirectUrl) { - throw new ForbiddenException(`redirectUrl mismatch for '${clientId}'`); + throw new AuthException( + `redirectUrl mismatch for '${clientId}'`, + AuthExceptionCode.FORBIDDEN_EXCEPTION, + ); } const authorizationCode = crypto.randomBytes(42).toString('hex'); @@ -272,13 +323,30 @@ export class AuthService { userId: string, newPassword: string, ): Promise { + if (!userId) { + throw new AuthException( + 'User ID is required', + AuthExceptionCode.INVALID_INPUT, + ); + } + const user = await this.userRepository.findOneBy({ id: userId }); - assert(user, 'User not found', NotFoundException); + if (!user) { + throw new AuthException( + 'User not found', + AuthExceptionCode.USER_NOT_FOUND, + ); + } const isPasswordValid = PASSWORD_REGEX.test(newPassword); - assert(isPasswordValid, 'Password too weak', BadRequestException); + if (!isPasswordValid) { + throw new AuthException( + 'Password is too weak', + AuthExceptionCode.INVALID_INPUT, + ); + } const newPasswordHash = await hashPassword(newPassword); @@ -311,4 +379,21 @@ export class AuthService { return { success: true }; } + + async findWorkspaceFromInviteHashOrFail( + inviteHash: string, + ): Promise { + const workspace = await this.workspaceRepository.findOneBy({ + inviteHash, + }); + + if (!workspace) { + throw new AuthException( + 'Workspace does not exist', + AuthExceptionCode.INVALID_INPUT, + ); + } + + return workspace; + } } diff --git a/packages/twenty-server/src/engine/core-modules/auth/services/sign-in-up.service.ts b/packages/twenty-server/src/engine/core-modules/auth/services/sign-in-up.service.ts index 839a69427fbc..a0dfe432451f 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/services/sign-in-up.service.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/services/sign-in-up.service.ts @@ -1,9 +1,5 @@ import { HttpService } from '@nestjs/axios'; -import { - BadRequestException, - ForbiddenException, - Injectable, -} from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import FileType from 'file-type'; @@ -12,6 +8,10 @@ import { v4 } from 'uuid'; import { FileFolder } from 'src/engine/core-modules/file/interfaces/file-folder.interface'; +import { + AuthException, + AuthExceptionCode, +} from 'src/engine/core-modules/auth/auth.exception'; import { PASSWORD_REGEX, compareHash, @@ -26,7 +26,6 @@ import { WorkspaceActivationStatus, } from 'src/engine/core-modules/workspace/workspace.entity'; import { EnvironmentService } from 'src/engine/integrations/environment/environment.service'; -import { assert } from 'src/utils/assert'; import { getImageBufferFromUrl } from 'src/utils/image'; export type SignInUpServiceInput = { @@ -66,12 +65,22 @@ export class SignInUpService { if (!firstName) firstName = ''; if (!lastName) lastName = ''; - assert(email, 'Email is required', BadRequestException); + if (!email) { + throw new AuthException( + 'Email is required', + AuthExceptionCode.INVALID_INPUT, + ); + } if (password) { const isPasswordValid = PASSWORD_REGEX.test(password); - assert(isPasswordValid, 'Password too weak', BadRequestException); + if (!isPasswordValid) { + throw new AuthException( + 'Password too weak', + AuthExceptionCode.INVALID_INPUT, + ); + } } const passwordHash = password ? await hashPassword(password) : undefined; @@ -89,7 +98,12 @@ export class SignInUpService { existingUser.passwordHash, ); - assert(isValid, 'Wrong password', ForbiddenException); + if (!isValid) { + throw new AuthException( + 'Wrong password', + AuthExceptionCode.FORBIDDEN_EXCEPTION, + ); + } } if (workspaceInviteHash) { @@ -137,17 +151,19 @@ export class SignInUpService { inviteHash: workspaceInviteHash, }); - assert( - workspace, - 'This workspace inviteHash is invalid', - ForbiddenException, - ); + if (!workspace) { + throw new AuthException( + 'Invit hash is invalid', + AuthExceptionCode.FORBIDDEN_EXCEPTION, + ); + } - assert( - workspace.activationStatus === WorkspaceActivationStatus.ACTIVE, - 'Workspace is not ready to welcome new members', - ForbiddenException, - ); + if (!(workspace.activationStatus === WorkspaceActivationStatus.ACTIVE)) { + throw new AuthException( + 'Workspace is not ready to welcome new members', + AuthExceptionCode.FORBIDDEN_EXCEPTION, + ); + } if (existingUser) { const updatedUser = await this.userWorkspaceService.addUserToWorkspace( @@ -203,11 +219,12 @@ export class SignInUpService { lastName: string; picture: SignInUpServiceInput['picture']; }) { - assert( - !this.environmentService.get('IS_SIGN_UP_DISABLED'), - 'Sign up is disabled', - ForbiddenException, - ); + if (this.environmentService.get('IS_SIGN_UP_DISABLED')) { + throw new AuthException( + 'Sign up is disabled', + AuthExceptionCode.FORBIDDEN_EXCEPTION, + ); + } const workspaceToCreate = this.workspaceRepository.create({ displayName: '', diff --git a/packages/twenty-server/src/engine/core-modules/auth/services/token.service.spec.ts b/packages/twenty-server/src/engine/core-modules/auth/services/token.service.spec.ts index 34fa1a954ca4..0d807db41bff 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/services/token.service.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/services/token.service.spec.ts @@ -1,8 +1,3 @@ -import { - BadRequestException, - InternalServerErrorException, - NotFoundException, -} from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; @@ -14,6 +9,7 @@ import { AppToken, AppTokenType, } from 'src/engine/core-modules/app-token/app-token.entity'; +import { AuthException } from 'src/engine/core-modules/auth/auth.exception'; import { JwtAuthStrategy } from 'src/engine/core-modules/auth/strategies/jwt.auth.strategy'; import { JwtWrapperService } from 'src/engine/core-modules/jwt/services/jwt-wrapper.service'; import { User } from 'src/engine/core-modules/user/user.entity'; @@ -106,7 +102,7 @@ describe('TokenService', () => { expect(result.passwordResetTokenExpiresAt).toBeDefined(); }); - it('should throw BadRequestException if an existing valid token is found', async () => { + it('should throw AuthException if an existing valid token is found', async () => { const mockUser = { id: '1', email: 'test@example.com' } as User; const mockToken = { userId: '1', @@ -120,18 +116,18 @@ describe('TokenService', () => { await expect( service.generatePasswordResetToken(mockUser.email), - ).rejects.toThrow(BadRequestException); + ).rejects.toThrow(AuthException); }); - it('should throw NotFoundException if no user is found', async () => { + it('should throw AuthException if no user is found', async () => { jest.spyOn(userRepository, 'findOneBy').mockResolvedValue(null); await expect( service.generatePasswordResetToken('nonexistent@example.com'), - ).rejects.toThrow(NotFoundException); + ).rejects.toThrow(AuthException); }); - it('should throw InternalServerErrorException if environment variable is not found', async () => { + it('should throw AuthException if environment variable is not found', async () => { const mockUser = { id: '1', email: 'test@example.com' } as User; jest.spyOn(userRepository, 'findOneBy').mockResolvedValue(mockUser); @@ -139,7 +135,7 @@ describe('TokenService', () => { await expect( service.generatePasswordResetToken(mockUser.email), - ).rejects.toThrow(InternalServerErrorException); + ).rejects.toThrow(AuthException); }); }); @@ -181,17 +177,17 @@ describe('TokenService', () => { expect(result).toEqual({ id: mockUser.id, email: mockUser.email }); }); - it('should throw NotFoundException if token is invalid or expired', async () => { + it('should throw AuthException if token is invalid or expired', async () => { const resetToken = 'invalid-reset-token'; jest.spyOn(appTokenRepository, 'findOne').mockResolvedValue(null); await expect( service.validatePasswordResetToken(resetToken), - ).rejects.toThrow(NotFoundException); + ).rejects.toThrow(AuthException); }); - it('should throw NotFoundException if user does not exist for a valid token', async () => { + it('should throw AuthException if user does not exist for a valid token', async () => { const resetToken = 'orphan-token'; const hashedToken = crypto .createHash('sha256') @@ -212,10 +208,10 @@ describe('TokenService', () => { await expect( service.validatePasswordResetToken(resetToken), - ).rejects.toThrow(NotFoundException); + ).rejects.toThrow(AuthException); }); - it('should throw NotFoundException if token is revoked', async () => { + it('should throw AuthException if token is revoked', async () => { const resetToken = 'revoked-token'; const hashedToken = crypto .createHash('sha256') @@ -234,7 +230,7 @@ describe('TokenService', () => { .mockResolvedValue(mockToken as AppToken); await expect( service.validatePasswordResetToken(resetToken), - ).rejects.toThrow(NotFoundException); + ).rejects.toThrow(AuthException); }); }); }); diff --git a/packages/twenty-server/src/engine/core-modules/auth/services/token.service.ts b/packages/twenty-server/src/engine/core-modules/auth/services/token.service.ts index 88cb7e088678..9a45a3a37abc 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/services/token.service.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/services/token.service.ts @@ -1,12 +1,4 @@ -import { - BadRequestException, - ForbiddenException, - Injectable, - InternalServerErrorException, - NotFoundException, - UnauthorizedException, - UnprocessableEntityException, -} from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import crypto from 'crypto'; @@ -24,6 +16,10 @@ import { AppToken, AppTokenType, } from 'src/engine/core-modules/app-token/app-token.entity'; +import { + AuthException, + AuthExceptionCode, +} from 'src/engine/core-modules/auth/auth.exception'; import { EmailPasswordResetLink } from 'src/engine/core-modules/auth/dto/email-password-reset-link.entity'; import { ExchangeAuthCode } from 'src/engine/core-modules/auth/dto/exchange-auth-code.entity'; import { ExchangeAuthCodeInput } from 'src/engine/core-modules/auth/dto/exchange-auth-code.input'; @@ -45,7 +41,6 @@ import { User } from 'src/engine/core-modules/user/user.entity'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { EmailService } from 'src/engine/integrations/email/email.service'; import { EnvironmentService } from 'src/engine/integrations/environment/environment.service'; -import { assert } from 'src/utils/assert'; @Injectable() export class TokenService { @@ -68,7 +63,13 @@ export class TokenService { ): Promise { const expiresIn = this.environmentService.get('ACCESS_TOKEN_EXPIRES_IN'); - assert(expiresIn, '', InternalServerErrorException); + if (!expiresIn) { + throw new AuthException( + 'Expiration time for access token is not set', + AuthExceptionCode.INTERNAL_SERVER_ERROR, + ); + } + const expiresAt = addMilliseconds(new Date().getTime(), ms(expiresIn)); const user = await this.userRepository.findOne({ @@ -77,11 +78,17 @@ export class TokenService { }); if (!user) { - throw new NotFoundException('User is not found'); + throw new AuthException( + 'User is not found', + AuthExceptionCode.INVALID_INPUT, + ); } if (!user.defaultWorkspace) { - throw new NotFoundException('User does not have a default workspace'); + throw new AuthException( + 'User does not have a default workspace', + AuthExceptionCode.INVALID_DATA, + ); } const jwtPayload: JwtPayload = { @@ -99,7 +106,13 @@ export class TokenService { const secret = this.environmentService.get('REFRESH_TOKEN_SECRET'); const expiresIn = this.environmentService.get('REFRESH_TOKEN_EXPIRES_IN'); - assert(expiresIn, '', InternalServerErrorException); + if (!expiresIn) { + throw new AuthException( + 'Expiration time for access token is not set', + AuthExceptionCode.INTERNAL_SERVER_ERROR, + ); + } + const expiresAt = addMilliseconds(new Date().getTime(), ms(expiresIn)); const refreshTokenPayload = { @@ -130,7 +143,13 @@ export class TokenService { const secret = this.environmentService.get('LOGIN_TOKEN_SECRET'); const expiresIn = this.environmentService.get('LOGIN_TOKEN_EXPIRES_IN'); - assert(expiresIn, '', InternalServerErrorException); + if (!expiresIn) { + throw new AuthException( + 'Expiration time for access token is not set', + AuthExceptionCode.INTERNAL_SERVER_ERROR, + ); + } + const expiresAt = addMilliseconds(new Date().getTime(), ms(expiresIn)); const jwtPayload = { sub: email, @@ -155,7 +174,13 @@ export class TokenService { 'SHORT_TERM_TOKEN_EXPIRES_IN', ); - assert(expiresIn, '', InternalServerErrorException); + if (!expiresIn) { + throw new AuthException( + 'Expiration time for access token is not set', + AuthExceptionCode.INTERNAL_SERVER_ERROR, + ); + } + const expiresAt = addMilliseconds(new Date().getTime(), ms(expiresIn)); const jwtPayload = { sub: workspaceMemberId, @@ -212,7 +237,10 @@ export class TokenService { const token = ExtractJwt.fromAuthHeaderAsBearerToken()(request); if (!token) { - throw new UnauthorizedException('missing authentication token'); + throw new AuthException( + 'missing authentication token', + AuthExceptionCode.FORBIDDEN_EXCEPTION, + ); } const decoded = await this.verifyJwt( token, @@ -257,22 +285,35 @@ export class TokenService { ): Promise { const userExists = await this.userRepository.findBy({ id: user.id }); - assert(userExists, 'User not found', NotFoundException); + if (!userExists) { + throw new AuthException( + 'User not found', + AuthExceptionCode.INVALID_INPUT, + ); + } const workspace = await this.workspaceRepository.findOne({ where: { id: workspaceId }, relations: ['workspaceUsers'], }); - assert(workspace, 'workspace doesnt exist', NotFoundException); + if (!workspace) { + throw new AuthException( + 'workspace doesnt exist', + AuthExceptionCode.INVALID_INPUT, + ); + } - assert( - workspace.workspaceUsers + if ( + !workspace.workspaceUsers .map((userWorkspace) => userWorkspace.userId) - .includes(user.id), - 'user does not belong to workspace', - ForbiddenException, - ); + .includes(user.id) + ) { + throw new AuthException( + 'user does not belong to workspace', + AuthExceptionCode.FORBIDDEN_EXCEPTION, + ); + } await this.userRepository.save({ id: user.id, @@ -293,29 +334,17 @@ export class TokenService { async verifyAuthorizationCode( exchangeAuthCodeInput: ExchangeAuthCodeInput, ): Promise { - const { authorizationCode, codeVerifier, clientSecret } = - exchangeAuthCodeInput; + const { authorizationCode, codeVerifier } = exchangeAuthCodeInput; - assert( - authorizationCode, - 'Authorization code not found', - NotFoundException, - ); - - assert( - !codeVerifier || !clientSecret, - 'client secret or code verifier not found', - NotFoundException, - ); + if (!authorizationCode) { + throw new AuthException( + 'Authorization code not found', + AuthExceptionCode.INVALID_INPUT, + ); + } let userId = ''; - if (clientSecret) { - // TODO: replace this with call to third party apps table - // assert(client.secret, 'client secret code does not exist', ForbiddenException); - throw new ForbiddenException(); - } - if (codeVerifier) { const authorizationCodeAppToken = await this.appTokenRepository.findOne({ where: { @@ -323,17 +352,19 @@ export class TokenService { }, }); - assert( - authorizationCodeAppToken, - 'Authorization code does not exist', - NotFoundException, - ); + if (!authorizationCodeAppToken) { + throw new AuthException( + 'Authorization code does not exist', + AuthExceptionCode.INVALID_INPUT, + ); + } - assert( - authorizationCodeAppToken.expiresAt.getTime() >= Date.now(), - 'Authorization code expired.', - ForbiddenException, - ); + if (!(authorizationCodeAppToken.expiresAt.getTime() >= Date.now())) { + throw new AuthException( + 'Authorization code expired.', + AuthExceptionCode.FORBIDDEN_EXCEPTION, + ); + } const codeChallenge = crypto .createHash('sha256') @@ -350,26 +381,32 @@ export class TokenService { }, }); - assert( - codeChallengeAppToken, - 'code verifier doesnt match the challenge', - ForbiddenException, - ); + if (!codeChallengeAppToken) { + throw new AuthException( + 'code verifier doesnt match the challenge', + AuthExceptionCode.FORBIDDEN_EXCEPTION, + ); + } - assert( - codeChallengeAppToken.expiresAt.getTime() >= Date.now(), - 'code challenge expired.', - ForbiddenException, - ); + if (!(codeChallengeAppToken.expiresAt.getTime() >= Date.now())) { + throw new AuthException( + 'code challenge expired.', + AuthExceptionCode.FORBIDDEN_EXCEPTION, + ); + } - assert( - codeChallengeAppToken.userId === authorizationCodeAppToken.userId, - 'authorization code / code verifier was not created by same client', - ForbiddenException, - ); + if (codeChallengeAppToken.userId !== authorizationCodeAppToken.userId) { + throw new AuthException( + 'authorization code / code verifier was not created by same client', + AuthExceptionCode.FORBIDDEN_EXCEPTION, + ); + } if (codeChallengeAppToken.revokedAt) { - throw new ForbiddenException('Token has been revoked.'); + throw new AuthException( + 'Token has been revoked.', + AuthExceptionCode.FORBIDDEN_EXCEPTION, + ); } await this.appTokenRepository.save({ @@ -386,13 +423,17 @@ export class TokenService { }); if (!user) { - throw new NotFoundException( + throw new AuthException( 'User who generated the token does not exist', + AuthExceptionCode.INVALID_INPUT, ); } if (!user.defaultWorkspace) { - throw new NotFoundException('User does not have a default workspace'); + throw new AuthException( + 'User does not have a default workspace', + AuthExceptionCode.INVALID_DATA, + ); } const accessToken = await this.generateAccessToken( @@ -414,24 +455,35 @@ export class TokenService { const coolDown = this.environmentService.get('REFRESH_TOKEN_COOL_DOWN'); const jwtPayload = await this.verifyJwt(refreshToken, secret); - assert( - jwtPayload.jti && jwtPayload.sub, - 'This refresh token is malformed', - UnprocessableEntityException, - ); + if (!(jwtPayload.jti && jwtPayload.sub)) { + throw new AuthException( + 'This refresh token is malformed', + AuthExceptionCode.INVALID_INPUT, + ); + } const token = await this.appTokenRepository.findOneBy({ id: jwtPayload.jti, }); - assert(token, "This refresh token doesn't exist", NotFoundException); + if (!token) { + throw new AuthException( + "This refresh token doesn't exist", + AuthExceptionCode.INVALID_INPUT, + ); + } const user = await this.userRepository.findOne({ where: { id: jwtPayload.sub }, relations: ['appTokens'], }); - assert(user, 'User not found', NotFoundException); + if (!user) { + throw new AuthException( + 'User not found', + AuthExceptionCode.INVALID_INPUT, + ); + } // Check if revokedAt is less than coolDown if ( @@ -452,8 +504,9 @@ export class TokenService { }), ); - throw new ForbiddenException( - 'Suspicious activity detected, this refresh token has been revoked. All tokens has been revoked.', + throw new AuthException( + 'Suspicious activity detected, this refresh token has been revoked. All tokens have been revoked.', + AuthExceptionCode.FORBIDDEN_EXCEPTION, ); } @@ -464,6 +517,13 @@ export class TokenService { accessToken: AuthToken; refreshToken: AuthToken; }> { + if (!token) { + throw new AuthException( + 'Refresh token not found', + AuthExceptionCode.INVALID_INPUT, + ); + } + const { user, token: { id }, @@ -502,11 +562,20 @@ export class TokenService { ); } catch (error) { if (error instanceof TokenExpiredError) { - throw new UnauthorizedException('Token has expired.'); + throw new AuthException( + 'Token has expired.', + AuthExceptionCode.FORBIDDEN_EXCEPTION, + ); } else if (error instanceof JsonWebTokenError) { - throw new UnauthorizedException('Token invalid.'); + throw new AuthException( + 'Token invalid.', + AuthExceptionCode.FORBIDDEN_EXCEPTION, + ); } else { - throw new UnprocessableEntityException(); + throw new AuthException( + 'Unknown token error.', + AuthExceptionCode.INVALID_INPUT, + ); } } } @@ -516,17 +585,23 @@ export class TokenService { email, }); - assert(user, 'User not found', NotFoundException); + if (!user) { + throw new AuthException( + 'User not found', + AuthExceptionCode.INVALID_INPUT, + ); + } const expiresIn = this.environmentService.get( 'PASSWORD_RESET_TOKEN_EXPIRES_IN', ); - assert( - expiresIn, - 'PASSWORD_RESET_TOKEN_EXPIRES_IN constant value not found', - InternalServerErrorException, - ); + if (!expiresIn) { + throw new AuthException( + 'PASSWORD_RESET_TOKEN_EXPIRES_IN constant value not found', + AuthExceptionCode.INTERNAL_SERVER_ERROR, + ); + } const existingToken = await this.appTokenRepository.findOne({ where: { @@ -543,10 +618,9 @@ export class TokenService { { long: true }, ); - assert( - false, + throw new AuthException( `Token has already been generated. Please wait for ${timeToWait} to generate again.`, - BadRequestException, + AuthExceptionCode.INVALID_INPUT, ); } @@ -579,7 +653,12 @@ export class TokenService { email, }); - assert(user, 'User not found', NotFoundException); + if (!user) { + throw new AuthException( + 'User not found', + AuthExceptionCode.INVALID_INPUT, + ); + } const frontBaseURL = this.environmentService.get('FRONT_BASE_URL'); const resetLink = `${frontBaseURL}/reset-password/${resetToken.passwordResetToken}`; @@ -636,13 +715,23 @@ export class TokenService { }, }); - assert(token, 'Token is invalid', NotFoundException); + if (!token) { + throw new AuthException( + 'Token is invalid', + AuthExceptionCode.FORBIDDEN_EXCEPTION, + ); + } const user = await this.userRepository.findOneBy({ id: token.userId, }); - assert(user, 'Token is invalid', NotFoundException); + if (!user) { + throw new AuthException( + 'User not found', + AuthExceptionCode.INVALID_INPUT, + ); + } return { id: user.id, @@ -657,7 +746,12 @@ export class TokenService { id: userId, }); - assert(user, 'User not found', NotFoundException); + if (!user) { + throw new AuthException( + 'User not found', + AuthExceptionCode.INVALID_INPUT, + ); + } await this.appTokenRepository.update( { diff --git a/packages/twenty-server/src/engine/core-modules/auth/strategies/jwt.auth.strategy.ts b/packages/twenty-server/src/engine/core-modules/auth/strategies/jwt.auth.strategy.ts index 392c6f44cafa..2807232fc9c6 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/strategies/jwt.auth.strategy.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/strategies/jwt.auth.strategy.ts @@ -1,8 +1,4 @@ -import { - ForbiddenException, - Injectable, - UnauthorizedException, -} from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { PassportStrategy } from '@nestjs/passport'; import { InjectRepository } from '@nestjs/typeorm'; @@ -10,12 +6,15 @@ import { ExtractJwt, Strategy } from 'passport-jwt'; import { Repository } from 'typeorm'; import { TypeORMService } from 'src/database/typeorm/typeorm.service'; +import { + AuthException, + AuthExceptionCode, +} from 'src/engine/core-modules/auth/auth.exception'; +import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type'; import { User } from 'src/engine/core-modules/user/user.entity'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { EnvironmentService } from 'src/engine/integrations/environment/environment.service'; import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; -import { assert } from 'src/utils/assert'; -import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type'; import { ApiKeyWorkspaceEntity } from 'src/modules/api-key/standard-objects/api-key.workspace-entity'; export type JwtPayload = { sub: string; workspaceId: string; jti?: string }; @@ -46,8 +45,12 @@ export class JwtAuthStrategy extends PassportStrategy(Strategy, 'jwt') { let apiKey: ApiKeyWorkspaceEntity | null = null; if (!workspace) { - throw new UnauthorizedException(); + throw new AuthException( + 'Workspace not found', + AuthExceptionCode.INVALID_INPUT, + ); } + if (payload.jti) { // TODO: Check why it's not working // const apiKeyRepository = @@ -71,11 +74,12 @@ export class JwtAuthStrategy extends PassportStrategy(Strategy, 'jwt') { apiKey = res?.[0]; - assert( - apiKey && !apiKey.revokedAt, - 'This API Key is revoked', - ForbiddenException, - ); + if (!apiKey || apiKey.revokedAt) { + throw new AuthException( + 'This API Key is revoked', + AuthExceptionCode.FORBIDDEN_EXCEPTION, + ); + } } if (payload.workspaceId) { @@ -84,7 +88,10 @@ export class JwtAuthStrategy extends PassportStrategy(Strategy, 'jwt') { relations: ['defaultWorkspace'], }); if (!user) { - throw new UnauthorizedException(); + throw new AuthException( + 'User not found', + AuthExceptionCode.INVALID_INPUT, + ); } } diff --git a/packages/twenty-server/src/engine/core-modules/auth/strategies/microsoft.auth.strategy.ts b/packages/twenty-server/src/engine/core-modules/auth/strategies/microsoft.auth.strategy.ts index e26e02e9f967..f2d002a788c9 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/strategies/microsoft.auth.strategy.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/strategies/microsoft.auth.strategy.ts @@ -1,10 +1,13 @@ -import { BadRequestException } from '@nestjs/common'; import { PassportStrategy } from '@nestjs/passport'; import { Request } from 'express'; import { VerifyCallback } from 'passport-google-oauth20'; import { Strategy } from 'passport-microsoft'; +import { + AuthException, + AuthExceptionCode, +} from 'src/engine/core-modules/auth/auth.exception'; import { EnvironmentService } from 'src/engine/integrations/environment/environment.service'; export type MicrosoftRequest = Omit< @@ -60,7 +63,10 @@ export class MicrosoftStrategy extends PassportStrategy(Strategy, 'microsoft') { const email = emails?.[0]?.value ?? null; if (!email) { - throw new BadRequestException('No email found in your Microsoft profile'); + throw new AuthException( + 'Email not found', + AuthExceptionCode.INVALID_INPUT, + ); } const user: MicrosoftRequest['user'] = { diff --git a/packages/twenty-server/src/engine/core-modules/auth/utils/google-apis-set-request-extra-params.util.ts b/packages/twenty-server/src/engine/core-modules/auth/utils/google-apis-set-request-extra-params.util.ts index 668426c23bc1..b6549ceff071 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/utils/google-apis-set-request-extra-params.util.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/utils/google-apis-set-request-extra-params.util.ts @@ -1,3 +1,7 @@ +import { + AuthException, + AuthExceptionCode, +} from 'src/engine/core-modules/auth/auth.exception'; import { GoogleAPIsRequest } from 'src/engine/core-modules/auth/types/google-api-request.type'; type GoogleAPIsRequestExtraParams = { @@ -19,7 +23,10 @@ export const setRequestExtraParams = ( } = params; if (!transientToken) { - throw new Error('transientToken is required'); + throw new AuthException( + 'transientToken is required', + AuthExceptionCode.INVALID_INPUT, + ); } request.params.transientToken = transientToken;