Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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