From 2afe933055eb18a885f73e95f733594fa40d3ba5 Mon Sep 17 00:00:00 2001 From: Charles Bochet Date: Tue, 4 Jul 2023 23:53:53 +0200 Subject: [PATCH] Make google auth optional on server side (#508) * Make google auth optional on server side * fix lint * Fix according to review --- server/.env.example | 6 +-- .../controllers/google-auth.controller.ts | 5 ++- .../guards/google-provider-enabled.guard.ts | 14 +++++++ .../auth/strategies/google.auth.strategy.ts | 13 ++++-- server/src/core/file/services/file.service.ts | 2 +- .../environment/environment.service.ts | 16 ++++++-- .../environment/environment.validation.ts | 40 +++++++++++++++---- .../src/integrations/integrations.module.ts | 20 +++------- .../interfaces/s3-storage-module.interface.ts | 1 + .../s3-storage/s3-storage.service.ts | 8 +++- 10 files changed, 86 insertions(+), 39 deletions(-) create mode 100644 server/src/core/auth/guards/google-provider-enabled.guard.ts diff --git a/server/.env.example b/server/.env.example index 69dd8ffdb..f01f16cbc 100644 --- a/server/.env.example +++ b/server/.env.example @@ -1,6 +1,3 @@ -AUTH_GOOGLE_CLIENT_ID=REPLACE_ME -AUTH_GOOGLE_CLIENT_SECRET=REPLACE_ME -AUTH_GOOGLE_CALLBACK_URL=http://localhost:3000/auth/google/redirect ACCESS_TOKEN_SECRET=secret_jwt ACCESS_TOKEN_EXPIRES_IN=5m LOGIN_TOKEN_SECRET=secret_login_token @@ -10,5 +7,4 @@ REFRESH_TOKEN_EXPIRES_IN=90d PG_DATABASE_URL=postgres://postgres:postgrespassword@postgres:5432/default?connection_limit=1 FRONT_AUTH_CALLBACK_URL=http://localhost:3001/auth/callback STORAGE_TYPE=local -STORAGE_REGION=eu-west-1 -STORAGE_LOCATION=.local-storage +STORAGE_LOCAL_PATH=.local-storage diff --git a/server/src/core/auth/controllers/google-auth.controller.ts b/server/src/core/auth/controllers/google-auth.controller.ts index 00063dd3e..0eaffb590 100644 --- a/server/src/core/auth/controllers/google-auth.controller.ts +++ b/server/src/core/auth/controllers/google-auth.controller.ts @@ -11,6 +11,7 @@ import { Response } from 'express'; import { GoogleRequest } from '../strategies/google.auth.strategy'; import { UserService } from '../../user/user.service'; import { TokenService } from '../services/token.service'; +import { GoogleProviderEnabledGuard } from '../guards/google-provider-enabled.guard'; @Controller('auth/google') export class GoogleAuthController { @@ -20,14 +21,14 @@ export class GoogleAuthController { ) {} @Get() - @UseGuards(AuthGuard('google')) + @UseGuards(GoogleProviderEnabledGuard, AuthGuard('google')) async googleAuth() { // As this method is protected by Google Auth guard, it will trigger Google SSO flow return; } @Get('redirect') - @UseGuards(AuthGuard('google')) + @UseGuards(GoogleProviderEnabledGuard, AuthGuard('google')) async googleAuthRedirect(@Req() req: GoogleRequest, @Res() res: Response) { const { firstName, lastName, email } = req.user; diff --git a/server/src/core/auth/guards/google-provider-enabled.guard.ts b/server/src/core/auth/guards/google-provider-enabled.guard.ts new file mode 100644 index 000000000..baf92d59a --- /dev/null +++ b/server/src/core/auth/guards/google-provider-enabled.guard.ts @@ -0,0 +1,14 @@ +import { Injectable, CanActivate, HttpException } from '@nestjs/common'; +import { Observable } from 'rxjs'; +import { EnvironmentService } from 'src/integrations/environment/environment.service'; + +@Injectable() +export class GoogleProviderEnabledGuard implements CanActivate { + constructor(private readonly environmentService: EnvironmentService) {} + canActivate(): boolean | Promise | Observable { + if (!this.environmentService.getAuthGoogleEnabled()) { + throw new HttpException('Google auth is not enabled', 404); + } + return true; + } +} diff --git a/server/src/core/auth/strategies/google.auth.strategy.ts b/server/src/core/auth/strategies/google.auth.strategy.ts index a14f52f7a..c71e38b6c 100644 --- a/server/src/core/auth/strategies/google.auth.strategy.ts +++ b/server/src/core/auth/strategies/google.auth.strategy.ts @@ -16,10 +16,17 @@ export type GoogleRequest = Request & { @Injectable() export class GoogleStrategy extends PassportStrategy(Strategy, 'google') { constructor(environmentService: EnvironmentService) { + const isAuthGoogleEnabled = environmentService.getAuthGoogleEnabled(); super({ - clientID: environmentService.getAuthGoogleClientId(), - clientSecret: environmentService.getAuthGoogleClientSecret(), - callbackURL: environmentService.getAuthGoogleCallbackUrl(), + clientID: isAuthGoogleEnabled + ? environmentService.getAuthGoogleClientId() + : 'disabled', + clientSecret: isAuthGoogleEnabled + ? environmentService.getAuthGoogleClientSecret() + : 'disabled', + callbackURL: isAuthGoogleEnabled + ? environmentService.getAuthGoogleCallbackUrl() + : 'disabled', scope: ['email', 'profile'], }); } diff --git a/server/src/core/file/services/file.service.ts b/server/src/core/file/services/file.service.ts index 4628b004c..51d3b993f 100644 --- a/server/src/core/file/services/file.service.ts +++ b/server/src/core/file/services/file.service.ts @@ -25,7 +25,7 @@ export class FileService { } private async getLocalFileStream(folderPath: string, filename: string) { - const storageLocation = this.environmentService.getStorageLocation(); + const storageLocation = this.environmentService.getStorageLocalPath(); const filePath = join( process.cwd(), diff --git a/server/src/integrations/environment/environment.service.ts b/server/src/integrations/environment/environment.service.ts index acc046643..57be8a7b8 100644 --- a/server/src/integrations/environment/environment.service.ts +++ b/server/src/integrations/environment/environment.service.ts @@ -40,6 +40,10 @@ export class EnvironmentService { return this.configService.get('FRONT_AUTH_CALLBACK_URL')!; } + getAuthGoogleEnabled(): boolean | undefined { + return this.configService.get('AUTH_GOOGLE_ENABLED'); + } + getAuthGoogleClientId(): string | undefined { return this.configService.get('AUTH_GOOGLE_CLIENT_ID'); } @@ -56,11 +60,15 @@ export class EnvironmentService { return this.configService.get('STORAGE_TYPE'); } - getStorageRegion(): AwsRegion | undefined { - return this.configService.get('STORAGE_REGION'); + getStorageS3Region(): AwsRegion | undefined { + return this.configService.get('STORAGE_S3_REGION'); } - getStorageLocation(): string { - return this.configService.get('STORAGE_LOCATION')!; + getStorageS3Name(): string | undefined { + return this.configService.get('STORAGE_S3_NAME'); + } + + getStorageLocalPath(): string | undefined { + return this.configService.get('STORAGE_LOCAL_PATH')!; } } diff --git a/server/src/integrations/environment/environment.validation.ts b/server/src/integrations/environment/environment.validation.ts index fe4b8cba9..0ea9fede1 100644 --- a/server/src/integrations/environment/environment.validation.ts +++ b/server/src/integrations/environment/environment.validation.ts @@ -1,4 +1,4 @@ -import { plainToClass } from 'class-transformer'; +import { plainToClass, Transform } from 'class-transformer'; import { IsEnum, IsOptional, @@ -6,6 +6,7 @@ import { IsUrl, ValidateIf, validateSync, + IsBoolean, } from 'class-validator'; import { assert } from 'src/utils/assert'; import { IsDuration } from './decorators/is-duration.decorator'; @@ -38,16 +39,21 @@ export class EnvironmentVariables { @IsUrl({ require_tld: false }) FRONT_AUTH_CALLBACK_URL: string; - @IsString() + @Transform(({ value }) => envValueToBoolean(value)) @IsOptional() + @IsBoolean() + AUTH_GOOGLE_ENABLED?: boolean; + + @IsString() + @ValidateIf((env) => env.AUTH_GOOGLE_ENABLED === true) AUTH_GOOGLE_CLIENT_ID?: string; @IsString() - @IsOptional() + @ValidateIf((env) => env.AUTH_GOOGLE_ENABLED === true) AUTH_GOOGLE_CLIENT_SECRET?: string; @IsUrl({ require_tld: false }) - @IsOptional() + @ValidateIf((env) => env.AUTH_GOOGLE_ENABLED === true) AUTH_GOOGLE_CALLBACK_URL?: string; // Storage @@ -55,17 +61,22 @@ export class EnvironmentVariables { @IsOptional() STORAGE_TYPE?: StorageType; - @ValidateIf((_, value) => value === StorageType.S3) + @ValidateIf((env) => env.STORAGE_TYPE === StorageType.S3) @IsAWSRegion() - STORAGE_REGION?: AwsRegion; + STORAGE_S3_REGION?: AwsRegion; + + @ValidateIf((env) => env.STORAGE_TYPE === StorageType.S3) + @IsString() + STORAGE_S3_NAME?: string; @IsString() - STORAGE_LOCATION: string; + @ValidateIf((env) => env.STORAGE_TYPE === StorageType.Local) + STORAGE_LOCAL_PATH?: string; } export function validate(config: Record) { const validatedConfig = plainToClass(EnvironmentVariables, config, { - enableImplicitConversion: true, + enableImplicitConversion: false, }); const errors = validateSync(validatedConfig, { @@ -75,3 +86,16 @@ export function validate(config: Record) { return validatedConfig; } + +const envValueToBoolean = (value: any) => { + if (typeof value === 'boolean') { + return value; + } + if (['true', 'on', 'yes', '1'].includes(value.toLowerCase())) { + return true; + } + if (['false', 'off', 'no', '0'].includes(value.toLowerCase())) { + return false; + } + return undefined; +}; diff --git a/server/src/integrations/integrations.module.ts b/server/src/integrations/integrations.module.ts index e388db98c..c8d868e66 100644 --- a/server/src/integrations/integrations.module.ts +++ b/server/src/integrations/integrations.module.ts @@ -6,7 +6,6 @@ import { LocalStorageModule } from './local-storage/local-storage.module'; import { LocalStorageModuleOptions } from './local-storage/interfaces'; import { EnvironmentModule } from './environment/environment.module'; import { EnvironmentService } from './environment/environment.service'; -import { assert } from 'src/utils/assert'; /** * S3 Storage Module factory @@ -16,23 +15,16 @@ import { assert } from 'src/utils/assert'; const S3StorageModuleFactory = async ( environmentService: EnvironmentService, ): Promise => { - const fileSystem = environmentService.getStorageType(); - const bucketName = environmentService.getStorageLocation(); - const region = environmentService.getStorageRegion(); - - if (fileSystem === 'local') { - return { bucketName }; - } - - assert(region, 'S3 region is not defined'); + const bucketName = environmentService.getStorageS3Name(); + const region = environmentService.getStorageS3Region(); return { - bucketName, + bucketName: bucketName ?? '', credentials: fromNodeProviderChain({ clientConfig: { region }, }), forcePathStyle: true, - region, + region: region ?? '', }; }; @@ -44,10 +36,10 @@ const S3StorageModuleFactory = async ( const localStorageModuleFactory = async ( environmentService: EnvironmentService, ): Promise => { - const folderName = environmentService.getStorageLocation(); + const storagePath = environmentService.getStorageLocalPath(); return { - storagePath: process.cwd() + '/' + folderName, + storagePath: process.cwd() + '/' + storagePath, }; }; diff --git a/server/src/integrations/s3-storage/interfaces/s3-storage-module.interface.ts b/server/src/integrations/s3-storage/interfaces/s3-storage-module.interface.ts index 2951d1553..426ba7615 100644 --- a/server/src/integrations/s3-storage/interfaces/s3-storage-module.interface.ts +++ b/server/src/integrations/s3-storage/interfaces/s3-storage-module.interface.ts @@ -2,4 +2,5 @@ import { S3ClientConfig } from '@aws-sdk/client-s3'; export interface S3StorageModuleOptions extends S3ClientConfig { bucketName: string; + region: string; } diff --git a/server/src/integrations/s3-storage/s3-storage.service.ts b/server/src/integrations/s3-storage/s3-storage.service.ts index f02f1d732..b4d82a807 100644 --- a/server/src/integrations/s3-storage/s3-storage.service.ts +++ b/server/src/integrations/s3-storage/s3-storage.service.ts @@ -23,9 +23,13 @@ export class S3StorageService { @Inject(MODULE_OPTIONS_TOKEN) private readonly options: S3StorageModuleOptions, ) { - const { bucketName, ...s3Options } = options; + const { bucketName, region, ...s3Options } = options; - this.s3Client = new S3(s3Options); + if (!bucketName || !region) { + return; + } + + this.s3Client = new S3({ ...s3Options, region }); this.bucketName = bucketName; }