From 6b87f71df65de575bc5b735d916a3f3210469323 Mon Sep 17 00:00:00 2001 From: varsha766 Date: Fri, 12 Jul 2024 13:33:06 +0530 Subject: [PATCH 1/5] implemented MFA --- package.json | 2 + src/app-auth/app-auth.module.ts | 5 ++ src/app-oauth/app-oauth.module.ts | 3 +- .../controller/social-login.controller.ts | 38 ++++++++++ src/social-login/dto/request.dto.ts | 33 +++++++++ src/social-login/dto/response.dto.ts | 31 +++++++- .../services/social-login.service.ts | 72 +++++++++++++++++++ src/social-login/social-login.module.ts | 22 ++++++ src/user/repository/user.repository.ts | 2 +- src/user/schema/user.schema.ts | 9 ++- .../2FA-jwt-authorization.middleware.ts | 27 +++++++ .../jwt-authorization.middleware.ts | 18 +++-- 12 files changed, 252 insertions(+), 10 deletions(-) create mode 100644 src/social-login/dto/request.dto.ts create mode 100644 src/utils/middleware/2FA-jwt-authorization.middleware.ts diff --git a/package.json b/package.json index 8c3cf130..2fb2fda0 100644 --- a/package.json +++ b/package.json @@ -47,10 +47,12 @@ "hypersign-edv-client": "github:hypersign-protocol/hypersign-edv-client#develop", "idb-keyval": "^6.2.1", "mongoose": "^6.8.3", + "otplib": "^12.0.1", "passport": "^0.6.0", "passport-google-oauth20": "^2.0.0", "passport-jwt": "^4.0.1", "passport-local": "^1.0.0", + "qrcode": "^1.5.3", "readline-sync": "^1.4.10", "reflect-metadata": "^0.1.13", "rxjs": "^7.2.0", diff --git a/src/app-auth/app-auth.module.ts b/src/app-auth/app-auth.module.ts index 300d9b0b..a7c3c3ac 100644 --- a/src/app-auth/app-auth.module.ts +++ b/src/app-auth/app-auth.module.ts @@ -23,6 +23,7 @@ import { SupportedServiceService } from 'src/supported-service/services/supporte import { SupportedServiceList } from 'src/supported-service/services/service-list'; import { JWTAuthorizeMiddleware } from 'src/utils/middleware/jwt-authorization.middleware'; import { UserModule } from 'src/user/user.module'; +import { TwoFAAuthorizationMiddleware } from 'src/utils/middleware/2FA-jwt-authorization.middleware'; @Module({ imports: [ @@ -60,5 +61,9 @@ export class AppAuthModule implements NestModule { .apply(JWTAuthorizeMiddleware) .exclude({ path: '/api/v1/app/marketplace', method: RequestMethod.GET }) .forRoutes(AppAuthController); + consumer + .apply(TwoFAAuthorizationMiddleware) + .exclude({ path: '/api/v1/app/marketplace', method: RequestMethod.GET }) + .forRoutes(AppAuthController); } } diff --git a/src/app-oauth/app-oauth.module.ts b/src/app-oauth/app-oauth.module.ts index 412c4dfa..e87285a9 100644 --- a/src/app-oauth/app-oauth.module.ts +++ b/src/app-oauth/app-oauth.module.ts @@ -7,8 +7,9 @@ import { import { AppOauthController } from './app-oauth.controller'; import { AppAuthModule } from 'src/app-auth/app-auth.module'; import { JWTAuthorizeMiddleware } from 'src/utils/middleware/jwt-authorization.middleware'; +import { UserModule } from 'src/user/user.module'; @Module({ - imports: [AppAuthModule], + imports: [AppAuthModule, UserModule], controllers: [AppOauthController], providers: [], }) diff --git a/src/social-login/controller/social-login.controller.ts b/src/social-login/controller/social-login.controller.ts index a484e1ae..ee5b5838 100644 --- a/src/social-login/controller/social-login.controller.ts +++ b/src/social-login/controller/social-login.controller.ts @@ -8,10 +8,12 @@ import { Post, Res, Query, + Body, } from '@nestjs/common'; import { SocialLoginService } from '../services/social-login.service'; import { AuthGuard } from '@nestjs/passport'; import { + ApiBearerAuth, ApiExcludeEndpoint, ApiOkResponse, ApiQuery, @@ -23,9 +25,12 @@ import { AllExceptionsFilter } from 'src/utils/utils'; import { ConfigService } from '@nestjs/config'; import { AuthResponse, + Generate2FARespDto, LoginResponse, UnauthorizedError, + Verify2FARespDto, } from '../dto/response.dto'; +import { Generate2FA, MFACodeVerificationDto } from '../dto/request.dto'; @UseFilters(AllExceptionsFilter) @ApiTags('Authentication') @Controller() @@ -66,6 +71,7 @@ export class SocialLoginController { const token = await this.socialLoginService.socialLogin(req); res.redirect(`${this.config.get('REDIRECT_URL')}?token=${token}`); } + @ApiBearerAuth('Authorization') @ApiOkResponse({ description: 'User Info', type: AuthResponse, @@ -83,4 +89,36 @@ export class SocialLoginController { error: null, }; } + + @ApiOkResponse({ + description: 'Generated QR successfully', + type: Generate2FARespDto, + }) + @ApiUnauthorizedResponse({ + status: 401, + type: UnauthorizedError, + }) + @ApiBearerAuth('Authorization') + @Post('/api/auth/mfa/generate') + async generateMfa(@Req() req, @Body() body: Generate2FA) { + const result = await this.socialLoginService.generate2FA(body, req.user); + return { twoFADataUrl: result }; + } + + @ApiOkResponse({ + description: 'Verified MFA code and generated new token', + type: Verify2FARespDto, + }) + @ApiUnauthorizedResponse({ + status: 401, + type: UnauthorizedError, + }) + @ApiBearerAuth('Authorization') + @Post('/api/auth/mfa/verify') + async verifyMFA( + @Req() req, + @Body() mfaVerificationDto: MFACodeVerificationDto, + ) { + return this.socialLoginService.verifyMFACode(req.user, mfaVerificationDto); + } } diff --git a/src/social-login/dto/request.dto.ts b/src/social-login/dto/request.dto.ts new file mode 100644 index 00000000..dd3e8311 --- /dev/null +++ b/src/social-login/dto/request.dto.ts @@ -0,0 +1,33 @@ +import { IsEnum, IsNotEmpty, IsString } from 'class-validator'; +import { AuthneticatorType } from './response.dto'; +import { ApiProperty } from '@nestjs/swagger'; + +export class MFACodeVerificationDto { + @ApiProperty({ + name: 'authenticatorType', + description: 'Type of authenticator used for 2FA', + example: AuthneticatorType.google, + enum: AuthneticatorType, + }) + @IsEnum(AuthneticatorType) + authenticatorType: string; + @ApiProperty({ + name: 'twoFactorAuthenticationCode', + description: 'Code generated in authenticator app', + example: '678324', + }) + @IsString() + @IsNotEmpty() + twoFactorAuthenticationCode: string; +} + +export class Generate2FA { + @ApiProperty({ + name: 'authenticatorType', + description: 'Type of authenticator used for 2FA', + example: AuthneticatorType.google, + enum: AuthneticatorType, + }) + @IsEnum(AuthneticatorType) + authenticatorType: string; +} diff --git a/src/social-login/dto/response.dto.ts b/src/social-login/dto/response.dto.ts index 3b8afa6f..7e5c6329 100644 --- a/src/social-login/dto/response.dto.ts +++ b/src/social-login/dto/response.dto.ts @@ -1,5 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsNumber, IsString } from 'class-validator'; +import { IsBoolean, IsNumber, IsString } from 'class-validator'; export class UnauthorizedError { @ApiProperty({ @@ -66,3 +66,32 @@ export class AuthResponse { }) error: string; } +export class Generate2FARespDto { + @ApiProperty({ + name: 'twoFADataUrl', + description: 'QR Data', + example: 'data:image/png;base64,iV......', + }) + @IsString() + twoFADataUrl: string; +} +export class Verify2FARespDto { + @ApiProperty({ + name: 'isVerified', + description: 'COde verification result', + example: true, + }) + @IsBoolean() + isVerified: boolean; + @ApiProperty({ + name: 'accessToken', + description: '2FA based accessToken', + example: 'eyJh.....', + }) + @IsString() + accessToken: string; +} +export enum AuthneticatorType { + google = 'google', + okta = 'okta', +} diff --git a/src/social-login/services/social-login.service.ts b/src/social-login/services/social-login.service.ts index 6bdaa563..cbe1814d 100644 --- a/src/social-login/services/social-login.service.ts +++ b/src/social-login/services/social-login.service.ts @@ -7,6 +7,10 @@ import { Providers } from '../strategy/social.strategy'; import { sanitizeUrl } from 'src/utils/utils'; import { SupportedServiceList } from 'src/supported-service/services/service-list'; import { SERVICE_TYPES } from 'src/supported-service/services/iServiceList'; +import { AuthneticatorType } from '../dto/response.dto'; +import { authenticator } from 'otplib'; +import { toDataURL } from 'qrcode'; +import { Generate2FA, MFACodeVerificationDto } from '../dto/request.dto'; @Injectable() export class SocialLoginService { @@ -75,4 +79,72 @@ export class SocialLoginService { }); return token; } + + async generate2FA(genrate2FADto: Generate2FA, user) { + Logger.log( + 'Inside generate2FA() method to generate 2FA QRCode', + 'SocialLoginService', + ); + const { authenticatorType } = genrate2FADto; + let secret; + if (authenticatorType == AuthneticatorType.google) { + if (user && !user.twoFAGoogleSecret) { + secret = authenticator.generateSecret(20); + this.userRepository.findOneUpdate( + { userId: user.userId }, + { twoFAGoogleSecret: secret, isGoogleTwoFAEnabled: true }, + ); + } else { + secret = user.twoFAGoogleSecret; + } + } else if (authenticatorType == AuthneticatorType.okta) { + if (user && !user.twoFAOktaSecret) { + secret = authenticator.generateSecret(20); + this.userRepository.findOneUpdate( + { userId: user.userId }, + { twoFAOktaSecret: secret, isOktaTwoFAEnabled: true }, + ); + } else { + secret = user.twoFAOktaSecret; + } + } + const otpAuthUrl = authenticator.keyuri( + user.email, + 'DeveloperDashboard', + secret, + ); + return toDataURL(otpAuthUrl); + } + async verifyMFACode(user, mfaVerificationDto: MFACodeVerificationDto) { + Logger.log( + 'Inside verifyMFACode() method to verify MFA code', + 'SocialLoginService', + ); + const { authenticatorType, twoFactorAuthenticationCode } = + mfaVerificationDto; + const secret = + authenticatorType === AuthneticatorType.google + ? user.twoFAGoogleSecret + : user.twoFAOktaSecret; + const isVerified = authenticator.verify({ + token: twoFactorAuthenticationCode, + secret, + }); + const payload = { + email: user.email, + appUserID: user.userId, + userAccessList: user.accessList, + isTwoFactorEnabled: + !!user.isGoogleTwoFAEnabled || !!user.isOktaTwoFAEnabled, + isTwoFactorAuthenticated: isVerified, + }; + const accessToken = await this.jwt.signAsync(payload, { + expiresIn: '24h', + secret: this.config.get('JWT_SECRET'), + }); + return { + isVerified, + accessToken, + }; + } } diff --git a/src/social-login/social-login.module.ts b/src/social-login/social-login.module.ts index a7dcbbfd..c0669202 100644 --- a/src/social-login/social-login.module.ts +++ b/src/social-login/social-login.module.ts @@ -14,6 +14,7 @@ import { AppAuthModule } from 'src/app-auth/app-auth.module'; import { JWTAuthorizeMiddleware } from 'src/utils/middleware/jwt-authorization.middleware'; import { SupportedServiceModule } from 'src/supported-service/supported-service.module'; import { SupportedServiceList } from 'src/supported-service/services/service-list'; +import { TwoFAAuthorizationMiddleware } from 'src/utils/middleware/2FA-jwt-authorization.middleware'; @Module({ imports: [ @@ -47,5 +48,26 @@ export class SocialLoginModule implements NestModule { }, ) .forRoutes(SocialLoginController); + consumer + .apply(TwoFAAuthorizationMiddleware) + .exclude( + { + path: '/api/v1/login', + method: RequestMethod.GET, + }, + { + path: '/api/v1/login/callback', + method: RequestMethod.GET, + }, + { + path: '/api/auth/mfa/generate', + method: RequestMethod.POST, + }, + { + path: '/api/auth/mfa/verify', + method: RequestMethod.POST, + }, + ) + .forRoutes(SocialLoginController); } } diff --git a/src/user/repository/user.repository.ts b/src/user/repository/user.repository.ts index c826d93b..9301f5a1 100644 --- a/src/user/repository/user.repository.ts +++ b/src/user/repository/user.repository.ts @@ -12,7 +12,7 @@ export class UserRepository { 'findOne() method: starts, finding particular user from db', 'UserRepository', ); - return this.userModel.findOne(userFilterQuery); + return this.userModel.findOne(userFilterQuery).lean(); } async create(user: User): Promise { diff --git a/src/user/schema/user.schema.ts b/src/user/schema/user.schema.ts index 196a4923..72f4e955 100644 --- a/src/user/schema/user.schema.ts +++ b/src/user/schema/user.schema.ts @@ -20,10 +20,17 @@ export class User { email: string; @Prop({ required: false }) // as we won't get did in google login did?: string; - @Prop({ required: false }) @Optional() accessList: Array; + @Prop({ required: false }) + twoFAOktaSecret?: string; + @Prop({ required: false }) + twoFAGoogleSecret?: string; + @Prop({ type: Boolean, required: false, default: false }) + isGoogleTwoFAEnabled?: boolean; + @Prop({ type: Boolean, required: false, default: false }) + isOktaTwoFAEnabled?: boolean; } export const UserSchema = SchemaFactory.createForClass(User); diff --git a/src/utils/middleware/2FA-jwt-authorization.middleware.ts b/src/utils/middleware/2FA-jwt-authorization.middleware.ts new file mode 100644 index 00000000..a95a25e6 --- /dev/null +++ b/src/utils/middleware/2FA-jwt-authorization.middleware.ts @@ -0,0 +1,27 @@ +import { + Injectable, + Logger, + NestMiddleware, + UnauthorizedException, +} from '@nestjs/common'; +import { Request, Response, NextFunction } from 'express'; + +@Injectable() +export class TwoFAAuthorizationMiddleware implements NestMiddleware { + async use(req: Request, res: Response, next: NextFunction) { + Logger.log( + 'Inside TwoFAAuthorizationMiddleware', + 'TwoFAAuthorizationMiddleware', + ); + if (!req['user'] || Object.keys(req['user']).length === 0) { + throw new UnauthorizedException(['User not authenticated']); + } + const user = req['user']; + if (user['isGoogleTwoFAEnabled'] || user['isOktaTwoFAEnabled']) { + if (!user['isTwoFactorAuthenticated']) { + throw new UnauthorizedException(['2FA authentication is required']); + } + } + next(); + } +} diff --git a/src/utils/middleware/jwt-authorization.middleware.ts b/src/utils/middleware/jwt-authorization.middleware.ts index 89945934..380c5ce5 100644 --- a/src/utils/middleware/jwt-authorization.middleware.ts +++ b/src/utils/middleware/jwt-authorization.middleware.ts @@ -6,8 +6,10 @@ import { } from '@nestjs/common'; import * as jwt from 'jsonwebtoken'; import { NextFunction, Request, Response } from 'express'; +import { UserRepository } from 'src/user/repository/user.repository'; @Injectable() export class JWTAuthorizeMiddleware implements NestMiddleware { + constructor(private readonly userRepository: UserRepository) {} async use(req: Request, res: Response, next: NextFunction) { Logger.log('Inside JWTAuthorizeMiddleware', 'JWTAuthorizeMiddleware'); if (!req.header('authorization') || req.headers['authorization'] === '') { @@ -25,14 +27,18 @@ export class JWTAuthorizeMiddleware implements NestMiddleware { try { decoded = jwt.verify(tokenParts[1], process.env.JWT_SECRET); if (decoded) { - req['user'] = { + const user = await this.userRepository.findOne({ userId: decoded.appUserID, - email: decoded.email, - name: decoded.name, - userAccessList: decoded.userAccessList, - id: decoded['id'], - }; + }); + req['user'] = user; + if (decoded.isTwoFactorEnabled !== undefined) { + req['user']['isTwoFactorEnabled'] = decoded.isTwoFactorEnabled; + } + if (decoded.isTwoFactorAuthenticated !== undefined) { + req['user']['isTwoFactorAuthenticated'] = + decoded.isTwoFactorAuthenticated; + } Logger.log(JSON.stringify(req.user), 'JWTAuthorizeMiddleware'); } } catch (e) { From e704d6dd1b0423e40081e701ca6c66d4f7ba390a Mon Sep 17 00:00:00 2001 From: varsha766 Date: Fri, 12 Jul 2024 14:08:16 +0530 Subject: [PATCH 2/5] removed hard coding --- src/social-login/services/social-login.service.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/social-login/services/social-login.service.ts b/src/social-login/services/social-login.service.ts index cbe1814d..90680ab7 100644 --- a/src/social-login/services/social-login.service.ts +++ b/src/social-login/services/social-login.service.ts @@ -108,11 +108,8 @@ export class SocialLoginService { secret = user.twoFAOktaSecret; } } - const otpAuthUrl = authenticator.keyuri( - user.email, - 'DeveloperDashboard', - secret, - ); + const issuer = this.config.get('MFA_ISSUER'); + const otpAuthUrl = authenticator.keyuri(user.email, issuer, secret); return toDataURL(otpAuthUrl); } async verifyMFACode(user, mfaVerificationDto: MFACodeVerificationDto) { From c5bdbf172c26a86dfc2ed6130590fb1dabe61c92 Mon Sep 17 00:00:00 2001 From: varsha766 Date: Fri, 12 Jul 2024 16:27:00 +0530 Subject: [PATCH 3/5] integrated api for removing particular mfa --- .../controller/social-login.controller.ts | 27 ++++++++++++++- src/social-login/dto/request.dto.ts | 28 ++++++++++++++++ src/social-login/dto/response.dto.ts | 9 +++++ .../services/social-login.service.ts | 33 ++++++++++++++++++- 4 files changed, 95 insertions(+), 2 deletions(-) diff --git a/src/social-login/controller/social-login.controller.ts b/src/social-login/controller/social-login.controller.ts index ee5b5838..5924d4e1 100644 --- a/src/social-login/controller/social-login.controller.ts +++ b/src/social-login/controller/social-login.controller.ts @@ -9,10 +9,12 @@ import { Res, Query, Body, + Delete, } from '@nestjs/common'; import { SocialLoginService } from '../services/social-login.service'; import { AuthGuard } from '@nestjs/passport'; import { + ApiBadRequestResponse, ApiBearerAuth, ApiExcludeEndpoint, ApiOkResponse, @@ -25,12 +27,18 @@ import { AllExceptionsFilter } from 'src/utils/utils'; import { ConfigService } from '@nestjs/config'; import { AuthResponse, + DeleteMFARespDto, Generate2FARespDto, LoginResponse, UnauthorizedError, Verify2FARespDto, } from '../dto/response.dto'; -import { Generate2FA, MFACodeVerificationDto } from '../dto/request.dto'; +import { + DeleteMFADto, + Generate2FA, + MFACodeVerificationDto, +} from '../dto/request.dto'; +import { AppError } from 'src/app-auth/dtos/fetch-app.dto'; @UseFilters(AllExceptionsFilter) @ApiTags('Authentication') @Controller() @@ -121,4 +129,21 @@ export class SocialLoginController { ) { return this.socialLoginService.verifyMFACode(req.user, mfaVerificationDto); } + @ApiOkResponse({ + description: 'Removed MFA successfully', + type: DeleteMFARespDto, + }) + @ApiBadRequestResponse({ + status: 400, + type: AppError, + }) + @ApiUnauthorizedResponse({ + status: 401, + type: UnauthorizedError, + }) + @ApiBearerAuth('Authorization') + @Delete('/api/auth/mfa') + async removeMFA(@Req() req, @Body() mfaremoveDto: DeleteMFADto) { + return this.socialLoginService.removeMFA(req.user, mfaremoveDto); + } } diff --git a/src/social-login/dto/request.dto.ts b/src/social-login/dto/request.dto.ts index dd3e8311..e79ee6a1 100644 --- a/src/social-login/dto/request.dto.ts +++ b/src/social-login/dto/request.dto.ts @@ -31,3 +31,31 @@ export class Generate2FA { @IsEnum(AuthneticatorType) authenticatorType: string; } + +export class DeleteMFADto { + @ApiProperty({ + name: 'authenticatorType', + description: 'Type of authenticator used for 2FA', + example: AuthneticatorType.google, + enum: AuthneticatorType, + }) + @IsEnum(AuthneticatorType) + authenticatorType: string; + @ApiProperty({ + name: 'twoFactorAuthenticationCode', + description: + 'Code generated in authenticator app of selected authenticatorType', + example: '678324', + }) + @IsString() + @IsNotEmpty() + twoFactorAuthenticationCode: string; + @ApiProperty({ + name: 'authenticatorToDelete', + description: 'Type of authenticator that user want to remove', + example: AuthneticatorType.google, + enum: AuthneticatorType, + }) + @IsEnum(AuthneticatorType) + authenticatorToDelete: string; +} diff --git a/src/social-login/dto/response.dto.ts b/src/social-login/dto/response.dto.ts index 7e5c6329..d33d1985 100644 --- a/src/social-login/dto/response.dto.ts +++ b/src/social-login/dto/response.dto.ts @@ -91,6 +91,15 @@ export class Verify2FARespDto { @IsString() accessToken: string; } +export class DeleteMFARespDto { + @ApiProperty({ + name: 'message', + description: 'A success message', + example: 'Removed authenticator successfully', + }) + @IsString() + message: string; +} export enum AuthneticatorType { google = 'google', okta = 'okta', diff --git a/src/social-login/services/social-login.service.ts b/src/social-login/services/social-login.service.ts index 90680ab7..3464a034 100644 --- a/src/social-login/services/social-login.service.ts +++ b/src/social-login/services/social-login.service.ts @@ -10,7 +10,11 @@ import { SERVICE_TYPES } from 'src/supported-service/services/iServiceList'; import { AuthneticatorType } from '../dto/response.dto'; import { authenticator } from 'otplib'; import { toDataURL } from 'qrcode'; -import { Generate2FA, MFACodeVerificationDto } from '../dto/request.dto'; +import { + DeleteMFADto, + Generate2FA, + MFACodeVerificationDto, +} from '../dto/request.dto'; @Injectable() export class SocialLoginService { @@ -144,4 +148,31 @@ export class SocialLoginService { accessToken, }; } + + async removeMFA(user, deleteMfaDto: DeleteMFADto) { + const { + twoFactorAuthenticationCode, + authenticatorToDelete, + authenticatorType, + } = deleteMfaDto; + const secret = + authenticatorType === AuthneticatorType.google + ? user.twoFAGoogleSecret + : user.twoFAOktaSecret; + const isVerified = authenticator.verify({ + token: twoFactorAuthenticationCode, + secret, + }); + if (!isVerified) { + throw new BadRequestException([ + "Your passcode doesn't match. Please try again", + ]); + } + const dataToUpdate = + authenticatorToDelete === AuthneticatorType.google + ? { $unset: { twoFAGoogleSecret: '' }, isGoogleTwoFAEnabled: false } + : { $unset: { twoFAOktaSecret: '' }, isOktaTwoFAEnabled: false }; + this.userRepository.findOneUpdate({ userId: user.userId }, dataToUpdate); + return { message: 'Removed authenticator successfully' }; + } } From fac1ac6a2d66b00e8b4f3c97729b65ed5ba3d399 Mon Sep 17 00:00:00 2001 From: varsha766 Date: Fri, 12 Jul 2024 20:41:43 +0530 Subject: [PATCH 4/5] some chnage in user schema --- src/social-login/dto/request.dto.ts | 2 +- .../services/social-login.service.ts | 86 ++++++++++--------- src/user/schema/user.schema.ts | 17 ++-- .../2FA-jwt-authorization.middleware.ts | 2 +- 4 files changed, 58 insertions(+), 49 deletions(-) diff --git a/src/social-login/dto/request.dto.ts b/src/social-login/dto/request.dto.ts index e79ee6a1..058d6b34 100644 --- a/src/social-login/dto/request.dto.ts +++ b/src/social-login/dto/request.dto.ts @@ -53,7 +53,7 @@ export class DeleteMFADto { @ApiProperty({ name: 'authenticatorToDelete', description: 'Type of authenticator that user want to remove', - example: AuthneticatorType.google, + example: AuthneticatorType.okta, enum: AuthneticatorType, }) @IsEnum(AuthneticatorType) diff --git a/src/social-login/services/social-login.service.ts b/src/social-login/services/social-login.service.ts index 3464a034..5542d630 100644 --- a/src/social-login/services/social-login.service.ts +++ b/src/social-login/services/social-login.service.ts @@ -1,4 +1,9 @@ -import { BadRequestException, Injectable, Logger } from '@nestjs/common'; +import { + BadRequestException, + Injectable, + Logger, + NotFoundException, +} from '@nestjs/common'; import { UserRepository } from 'src/user/repository/user.repository'; import { ConfigService } from '@nestjs/config'; import { JwtService } from '@nestjs/jwt'; @@ -90,28 +95,26 @@ export class SocialLoginService { 'SocialLoginService', ); const { authenticatorType } = genrate2FADto; - let secret; - if (authenticatorType == AuthneticatorType.google) { - if (user && !user.twoFAGoogleSecret) { - secret = authenticator.generateSecret(20); - this.userRepository.findOneUpdate( - { userId: user.userId }, - { twoFAGoogleSecret: secret, isGoogleTwoFAEnabled: true }, - ); - } else { - secret = user.twoFAGoogleSecret; - } - } else if (authenticatorType == AuthneticatorType.okta) { - if (user && !user.twoFAOktaSecret) { - secret = authenticator.generateSecret(20); - this.userRepository.findOneUpdate( - { userId: user.userId }, - { twoFAOktaSecret: secret, isOktaTwoFAEnabled: true }, - ); - } else { - secret = user.twoFAOktaSecret; - } + let secret: string; + const existingAuthenticator = user.authenticators?.find( + (auth) => auth.type === authenticatorType, + ); + if (existingAuthenticator) { + secret = existingAuthenticator.secret; + } else { + secret = authenticator.generateSecret(20); } + if (!user.authenticators) { + user.authenticators = []; + } + user.authenticators.push({ + type: authenticatorType, + secret, + }); + this.userRepository.findOneUpdate( + { userId: user.userId }, + { authenticators: user.authenticators }, + ); const issuer = this.config.get('MFA_ISSUER'); const otpAuthUrl = authenticator.keyuri(user.email, issuer, secret); return toDataURL(otpAuthUrl); @@ -123,20 +126,18 @@ export class SocialLoginService { ); const { authenticatorType, twoFactorAuthenticationCode } = mfaVerificationDto; - const secret = - authenticatorType === AuthneticatorType.google - ? user.twoFAGoogleSecret - : user.twoFAOktaSecret; + const authenticatorDetail = user.authenticators.find( + (auth) => auth.type === authenticatorType, + ); const isVerified = authenticator.verify({ token: twoFactorAuthenticationCode, - secret, + secret: authenticatorDetail.secret, }); const payload = { email: user.email, appUserID: user.userId, userAccessList: user.accessList, - isTwoFactorEnabled: - !!user.isGoogleTwoFAEnabled || !!user.isOktaTwoFAEnabled, + isTwoFactorEnabled: user.authenticators && user.authenticators.length > 0, isTwoFactorAuthenticated: isVerified, }; const accessToken = await this.jwt.signAsync(payload, { @@ -155,24 +156,31 @@ export class SocialLoginService { authenticatorToDelete, authenticatorType, } = deleteMfaDto; - const secret = - authenticatorType === AuthneticatorType.google - ? user.twoFAGoogleSecret - : user.twoFAOktaSecret; + const authDetail = user.authenticators.find( + (auth) => auth.type === authenticatorType, + ); const isVerified = authenticator.verify({ token: twoFactorAuthenticationCode, - secret, + secret: authDetail.secret, }); if (!isVerified) { throw new BadRequestException([ "Your passcode doesn't match. Please try again", ]); } - const dataToUpdate = - authenticatorToDelete === AuthneticatorType.google - ? { $unset: { twoFAGoogleSecret: '' }, isGoogleTwoFAEnabled: false } - : { $unset: { twoFAOktaSecret: '' }, isOktaTwoFAEnabled: false }; - this.userRepository.findOneUpdate({ userId: user.userId }, dataToUpdate); + const authenticatorIndex = user.authenticators.findIndex( + (auth) => auth.type === authenticatorToDelete, + ); + if (authenticatorIndex === -1) { + throw new NotFoundException( + `${authenticatorToDelete} Authenticator not found`, + ); + } + user.authenticators.splice(authenticatorIndex, 1); + this.userRepository.findOneUpdate( + { userId: user.userId }, + { authenticators: user.authenticators }, + ); return { message: 'Removed authenticator successfully' }; } } diff --git a/src/user/schema/user.schema.ts b/src/user/schema/user.schema.ts index 72f4e955..a4131a82 100644 --- a/src/user/schema/user.schema.ts +++ b/src/user/schema/user.schema.ts @@ -1,5 +1,6 @@ import { Optional } from '@nestjs/common'; import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; +import { AuthneticatorType } from 'src/social-login/dto/response.dto'; import { SERVICES, SERVICE_TYPES, @@ -12,6 +13,12 @@ export interface UserAccess { } export type UserDocument = User & Document; +class Authenticator { + @Prop({ required: true, enum: AuthneticatorType }) + type: string; + @Prop({ required: true }) + secret: string; +} @Schema({ timestamps: true }) export class User { @Prop({ required: true }) @@ -23,14 +30,8 @@ export class User { @Prop({ required: false }) @Optional() accessList: Array; - @Prop({ required: false }) - twoFAOktaSecret?: string; - @Prop({ required: false }) - twoFAGoogleSecret?: string; - @Prop({ type: Boolean, required: false, default: false }) - isGoogleTwoFAEnabled?: boolean; - @Prop({ type: Boolean, required: false, default: false }) - isOktaTwoFAEnabled?: boolean; + @Prop({ default: [] }) + authenticators?: Authenticator[]; } export const UserSchema = SchemaFactory.createForClass(User); diff --git a/src/utils/middleware/2FA-jwt-authorization.middleware.ts b/src/utils/middleware/2FA-jwt-authorization.middleware.ts index a95a25e6..88f793aa 100644 --- a/src/utils/middleware/2FA-jwt-authorization.middleware.ts +++ b/src/utils/middleware/2FA-jwt-authorization.middleware.ts @@ -17,7 +17,7 @@ export class TwoFAAuthorizationMiddleware implements NestMiddleware { throw new UnauthorizedException(['User not authenticated']); } const user = req['user']; - if (user['isGoogleTwoFAEnabled'] || user['isOktaTwoFAEnabled']) { + if (user['authenticators'] && user['authenticators'].length > 0) { if (!user['isTwoFactorAuthenticated']) { throw new UnauthorizedException(['2FA authentication is required']); } From 19b20342a24a550777c77ec7a94db45861e65862 Mon Sep 17 00:00:00 2001 From: vishwas1 Date: Mon, 15 Jul 2024 01:17:06 +0530 Subject: [PATCH 5/5] updated new env --- .deploy/deployment.yaml | 2 ++ .github/workflows/pipeline.yaml | 2 ++ src/social-login/controller/social-login.controller.ts | 6 +++--- src/social-login/services/social-login.service.ts | 2 +- src/social-login/social-login.module.ts | 4 ++-- 5 files changed, 10 insertions(+), 6 deletions(-) diff --git a/.deploy/deployment.yaml b/.deploy/deployment.yaml index 11a9b6f9..0474f00e 100644 --- a/.deploy/deployment.yaml +++ b/.deploy/deployment.yaml @@ -64,6 +64,8 @@ spec: value: __TENANT_SUBDOMAIN_PREFIX__ - name: SSI_API_DOMAIN value: __SSI_API_DOMAIN__ + - name: MFA_ISSUER + value: __MFA_ISSUER__ - name: CAVACH_API_DOMAIN value: __CAVACH_API_DOMAIN__ - name: VAULT_PREFIX diff --git a/.github/workflows/pipeline.yaml b/.github/workflows/pipeline.yaml index 5f0129cf..916bde70 100644 --- a/.github/workflows/pipeline.yaml +++ b/.github/workflows/pipeline.yaml @@ -100,6 +100,8 @@ jobs: run: find .deploy/deployment.yaml -type f -exec sed -i "s#__TENANT_SUBDOMAIN_PREFIX__#${{ secrets.TENANT_SUBDOMAIN_PREFIX }}#" {} \; - name: "Replace Secrets" run: find .deploy/deployment.yaml -type f -exec sed -i "s#__SSI_API_DOMAIN__#${{ secrets.SSI_API_DOMAIN }}#" {} \; + - name: "Replace Secrets" + run: find .deploy/deployment.yaml -type f -exec sed -i "s#__MFA_ISSUER__#${{ secrets.MFA_ISSUER }}#" {} \; - name: "Replace Secrets" run: find .deploy/deployment.yaml -type f -exec sed -i "s#__CAVACH_API_DOMAIN__#${{ secrets.CAVACH_API_DOMAIN }}#" {} \; - name: "Replace Secrets" diff --git a/src/social-login/controller/social-login.controller.ts b/src/social-login/controller/social-login.controller.ts index 5924d4e1..85ff4dde 100644 --- a/src/social-login/controller/social-login.controller.ts +++ b/src/social-login/controller/social-login.controller.ts @@ -107,7 +107,7 @@ export class SocialLoginController { type: UnauthorizedError, }) @ApiBearerAuth('Authorization') - @Post('/api/auth/mfa/generate') + @Post('/api/v1/auth/mfa/generate') async generateMfa(@Req() req, @Body() body: Generate2FA) { const result = await this.socialLoginService.generate2FA(body, req.user); return { twoFADataUrl: result }; @@ -122,7 +122,7 @@ export class SocialLoginController { type: UnauthorizedError, }) @ApiBearerAuth('Authorization') - @Post('/api/auth/mfa/verify') + @Post('/api/v1/auth/mfa/verify') async verifyMFA( @Req() req, @Body() mfaVerificationDto: MFACodeVerificationDto, @@ -142,7 +142,7 @@ export class SocialLoginController { type: UnauthorizedError, }) @ApiBearerAuth('Authorization') - @Delete('/api/auth/mfa') + @Delete('/api/v1/auth/mfa') async removeMFA(@Req() req, @Body() mfaremoveDto: DeleteMFADto) { return this.socialLoginService.removeMFA(req.user, mfaremoveDto); } diff --git a/src/social-login/services/social-login.service.ts b/src/social-login/services/social-login.service.ts index 5542d630..5fc38b3c 100644 --- a/src/social-login/services/social-login.service.ts +++ b/src/social-login/services/social-login.service.ts @@ -146,7 +146,7 @@ export class SocialLoginService { }); return { isVerified, - accessToken, + authToken: accessToken, }; } diff --git a/src/social-login/social-login.module.ts b/src/social-login/social-login.module.ts index c0669202..c083503d 100644 --- a/src/social-login/social-login.module.ts +++ b/src/social-login/social-login.module.ts @@ -60,11 +60,11 @@ export class SocialLoginModule implements NestModule { method: RequestMethod.GET, }, { - path: '/api/auth/mfa/generate', + path: '/api/v1/auth/mfa/generate', method: RequestMethod.POST, }, { - path: '/api/auth/mfa/verify', + path: '/api/v1/auth/mfa/verify', method: RequestMethod.POST, }, )