import { authenticate , UserProfileFactory } from '@loopback/authentication' ; import { inject } from '@loopback/core' ; import { MockTestOauth2SocialApp , MyUser , userRepository , } from '@loopback/mock-oauth2-provider' ; import { get , Response , RestApplication , RestBindings } from '@loopback/rest' ; import { SecurityBindings , securityId , UserProfile } from '@loopback/security' ; import { Client , createClientForHandler , expect , supertest , } from '@loopback/testlab' ; import axios from 'axios' ; import { AddressInfo } from 'net' ; import { Strategy as Oauth2Strategy , StrategyOptions , VerifyCallback , VerifyFunction , } from 'passport-oauth2' ; import qs from 'qs' ; import * as url from 'url' ; import { StrategyAdapter } from '../../strategy-adapter' ; import { configureApplication , simpleRestApplication , } from './fixtures/simple-rest-app' ; /**
 * This test consists of three main components -> the supertest client, the LoopBack app (simple-rest-app.ts)
 * and the Mock Authorization Server (mock-oauth2-social-app.ts)
 *
 *  PLEASE LOOK AT fixtures/README.md file before going thru the below
 *
 *  Mock Authorization Server (mock-oauth2-social-app.ts) :
 *           mocks the authorization flow login with a social app like facebook, google, etc
 *  LoopBack app (simple-rest-app.ts) :
 *           has a simple app with no controller, Oauth2Controller is added in this test
 */ /**
 * options to pass to the Passport Strategy
 */ const oauth2Options = { clientID : '1111' , clientSecret : '1917e2b73a87fd0c9a92afab64f6c8d4' , callbackURL : 'http://localhost:8080/auth/thirdparty/callback' , authorizationURL : 'http://localhost:9000/oauth/dialog' , tokenURL : 'http://localhost:9000/oauth/token' , } ; param accessToken param refreshToken param profile param done const verify = function ( accessToken , refreshToken , userProfile , done ) { userProfile . token = accessToken ; if ( ! userRepository . findById ( userProfile . id ) ) { userRepository . createExternalUser ( userProfile ) ; } return done ( null , userProfile ) ; } ; param user const myUserProfileFactory = function ( user ) : UserProfile { const userProfile = { [ securityId ] : user . id , ... user } ; 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 ( ) { } @ authenticate ( 'oauth2' ) @ get ( '/auth/thirdparty' ) loginToThirdParty ( @ inject ( 'authentication.redirect.url' ) redirectUrl , @ inject ( 'authentication.redirect.status' ) status , @ inject ( RestBindings . Http . RESPONSE ) response ) { response . statusCode = status || 302 ; response . setHeader ( 'Location' , redirectUrl ) ; response . end ( ) ; return response ; } @ authenticate ( 'oauth2' ) @ get ( '/auth/thirdparty/callback' ) thirdPartyCallBack ( @ inject ( SecurityBindings . USER ) user ) { return { access_token : user . token } ; } } describe ( 'Oauth2 authorization flow' , ( ) => { let app ; let oauth2Strategy ; let client ; let oauth2Client ; before ( ( ) => { const server = MockTestOauth2SocialApp . startMock ( ) ; const port = ( server . address ( ) as AddressInfo ) . port ; oauth2Client = supertest ( `http://localhost:${ port }` ) ; } ) ; after ( MockTestOauth2SocialApp . stopMock ) ; before ( givenLoopBackApp ) ; before ( givenOauth2Strategy ) ; before ( setupAuthentication ) ; before ( givenControllerInApp ) ; before ( givenClient ) ; let oauthProviderUrl ; let providerLoginUrl ; let callbackToLbApp ; let loginPageParams ; context ( 'Stage 1 - Authorization code grant' , ( ) => { describe ( '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' ) ; loginPageParams = url . parse ( providerLoginUrl ) . query ?? '' ; expect ( url . parse ( response . get ( 'Location' ) ) . pathname ) . to . equal ( '/login' ) ; } ) ; it ( 'login page redirects to authorization app callback endpoint' , async ( ) => { const loginPageHiddenParams = qs . parse ( loginPageParams ) ; const params = { username : 'user1' , password : 'abc' , client_id : loginPageHiddenParams . client_id , redirect_uri : loginPageHiddenParams . redirect_uri , scope : loginPageHiddenParams . scope , } ; const response = await oauth2Client . post ( '/login_submit' ) . send ( qs . stringify ( params ) ) . expect ( 302 ) ; callbackToLbApp = response . get ( 'Location' ) ; expect ( url . parse ( callbackToLbApp ) . pathname ) . to . equal ( '/auth/thirdparty/callback' ) ; } ) ; it ( 'callback url contains access code' , async ( ) => { expect ( url . parse ( callbackToLbApp ) . query ) . to . containEql ( 'code' ) ; } ) ; } ) ; } ) ; context ( 'Stage 2: Authentication' , ( ) => { describe ( 'Invoking call back url returns access token' , ( ) => { it ( 'access code can be exchanged for token' , async ( ) => { expect ( url . parse ( callbackToLbApp ) . path ) . to . containEql ( '/auth/thirdparty/callback' ) ; const path = url . parse ( callbackToLbApp ) . path ?? '' ; const response = await client . get ( path ) . expect ( 200 ) ; expect ( response . body ) . property ( 'access_token' ) ; } ) ; } ) ; } ) ; function givenLoopBackApp ( ) { app = simpleRestApplication ( ) ; } function givenOauth2Strategy ( ) { const passport = new Oauth2Strategy ( oauth2Options , verify ) ; passport . userProfile = ( accessToken , done ) => { axios . get ( 'http://localhost:9000/verify?access_token=' + accessToken , { headers : { Authorization : accessToken } , } ) . then ( response => { done ( null , response . data ) ; } ) . catch ( err => { done ( err ) ; } ) ; } ; oauth2Strategy = new StrategyAdapter ( passport , 'oauth2' , myUserProfileFactory ) ; } async function setupAuthentication ( ) { await configureApplication ( oauth2Strategy , 'oauth2' ) ; } function givenControllerInApp ( ) { return app . controller ( Oauth2Controller ) ; } function givenClient ( ) { client = createClientForHandler ( app . requestHandler ) ; } } ) ;