Skip to content

Commit

Permalink
feat(core): add support for extension point/extension
Browse files Browse the repository at this point in the history
Extension point/extension is a pattern for extensibility.
This PR illustrates how we can support it using the Context apis
and naming conventions. It also serves as an invitation to discuss
if we should support such entities out of box.
  • Loading branch information
Raymond Feng authored and raymondfeng committed Feb 26, 2018
1 parent 7a644ea commit 2b7268c
Show file tree
Hide file tree
Showing 13 changed files with 686 additions and 42 deletions.
65 changes: 65 additions & 0 deletions packages/authentication/src/authentication.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Copyright IBM Corp. 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 {ParsedRequest} from '@loopback/rest';

/**
* interface definition of a function which accepts a request
* and returns an authenticated user
*/
export interface AuthenticateFn {
(request: ParsedRequest): Promise<UserProfile | undefined>;
}

/**
* interface definition of a user profile
* http://openid.net/specs/openid-connect-core-1_0.html#StandardClaims
*/
export interface UserProfile {
id: string;
name?: string;
email?: string;
}

/**
* Authentication metadata stored via Reflection API
*/
export interface AuthenticationMetadata {
/**
* Name of the authentication strategy
*/
strategy: string;
/**
* Options for authentication
*/
options?: Object;
}

/**
* Interface for authentication providers
*/
export interface Authenticator {
/**
* Check if the given strategy is supported by the authenticator
* @param stragety Name of the authentication strategy
*/
isSupported(strategy: string): boolean;

/**
* Authenticate a request with given options
* @param request HTTP request
* @param metadata Authentication metadata
*/
authenticate(
request: ParsedRequest,
metadata?: AuthenticationMetadata,
): Promise<UserProfile | undefined>;
}

/**
* Passport monkey-patches Node.js' IncomingMessage prototype
* and adds extra methods like "login" and "isAuthenticated"
*/
export type PassportRequest = ParsedRequest & Express.Request;
9 changes: 1 addition & 8 deletions packages/authentication/src/decorators/authenticate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,7 @@ import {
MethodDecoratorFactory,
} from '@loopback/context';
import {AuthenticationBindings} from '../keys';

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

/**
* Mark a controller method as requiring authenticated user.
Expand Down
1 change: 1 addition & 0 deletions packages/authentication/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

export * from './authentication';
export * from './auth-component';
export * from './decorators/authenticate';
export * from './keys';
Expand Down
40 changes: 40 additions & 0 deletions packages/authentication/src/providers/auth-extension-point.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Copyright IBM Corp. 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 {ExtensionPoint, Context} from '@loopback/core';
import {ParsedRequest} from '@loopback/rest';
import {
UserProfile,
AuthenticationMetadata,
Authenticator,
} from '../authentication';
import {AuthenticationBindings} from '../keys';

export class AuthenticationExtensionPoint extends ExtensionPoint<
Authenticator
> {
static extensionPointName = 'authenticators';

async authenticate(
ctx: Context,
request: ParsedRequest,
): Promise<UserProfile | undefined> {
const meta: AuthenticationMetadata | undefined = await ctx.get(
AuthenticationBindings.METADATA,
);
if (meta == undefined) {
return undefined;
}
const authenticators = await this.getAllExtensions(ctx);
let user: UserProfile | undefined = undefined;
for (const authenticator of authenticators) {
if (authenticator.isSupported(meta.strategy)) {
user = await authenticator.authenticate(request, meta);
if (user === undefined) continue;
}
}
return user;
}
}
6 changes: 2 additions & 4 deletions packages/authentication/src/providers/auth-metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,8 @@

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

/**
* @description Provides authentication metadata of a controller method
Expand Down
25 changes: 1 addition & 24 deletions packages/authentication/src/providers/authenticate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,30 +9,7 @@ import {Provider, Getter, Setter} from '@loopback/context';
import {Strategy} from 'passport';
import {StrategyAdapter} from '../strategy-adapter';
import {AuthenticationBindings} from '../keys';

/**
* Passport monkey-patches Node.js' IncomingMessage prototype
* and adds extra methods like "login" and "isAuthenticated"
*/
export type PassportRequest = ParsedRequest & Express.Request;

