Skip to content

Commit

Permalink
feat: adapter that wraps passport middleware
Browse files Browse the repository at this point in the history
  • Loading branch information
jannyHou committed Jun 6, 2019
1 parent c41d538 commit 561f6b5
Show file tree
Hide file tree
Showing 9 changed files with 468 additions and 50 deletions.
78 changes: 77 additions & 1 deletion labs/passport-adapter/README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
## Passport Adapter
# Passport Strategy Adapter

_Important: We suggest users understand LoopBack's authentication system[Link
TBD](some loopback.io link) before using this module_
Expand Down Expand Up @@ -127,3 +127,79 @@ class MyController {
}
}
```

# Passport Adapter

This adapter is different than the "Passport strategy adapter" described above in the way that it wraps the **passport middleware** itself instead of a passport strategy.

`PassportAdapter` has an `authenticate` function, which creates a passport instance, uses the injected strategy and invokes `passport.authenticate()` to perform the authentication.

## Usage

1. Create a provider for the strategy

It returns a configured `BasicStrategy` that's wrapped with the `PassportAdapter` class.

```ts
class PassportBasicAuthProvider implements Provider<AuthenticationStrategy> {
value(): AuthenticationStrategy {
const basicStrategy = this.configuredBasicStrategy(verify);
cosnt passport = new Passport();
passport.use(basicStrategy);
return new PassportAdapter(passport);
}

configuredBasicStrategy(verifyFn: BasicVerifyFunction): BasicStrategy {
return new BasicStrategy(verifyFn);
}
}
```

2. Register the strategy provider

*Exactly same as the strategy adapter*

Register the strategy provider in your LoopBack application so that the
authentication system can look for your strategy by name and invoke it:

```ts
// In the main file

import {addExtension} from '@loopback/core';
import {MyApplication} from '<path_to_your_app>';
import {AuthenticationBindings} from '@loopback/authentication';

const app = new MyApplication();

addExtension(
app,
AuthenticationBindings.AUTHENTICATION_STRATEGY_EXTENSION_POINT_NAME,
PassportBasicAuthProvider,
{
namespace:
AuthenticationBindings.AUTHENTICATION_STRATEGY_EXTENSION_POINT_NAME,
},
);
```

3. Decorate your endpoint

*Exactly same as the strategy adapter*

To authenticate your request with the basic strategy, decorate your controller
function like:

```ts
class MyController {
constructor(
@inject(AuthenticationBindings.CURRENT_USER) private user: UserProfile,
) {}

// Define your strategy name as a constant so that
// it is consistent with the name you provide in the adapter
@authenticate(AUTH_STRATEGY_NAME)
async whoAmI(): Promise<string> {
return this.user.id;
}
}
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
// Copyright IBM Corp. 2019. All Rights Reserved.
// Node module: @loopback/authentication
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

// FOR REVIWERS: THIS ACCEPTANCE TEST IS FOR `PassportAdapter`

import { authenticate, AuthenticateFn, AuthenticationBindings, AuthenticationComponent, AuthenticationStrategy, UserProfile } from '@loopback/authentication';
import { inject } from '@loopback/context';
import { addExtension, Application, Provider } from '@loopback/core';
import { anOpenApiSpec } from '@loopback/openapi-spec-builder';
import { api, get } from '@loopback/openapi-v3';
import { FindRoute, InvokeMethod, ParseParams, Reject, RequestContext, RestBindings, RestComponent, RestServer, Send, SequenceHandler } from '@loopback/rest';
import { Client, createClientForHandler } from '@loopback/testlab';
import { BasicStrategy, BasicVerifyFunction } from 'passport-http';
import { PassportAdapter } from '../../';
const SequenceActions = RestBindings.SequenceActions;
const AUTH_STRATEGY_NAME = 'basic';

