Skip to content

Commit

Permalink
Acceptance test with Basic Authentication
Browse files Browse the repository at this point in the history
* Given app with an authenticated sequence

* Given controller decorated for basic auth

* Configure app with built-in AuthenticationProvider

* Configure app with controller metadata provider

* Configure app with custom StrategyProvider

* Add tests for basic authentication
  • Loading branch information
deepakrkris committed Jun 26, 2017
1 parent 85c3c5a commit 42047a8
Show file tree
Hide file tree
Showing 7 changed files with 227 additions and 5 deletions.
3 changes: 2 additions & 1 deletion packages/authentication/package.json
Expand Up @@ -27,7 +27,8 @@
"devDependencies": {
"@loopback/openapi-spec-builder": "^4.0.0-alpha.4",
"@loopback/testlab": "^4.0.0-alpha.5",
"mocha": "^3.2.0"
"mocha": "^3.2.0",
"passport-http": "^0.3.0"
},
"keywords": [
"LoopBack",
Expand Down
203 changes: 203 additions & 0 deletions packages/authentication/test/acceptance/basic-auth.ts
@@ -0,0 +1,203 @@
// Copyright IBM Corp. 2013,2017. All Rights Reserved.
// Node module: loopback
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import {
Application,
Server,
api,
OpenApiSpec,
ParameterObject,
ServerRequest,
ServerResponse,
parseOperationArgs,
writeResultToResponse,
ParsedRequest,
OperationArgs,
FindRoute,
InvokeMethod,
getFromContext,
bindElement,
HttpErrors,
} from '@loopback/core';
import {expect, Client, createClientForServer} from '@loopback/testlab';
import {givenOpenApiSpec} from '@loopback/openapi-spec-builder';
import {inject, Provider, ValueOrPromise} from '@loopback/context';
import {authenticate,
UserProfile,
BindingKeys,
AuthenticateFn,
AuthenticationProvider,
AuthenticationMetadata,
AuthMetadataProvider,
} from '../..';
import {Strategy} from 'passport';
import {HttpError} from 'http-errors';

const BasicStrategy = require('passport-http').BasicStrategy;

describe('Basic Authentication', () => {
let app: Application;
let users: UserRepository;

beforeEach(givenUserRespository);
beforeEach(givenAnApplication);
beforeEach(givenControllerInApp);
beforeEach(givenAuthenticatedSequence);
beforeEach(givenProviders);

it ('authenticates successfully for correct credentials', async () => {
const client = await whenIMakeRequestTo(app);
const credential =
users.list.joe.profile.id + ':' + users.list.joe.password;
const hash = new Buffer(credential).toString('base64');
await client.get('/whoAmI')
.set('Authorization', 'Basic ' + hash)
.expect(users.list.joe.profile.id);
});

it('returns error for invalid credentials', async () => {
const client = await whenIMakeRequestTo(app);
const credential = users.list.Simpson.profile.id + ':' + 'invalid';
const hash = new Buffer(credential).toString('base64');
await client.get('/whoAmI')
.set('Authorization', 'Basic ' + hash)
.expect(401);
});

function givenUserRespository() {
users = new UserRepository({
joe : {profile: {id: 'joe'}, password: '12345'},
Simpson: {profile: {id: 'sim123'}, password: 'alpha'},
Flintstone: {profile: {id: 'Flint'}, password: 'beta'},
George: {profile: {id: 'Curious'}, password: 'gamma'},
});
}

function givenAnApplication() {
app = new Application();
app.bind('application.name').to('SequenceApp');
}

function givenControllerInApp() {
const apispec = givenOpenApiSpec()
.withOperation('get', '/whoAmI', {
'x-operation-name': 'whoAmI',
responses: {
'200': {
type: 'string',
},
},
})
.build();

@api(apispec)
class MyController {
constructor(@inject('authentication.user') private user: UserProfile) {}

@authenticate('BasicStrategy')
async whoAmI() : Promise<string> {
return this.user.id;
}
}
app.controller(MyController);
}

function givenAuthenticatedSequence() {
class MySequence {
constructor(
@inject('findRoute') protected findRoute: FindRoute,
@inject('getFromContext') protected getFromContext: getFromContext,
@inject('invokeMethod') protected invoke: InvokeMethod,
@inject('bindElement') protected bindElement: bindElement,
) {}

async run(req: ParsedRequest, res: ServerResponse) {
try {
const {
controller,
methodName,
spec: routeSpec,
pathParams,
} = this.findRoute(req);

// Resolve authenticate() from AuthenticationProvider
const authenticate: AuthenticateFn =
await this.getFromContext(BindingKeys.Authentication.PROVIDER);

// Authenticate
const user: UserProfile = await authenticate(req);

// User is expected to be returned or an exception should be thrown
if (user) this.bindElement('authentication.user').to(user);
else throw new HttpErrors.InternalServerError('auth error');

// Authentication successful, proceed to invoke controller
const args = await parseOperationArgs(req, routeSpec, pathParams);
const result = await this.invoke(controller, methodName, args);
writeResultToResponse(res, result);
} catch (err) {
this.sendError(res, req, err);
return;
}
}
sendError(res: ServerResponse, req: ServerRequest, err: HttpError) {
const statusCode = err.statusCode || err.status || 500;
res.statusCode = statusCode;
res.end(err.message);
}
}
// bind user defined sequence
app.bind('sequence').toClass(MySequence);
}

function givenProviders() {
class MyPassportStrategyProvider implements Provider<Strategy> {
constructor(
@inject(BindingKeys.Authentication.METADATA)
private metadata: AuthenticationMetadata,
) {}
async value() : Promise<Strategy> {
if (this.metadata.strategy === 'BasicStrategy') {
return new BasicStrategy(this.verify);
} else {
return Promise.reject('configured strategy is not available');
}
}
// callback method for BasicStrategy
verify(username: string, password: string, cb: Function) {
process.nextTick(() => {
users.find(username, password, cb);
});
}
}
app.bind(BindingKeys.Authentication.METADATA)
.toProvider(AuthMetadataProvider);
app.bind(BindingKeys.Authentication.STRATEGY)
.toProvider(MyPassportStrategyProvider);
app.bind(BindingKeys.Authentication.PROVIDER)
.toProvider(AuthenticationProvider);
}

function whenIMakeRequestTo(application: Application): Promise<Client> {
const server = new Server(application, {port: 0});
return createClientForServer(server);
}
});

class UserRepository {
constructor(
readonly list: {[key: string] : {profile: UserProfile, password: string}},
) {}
find(username: string, password: string, cb: Function): void {
const userList = this.list;
function search(key: string) {
return userList[key].profile.id === username;
}
const key = Object.keys(userList).find(search);
if (!key) return cb(null, false);
if (userList[key].password !== password) return cb(null, false);
cb(null, userList[key].profile);
}
}
2 changes: 1 addition & 1 deletion packages/context/src/index.ts
Expand Up @@ -3,7 +3,7 @@
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

export {Binding, BindingScope, BoundValue} from './binding';
export {Binding, BindingScope, BoundValue, ValueOrPromise} from './binding';
export {Context} from './context';
export {Constructor} from './resolver';
export {inject} from './inject';
Expand Down
13 changes: 12 additions & 1 deletion packages/core/src/http-handler.ts
Expand Up @@ -3,7 +3,7 @@
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import {Context} from '@loopback/context';
import {Binding, Context, ValueOrPromise, BoundValue} from '@loopback/context';
import {OpenApiSpec} from '@loopback/openapi-spec';
import {ServerRequest, ServerResponse} from 'http';
import {getApiSpec} from './router/metadata';
Expand Down Expand Up @@ -45,6 +45,8 @@ export class HttpHandler {

this._bindFindRoute(requestContext);
this._bindInvokeMethod(requestContext);
this._bindGetFromContext(requestContext);
this._bindBindElement(requestContext);

const sequence: Sequence = await requestContext.get('sequence');
return sequence.run(parsedRequest, response);
Expand All @@ -60,6 +62,15 @@ export class HttpHandler {
return requestContext;
}

protected _bindGetFromContext(context: Context): void {
context.bind('getFromContext').to(
(key: string): Promise<BoundValue> => context.get(key));
}

protected _bindBindElement(context: Context): void {
context.bind('bindElement').to((key: string): Binding => context.bind(key));
}

protected _bindFindRoute(context: Context): void {
context.bind('findRoute').toDynamicValue(() => {
return (request: ParsedRequest) => {
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/index.ts
Expand Up @@ -31,6 +31,8 @@ export {
InvokeMethod,
LogError,
OperationArgs,
getFromContext,
bindElement,
} from './internal-types';
export {parseOperationArgs} from './parser';
export {parseRequestUrl} from './router/routing-table';
Expand Down
4 changes: 4 additions & 0 deletions packages/core/src/internal-types.ts
Expand Up @@ -3,6 +3,7 @@
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import {Binding, BoundValue, ValueOrPromise} from '@loopback/context';
import {ServerRequest} from 'http';
import {ResolvedRoute} from './router/routing-table';

Expand Down Expand Up @@ -43,3 +44,6 @@ export type PathParameterValues = {[key: string]: any};
export type OperationArgs = any[];
export type OperationRetval = any;
// tslint:enable:no-any

export type getFromContext = (key: string) => BoundValue;
export type bindElement = (key: string) => Binding;
5 changes: 3 additions & 2 deletions packages/core/src/keys.ts
Expand Up @@ -5,8 +5,9 @@

export namespace BindingKeys {
export namespace Context {
export const CONTROLLER_CLASS: string = 'controller.class';
export const CONTROLLER_METHOD_NAME: string = 'controller.method.name';
export const CONTROLLER_CLASS: string = 'controller.current.ctor';
export const CONTROLLER_METHOD_NAME: string =
'controller.current.operation';
export const CONTROLLER_METHOD_META: string = 'controller.method.meta';
}
}

0 comments on commit 42047a8

Please sign in to comment.