Skip to content

Commit

Permalink
feat(component): add a new strategy for otp (#67)
Browse files Browse the repository at this point in the history
* feat(component): add a new strategy for otp

added a new 2-factor authentication strategy

GH-69

* feat(component): add a new strategy for otp

added a new 2-factor authentication strategy

GH-69

* feat(component): add a new strategy for otp

added a new 2-factor authentication strategy.

gh-69
  • Loading branch information
AnkurBansalSF authored Apr 21, 2022
1 parent b7bd5b5 commit 5f8cd5e
Show file tree
Hide file tree
Showing 11 changed files with 352 additions and 0 deletions.
193 changes: 193 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ It provides support for seven passport based strategies.
7. [passport-instagram](https://github.com/jaredhanson/passport-instagram) - Passport strategy for authenticating with Instagram using the Instagram OAuth 2.0 API. This module lets you authenticate using Instagram in your Node.js applications.
8. [passport-apple](https://github.com/ananay/passport-apple) - Passport strategy for authenticating with Apple using the Apple OAuth 2.0 API. This module lets you authenticate using Apple in your Node.js applications.
9. [passport-facebook](https://github.com/jaredhanson/passport-facebook) - Passport strategy for authenticating with Facebook using the Facebook OAuth 2.0 API. This module lets you authenticate using Facebook in your Node.js applications.
10. custom-passport-otp - Created a Custom Passport strategy for 2-Factor-Authentication using OTP (One Time Password).

You can use one or more strategies of the above in your application. For each of the strategy (only which you use), you just need to provide your own verifier function, making it easily configurable. Rest of the strategy implementation intricacies is handled by extension.

Expand Down Expand Up @@ -793,6 +794,198 @@ For accessing the authenticated AuthUser and AuthClient model reference, you can
private readonly getCurrentClient: Getter<AuthClient>,
```

### OTP

First, create a OtpCache model. This model should have OTP and few details of user and client (which will be used to retrieve them from database), it will be used to verify otp and get user, client. See sample below.

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

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

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

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

constructor(data?: Partial<OtpCache>) {
super(data);
}
}
```

Create [redis-repository](https://loopback.io/doc/en/lb4/Repository.html#define-a-keyvaluerepository) for the above model. Use loopback CLI.

```sh
lb4 repository
```

Here is a simple example.

```ts
import {OtpCache} from '../models';
import {AuthCacheSourceName} from 'loopback4-authentication';

export class OtpCacheRepository extends DefaultKeyValueRepository<OtpCache> {
constructor(
@inject(`datasources.${AuthCacheSourceName}`)
dataSource: juggler.DataSource,
) {
super(OtpCache, dataSource);
}
}
```

Add the verifier function for the strategy. You need to create a provider for the same. You can add your application specific business logic for auth here. Here is a simple example.

```ts
export class OtpVerifyProvider implements Provider<VerifyFunction.OtpAuthFn> {
constructor(
@repository(UserRepository)
public userRepository: UserRepository,
@repository(OtpCacheRepository)
public otpCacheRepo: OtpCacheRepository,
) {}

value(): VerifyFunction.OtpAuthFn {
return async (key: string, otp: string) => {
const otpCache = await this.otpCacheRepo.get(key);
if (!otpCache) {
throw new HttpErrors.Unauthorized(AuthErrorKeys.InvalidCredentials);
}
if (otpCache.otp.toString() !== otp) {
throw new HttpErrors.Unauthorized('Invalid OTP');
}
return this.userRepository.findById(otpCache.userId);
};
}
}
```

Please note the Verify function type _VerifyFunction.OtpAuthFn_

Now bind this provider to the application in application.ts.

```ts
import {AuthenticationComponent, Strategies} from 'loopback4-authentication';
```

```ts
// Add authentication component
this.component(AuthenticationComponent);
// Customize authentication verify handlers
this.bind(Strategies.Passport.OTP_VERIFIER).toProvider(OtpVerifyProvider);
```

Finally, add the authenticate function as a sequence action to sequence.ts.

```ts
export class MySequence implements SequenceHandler {
constructor(
@inject(SequenceActions.FIND_ROUTE) protected findRoute: FindRoute,
@inject(SequenceActions.PARSE_PARAMS) protected parseParams: ParseParams,
@inject(SequenceActions.INVOKE_METHOD) protected invoke: InvokeMethod,
@inject(SequenceActions.SEND) public send: Send,
@inject(SequenceActions.REJECT) public reject: Reject,
@inject(AuthenticationBindings.USER_AUTH_ACTION)
protected authenticateRequest: AuthenticateFn<AuthUser>,
) {}

async handle(context: RequestContext) {
try {
const {request, response} = context;

const route = this.findRoute(request);
const args = await this.parseParams(request, route);
request.body = args[args.length - 1];
const authUser: AuthUser = await this.authenticateRequest(request);
const result = await this.invoke(route, args);
this.send(response, result);
} catch (err) {
this.reject(context, err);
}
}
}
```

Then, you need to create APIs, where you will first authenticate the user, and then send the OTP to user's email/phone. See below.

```ts
//You can use your other strategies also
@authenticate(STRATEGY.LOCAL)
@post('/auth/send-otp', {
responses: {
[STATUS_CODE.OK]: {
description: 'Send Otp',
content: {
[CONTENT_TYPE.JSON]: Object,
},
},
},
})
async login(
@requestBody()
req: LoginRequest,
): Promise<{
key: string;
}> {

// User is authenticated before this step.
// Now follow these steps:
// 1. Create a unique key.
// 2. Generate and send OTP to user's email/phone.
// 3. Store the details in redis-cache using key created in step-1. (Refer OtpCache model mentioned above)
// 4. Response will be the key created in step-1
}
```

After this, create an API with @@authenticate(STRATEGY.OTP) decorator. See below.

```ts
@authenticate(STRATEGY.OTP)
@post('/auth/login-otp', {
responses: {
[STATUS_CODE.OK]: {
description: 'Auth Code',
content: {
[CONTENT_TYPE.JSON]: Object,
},
},
},
})
async login(
@requestBody()
req: {
key: 'string';
otp: 'string';
},
): Promise<{
code: string;
}> {
......
}
```

For accessing the authenticated AuthUser model reference, you can inject the CURRENT_USER provider, provided by the extension, which is populated by the auth action sequence above.

```ts
@inject.getter(AuthenticationBindings.CURRENT_USER)
private readonly getCurrentUser: Getter<User>,
```

### Google Oauth 2

First, create a AuthUser model implementing the IAuthUser interface. You can implement the interface in the user model itself. See sample below.
Expand Down
5 changes: 5 additions & 0 deletions src/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ import {
LocalPasswordVerifyProvider,
ResourceOwnerPasswordStrategyFactoryProvider,
ResourceOwnerVerifyProvider,
PassportOtpStrategyFactoryProvider,
OtpVerifyProvider,
} from './strategies';
import {Strategies} from './strategies/keys';

Expand All @@ -47,6 +49,8 @@ export class AuthenticationComponent implements Component {
// Strategy function factories
[Strategies.Passport.LOCAL_STRATEGY_FACTORY.key]:
LocalPasswordStrategyFactoryProvider,
[Strategies.Passport.OTP_AUTH_STRATEGY_FACTORY.key]:
PassportOtpStrategyFactoryProvider,
[Strategies.Passport.CLIENT_PASSWORD_STRATEGY_FACTORY.key]:
ClientPasswordStrategyFactoryProvider,
[Strategies.Passport.BEARER_STRATEGY_FACTORY.key]:
Expand All @@ -71,6 +75,7 @@ export class AuthenticationComponent implements Component {
ClientPasswordVerifyProvider,
[Strategies.Passport.LOCAL_PASSWORD_VERIFIER.key]:
LocalPasswordVerifyProvider,
[Strategies.Passport.OTP_VERIFIER.key]: OtpVerifyProvider,
[Strategies.Passport.BEARER_TOKEN_VERIFIER.key]:
BearerTokenVerifyProvider,
[Strategies.Passport.RESOURCE_OWNER_PASSWORD_VERIFIER.key]:
Expand Down
10 changes: 10 additions & 0 deletions src/strategies/keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,16 @@ export namespace Strategies {
'sf.passport.verifier.localPassword',
);

// Passport-local-with-otp startegy
export const OTP_AUTH_STRATEGY_FACTORY =
BindingKey.create<LocalPasswordStrategyFactory>(
'sf.passport.strategyFactory.otpAuth',
);
export const OTP_VERIFIER =
BindingKey.create<VerifyFunction.LocalPasswordFn>(
'sf.passport.verifier.otpAuth',
);

// Passport-oauth2-client-password strategy
export const CLIENT_PASSWORD_STRATEGY_FACTORY =
BindingKey.create<ClientPasswordStrategyFactory>(
Expand Down
1 change: 1 addition & 0 deletions src/strategies/passport/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ export * from './passport-azure-ad';
export * from './passport-insta-oauth2';
export * from './passport-apple-oauth2';
export * from './passport-facebook-oauth2';
export * from './passport-otp';
3 changes: 3 additions & 0 deletions src/strategies/passport/passport-otp/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './otp-auth';
export * from './otp-strategy-factory.provider';
export * from './otp-verify.provider';
60 changes: 60 additions & 0 deletions src/strategies/passport/passport-otp/otp-auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import * as passport from 'passport';

export namespace Otp {
export interface VerifyFunction {
(
key: string,
otp: string,
done: (error: any, user?: any, info?: any) => void,
): void;
}

export interface StrategyOptions {
key?: string;
otp?: string;
}

export type VerifyCallback = (
err?: string | Error | null,
user?: any,
info?: any,
) => void;

export class Strategy extends passport.Strategy {
constructor(_options?: StrategyOptions, verify?: VerifyFunction) {
super();
this.name = 'otp';
if (verify) {
this.verify = verify;
}
}

name: string;
private readonly verify: VerifyFunction;

authenticate(req: any, options?: StrategyOptions): void {
const key = req.body.key || options?.key;
const otp = req.body.otp || options?.otp;

if (!key || !otp) {
this.fail();
return;
}

const verified = (err?: any, user?: any, _info?: any) => {
if (err) {
this.error(err);
return;
}
if (!user) {
this.fail();
return;
}
this.success(user);
};

this.verify(key, otp, verified);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import {inject, Provider} from '@loopback/core';
import {HttpErrors} from '@loopback/rest';
import {AuthErrorKeys} from '../../../error-keys';
import {Strategies} from '../../keys';
import {VerifyFunction} from '../../types';
import {Otp} from './otp-auth';

export interface PassportOtpStrategyFactory {
(
options: Otp.StrategyOptions,
verifierPassed?: VerifyFunction.OtpAuthFn,
): Otp.Strategy;
}

export class PassportOtpStrategyFactoryProvider
implements Provider<PassportOtpStrategyFactory>
{
constructor(
@inject(Strategies.Passport.OTP_VERIFIER)
private readonly verifierOtp: VerifyFunction.OtpAuthFn,
) {}

value(): PassportOtpStrategyFactory {
return (options, verifier) =>
this.getPassportOtpStrategyVerifier(options, verifier);
}

getPassportOtpStrategyVerifier(
options?: Otp.StrategyOptions,
verifierPassed?: VerifyFunction.OtpAuthFn,
): Otp.Strategy {
const verifyFn = verifierPassed ?? this.verifierOtp;
return new Otp.Strategy(
options,
// eslint-disable-next-line @typescript-eslint/no-misused-promises
async (key: string, otp: string, cb: Otp.VerifyCallback) => {
try {
const user = await verifyFn(key, otp);
if (!user) {
throw new HttpErrors.Unauthorized(AuthErrorKeys.InvalidCredentials);
}
cb(null, user);
} catch (err) {
cb(err);
}
},
);
}
}
16 changes: 16 additions & 0 deletions src/strategies/passport/passport-otp/otp-verify.provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import {Provider} from '@loopback/context';
import {HttpErrors} from '@loopback/rest';

import {VerifyFunction} from '../../types';

export class OtpVerifyProvider implements Provider<VerifyFunction.OtpAuthFn> {
constructor() {}

value(): VerifyFunction.OtpAuthFn {
return async (_key: string, _otp: string) => {
throw new HttpErrors.NotImplemented(
`VerifyFunction.OtpAuthFn is not implemented`,
);
};
}
}
Loading

0 comments on commit 5f8cd5e

Please sign in to comment.