Skip to content

Commit

Permalink
Implement passport strategies
Browse files Browse the repository at this point in the history
* create adapter for passport strategies

* create unit test for adapter
  • Loading branch information
deepakrkris committed May 26, 2017
1 parent 8ae39b0 commit 920c58d
Show file tree
Hide file tree
Showing 11 changed files with 215 additions and 8 deletions.
1 change: 0 additions & 1 deletion packages/authentication/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,3 @@

// NOTE(bajtos) This file is used by VSCode/TypeScriptServer at dev time only
export * from './src';

2 changes: 2 additions & 0 deletions packages/authentication/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
"author": "IBM",
"license": "MIT",
"dependencies": {
"@loopback/core": "^4.0.0-alpha.5",
"passport-strategy": "^1.0.0",
"reflect-metadata": "^0.1.10"
},
"devDependencies": {
Expand Down
2 changes: 2 additions & 0 deletions packages/authentication/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,7 @@
// Node module: @loopback/authentication
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
// NOTE(bajtos) This file is used by VSCode/TypeScriptServer at dev time only

export * from './decorator';
export * from './strategy-adapter';
80 changes: 80 additions & 0 deletions packages/authentication/src/strategy-adapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// 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 * as http from 'http';
import {ServerRequest as Request} from 'http';
import {HttpErrors, ParsedRequest} from '@loopback/core';

/**
* Interface definition of a passport strategy.
*/
export interface Strategy {
authenticate(req: http.ServerRequest): void;
}

/**
* Shimmed Request to satisfy express requirements of passport strategies.
*/
export class ShimRequest {
headers: Object;
query: Object;
url: string;
path: string;
method: string;
constructor(request?: ParsedRequest) {
if (request) {
this.headers = request.headers;
this.query = request.query;
this.url = request.url;
this.path = request.path;
this.method = request.method;
}
}
}

/**
* Adapter class to invoke a passport strategy.
* Instance is created by passing a passport strategy in the constructor
*/
export class StrategyAdapter {
private strategyCtor: Strategy;

constructor(strategy: Strategy) {
this.strategyCtor = strategy;
}

/**
* The function to invoke the contained passport strategy.
* 1. Create an instance of the strategy
* 2. add success and failure state handlers
* 3. authenticate using the strategy
* @param req {http.ServerRequest} The incoming request.
*/
authenticate(req: ParsedRequest) {
const shimReq = new ShimRequest(req);
return new Promise<Object>((resolve, reject) => {
// create an instance of the strategy
const strategy = Object.create(this.strategyCtor);
const self = this;

// add success state handler to strategy instance
strategy.success = function(user: object) {
resolve(user);
};

// add failure state handler to strategy instance
strategy.fail = function(challenge: string) {
reject(new HttpErrors.Unauthorized(challenge));
};

// add error state handler to strategy instance
strategy.error = function(error: string) {
reject(new HttpErrors.InternalServerError(error));
};

// authenticate
strategy.authenticate(shimReq);
});
}
}
48 changes: 48 additions & 0 deletions packages/authentication/test/unit/fixtures/mock-strategy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// 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

/**
* Test fixture for an asynchronous strategy
*/
import {ParsedRequest} from '@loopback/core';

export class MockStrategy {
private mockUser: Object;
constructor() {}
setMockUser(userObj: Object) {
this.mockUser = userObj;
}
async verify(req: ParsedRequest) {
if (req.query && req.query.testState && req.query.testState === 'fail') {
this.returnUnAuthourized({error: 'authorization failed'});
return;
} else if (req.query && req.query.testState && req.query.testState === 'error') {
this.returnError('unexpected error');
return;
}
process.nextTick(this.returnMockUser.bind(this));
}
returnMockUser() {
this.success(this.mockUser);
}
returnUnAuthourized(challenge: Object) {
this.fail(challenge);
}
returnError(err: string) {
this.error(err);
}
async authenticate(req: ParsedRequest) {
await this.verify(req);
}
success(user: Object) {
throw new Error('should be overrided by adapter');
}
fail(challenge: Object) {
throw new Error('should be overrided by adapter');
}
error(error: string) {
throw new Error('should be overrided by adapter');
}
}
70 changes: 70 additions & 0 deletions packages/authentication/test/unit/strategy-adapter.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// 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 {expect} from '@loopback/testlab';
import {ShimRequest} from '../..';
import {StrategyAdapter} from '../..';
import {ParsedRequest, HttpErrors} from '@loopback/core';
import {MockStrategy} from './fixtures/mock-strategy';

const passportStrategy = require('passport-strategy');

describe('Strategy Adapter', () => {
const mockUser: User = {id: 'mock-user', role: 'mock-role'};
interface User {
id: string;
role: string;
}

describe('authenticate()', () => {
it('calls the authenticate method of the strategy', () => {
const strategy = new passportStrategy();
let calledFlag = false;
// TODO: (as suggested by @bajtos) use sinon spy
strategy.authenticate = function() {
calledFlag = true;
};
const adapter = new StrategyAdapter(strategy);
const request = <ParsedRequest> {};
const user: Object = adapter.authenticate(request);
expect(calledFlag).to.be.true();
});

it('returns a promise which resolves to an object', async () => {
const strategy = new MockStrategy();
strategy.setMockUser(mockUser);
const adapter = new StrategyAdapter(strategy);
const request = <ParsedRequest> {};
const user: Object = await adapter.authenticate(request);
expect(user).to.be.eql(mockUser);
});

it('throws Unauthorized error when authentication fails', async () => {
const strategy = new MockStrategy();
strategy.setMockUser(mockUser);
const adapter = new StrategyAdapter(strategy);
const request = <ParsedRequest> {};
request.query = {testState: 'fail'};
try {
const user: Object = await adapter.authenticate(request);
} catch (err) {
expect(err).to.be.instanceof(HttpErrors.Unauthorized);
}
});

it('throws InternalServerError when strategy returns error', async () => {
const strategy = new MockStrategy();
strategy.setMockUser(mockUser);
const adapter = new StrategyAdapter(strategy);
const request = <ParsedRequest> {};
request.query = {testState: 'error'};
try {
const user: Object = await adapter.authenticate(request);
} catch (err) {
expect(err).to.be.instanceof(HttpErrors.InternalServerError);
}
});
});
});
1 change: 0 additions & 1 deletion packages/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,3 @@

// NOTE(bajtos) This file is used by VSCode/TypeScriptServer at dev time only
export * from './src';

4 changes: 3 additions & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@
"body": "^5.1.0",
"debug": "^2.6.0",
"path-to-regexp": "^1.7.0",
"reflect-metadata": "^0.1.10"
"reflect-metadata": "^0.1.10",
"@types/http-errors": "^1.5.34",
"http-errors": "^1.6.1"
},
"devDependencies": {
"@loopback/openapi-spec-builder": "^4.0.0-alpha.2",
Expand Down
6 changes: 6 additions & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,9 @@ export {ServerRequest, ServerResponse} from 'http';
// internals used by unit-tests
export {parseOperationArgs} from './parser';
export {ParsedRequest, parseRequestUrl} from './router/SwaggerRouter';

// import all errors from external http-errors package
import * as HttpErrors from 'http-errors';

// http errors
export {HttpErrors};
4 changes: 1 addition & 3 deletions packages/example-codehub/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,7 @@
"author": "IBM",
"license": "MIT",
"dependencies": {
"@loopback/core": "^4.0.0-alpha.5",
"@types/http-errors": "^1.5.34",
"http-errors": "^1.6.1"
"@loopback/core": "^4.0.0-alpha.5"
},
"devDependencies": {
"@loopback/testlab": "^4.0.0-alpha.3",
Expand Down
5 changes: 3 additions & 2 deletions packages/example-codehub/src/controllers/UserController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@

// https://www.npmjs.com/package/@types/http-errors
// https://github.com/jshttp/http-errors
import * as HttpError from 'http-errors';

import {HttpErrors} from '@loopback/core';

// Load OpenAPI specification for this controller
import {def} from './UserController.api';
Expand Down Expand Up @@ -39,7 +40,7 @@ export class UserController {
public async getAuthenticatedUser(@inject('userId') userId : number) : Promise<UserResponse> {
if (userId !== 42) {
// using "return Promise.reject(err)" would be probably faster (a possible micro-optimization)
throw new HttpError.NotFound('Current user not found (?!).');
throw new HttpErrors.NotFound('Current user not found (?!).');
}

return new UserResponse({
Expand Down

0 comments on commit 920c58d

Please sign in to comment.