Skip to content

Commit

Permalink
feat(authentication-service): implement 2-factor-authentication (#686)
Browse files Browse the repository at this point in the history
* feat(authentication-service): implement 2-factor-authentication

implemented otp strategy using loopback4-authentication package

* feat(authentication-service): implement 2-factor-authentication

gh-453

* fix(authentication-service): update error keys

* fix(authentication-service): update version in package.json

* fix(authentication-service): minor name fix

* feat(authentication-service): implement otp and google authenticator strategies

* feat(authentication-service): implement 2-factor-authentication

Co-authored-by: akshatdubeysf <77672713+akshatdubeysf@users.noreply.github.com>
  • Loading branch information
AnkurBansalSF and akshatdubeysf authored May 10, 2022
1 parent b7689be commit ea571ac
Show file tree
Hide file tree
Showing 24 changed files with 543 additions and 10 deletions.
5 changes: 4 additions & 1 deletion services/authentication-service/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,16 +72,18 @@
"https-proxy-agent": "^5.0.1",
"jsonwebtoken": "^8.5.1",
"lodash": "^4.17.21",
"loopback4-authentication": "^6.0.3",
"loopback4-authentication": "^6.1.1",
"loopback4-authorization": "^5.0.3",
"loopback4-soft-delete": "^5.0.4",
"moment": "^2.29.3",
"moment-timezone": "^0.5.34",
"node-fetch": "^2.6.6",
"otplib": "^12.0.1",
"passport-apple": "^2.0.1",
"passport-facebook": "^3.0.0",
"passport-google-oauth20": "^2.0.0",
"passport-instagram": "^1.0.0",
"qrcode": "^1.5.0",
"tslib": "^2.3.1"
},
"devDependencies": {
Expand All @@ -101,6 +103,7 @@
"@types/passport-facebook": "^2.1.11",
"@types/passport-google-oauth20": "^2.0.11",
"@types/passport-instagram": "^1.0.2",
"@types/qrcode": "^1.4.2",
"@types/sinon": "^10.0.11",
"db-migrate": "^1.0.0-beta.18",
"db-migrate-pg": "^1.2.2",
Expand Down
27 changes: 25 additions & 2 deletions services/authentication-service/src/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,10 @@ import {
BearerTokenVerifyProvider,
ClientPasswordVerifyProvider,
FacebookOauth2VerifyProvider,
GoogleAuthenticatorVerifyProvider,
GoogleOauth2VerifyProvider,
LocalPasswordVerifyProvider,
OtpVerifyProvider,
ResourceOwnerVerifyProvider,
} from './modules/auth';
import {KeycloakVerifyProvider} from './modules/auth/providers/keycloak-verify.provider';
Expand All @@ -37,6 +39,7 @@ import {
FacebookPostVerifyProvider,
FacebookPreVerifyProvider,
ForgotPasswordProvider,
GoogleAuthenticatorProvider,
GoogleOauth2SignupProvider,
GooglePostVerifyProvider,
GooglePreVerifyProvider,
Expand All @@ -46,6 +49,9 @@ import {
JwtPayloadProvider,
KeyCloakPostVerifyProvider,
KeyCloakPreVerifyProvider,
OtpGenerateProvider,
OtpProvider,
OtpSenderProvider,
SignUpBindings,
SignupTokenHandlerProvider,
VerifyBindings,
Expand All @@ -57,13 +63,15 @@ import {LocalPreSignupProvider} from './providers/local-presignup.provider';
import {LocalSignupProvider} from './providers/local-signup.provider';
import {repositories} from './repositories';
import {MySequence} from './sequence';
import {LoginHelperService} from './services';
import {IAuthServiceConfig} from './types';
import {LoginHelperService, OtpSenderService} from './services';
import {IAuthServiceConfig, IOtpAuthConfig} from './types';

export class AuthenticationServiceComponent implements Component {
constructor(
@inject(CoreBindings.APPLICATION_INSTANCE)
private readonly application: RestApplication,
@inject(AuthServiceBindings.OtpConfig, {optional: true})
private readonly otpAuthConfig: IOtpAuthConfig,
@inject(AuthServiceBindings.Config, {optional: true})
private readonly authConfig?: IAuthServiceConfig,
) {
Expand Down Expand Up @@ -102,6 +110,9 @@ export class AuthenticationServiceComponent implements Component {
this.application
.bind('services.LoginHelperService')
.toClass(LoginHelperService);
this.application
.bind('services.OtpSenderService')
.toClass(OtpSenderService);
this.models = models;

this.controllers = controllers;
Expand Down Expand Up @@ -157,6 +168,7 @@ export class AuthenticationServiceComponent implements Component {
FacebookOauth2VerifyProvider;
this.providers[Strategies.Passport.KEYCLOAK_VERIFIER.key] =
KeycloakVerifyProvider;
this.providers[Strategies.Passport.OTP_VERIFIER.key] = OtpVerifyProvider;
this.providers[SignUpBindings.KEYCLOAK_SIGN_UP_PROVIDER.key] =
KeyCloakSignupProvider;
this.providers[SignUpBindings.GOOGLE_SIGN_UP_PROVIDER.key] =
Expand Down Expand Up @@ -195,6 +207,17 @@ export class AuthenticationServiceComponent implements Component {
FacebookPostVerifyProvider;
this.providers[VerifyBindings.BEARER_SIGNUP_VERIFY_PROVIDER.key] =
SignupBearerVerifyProvider;
this.providers[VerifyBindings.OTP_PROVIDER.key] = OtpProvider;
this.providers[VerifyBindings.OTP_GENERATE_PROVIDER.key] =
OtpGenerateProvider;
this.providers[VerifyBindings.OTP_SENDER_PROVIDER.key] = OtpSenderProvider;

if (this.otpAuthConfig?.useGoogleAuthenticator) {
this.providers[VerifyBindings.OTP_PROVIDER.key] =
GoogleAuthenticatorProvider;
this.providers[Strategies.Passport.OTP_VERIFIER.key] =
GoogleAuthenticatorVerifyProvider;
}

this.providers[AuthCodeBindings.CODEREADER_PROVIDER.key] =
OauthCodeReaderProvider;
Expand Down
6 changes: 5 additions & 1 deletion services/authentication-service/src/keys.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import {BindingKey} from '@loopback/core';
import {BINDING_PREFIX} from '@sourceloop/core';
import {ForgotPasswordHandlerFn, JwtPayloadFn} from './providers';
import {IAuthServiceConfig} from './types';
import {IAuthServiceConfig, IOtpAuthConfig} from './types';

export namespace AuthServiceBindings {
export const Config = BindingKey.create<IAuthServiceConfig | null>(
`${BINDING_PREFIX}.auth.config`,
);

export const OtpConfig = BindingKey.create<IOtpAuthConfig | null>(
`${BINDING_PREFIX}.auth.otp.config`,
);

export const JWTPayloadProvider = BindingKey.create<JwtPayloadFn>(
`${BINDING_PREFIX}.auth.jwt.payload`,
);
Expand Down
3 changes: 3 additions & 0 deletions services/authentication-service/src/models/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {ForgetPasswordDto} from './forget-password-dto.model';
import {ForgetPasswordResponseDto} from './forget-password-response-dto.model';
import {LocalUserProfileDto} from './local-user-profile';
import {Otp} from './otp.model';
import {OtpCache} from './otp-cache.model';
import {RefreshTokenRequest} from './refresh-token-request.model';
import {RefreshToken} from './refresh-token.model';
import {ResetPasswordWithClient} from './reset-password-with-client.model';
Expand All @@ -25,6 +26,7 @@ export * from './auth-client.model';
export * from './forget-password-dto.model';
export * from './forget-password-response-dto.model';
export * from './local-user-profile';
export * from './otp-cache.model';
export * from './otp.model';
export * from './refresh-token-request.model';
export * from './refresh-token.model';
Expand Down Expand Up @@ -54,6 +56,7 @@ export const models = [
RevokedToken,
UserCredentials,
Otp,
OtpCache,
TenantConfig,
UserTenant,
RefreshTokenRequest,
Expand Down
33 changes: 33 additions & 0 deletions services/authentication-service/src/models/otp-cache.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import {Entity, model, property} from '@loopback/repository';

@model()
export class OtpCache extends Entity {
@property({
type: 'string',
})
otp?: string;

@property({
type: 'string',
})
userId?: string;

@property({
type: 'string',
})
otpSecret?: string;

@property({
type: 'string',
})
clientId: string;

@property({
type: 'string',
})
clientSecret: string;

constructor(data?: Partial<OtpCache>) {
super(data);
}
}
4 changes: 4 additions & 0 deletions services/authentication-service/src/modules/auth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,7 @@ export * from './providers/instagram-oauth2-verify.provider';
export * from './providers/facebook-oauth-verify.provider';
export * from './models/auth-user.model';
export * from './providers/apple-oauth2-verify.provider';
export * from './providers/otp-verify.provider';
export * from './providers/google-authenticator-verify.provider';
export * from './models/otp-login-request.dto';
export * from './types';
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,15 @@ import moment from 'moment-timezone';
import {ExternalTokens} from '../../types';
import {AuthServiceBindings} from '../../keys';
import {AuthClient, RefreshToken, User} from '../../models';
import {AuthCodeBindings, CodeReaderFn, JwtPayloadFn} from '../../providers';
import {
AuthCodeBindings,
CodeReaderFn,
CodeWriterFn,
JwtPayloadFn,
} from '../../providers';
import {
AuthClientRepository,
OtpCacheRepository,
RefreshTokenRepository,
RevokedTokenRepository,
RoleRepository,
Expand All @@ -52,10 +58,19 @@ import {
} from '../../repositories';
import {TenantConfigRepository} from '../../repositories/tenant-config.repository';
import {LoginHelperService} from '../../services';
import {AuthRefreshTokenRequest, AuthTokenRequest, LoginRequest} from './';
import {
AuthRefreshTokenRequest,
AuthTokenRequest,
CodeResponse,
LoginRequest,
OtpLoginRequest,
OtpResponse,
} from './';
import {AuthUser, DeviceInfo} from './models/auth-user.model';
import {ResetPassword} from './models/reset-password.dto';
import {TokenResponse} from './models/token-response.dto';
import {OtpSenderService} from '../../services/otp-sender.service';
import {OtpSendRequest} from './models/otp-send-request.dto';

const userAgentKey = 'user-agent';

Expand All @@ -69,6 +84,8 @@ export class LoginController {
public authClientRepository: AuthClientRepository,
@repository(UserRepository)
public userRepo: UserRepository,
@repository(OtpCacheRepository)
public otpCacheRepo: OtpCacheRepository,
@repository(RoleRepository)
public roleRepo: RoleRepository,
@repository(UserLevelPermissionRepository)
Expand Down Expand Up @@ -116,9 +133,7 @@ export class LoginController {
client: AuthClient | undefined,
@inject(AuthenticationBindings.CURRENT_USER)
user: AuthUser | undefined,
): Promise<{
code: string;
}> {
): Promise<CodeResponse> {
await this.loginHelperService.verifyClientUserLogin(req, client, user);

try {
Expand Down Expand Up @@ -436,6 +451,76 @@ export class LoginController {
return new AuthUser(this.user);
}

// OTP,Google Authenticator APIs
@authenticateClient(STRATEGY.CLIENT_PASSWORD)
@authenticate(STRATEGY.OTP)
@authorize({permissions: ['*']})
@post('/auth/send-otp', {
description: 'Sends OTP',
responses: {
[STATUS_CODE.OK]: {
description: 'Sends otp to user',
content: {
[CONTENT_TYPE.JSON]: Object,
},
},
...ErrorCodes,
},
})
async sendOtp(
@requestBody()
req: OtpSendRequest,
@inject(AuthenticationBindings.CURRENT_CLIENT)
client: AuthClient,
@inject(AuthenticationBindings.CURRENT_USER)
user: AuthUser,
): Promise<OtpResponse | void> {}

@authenticate(STRATEGY.OTP)
@authorize({permissions: ['*']})
@post('/auth/verify-otp', {
description:
'Gets you the code that will be used for getting token (webapps)',
responses: {
[STATUS_CODE.OK]: {
description:
'Auth Code that you can use to generate access and refresh tokens using the POST /auth/token API',
content: {
[CONTENT_TYPE.JSON]: Object,
},
},
...ErrorCodes,
},
})
async verifyOtp(
@requestBody()
req: OtpLoginRequest,
@inject(AuthenticationBindings.CURRENT_USER)
user: AuthUser | undefined,
@inject(AuthCodeBindings.CODEWRITER_PROVIDER)
codeWriter: CodeWriterFn,
): Promise<CodeResponse> {
const otpCache = await this.otpCacheRepo.get(req.key);
if (user?.id) {
otpCache.userId = user.id;
}
const codePayload: ClientAuthCode<User, typeof User.prototype.id> = {
clientId: otpCache.clientId,
userId: otpCache.userId,
};
const token = await codeWriter(
jwt.sign(codePayload, otpCache.clientSecret, {
expiresIn: 180,
audience: otpCache.clientId,
issuer: process.env.JWT_ISSUER,
algorithm: 'HS256',
}),
);
return {
code: token,
};
}

private async createJWT(
payload: ClientAuthCode<User, typeof User.prototype.id> & ExternalTokens,
authClient: AuthClient,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import {Model, model, property} from '@loopback/repository';
import {ModelPropertyDescriptionString} from './model-property-description.enum';

@model({
description: 'This is the signature for OTP login request.',
})
export class OtpLoginRequest extends Model {
@property({
type: 'string',
description: ModelPropertyDescriptionString.reqStrPropDesc,
required: true,
})
key: string;

@property({
type: 'string',
description: ModelPropertyDescriptionString.reqStrPropDesc,
required: true,
})
otp: string;

constructor(data?: Partial<OtpLoginRequest>) {
super(data);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import {Model, model, property} from '@loopback/repository';
import {ModelPropertyDescriptionString} from './model-property-description.enum';

@model({
description: 'This is the signature for OTP login request.',
})
export class OtpSendRequest extends Model {
@property({
type: 'string',
description: ModelPropertyDescriptionString.reqStrPropDesc,
required: true,
})
// eslint-disable-next-line @typescript-eslint/naming-convention
client_id: string;

@property({
type: 'string',
description: ModelPropertyDescriptionString.reqStrPropDesc,
required: true,
})
// eslint-disable-next-line @typescript-eslint/naming-convention
client_secret: string;

@property({
type: 'string',
description: ModelPropertyDescriptionString.reqStrPropDesc,
required: true,
})
key: string;

constructor(data?: Partial<OtpSendRequest>) {
super(data);
}
}
Loading

0 comments on commit ea571ac

Please sign in to comment.