From a8ccaa7ef0ca82a92adefe7e5fc63cf50fee22d0 Mon Sep 17 00:00:00 2001 From: Thomas Trompette Date: Tue, 30 Jul 2024 17:31:05 +0200 Subject: [PATCH 1/6] Build exceptions and handler --- .../core-modules/auth/auth.exception.ts | 17 +++ .../engine/core-modules/auth/auth.resolver.ts | 64 +++++---- .../auth/services/auth.service.ts | 123 ++++++++++++------ .../auth/services/sign-in-up.service.ts | 67 ++++++---- ...auth-graphql-api-exception-handler.util.ts | 30 +++++ 5 files changed, 216 insertions(+), 85 deletions(-) create mode 100644 packages/twenty-server/src/engine/core-modules/auth/auth.exception.ts create mode 100644 packages/twenty-server/src/engine/core-modules/auth/utils/auth-graphql-api-exception-handler.util.ts 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..97f14239d6ca 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 @@ -24,6 +24,8 @@ 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 { authGraphqlApiExceptionHandler } from 'src/engine/core-modules/auth/utils/auth-graphql-api-exception-handler.util'; +import { NotFoundError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util'; 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'; @@ -61,33 +63,41 @@ export class AuthResolver { @Query(() => UserExists) async checkUserExists( @Args() checkUserExistsInput: CheckUserExistsInput, - ): Promise { - const { exists } = await this.authService.checkUserExists( - checkUserExistsInput.email, - ); - - return { exists }; + ): Promise { + try { + const { exists } = await this.authService.checkUserExists( + checkUserExistsInput.email, + ); + + return { exists }; + } catch (error) { + authGraphqlApiExceptionHandler(error); + } } @Query(() => WorkspaceInviteHashValid) async checkWorkspaceInviteHashIsValid( @Args() workspaceInviteHashValidInput: WorkspaceInviteHashValidInput, - ): Promise { - return await this.authService.checkWorkspaceInviteHashIsValid( - workspaceInviteHashValidInput.inviteHash, - ); + ): Promise { + try { + return await this.authService.checkWorkspaceInviteHashIsValid( + workspaceInviteHashValidInput.inviteHash, + ); + } catch (error) { + authGraphqlApiExceptionHandler(error); + } } @Query(() => Workspace) async findWorkspaceFromInviteHash( @Args() workspaceInviteHashValidInput: WorkspaceInviteHashValidInput, - ) { + ): Promise { const workspace = await this.workspaceRepository.findOneBy({ inviteHash: workspaceInviteHashValidInput.inviteHash, }); if (!workspace) { - throw new BadRequestException('Workspace does not exist'); + throw new NotFoundError('Workspace does not exist'); } return workspace; @@ -163,13 +173,17 @@ export class AuthResolver { async authorizeApp( @Args() authorizeAppInput: AuthorizeAppInput, @AuthUser() user: User, - ): Promise { - const authorizedApp = await this.authService.generateAuthorizationCode( - authorizeAppInput, - user, - ); - - return authorizedApp; + ): Promise { + try { + const authorizedApp = await this.authService.generateAuthorizationCode( + authorizeAppInput, + user, + ); + + return authorizedApp; + } catch (error) { + authGraphqlApiExceptionHandler(error); + } } @Mutation(() => AuthTokens) @@ -203,12 +217,12 @@ export class AuthResolver { @Mutation(() => Verify) async impersonate( @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); + ): Promise { + try { + return await this.authService.impersonate(impersonateInput.userId); + } catch (error) { + authGraphqlApiExceptionHandler(error); + } } @UseGuards(JwtAuthGuard) 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..75b1d8e8fd61 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.FORBIDDEN_EXCEPTION, + ); + } 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; } @@ -119,13 +133,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 = ''; @@ -173,10 +193,18 @@ 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, + ); + } 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 +243,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'); @@ -274,11 +311,21 @@ export class AuthService { ): Promise { 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); 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..219dc2f0e198 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,11 @@ import { HttpService } from '@nestjs/axios'; +<<<<<<< HEAD import { - BadRequestException, - ForbiddenException, - Injectable, + Injectable } from '@nestjs/common'; +======= +import { Injectable } from '@nestjs/common'; +>>>>>>> 57971a6bf (Build exceptions and handler) import { InjectRepository } from '@nestjs/typeorm'; import FileType from 'file-type'; @@ -12,6 +14,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 +32,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 +71,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 +104,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 +157,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 +225,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/utils/auth-graphql-api-exception-handler.util.ts b/packages/twenty-server/src/engine/core-modules/auth/utils/auth-graphql-api-exception-handler.util.ts new file mode 100644 index 000000000000..81c52945a769 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/auth/utils/auth-graphql-api-exception-handler.util.ts @@ -0,0 +1,30 @@ +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'; + +export const authGraphqlApiExceptionHandler = (error: Error) => { + if (error instanceof AuthException) { + switch (error.code) { + case AuthExceptionCode.USER_NOT_FOUND: + case AuthExceptionCode.CLIENT_NOT_FOUND: + throw new NotFoundError(error.message); + case AuthExceptionCode.INVALID_INPUT: + throw new UserInputError(error.message); + case AuthExceptionCode.FORBIDDEN_EXCEPTION: + throw new ForbiddenError(error.message); + case AuthExceptionCode.INVALID_DATA: + case AuthExceptionCode.INTERNAL_SERVER_ERROR: + default: + throw new InternalServerError(error.message); + } + } + + throw error; +}; From 34a76fb906ca9cb595e21ef8e99b1003e4259c58 Mon Sep 17 00:00:00 2001 From: Thomas Trompette Date: Thu, 1 Aug 2024 17:31:27 +0200 Subject: [PATCH 2/6] Update token service and guards --- .../engine/core-modules/auth/auth.resolver.ts | 220 ++++++++------ .../google-apis-auth.controller.ts | 21 +- ...pis-oauth-exchange-code-for-token.guard.ts | 15 +- .../google-apis-oauth-request-code.guard.ts | 15 +- .../guards/google-provider-enabled.guard.ts | 13 +- .../microsoft-provider-enabled.guard.ts | 13 +- .../auth/services/auth.service.ts | 40 ++- .../auth/services/sign-in-up.service.ts | 8 +- .../auth/services/token.service.ts | 281 ++++++++++++------ .../auth/strategies/jwt.auth.strategy.ts | 35 ++- .../strategies/microsoft.auth.strategy.ts | 10 +- ...ogle-apis-set-request-extra-params.util.ts | 9 +- 12 files changed, 432 insertions(+), 248 deletions(-) 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 97f14239d6ca..fd767793ba03 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,10 +1,4 @@ -import { - BadRequestException, - ForbiddenException, - InternalServerErrorException, - NotFoundException, - UseGuards, -} from '@nestjs/common'; +import { UseGuards } from '@nestjs/common'; import { Args, Mutation, Query, Resolver } from '@nestjs/graphql'; import { InjectRepository } from '@nestjs/typeorm'; @@ -25,7 +19,6 @@ import { UpdatePasswordViaResetTokenInput } from 'src/engine/core-modules/auth/d 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 { authGraphqlApiExceptionHandler } from 'src/engine/core-modules/auth/utils/auth-graphql-api-exception-handler.util'; -import { NotFoundError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util'; 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'; @@ -33,7 +26,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'; @@ -91,49 +83,61 @@ export class AuthResolver { @Query(() => Workspace) async findWorkspaceFromInviteHash( @Args() workspaceInviteHashValidInput: WorkspaceInviteHashValidInput, - ): Promise { - const workspace = await this.workspaceRepository.findOneBy({ - inviteHash: workspaceInviteHashValidInput.inviteHash, - }); - - if (!workspace) { - throw new NotFoundError('Workspace does not exist'); + ): Promise { + try { + return await this.authService.findWorkspaceFromInviteHash( + workspaceInviteHashValidInput.inviteHash, + ); + } catch (error) { + authGraphqlApiExceptionHandler(error); } - - return workspace; } @UseGuards(CaptchaGuard) @Mutation(() => LoginToken) - async challenge(@Args() challengeInput: ChallengeInput): Promise { - const user = await this.authService.challenge(challengeInput); - const loginToken = await this.tokenService.generateLoginToken(user.email); + async challenge( + @Args() challengeInput: ChallengeInput, + ): Promise { + try { + const user = await this.authService.challenge(challengeInput); + const loginToken = await this.tokenService.generateLoginToken(user.email); - return { loginToken }; + return { loginToken }; + } catch (error) { + authGraphqlApiExceptionHandler(error); + } } @UseGuards(CaptchaGuard) @Mutation(() => LoginToken) - async signUp(@Args() signUpInput: SignUpInput): Promise { - const user = await this.authService.signInUp({ - ...signUpInput, - fromSSO: false, - }); + async signUp(@Args() signUpInput: SignUpInput): Promise { + try { + const user = await this.authService.signInUp({ + ...signUpInput, + fromSSO: false, + }); - const loginToken = await this.tokenService.generateLoginToken(user.email); + const loginToken = await this.tokenService.generateLoginToken(user.email); - return { loginToken }; + return { loginToken }; + } catch (error) { + authGraphqlApiExceptionHandler(error); + } } @Mutation(() => ExchangeAuthCode) async exchangeAuthorizationCode( @Args() exchangeAuthCodeInput: ExchangeAuthCodeInput, ) { - const tokens = await this.tokenService.verifyAuthorizationCode( - exchangeAuthCodeInput, - ); + try { + const tokens = await this.tokenService.verifyAuthorizationCode( + exchangeAuthCodeInput, + ); - return tokens; + return tokens; + } catch (error) { + authGraphqlApiExceptionHandler(error); + } } @Mutation(() => TransientToken) @@ -141,31 +145,37 @@ export class AuthResolver { async generateTransientToken( @AuthUser() user: User, ): Promise { - const workspaceMember = await this.userService.loadWorkspaceMember(user); + try { + const workspaceMember = await this.userService.loadWorkspaceMember(user); + + if (!workspaceMember) { + return; + } + const transientToken = await this.tokenService.generateTransientToken( + workspaceMember.id, + user.id, + user.defaultWorkspace.id, + ); - if (!workspaceMember) { - return; + return { transientToken }; + } catch (error) { + authGraphqlApiExceptionHandler(error); } - const transientToken = await this.tokenService.generateTransientToken( - workspaceMember.id, - user.id, - user.defaultWorkspace.id, - ); - - return { transientToken }; } @Mutation(() => Verify) - async verify(@Args() verifyInput: VerifyInput): Promise { - const email = await this.tokenService.verifyLoginToken( - verifyInput.loginToken, - ); - - assert(email, 'Invalid token', ForbiddenException); + async verify(@Args() verifyInput: VerifyInput): Promise { + try { + const email = await this.tokenService.verifyLoginToken( + verifyInput.loginToken, + ); - const result = await this.authService.verify(email); + const result = await this.authService.verify(email); - return result; + return result; + } catch (error) { + authGraphqlApiExceptionHandler(error); + } } @Mutation(() => AuthorizeApp) @@ -191,35 +201,40 @@ export class AuthResolver { async generateJWT( @AuthUser() user: User, @Args() args: GenerateJwtInput, - ): Promise { - const token = await this.tokenService.generateSwitchWorkspaceToken( - user, - args.workspaceId, - ); + ): Promise { + try { + const token = await this.tokenService.generateSwitchWorkspaceToken( + user, + args.workspaceId, + ); - return token; + return token; + } catch (error) { + authGraphqlApiExceptionHandler(error); + } } @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, - ); + async renewToken(@Args() args: AppTokenInput): Promise { + try { + const tokens = await this.tokenService.generateTokensFromRefreshToken( + args.appToken, + ); - return { tokens: tokens }; + return { tokens: tokens }; + } catch (error) { + authGraphqlApiExceptionHandler(error); + } } @UseGuards(JwtAuthGuard) @Mutation(() => Verify) async impersonate( @Args() impersonateInput: ImpersonateInput, + @AuthUser() user: User, ): Promise { try { - return await this.authService.impersonate(impersonateInput.userId); + return await this.authService.impersonate(impersonateInput.userId, user); } catch (error) { authGraphqlApiExceptionHandler(error); } @@ -231,53 +246,62 @@ export class AuthResolver { @Args() args: ApiKeyTokenInput, @AuthWorkspace() { id: workspaceId }: Workspace, ): Promise { - return await this.tokenService.generateApiKeyToken( - workspaceId, - args.apiKeyId, - args.expiresAt, - ); + try { + return await this.tokenService.generateApiKeyToken( + workspaceId, + args.apiKeyId, + args.expiresAt, + ); + } catch (error) { + authGraphqlApiExceptionHandler(error); + } } @Mutation(() => EmailPasswordResetLink) async emailPasswordResetLink( @Args() emailPasswordResetInput: EmailPasswordResetLinkInput, - ): Promise { - const resetToken = await this.tokenService.generatePasswordResetToken( - emailPasswordResetInput.email, - ); - - return await this.tokenService.sendEmailPasswordResetLink( - resetToken, - emailPasswordResetInput.email, - ); + ): Promise { + try { + const resetToken = await this.tokenService.generatePasswordResetToken( + emailPasswordResetInput.email, + ); + + return await this.tokenService.sendEmailPasswordResetLink( + resetToken, + emailPasswordResetInput.email, + ); + } catch (error) { + authGraphqlApiExceptionHandler(error); + } } @Mutation(() => InvalidatePassword) async updatePasswordViaResetToken( @Args() args: 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, - ); + ): Promise { + try { + const { id } = await this.tokenService.validatePasswordResetToken( + args.passwordResetToken, + ); - assert(success, 'Password update failed', InternalServerErrorException); + await this.authService.updatePassword(id, args.newPassword); - return await this.tokenService.invalidatePasswordResetToken(id); + return await this.tokenService.invalidatePasswordResetToken(id); + } catch (error) { + authGraphqlApiExceptionHandler(error); + } } @Query(() => ValidatePasswordResetToken) async validatePasswordResetToken( @Args() args: ValidatePasswordResetTokenInput, - ): Promise { - return this.tokenService.validatePasswordResetToken( - args.passwordResetToken, - ); + ): Promise { + try { + return this.tokenService.validatePasswordResetToken( + args.passwordResetToken, + ); + } catch (error) { + authGraphqlApiExceptionHandler(error); + } } } 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..a2baa0a90445 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 @@ -1,14 +1,11 @@ -import { - Controller, - Get, - Req, - Res, - UnauthorizedException, - UseGuards, -} from '@nestjs/common'; +import { Controller, Get, Req, Res, UseGuards } from '@nestjs/common'; import { Response } from 'express'; +import { + AuthException, + AuthExceptionCode, +} from 'src/engine/core-modules/auth/auth.exception'; 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'; @@ -59,13 +56,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/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 75b1d8e8fd61..b9cd7881643d 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 @@ -126,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, @@ -185,10 +192,17 @@ 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'], }); @@ -309,6 +323,13 @@ 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 }); if (!user) { @@ -358,4 +379,19 @@ export class AuthService { return { success: true }; } + + async findWorkspaceFromInviteHash(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 219dc2f0e198..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,11 +1,5 @@ import { HttpService } from '@nestjs/axios'; -<<<<<<< HEAD -import { - Injectable -} from '@nestjs/common'; -======= import { Injectable } from '@nestjs/common'; ->>>>>>> 57971a6bf (Build exceptions and handler) import { InjectRepository } from '@nestjs/typeorm'; import FileType from 'file-type'; @@ -225,7 +219,7 @@ export class SignInUpService { lastName: string; picture: SignInUpServiceInput['picture']; }) { - if (!this.environmentService.get('IS_SIGN_UP_DISABLED')) { + if (this.environmentService.get('IS_SIGN_UP_DISABLED')) { throw new AuthException( 'Sign up is disabled', AuthExceptionCode.FORBIDDEN_EXCEPTION, 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..aa5754af716a 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; - - assert( - authorizationCode, - 'Authorization code not found', - NotFoundException, - ); + const { authorizationCode, codeVerifier } = exchangeAuthCodeInput; - 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,34 @@ 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 +425,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 +457,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 +506,9 @@ export class TokenService { }), ); - throw new ForbiddenException( + throw new AuthException( 'Suspicious activity detected, this refresh token has been revoked. All tokens has been revoked.', + AuthExceptionCode.FORBIDDEN_EXCEPTION, ); } @@ -502,11 +557,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 +580,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 +613,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 +648,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 +710,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 +741,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; From ea83099192f94dd35f7958fa7b6fd0b355d73e64 Mon Sep 17 00:00:00 2001 From: Thomas Trompette Date: Thu, 1 Aug 2024 18:52:40 +0200 Subject: [PATCH 3/6] Fix test --- .../engine/core-modules/auth/auth.resolver.ts | 10 +++---- .../auth/services/token.service.spec.ts | 30 ++++++++----------- 2 files changed, 18 insertions(+), 22 deletions(-) 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 fd767793ba03..abbe55077dec 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 @@ -277,14 +277,14 @@ export class AuthResolver { @Mutation(() => InvalidatePassword) async updatePasswordViaResetToken( - @Args() args: UpdatePasswordViaResetTokenInput, + @Args() + { passwordResetToken, newPassword }: UpdatePasswordViaResetTokenInput, ): Promise { try { - const { id } = await this.tokenService.validatePasswordResetToken( - args.passwordResetToken, - ); + const { id } = + await this.tokenService.validatePasswordResetToken(passwordResetToken); - await this.authService.updatePassword(id, args.newPassword); + await this.authService.updatePassword(id, newPassword); return await this.tokenService.invalidatePasswordResetToken(id); } catch (error) { 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); }); }); }); From 764e8e24e2048e55f10287129bd73cc7182867a8 Mon Sep 17 00:00:00 2001 From: Thomas Trompette Date: Tue, 6 Aug 2024 15:05:45 +0200 Subject: [PATCH 4/6] Use filters --- .../engine/core-modules/auth/auth.resolver.ts | 264 +++++++----------- .../google-apis-auth.controller.ts | 11 +- .../controllers/google-auth.controller.ts | 17 +- .../controllers/microsoft-auth.controller.ts | 11 +- .../controllers/verify-auth.controller.ts | 8 +- .../auth-graphql-api-exception.filter.ts} | 21 +- .../filters/auth-rest-api-exception.filter.ts | 33 +++ .../auth/services/token.service.ts | 13 +- 8 files changed, 189 insertions(+), 189 deletions(-) rename packages/twenty-server/src/engine/core-modules/auth/{utils/auth-graphql-api-exception-handler.util.ts => filters/auth-graphql-api-exception.filter.ts} (58%) create mode 100644 packages/twenty-server/src/engine/core-modules/auth/filters/auth-rest-api-exception.filter.ts 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 abbe55077dec..06c8f100e5d4 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,9 +1,7 @@ -import { 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'; import { AuthorizeApp } from 'src/engine/core-modules/auth/dto/authorize-app.entity'; @@ -18,7 +16,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 { authGraphqlApiExceptionHandler } from 'src/engine/core-modules/auth/utils/auth-graphql-api-exception-handler.util'; +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'; @@ -42,10 +40,10 @@ 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, @@ -55,89 +53,63 @@ export class AuthResolver { @Query(() => UserExists) async checkUserExists( @Args() checkUserExistsInput: CheckUserExistsInput, - ): Promise { - try { - const { exists } = await this.authService.checkUserExists( - checkUserExistsInput.email, - ); - - return { exists }; - } catch (error) { - authGraphqlApiExceptionHandler(error); - } + ): Promise { + const { exists } = await this.authService.checkUserExists( + checkUserExistsInput.email, + ); + + return { exists }; } @Query(() => WorkspaceInviteHashValid) async checkWorkspaceInviteHashIsValid( @Args() workspaceInviteHashValidInput: WorkspaceInviteHashValidInput, - ): Promise { - try { - return await this.authService.checkWorkspaceInviteHashIsValid( - workspaceInviteHashValidInput.inviteHash, - ); - } catch (error) { - authGraphqlApiExceptionHandler(error); - } + ): Promise { + return await this.authService.checkWorkspaceInviteHashIsValid( + workspaceInviteHashValidInput.inviteHash, + ); } @Query(() => Workspace) async findWorkspaceFromInviteHash( @Args() workspaceInviteHashValidInput: WorkspaceInviteHashValidInput, - ): Promise { - try { - return await this.authService.findWorkspaceFromInviteHash( - workspaceInviteHashValidInput.inviteHash, - ); - } catch (error) { - authGraphqlApiExceptionHandler(error); - } + ): Promise { + return await this.authService.findWorkspaceFromInviteHash( + workspaceInviteHashValidInput.inviteHash, + ); } @UseGuards(CaptchaGuard) @Mutation(() => LoginToken) - async challenge( - @Args() challengeInput: ChallengeInput, - ): Promise { - try { - const user = await this.authService.challenge(challengeInput); - const loginToken = await this.tokenService.generateLoginToken(user.email); - - return { loginToken }; - } catch (error) { - authGraphqlApiExceptionHandler(error); - } + async challenge(@Args() challengeInput: ChallengeInput): Promise { + const user = await this.authService.challenge(challengeInput); + const loginToken = await this.tokenService.generateLoginToken(user.email); + + return { loginToken }; } @UseGuards(CaptchaGuard) @Mutation(() => LoginToken) - async signUp(@Args() signUpInput: SignUpInput): Promise { - try { - const user = await this.authService.signInUp({ - ...signUpInput, - fromSSO: false, - }); - - const loginToken = await this.tokenService.generateLoginToken(user.email); - - return { loginToken }; - } catch (error) { - authGraphqlApiExceptionHandler(error); - } + async signUp(@Args() signUpInput: SignUpInput): Promise { + const user = await this.authService.signInUp({ + ...signUpInput, + fromSSO: false, + }); + + const loginToken = await this.tokenService.generateLoginToken(user.email); + + return { loginToken }; } @Mutation(() => ExchangeAuthCode) async exchangeAuthorizationCode( @Args() exchangeAuthCodeInput: ExchangeAuthCodeInput, ) { - try { - const tokens = await this.tokenService.verifyAuthorizationCode( - exchangeAuthCodeInput, - ); - - return tokens; - } catch (error) { - authGraphqlApiExceptionHandler(error); - } + const tokens = await this.tokenService.verifyAuthorizationCode( + exchangeAuthCodeInput, + ); + + return tokens; } @Mutation(() => TransientToken) @@ -145,37 +117,29 @@ export class AuthResolver { async generateTransientToken( @AuthUser() user: User, ): Promise { - try { - const workspaceMember = await this.userService.loadWorkspaceMember(user); - - if (!workspaceMember) { - return; - } - const transientToken = await this.tokenService.generateTransientToken( - workspaceMember.id, - user.id, - user.defaultWorkspace.id, - ); - - return { transientToken }; - } catch (error) { - authGraphqlApiExceptionHandler(error); + const workspaceMember = await this.userService.loadWorkspaceMember(user); + + if (!workspaceMember) { + return; } + const transientToken = await this.tokenService.generateTransientToken( + workspaceMember.id, + user.id, + user.defaultWorkspace.id, + ); + + return { transientToken }; } @Mutation(() => Verify) - async verify(@Args() verifyInput: VerifyInput): Promise { - try { - const email = await this.tokenService.verifyLoginToken( - verifyInput.loginToken, - ); + async verify(@Args() verifyInput: VerifyInput): Promise { + const email = await this.tokenService.verifyLoginToken( + verifyInput.loginToken, + ); - const result = await this.authService.verify(email); + const result = await this.authService.verify(email); - return result; - } catch (error) { - authGraphqlApiExceptionHandler(error); - } + return result; } @Mutation(() => AuthorizeApp) @@ -183,17 +147,13 @@ export class AuthResolver { async authorizeApp( @Args() authorizeAppInput: AuthorizeAppInput, @AuthUser() user: User, - ): Promise { - try { - const authorizedApp = await this.authService.generateAuthorizationCode( - authorizeAppInput, - user, - ); - - return authorizedApp; - } catch (error) { - authGraphqlApiExceptionHandler(error); - } + ): Promise { + const authorizedApp = await this.authService.generateAuthorizationCode( + authorizeAppInput, + user, + ); + + return authorizedApp; } @Mutation(() => AuthTokens) @@ -201,30 +161,22 @@ export class AuthResolver { async generateJWT( @AuthUser() user: User, @Args() args: GenerateJwtInput, - ): Promise { - try { - const token = await this.tokenService.generateSwitchWorkspaceToken( - user, - args.workspaceId, - ); - - return token; - } catch (error) { - authGraphqlApiExceptionHandler(error); - } + ): Promise { + const token = await this.tokenService.generateSwitchWorkspaceToken( + user, + args.workspaceId, + ); + + return token; } @Mutation(() => AuthTokens) - async renewToken(@Args() args: AppTokenInput): Promise { - try { - const tokens = await this.tokenService.generateTokensFromRefreshToken( - args.appToken, - ); - - return { tokens: tokens }; - } catch (error) { - authGraphqlApiExceptionHandler(error); - } + async renewToken(@Args() args: AppTokenInput): Promise { + const tokens = await this.tokenService.generateTokensFromRefreshToken( + args.appToken, + ); + + return { tokens: tokens }; } @UseGuards(JwtAuthGuard) @@ -232,12 +184,8 @@ export class AuthResolver { async impersonate( @Args() impersonateInput: ImpersonateInput, @AuthUser() user: User, - ): Promise { - try { - return await this.authService.impersonate(impersonateInput.userId, user); - } catch (error) { - authGraphqlApiExceptionHandler(error); - } + ): Promise { + return await this.authService.impersonate(impersonateInput.userId, user); } @UseGuards(JwtAuthGuard) @@ -246,62 +194,46 @@ export class AuthResolver { @Args() args: ApiKeyTokenInput, @AuthWorkspace() { id: workspaceId }: Workspace, ): Promise { - try { - return await this.tokenService.generateApiKeyToken( - workspaceId, - args.apiKeyId, - args.expiresAt, - ); - } catch (error) { - authGraphqlApiExceptionHandler(error); - } + return await this.tokenService.generateApiKeyToken( + workspaceId, + args.apiKeyId, + args.expiresAt, + ); } @Mutation(() => EmailPasswordResetLink) async emailPasswordResetLink( @Args() emailPasswordResetInput: EmailPasswordResetLinkInput, - ): Promise { - try { - const resetToken = await this.tokenService.generatePasswordResetToken( - emailPasswordResetInput.email, - ); - - return await this.tokenService.sendEmailPasswordResetLink( - resetToken, - emailPasswordResetInput.email, - ); - } catch (error) { - authGraphqlApiExceptionHandler(error); - } + ): Promise { + const resetToken = await this.tokenService.generatePasswordResetToken( + emailPasswordResetInput.email, + ); + + return await this.tokenService.sendEmailPasswordResetLink( + resetToken, + emailPasswordResetInput.email, + ); } @Mutation(() => InvalidatePassword) async updatePasswordViaResetToken( @Args() { passwordResetToken, newPassword }: UpdatePasswordViaResetTokenInput, - ): Promise { - try { - const { id } = - await this.tokenService.validatePasswordResetToken(passwordResetToken); + ): Promise { + const { id } = + await this.tokenService.validatePasswordResetToken(passwordResetToken); - await this.authService.updatePassword(id, newPassword); + await this.authService.updatePassword(id, newPassword); - return await this.tokenService.invalidatePasswordResetToken(id); - } catch (error) { - authGraphqlApiExceptionHandler(error); - } + return await this.tokenService.invalidatePasswordResetToken(id); } @Query(() => ValidatePasswordResetToken) async validatePasswordResetToken( @Args() args: ValidatePasswordResetTokenInput, - ): Promise { - try { - return this.tokenService.validatePasswordResetToken( - args.passwordResetToken, - ); - } catch (error) { - authGraphqlApiExceptionHandler(error); - } + ): Promise { + return this.tokenService.validatePasswordResetToken( + args.passwordResetToken, + ); } } 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 a2baa0a90445..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 @@ -1,4 +1,11 @@ -import { Controller, Get, Req, Res, UseGuards } from '@nestjs/common'; +import { + Controller, + Get, + Req, + Res, + UseFilters, + UseGuards, +} from '@nestjs/common'; import { Response } from 'express'; @@ -6,6 +13,7 @@ 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'; @@ -16,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, 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/utils/auth-graphql-api-exception-handler.util.ts b/packages/twenty-server/src/engine/core-modules/auth/filters/auth-graphql-api-exception.filter.ts similarity index 58% rename from packages/twenty-server/src/engine/core-modules/auth/utils/auth-graphql-api-exception-handler.util.ts rename to packages/twenty-server/src/engine/core-modules/auth/filters/auth-graphql-api-exception.filter.ts index 81c52945a769..ebabba88f127 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/utils/auth-graphql-api-exception-handler.util.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/filters/auth-graphql-api-exception.filter.ts @@ -1,3 +1,5 @@ +import { Catch } from '@nestjs/common'; + import { AuthException, AuthExceptionCode, @@ -9,22 +11,21 @@ import { UserInputError, } from 'src/engine/core-modules/graphql/utils/graphql-errors.util'; -export const authGraphqlApiExceptionHandler = (error: Error) => { - if (error instanceof AuthException) { - switch (error.code) { +@Catch(AuthException) +export class AuthGraphqlApiExceptionFilter { + catch(exception: AuthException) { + switch (exception.code) { case AuthExceptionCode.USER_NOT_FOUND: case AuthExceptionCode.CLIENT_NOT_FOUND: - throw new NotFoundError(error.message); + throw new NotFoundError(exception.message); case AuthExceptionCode.INVALID_INPUT: - throw new UserInputError(error.message); + throw new UserInputError(exception.message); case AuthExceptionCode.FORBIDDEN_EXCEPTION: - throw new ForbiddenError(error.message); + throw new ForbiddenError(exception.message); case AuthExceptionCode.INVALID_DATA: case AuthExceptionCode.INTERNAL_SERVER_ERROR: default: - throw new InternalServerError(error.message); + throw new InternalServerError(exception.message); } } - - throw error; -}; +} 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/services/token.service.ts b/packages/twenty-server/src/engine/core-modules/auth/services/token.service.ts index aa5754af716a..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 @@ -395,9 +395,7 @@ export class TokenService { ); } - if ( - !(codeChallengeAppToken.userId === authorizationCodeAppToken.userId) - ) { + if (codeChallengeAppToken.userId !== authorizationCodeAppToken.userId) { throw new AuthException( 'authorization code / code verifier was not created by same client', AuthExceptionCode.FORBIDDEN_EXCEPTION, @@ -507,7 +505,7 @@ export class TokenService { ); throw new AuthException( - 'Suspicious activity detected, this refresh token has been revoked. All tokens has been revoked.', + 'Suspicious activity detected, this refresh token has been revoked. All tokens have been revoked.', AuthExceptionCode.FORBIDDEN_EXCEPTION, ); } @@ -519,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 }, From d1f0cbec12f27500e2a218cb1bb853797b72d914 Mon Sep 17 00:00:00 2001 From: Thomas Trompette Date: Tue, 6 Aug 2024 18:10:57 +0200 Subject: [PATCH 5/6] Rename endpoint --- .../src/engine/core-modules/auth/auth.resolver.ts | 2 +- .../src/engine/core-modules/auth/services/auth.service.ts | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) 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 06c8f100e5d4..931ade625214 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 @@ -74,7 +74,7 @@ export class AuthResolver { async findWorkspaceFromInviteHash( @Args() workspaceInviteHashValidInput: WorkspaceInviteHashValidInput, ): Promise { - return await this.authService.findWorkspaceFromInviteHash( + return await this.authService.findWorkspaceFromInviteHashOrFail( workspaceInviteHashValidInput.inviteHash, ); } 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 b9cd7881643d..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 @@ -78,7 +78,7 @@ export class AuthService { if (!user.passwordHash) { throw new AuthException( 'Incorrect login method', - AuthExceptionCode.FORBIDDEN_EXCEPTION, + AuthExceptionCode.INVALID_INPUT, ); } @@ -380,7 +380,9 @@ export class AuthService { return { success: true }; } - async findWorkspaceFromInviteHash(inviteHash: string): Promise { + async findWorkspaceFromInviteHashOrFail( + inviteHash: string, + ): Promise { const workspace = await this.workspaceRepository.findOneBy({ inviteHash, }); From 8ee6310a040891ab2c2258e7b2c8a38660e88e77 Mon Sep 17 00:00:00 2001 From: Weiko Date: Wed, 7 Aug 2024 11:06:35 +0200 Subject: [PATCH 6/6] fix import --- .../twenty-server/src/engine/core-modules/auth/auth.resolver.ts | 2 -- 1 file changed, 2 deletions(-) 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 931ade625214..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,6 +1,5 @@ import { UseFilters, UseGuards } from '@nestjs/common'; import { Args, Mutation, Query, Resolver } from '@nestjs/graphql'; -import { InjectRepository } from '@nestjs/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'; @@ -43,7 +42,6 @@ import { TokenService } from './services/token.service'; @UseFilters(AuthGraphqlApiExceptionFilter) export class AuthResolver { constructor( - @InjectRepository(Workspace, 'core') private authService: AuthService, private tokenService: TokenService, private userService: UserService,