Build exceptions and handler (#6459)

Adding exceptions and handler for auth services.

Tested with:
- Workspace creation
- Workspace signup
- Workspace invitation
- Reset password
- Adding email account
- Impersonation

---------

Co-authored-by: Weiko <corentin@twenty.com>
This commit is contained in:
Thomas Trompette 2024-08-07 11:42:49 +02:00 committed by GitHub
parent f09e61bb9f
commit 2abb6adb61
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 580 additions and 269 deletions

View File

@ -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',
}

View File

@ -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<Workspace>,
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<Workspace> {
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<AuthTokens> {
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<Verify> {
// 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<InvalidatePassword> {
const { id } = await this.tokenService.validatePasswordResetToken(
args.passwordResetToken,
);
const { id } =
await this.tokenService.validatePasswordResetToken(passwordResetToken);
assert(id, 'User not found', NotFoundException);
const { success } = await this.authService.updatePassword(
id,
args.newPassword,
);
assert(success, 'Password update failed', InternalServerErrorException);
await this.authService.updatePassword(id, newPassword);
return await this.tokenService.invalidatePasswordResetToken(id);
}

View File

@ -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;

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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(

View File

@ -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(

View File

@ -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<boolean> | Observable<boolean> {
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);

View File

@ -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<boolean> | Observable<boolean> {
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);

View File

@ -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<Verify> {
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<UpdatePassword> {
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<Workspace> {
const workspace = await this.workspaceRepository.findOneBy({
inviteHash,
});
if (!workspace) {
throw new AuthException(
'Workspace does not exist',
AuthExceptionCode.INVALID_INPUT,
);
}
return workspace;
}
}

View File

@ -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: '',

View File

@ -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);
});
});
});

View File

@ -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<AuthToken> {
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<AuthTokens> {
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<ExchangeAuthCode> {
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(
{

View File

@ -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,
);
}
}

View File

@ -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'] = {

View File

@ -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;