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

[WIP] feat(core): Add support for extension point/extension pattern on top of Context #657

Closed
wants to merge 8 commits into from
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;
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 '../authentication';

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

export * from './authentication';
export * from './authentication.component';
export * from './decorators';
export * from './decorators/authenticate.decorator';
export * from './keys';
export * from './strategy-adapter';

Expand Down
7 changes: 5 additions & 2 deletions packages/authentication/src/keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@
// License text available at https://opensource.org/licenses/MIT

import {Strategy} from 'passport';
import {AuthenticateFn, UserProfile} from './providers/authentication.provider';
import {AuthenticationMetadata} from './decorators/authenticate.decorator';
import {
AuthenticateFn,
UserProfile,
AuthenticationMetadata,
} from './authentication';
import {BindingKey, MetadataAccessor} from '@loopback/context';

/**
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;
}
}
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.decorator';
import {getAuthenticateMetadata} from '../decorators/authenticate.decorator';
import {AuthenticationMetadata} from '../authentication';

/**
* @description Provides authentication metadata of a controller method
Expand Down
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/authentication.provider';
import {UserProfile, Authenticator} from './authentication';
import {AuthenticationMetadata} from '..';

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:');
}
}
16 changes: 16 additions & 0 deletions packages/context/src/binding-key.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,4 +103,20 @@ export class BindingKey<ValueType> {
keyWithPath.substr(index + 1),
);
}

static CONFIG_NAMESPACE = '$config';
/**
* Build a binding key for the configuration of the given binding and env.
* The format is `$config.<env>:<key>`
*
* @param key The binding key that accepts the configuration
* @param env The environment such as `dev`, `test`, and `prod`
*/
static buildKeyForConfig(key: string = '', env: string = '') {
const namespace = env
? `${BindingKey.CONFIG_NAMESPACE}.${env}`
: BindingKey.CONFIG_NAMESPACE;
const bindingKey = key ? `${namespace}${key}` : BindingKey.CONFIG_NAMESPACE;
return bindingKey;
}
}
50 changes: 25 additions & 25 deletions packages/context/src/binding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
isPromiseLike,
BoundValue,
ValueOrPromise,
resolveValueOrPromise,
} from './value-promise';
import {Provider} from './provider';

Expand Down Expand Up @@ -129,30 +130,16 @@ export class Binding<T = BoundValue> {
): ValueOrPromise<T> {
// Initialize the cache as a weakmap keyed by context
if (!this._cache) this._cache = new WeakMap<Context, T>();
if (isPromiseLike(result)) {
if (this.scope === BindingScope.SINGLETON) {
// Cache the value at owning context level
result = result.then(val => {
this._cache.set(ctx.getOwnerContext(this.key)!, val);
return val;
});
} else if (this.scope === BindingScope.CONTEXT) {
// Cache the value at the current context
result = result.then(val => {
this._cache.set(ctx, val);
return val;
});
}
} else {
return resolveValueOrPromise(result, val => {
if (this.scope === BindingScope.SINGLETON) {
// Cache the value
this._cache.set(ctx.getOwnerContext(this.key)!, result);
this._cache.set(ctx.getOwnerContext(this.key)!, val);
} else if (this.scope === BindingScope.CONTEXT) {
// Cache the value at the current context
this._cache.set(ctx, result);
this._cache.set(ctx, val);
}
}
return result;
return val;
});
}

/**
Expand All @@ -169,7 +156,7 @@ export class Binding<T = BoundValue> {
*
* ```
* const result = binding.getValue(ctx);
* if (isPromise(result)) {
* if (isPromiseLike(result)) {
* result.then(doSomething)
* } else {
* doSomething(result);
Expand Down Expand Up @@ -210,11 +197,18 @@ export class Binding<T = BoundValue> {
);
}

/**
* Lock the binding so that it cannot be rebound
*/
lock(): this {
this.isLocked = true;
return this;
}

/**
* Add a tag to the binding
* @param tagName Tag name or an array of tag names
*/
tag(tagName: string | string[]): this {
if (typeof tagName === 'string') {
this.tags.add(tagName);
Expand All @@ -226,6 +220,10 @@ export class Binding<T = BoundValue> {
return this;
}

/**
* Set the binding scope
* @param scope Binding scope
*/
inScope(scope: BindingScope): this {
this.scope = scope;
return this;
Expand Down Expand Up @@ -330,11 +328,7 @@ export class Binding<T = BoundValue> {
ctx!,
session,
);
if (isPromiseLike(providerOrPromise)) {
return providerOrPromise.then(p => p.value());
} else {
return providerOrPromise.value();
}
return resolveValueOrPromise(providerOrPromise, p => p.value());
};
return this;
}
Expand All @@ -357,11 +351,17 @@ export class Binding<T = BoundValue> {
return this;
}

/**
* Unlock the binding
*/
unlock(): this {
this.isLocked = false;
return this;
}

/**
* Convert to a plain JSON object
*/
toJSON(): Object {
// tslint:disable-next-line:no-any
const json: {[name: string]: any} = {
Expand Down
Loading