Make google auth optional on server side (#508)

* Make google auth optional on server side

* fix lint

* Fix according to review
This commit is contained in:
Charles Bochet
2023-07-04 23:53:53 +02:00
committed by GitHub
parent 6fc416da76
commit 2afe933055
10 changed files with 86 additions and 39 deletions

View File

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

View File

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

View File

@ -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<boolean> | Observable<boolean> {
if (!this.environmentService.getAuthGoogleEnabled()) {
throw new HttpException('Google auth is not enabled', 404);
}
return true;
}
}

View File

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

View File

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

View File

@ -40,6 +40,10 @@ export class EnvironmentService {
return this.configService.get<string>('FRONT_AUTH_CALLBACK_URL')!;
}
getAuthGoogleEnabled(): boolean | undefined {
return this.configService.get<boolean>('AUTH_GOOGLE_ENABLED');
}
getAuthGoogleClientId(): string | undefined {
return this.configService.get<string>('AUTH_GOOGLE_CLIENT_ID');
}
@ -56,11 +60,15 @@ export class EnvironmentService {
return this.configService.get<StorageType>('STORAGE_TYPE');
}
getStorageRegion(): AwsRegion | undefined {
return this.configService.get<AwsRegion>('STORAGE_REGION');
getStorageS3Region(): AwsRegion | undefined {
return this.configService.get<AwsRegion>('STORAGE_S3_REGION');
}
getStorageLocation(): string {
return this.configService.get<string>('STORAGE_LOCATION')!;
getStorageS3Name(): string | undefined {
return this.configService.get<AwsRegion>('STORAGE_S3_NAME');
}
getStorageLocalPath(): string | undefined {
return this.configService.get<string>('STORAGE_LOCAL_PATH')!;
}
}

View File

@ -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<string, unknown>) {
const validatedConfig = plainToClass(EnvironmentVariables, config, {
enableImplicitConversion: true,
enableImplicitConversion: false,
});
const errors = validateSync(validatedConfig, {
@ -75,3 +86,16 @@ export function validate(config: Record<string, unknown>) {
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;
};

View File

@ -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<S3StorageModuleOptions> => {
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<LocalStorageModuleOptions> => {
const folderName = environmentService.getStorageLocation();
const storagePath = environmentService.getStorageLocalPath();
return {
storagePath: process.cwd() + '/' + folderName,
storagePath: process.cwd() + '/' + storagePath,
};
};

View File

@ -2,4 +2,5 @@ import { S3ClientConfig } from '@aws-sdk/client-s3';
export interface S3StorageModuleOptions extends S3ClientConfig {
bucketName: string;
region: string;
}

View File

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