/**
* interface definition of a function which accepts a request
* and returns an authenticated user
*/
export interface AuthenticateFn {
(request: ParsedRequest): Promise<UserProfile | undefined>;
}

/**
* interface definition of a user profile
* http://openid.net/specs/openid-connect-core-1_0.html#StandardClaims
*/
export interface UserProfile {
id: string;
name?: string;
email?: string;
}
import {AuthenticateFn, UserProfile} from '../authentication';

/**
* @description Provider of a function which authenticates
Expand Down
13 changes: 9 additions & 4 deletions packages/authentication/src/strategy-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@

import {HttpErrors, ParsedRequest} from '@loopback/rest';
import {Strategy} from 'passport';
import {UserProfile} from './providers/authenticate';
import {UserProfile, Authenticator} from './authentication';
import {AuthenticationMetadata} from '../index';

const PassportRequestExtras: Express.Request = require('passport/lib/http/request');

Expand Down Expand Up @@ -65,7 +66,7 @@ export class ShimRequest implements Express.Request {
* 3. provides state methods to the strategy instance
* see: https://github.com/jaredhanson/passport
*/
export class StrategyAdapter {
export class StrategyAdapter implements Authenticator {
/**
* @param strategy instance of a class which implements a passport-strategy;
* @description http://passportjs.org/
Expand All @@ -79,7 +80,7 @@ export class StrategyAdapter {
* 3. authenticate using the strategy
* @param req {http.ServerRequest} The incoming request.
*/
authenticate(req: ParsedRequest) {
authenticate(req: ParsedRequest, metadata?: AuthenticationMetadata) {
const shimReq = new ShimRequest(req);
return new Promise<UserProfile>((resolve, reject) => {
// create a prototype chain of an instance of a passport strategy
Expand All @@ -101,7 +102,11 @@ export class StrategyAdapter {
};

// authenticate
strategy.authenticate(shimReq);
strategy.authenticate(shimReq, metadata && metadata.options);
});
}

isSupported(strategy: string) {
return strategy.startsWith('passport:');
}
}
2 changes: 1 addition & 1 deletion packages/context/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export {
export {Binding, BindingScope, BindingType} from './binding';

export {Context} from './context';
export {ResolutionSession} from './resolution-session';
export {ResolutionSession, ResolutionOptions} from './resolution-session';
export {inject, Setter, Getter, Injection, InjectionMetadata} from './inject';
export {Provider} from './provider';

Expand Down
43 changes: 42 additions & 1 deletion packages/core/src/application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {Context, Binding, BindingScope, Constructor} from '@loopback/context';
import {Server} from './server';
import {Component, mountComponent} from './component';
import {CoreBindings} from './keys';
import {ExtensionPoint} from './extension-point';

/**
* Application is the container for various types of artifacts, such as
Expand Down Expand Up @@ -194,10 +195,50 @@ export class Application extends Context {
const instance = this.getSync(componentKey);
mountComponent(this, instance);
}

/**
* Register an extension point
* @param extensionPointClass Extension point class
* @param extensionPointName Name of the extension point, if not present,
* default to extensionPoints.<extensionPoint-class-name>
*/
public extensionPoint(
// tslint:disable-next-line:no-any
extensionPointClass: Constructor<ExtensionPoint<any>>,
extensionPointName?: string,
): Binding {
extensionPointName =
extensionPointName || `extensionPoints.${extensionPointClass.name}`;
return this.bind(extensionPointName)
.toClass(extensionPointClass)
.inScope(BindingScope.CONTEXT)
.tag('extensionPoint')
.tag(`name:${extensionPointName}`);
}

/**
* Register an extension of the given extension point
* @param extensionPointName Name of the extension point
* @param extensionClass Extension class
* @param extensionName Name of the extension. If not present, default to
* the name of extension class
*/
public extension(
extensionPointName: string,
// tslint:disable-next-line:no-any
extensionClass: Constructor<any>,
extensionName?: string,
): Binding {
extensionName = extensionName || extensionClass.name;
return this.bind(`${extensionPointName}.${extensionName}`)
.toClass(extensionClass)
.tag(`extensionPoint:${extensionPointName}`)
.tag(`name:${extensionName}`);
}
}

/**
* Configuration for application
* Configuration for an application
*/
export interface ApplicationConfig {
/**
Expand Down
Loading

0 comments on commit 2b7268c

Please sign in to comment.