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

[PoC][WIP, not ready for review]feat: add abstractions in authentication module #2445

Closed
wants to merge 3 commits into from
Closed
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
1 change: 1 addition & 0 deletions packages/authentication/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"@loopback/metadata": "^1.0.6",
"@loopback/openapi-v3": "^1.2.1",
"@loopback/rest": "^1.5.5",
"lodash": "^4.17.11",
"passport": "^0.4.0",
"passport-strategy": "^1.0.0"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,7 @@ import {
MethodDecoratorFactory,
} from '@loopback/context';
import {AUTHENTICATION_METADATA_KEY} from '../keys';

/**
* Authentication metadata stored via Reflection API
*/
export interface AuthenticationMetadata {
strategy: string;
options?: Object;
}
import {AuthenticationMetadata} from '..';

/**
* Mark a controller method as requiring authenticated user.
Expand Down
8 changes: 6 additions & 2 deletions packages/authentication/src/keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,15 @@ import {AuthenticateFn, UserProfile} from './types';
import {AuthenticationMetadata} from './decorators';
import {BindingKey} from '@loopback/context';
import {MetadataAccessor} from '@loopback/metadata';
import {AuthenticationServices} from './services/authentication.service';

/**
* Binding keys used by this component.
*/
export namespace AuthenticationBindings {
export const SERVICES = BindingKey.create<AuthenticationServices>(
'authentication.services',
);
/**
* Key used to bind an authentication strategy to the context for the
* authentication function to use.
Expand All @@ -23,8 +27,8 @@ export namespace AuthenticationBindings {
* .toProvider(MyPassportStrategyProvider);
* ```
*/
export const STRATEGY = BindingKey.create<Strategy | undefined>(
'authentication.strategy',
export const STRATEGY_RESOLVER = BindingKey.create<Strategy | undefined>(
'authentication.strategy.resolver',
);

/**
Expand Down
125 changes: 125 additions & 0 deletions packages/authentication/src/providers/auth-action.provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
// Copyright IBM Corp. 2017,2018. All Rights Reserved.
// Node module: @loopback/authentication
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import {Getter, Provider, Setter, inject} from '@loopback/context';
import {Request} from '@loopback/rest';
import {AuthenticationBindings} from '../keys';
import {ActionType, AuthenticationMetadata} from '../types';
import {Model} from '@loopback/repository';
import {AuthStrategy} from '../strategies';

/**
* @description Provider of a function which authenticates
* @example `context.bind('authentication_key')
* .toProvider(AuthenticateActionProvider)`
*/
export class AuthenticateActionProvider<U extends Model>
implements Provider<Function> {
constructor(
// The provider is instantiated for Sequence constructor,
// at which time we don't have information about the current
// route yet. This information is needed to determine
// what auth strategy should be used.
// To solve this, we are injecting a getter function that will
// defer resolution of the strategy until authenticate() action
// is executed.
@inject.getter(AuthenticationBindings.STRATEGY_RESOLVER)
readonly getStrategy: Getter<AuthStrategy<U>>,
@inject(AuthenticationBindings.METADATA)
private metadata: AuthenticationMetadata,
// what if the user is not set in the action
@inject.setter(AuthenticationBindings.CURRENT_USER)
readonly setCurrentUser: Setter<U>,
) {}

/**
* @returns authenticateFn
*/
value(): Function {
const actionName = this.getActionName();

// A workaround for
// return (request: Request) => this[actionName].call(request)
// got error for ^ and solving it.
switch (actionName) {
case 'verify':
return (request: Request) => this.verify(request);
case 'login':
return (request: Request) => this.login(request);
case 'register':
return (request: Request) => this.register(request);
default:
// tslint:disable-next-line:no-unused
return (request: Request) => {
return;
};
}
}

/**
* Get the name of authentication action to perform from
* an endpoint's metadata
*/
getActionName(): ActionType | undefined {
if (!this.metadata || !this.metadata.action) {
return;
}
return this.metadata.action;
}

/**
* The implementation of authenticate() sequence action(verify).
* @param request The incoming request provided by the REST layer
*/
async verify(request: Request): Promise<U | undefined> {
const strategy = await this.getStrategy();
if (!strategy) {
// The invoked operation does not require authentication.
return undefined;
}
if (!strategy.verify) {
throw new Error('invalid strategy parameter');
}

const user = await strategy.verify(request);

if (user) this.setCurrentUser(user);
return user;
}

/**
* The implementation of authenticate() sequence action(login).
* @param request The incoming request provided by the REST layer
*/
async login(request: Request): Promise<U | undefined> {
const strategy = await this.getStrategy();
if (!strategy) {
// The invoked operation does not require authentication.
return undefined;
}
if (!strategy.login) {
throw new Error('invalid strategy parameter');
}

return await strategy.login(request);
}

/**
* The implementation of authenticate() sequence action(register).
* @param request The incoming request provided by the REST layer
*/
async register(request: Request): Promise<U | undefined> {
const strategy = await this.getStrategy();
if (!strategy) {
// The invoked operation does not require authentication.
return undefined;
}
if (!strategy.register) {
throw new Error('invalid strategy parameter');
}

return await strategy.register(request);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@

import {CoreBindings} from '@loopback/core';
import {Constructor, Provider, inject} from '@loopback/context';
import {AuthenticationMetadata, getAuthenticateMetadata} from '../decorators';
import {getAuthenticateMetadata} from '../decorators';
import {AuthenticationMetadata} from '../';

/**
* @description Provides authentication metadata of a controller method
Expand Down

This file was deleted.

2 changes: 1 addition & 1 deletion packages/authentication/src/providers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@
// License text available at https://opensource.org/licenses/MIT

export * from './auth-metadata.provider';
export * from './authentication.provider';
export * from './auth-action/passport/auth-action.provider';
31 changes: 31 additions & 0 deletions packages/authentication/src/providers/strategy-resolver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import {Provider, inject, ValueOrPromise} from '@loopback/context';
import {AuthStrategy} from '../strategies';
import {AuthenticationBindings, AuthenticationMetadata} from '../..';
import * as _ from 'lodash';

export class StrategyResolverProvider
implements Provider<AuthStrategy | undefined> {
constructor(
@inject(AuthenticationBindings.METADATA)
private metadata: AuthenticationMetadata,
) {}

value(): ValueOrPromise<AuthStrategy | undefined> {
// tslint:disable-next-line:no-unused
const strategyName = this.metadata && this.metadata.strategy;

// Extension point
// Responsibility of the extension point:
// 1. return corresponding strategy if found
// 2. throw error if strategy is not available
// switch (name) {
// case 'jwt':
// return JWTStrategy();
// case 'openid':
// return OpenidStrategy();
// default:
// return;
// }
return;
}
}
10 changes: 10 additions & 0 deletions packages/authentication/src/services/authentication.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import {UserProfile, Credentials, AuthenticatedUser} from '../types';
import {Entity} from '@loopback/repository';
export interface AuthenticationServices {
authenticateUser<U extends Entity>(
credentials: Credentials,
): Promise<AuthenticatedUser<U>>;
comparePassword<T = string>(credentialPass: T, userPass: T): Promise<boolean>;
generateAccessToken(user: UserProfile): Promise<string>;
decodeAccessToken(token: string): Promise<UserProfile | undefined>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
// To be created, see the discussion in login and token service.
1 change: 1 addition & 0 deletions packages/authentication/src/services/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './authentication.service';
28 changes: 28 additions & 0 deletions packages/authentication/src/services/login.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import {Credentials} from '../types';
import {Request} from '@loopback/rest';

/**
* A service that provide login operations.
*
* Discussion:
* 1. should we turn the following 2 functions into standalone interfaces
* so that each service itself could be an extension point
* 2. should we use a generic type for Credentials instead of hardcode it?
*/
export interface LoginService<U> {
/**
* Extract credentials like `username` and `password` from incoming request
* Discussion:
* 1. should we move extractors into a separate extractor service?
* 2. should controller execute extractCredentials or auth action does it?
* Or it's on user's choice?
* 3. should we specify `Request` from the rest module as the request type?
* @param request The incoming HTTP request
*/
extractCredentials(request: Request): Promise<Credentials>;
/**
* Verify the credential maps to a valid user and return the user if found
* @param credentials the credentials returned by method `extractCredentials`
*/
verifyCredentials(credentials: Credentials): Promise<U>;
}
28 changes: 28 additions & 0 deletions packages/authentication/src/services/token.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import {Response} from '@loopback/rest';
import {Model} from '@loopback/repository';

/**
* A service that provide access token operations.
*/
export interface TokenService<U extends Model> {
/**
* Generate the access token for a given user
* Consumed by login action
* @param user
* @param options
*/
generateAccessToken(user: U, options: Object): Promise<string>;
/**
* Write the token to HTTP response
* Consumed by login action
* @param response The HTTP response to return
*/
serializeAccessToken(response: Response): Promise<void>;
/**
* Discussion:
* 1. should we move extractors into a separate extractor service?
* Extract the access token from request, it could be from header or cookie
* @param request The incoming HTTP request
*/
extractAccessToken(request: Request): Promise<string>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import {Request} from '@loopback/rest';
import {Model} from '@loopback/repository';

/**
* An interface describes a typical authentication strategy.
*
* An authentication strategy is usually a class with an
* authenticate method that verifies a user's identity and
* returns the corresponding user profile.
*/

export interface AuthStrategy<U extends Model> {
verify(request: Request): Promise<U | undefined>;

register(request: Request): Promise<U | undefined>;

/**
* Discussion:
* 1. how do we decide what's the return data of login operation?
* it could be an access token, or a user, or something else.
* @param request
*/
login(request: Request): Promise<U | undefined>;
}
2 changes: 2 additions & 0 deletions packages/authentication/src/strategies/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './authentication-strategy';
export * from './passport/passport-strategy-adapter';
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import {HttpErrors, Request} from '@loopback/rest';
import {Strategy} from 'passport';
import {UserProfile} from './types';
import {UserProfile} from '../../types';

const passportRequestMixin = require('passport/lib/http/request');

Expand Down
Loading