Skip to content

Commit

Permalink
fix: passport strategy adapter must support oauth2 flows
Browse files Browse the repository at this point in the history
fixes: #4902
  • Loading branch information
deepakrkris committed Mar 20, 2020
1 parent 8339c2e commit bc9d387
Show file tree
Hide file tree
Showing 11 changed files with 1,371 additions and 7 deletions.
732 changes: 732 additions & 0 deletions extensions/authentication-passport/package-lock.json

Large diffs are not rendered by default.

9 changes: 8 additions & 1 deletion extensions/authentication-passport/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,13 @@
"@types/node": "^10.17.17",
"@types/passport": "^1.0.3",
"@types/passport-http": "^0.3.8",
"passport-http": "^0.3.0"
"@types/passport-oauth2": "^1.4.8",
"body-parser": "^1.19.0",
"express": "^4.17.1",
"jsonwebtoken": "^8.5.1",
"lodash": "^4.17.15",
"passport-http": "^0.3.0",
"passport-oauth2": "^1.5.0",
"supertest": "^4.0.2"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
// Copyright IBM Corp. 2019. All Rights Reserved.
// Node module: @loopback/authentication-passport
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import {
UserProfileFactory, authenticate,
} from '@loopback/authentication';
import {Strategy as Oauth2Strategy, StrategyOptions, VerifyFunction, VerifyCallback} from 'passport-oauth2';
import {MyUser, userRepository} from './fixtures/user-repository';
import {simpleRestApplication, configureApplication} from './fixtures/simple-rest-app';
import {securityId, UserProfile, SecurityBindings} from '@loopback/security';
import {StrategyAdapter} from '../../strategy-adapter';
import {get, param} from '@loopback/openapi-v3';
import {
Client,
createClientForHandler,
expect,
supertest
} from '@loopback/testlab';
import {RestApplication, RedirectRoute} from '@loopback/rest';
import {startApp as startMockProvider, stopApp as stopMockProvider} from './fixtures/oauth2-provider';
import * as url from 'url';
import { inject } from '@loopback/core';

/**
* options to pass to the Passport Strategy
*/
const oauth2Options: StrategyOptions = {
clientID: '1111',
clientSecret: '1917e2b73a87fd0c9a92afab64f6c8d4',
callbackURL: 'http://localhost:8080/auth/thirdparty/callback',
authorizationURL: 'http://localhost:9000/oauth/dialog',
tokenURL: 'http://localhost:9000/oauth/token',
}

/**
* verify function for the oauth2 strategy
* This function mocks a lookup against a user profile datastore
*
* @param accessToken
* @param refreshToken
* @param profile
* @param done
*/
const verify: VerifyFunction = function (accessToken: string, refreshToken: string, profile: any, done: VerifyCallback) {
console.log(accessToken);
const userProfile: MyUser = profile as MyUser;
let user: UserProfile = userRepository.findUser(userProfile.id);
if (!user) {
user = {
[securityId]: 'token',
}
}
user.token = accessToken;
console.log(user);
return done(null, user);
}

const myUserProfileFactory: UserProfileFactory<MyUser> = function(
user: MyUser,
): UserProfile {
const userProfile = {[securityId]: user.id};
return userProfile;
};

/**
* Login controller for third party oauth provider
*
* This creates an authentication endpoint for the third party oauth provider
*
* Two methods are expected
*
* 1. loginToThirdParty
* i. an endpoint for api clients to login via a third party app
* ii. the passport strategy identifies this call as a redirection to third party
* iii. this endpoint redirects to the third party authorization url
*
* 2. thirdPartyCallBack
* i. this is the callback for the thirdparty app
* ii. on successful user login the third party calls this endpoint with an access code
* iii. the passport oauth2 strategy exchanges the code for an access token
* iv. the passport oauth2 strategy then calls the provided `verify()` function with the access token
*/
export class Oauth2Controller {
constructor() {}

// this configures the oauth2 strategy
@authenticate('oauth2')
// we have modeled this as a GET endpoint
@get('/auth/thirdparty')
// loginToThirdParty() is the handler for '/auth/thirdparty'
// this method is injected with 'x-loopback-authentication-redirect-url'
// the value for 'x-loopback-authentication-redirect-url' is set by the passport strategy adapter
loginToThirdParty(@param.query.string('x-loopback-authentication-redirect-url') redirectUrl: string,
@param.query.number('x-loopback-authentication-redirect-status') status: number) {
return new RedirectRoute('/', redirectUrl, status);
}

// we configure the callback url also with the same oauth2 strategy
@authenticate('oauth2')
// this SHOULD be a GET call so that the third party can redirect
@get('/auth/thirdparty/callback')
// thirdPartyCallBack() is the handler for '/auth/thirdparty/callback'
// the oauth2 strategy identifies this as a callback with the request.query.code sent by the third party app
// the oauth2 strategy exchanges the access code for a access token and then calls the provided verify() function
// the verify function creates a user profile after verifying the access token
thirdPartyCallBack(@inject(SecurityBindings.USER) user: UserProfile) {
console.log(user);
return user.token;
}
}

describe.only('Oauth2 authorization flow', () => {
let app: RestApplication;
let oauth2Strategy: StrategyAdapter<MyUser>;
let client: Client;

before(startMockProvider);
after(stopMockProvider);

before(givenLoopBackApp);
before(givenOauth2Strategy);
before(setupAuthentication);
before(givenControllerInApp);
before(givenClient);

let oauthProviderUrl: string;
let providerLoginUrl: string;
let callbackToLbApp: string;

context('when client invokes oauth flow', () => {

it('call is redirected to third party authorization url', async () => {
const response = await client.get('/auth/thirdparty').expect(303);
oauthProviderUrl = response.get('Location');
expect(url.parse(response.get('Location')).pathname).to.equal(url.parse(oauth2Options.authorizationURL).pathname);
});

it('call to authorization url is redirected to oauth providers login page', async () => {
const response = await supertest('').get(oauthProviderUrl).expect(302);
providerLoginUrl = response.get('Location');
expect(url.parse(response.get('Location')).pathname).to.equal('/login');
});
});

context('when user logs into provider login page', () => {
it('login page redirects to authorization app callback endpoint', async () => {
let params = url.parse(providerLoginUrl).query;
params = params + '&&username=user1&&password=abc';
const response = await supertest('').post('http://localhost:9000/login_submit?' + params).expect(302);
callbackToLbApp = response.get('Location');
expect(url.parse(response.get('Location')).pathname).to.equal('/auth/thirdparty/callback');
});

it('callback url contains access code', async () => {
expect(url.parse(callbackToLbApp).query).to.containEql('code');
});
});

context('Invoking call back url returns access token', () => {
it('access code can be exchanged for token', async () => {
const path: string = url.parse(callbackToLbApp).path ?? '/auth/thirdparty/callback';
const response = await client.get(path).expect(200);
expect(response).property('access_token');
});
});

function givenLoopBackApp() {
app = simpleRestApplication();
}

function givenOauth2Strategy() {
const passport = new Oauth2Strategy(oauth2Options, verify);
oauth2Strategy = new StrategyAdapter(
passport,
'oauth2',
myUserProfileFactory,
);
}

function setupAuthentication() {
configureApplication(oauth2Strategy, 'oauth2');
}

function givenControllerInApp() {
return app.controller(Oauth2Controller);
}

function givenClient() {
client = createClientForHandler(app.requestHandler);
}
});
Loading

0 comments on commit bc9d387

Please sign in to comment.