describe('Basic Authentication - passport', () => {
let app: Application;
let server: RestServer;
let users: UserRepository;
beforeEach(givenAServer);
beforeEach(givenUserRepository);
beforeEach(givenControllerInApp);
beforeEach(givenAuthenticatedSequence);

it('authenticates successfully for correct credentials', async () => {
const client = whenIMakeRequestTo(server);
const credential =
users.list.joe.profile.id + ':' + users.list.joe.password;
const hash = Buffer.from(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 = whenIMakeRequestTo(server);
const credential = users.list.Simpson.profile.id + ':' + 'invalid';
const hash = Buffer.from(credential).toString('base64');
await client
.get('/whoAmI')
.set('Authorization', 'Basic ' + hash)
.expect(401);
});

it('allows anonymous requests to methods with no decorator', async () => {
class InfoController {
@get('/status')
status() {
return { running: true };
}
}

app.controller(InfoController);
await whenIMakeRequestTo(server)
.get('/status')
.expect(200, { running: true });
});

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

class PassportBasicAuthProvider implements Provider<AuthenticationStrategy> {
value(): AuthenticationStrategy {
const basicStrategy = this.configuredBasicStrategy(verify);
// Ideally the passport adapter should accept a configured
// passport instance like:

// const passport = new Passport();
// passport.use(basicStrategy);
// return new PassportAdapter(passport);

// The workaround still takes a strategy because the types exported from
// `passport` module gives error when using it as a type:
// afunction(passport: Passport){}
// the code above has type error. I've tried all the possible types exported
// from `passport` module and none of them work.
return new PassportAdapter(basicStrategy, 'basic');
}

configuredBasicStrategy(verifyFn: BasicVerifyFunction): BasicStrategy {
return new BasicStrategy(verifyFn);
}
}

function verify(username: string, password: string, cb: Function) {
process.nextTick(() => {
users.find(username, password, cb);
});
}

async function givenAServer() {
app = new Application();
app.component(AuthenticationComponent);
app.component(RestComponent);
addExtension(
app,
AuthenticationBindings.AUTHENTICATION_STRATEGY_EXTENSION_POINT_NAME,
PassportBasicAuthProvider,
{
namespace:
AuthenticationBindings.AUTHENTICATION_STRATEGY_EXTENSION_POINT_NAME,
},
);
server = await app.getServer(RestServer);
}

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

@api(apispec)
class MyController {
constructor(
@inject(AuthenticationBindings.CURRENT_USER) private user: UserProfile,
) { }

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

function givenAuthenticatedSequence() {
class MySequence implements SequenceHandler {
constructor(
@inject(SequenceActions.FIND_ROUTE) protected findRoute: FindRoute,
@inject(SequenceActions.PARSE_PARAMS)
protected parseParams: ParseParams,
@inject(SequenceActions.INVOKE_METHOD) protected invoke: InvokeMethod,
@inject(SequenceActions.SEND) protected send: Send,
@inject(SequenceActions.REJECT) protected reject: Reject,
@inject(AuthenticationBindings.AUTH_ACTION)
protected authenticateRequest: AuthenticateFn,
) { }

async handle(context: RequestContext) {
try {
const { request, response } = context;
const route = this.findRoute(request);

// Authenticate
await this.authenticateRequest(request);

// Authentication successful, proceed to invoke controller
const args = await this.parseParams(request, route);
const result = await this.invoke(route, args);
this.send(response, result);
} catch (error) {
this.reject(context, error);
return;
}
}
}
// bind user defined sequence
server.sequence(MySequence);
}

function whenIMakeRequestTo(restServer: RestServer): Client {
return createClientForHandler(restServer.requestHandler);
}
});

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 found = Object.keys(userList).find(search);
if (!found) return cb(null, false);
if (userList[found].password !== password) return cb(null, false);
cb(null, userList[found].profile);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,33 +3,17 @@
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import {inject} from '@loopback/context';
import {addExtension, Application, Provider} from '@loopback/core';
import {anOpenApiSpec} from '@loopback/openapi-spec-builder';
import {api, get} from '@loopback/openapi-v3';
import {
FindRoute,
InvokeMethod,
ParseParams,
Reject,
RequestContext,
RestBindings,
RestComponent,
RestServer,
Send,
SequenceHandler,
} from '@loopback/rest';
import {Client, createClientForHandler} from '@loopback/testlab';
import {BasicStrategy, BasicVerifyFunction} from 'passport-http';
import {
authenticate,
AuthenticationStrategy,
AuthenticateFn,
AuthenticationBindings,
AuthenticationComponent,
UserProfile,
} from '@loopback/authentication';
import {StrategyAdapter} from '../../';
// FOR REVIWERS: THIS ACCEPTANCE TEST IS FOR `StrategyAdapter`

import { authenticate, AuthenticateFn, AuthenticationBindings, AuthenticationComponent, AuthenticationStrategy, UserProfile } from '@loopback/authentication';
import { inject } from '@loopback/context';
import { addExtension, Application, Provider } from '@loopback/core';
import { anOpenApiSpec } from '@loopback/openapi-spec-builder';
import { api, get } from '@loopback/openapi-v3';
import { FindRoute, InvokeMethod, ParseParams, Reject, RequestContext, RestBindings, RestComponent, RestServer, Send, SequenceHandler } from '@loopback/rest';
import { Client, createClientForHandler } from '@loopback/testlab';
import { BasicStrategy, BasicVerifyFunction } from 'passport-http';
import { StrategyAdapter } from '../../';
const SequenceActions = RestBindings.SequenceActions;
const AUTH_STRATEGY_NAME = 'basic';

Expand Down Expand Up @@ -67,22 +51,22 @@ describe('Basic Authentication', () => {
class InfoController {
@get('/status')
status() {
return {running: true};
return { running: true };
}
}

app.controller(InfoController);
await whenIMakeRequestTo(server)
.get('/status')
.expect(200, {running: true});
.expect(200, { running: true });
});

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

Expand Down Expand Up @@ -147,7 +131,7 @@ describe('Basic Authentication', () => {
class MyController {
constructor(
@inject(AuthenticationBindings.CURRENT_USER) private user: UserProfile,
) {}
) { }

@authenticate(AUTH_STRATEGY_NAME)
async whoAmI(): Promise<string> {
Expand All @@ -168,11 +152,11 @@ describe('Basic Authentication', () => {
@inject(SequenceActions.REJECT) protected reject: Reject,
@inject(AuthenticationBindings.AUTH_ACTION)
protected authenticateRequest: AuthenticateFn,
) {}
) { }

async handle(context: RequestContext) {
try {
const {request, response} = context;
const { request, response } = context;
const route = this.findRoute(request);

// Authenticate
Expand All @@ -199,8 +183,8 @@ describe('Basic Authentication', () => {

class UserRepository {
constructor(
readonly list: {[key: string]: {profile: UserProfile; password: string}},
) {}
readonly list: { [key: string]: { profile: UserProfile; password: string } },
) { }
find(username: string, password: string, cb: Function): void {
const userList = this.list;
function search(key: string) {
Expand Down
Loading

0 comments on commit 561f6b5

Please sign in to comment.