Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(component): add a new strategy for otp #67

Merged
merged 4 commits into from
Apr 21, 2022
Merged
Show file tree
Hide file tree
Changes from 3 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
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`,
);
};
}
}
5 changes: 5 additions & 0 deletions src/strategies/types/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import * as AppleStrategy from 'passport-apple';
import {DecodedIdToken} from 'passport-apple';
import {IAuthClient, IAuthUser} from '../../types';
import {Keycloak} from './keycloak.types';
import {Otp} from '../passport';

export type VerifyCallback = (
err?: string | Error | null,
Expand All @@ -25,6 +26,10 @@ export namespace VerifyFunction {
(username: string, password: string, req?: Request): Promise<T | null>;
}

export interface OtpAuthFn<T = IAuthUser> extends GenericAuthFn<T> {
(key: string, otp: string, cb: Otp.VerifyCallback): Promise<T | null>;
}

export interface BearerFn<T = IAuthUser> extends GenericAuthFn<T> {
(token: string, req?: Request): Promise<T | null>;
}
Expand Down
9 changes: 9 additions & 0 deletions src/strategies/user-auth-strategy.provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import {
InstagramAuthStrategyFactory,
KeycloakStrategyFactory,
FacebookAuthStrategyFactory,
PassportOtpStrategyFactory,
Otp,
} from './passport';
import {Keycloak, VerifyFunction} from './types';

Expand All @@ -38,6 +40,8 @@ export class AuthStrategyProvider implements Provider<Strategy | undefined> {
private readonly metadata: AuthenticationMetadata,
@inject(Strategies.Passport.LOCAL_STRATEGY_FACTORY)
private readonly getLocalStrategyVerifier: LocalPasswordStrategyFactory,
@inject(Strategies.Passport.OTP_AUTH_STRATEGY_FACTORY)
private readonly getOtpVerifier: PassportOtpStrategyFactory,
@inject(Strategies.Passport.BEARER_STRATEGY_FACTORY)
private readonly getBearerStrategyVerifier: BearerStrategyFactory,
@inject(Strategies.Passport.RESOURCE_OWNER_STRATEGY_FACTORY)
Expand Down Expand Up @@ -129,6 +133,11 @@ export class AuthStrategyProvider implements Provider<Strategy | undefined> {
| ExtendedStrategyOption,
verifier as VerifyFunction.FacebookAuthFn,
);
} else if (name === STRATEGY.OTP) {
return this.getOtpVerifier(
this.metadata.options as Otp.StrategyOptions,
verifier as VerifyFunction.OtpAuthFn,
);
} else {
return Promise.reject(`The strategy ${name} is not available.`);
}
Expand Down
1 change: 1 addition & 0 deletions src/strategy-name.enum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ export const enum STRATEGY {
FACEBOOK_OAUTH2 = 'Facebook Oauth 2.0',
AZURE_AD = 'Azure AD',
KEYCLOAK = 'keycloak',
OTP = 'otp',
}