import { authenticate , AuthenticateFn , AuthenticationBindings , AuthenticationComponent , AuthenticationStrategy , AUTHENTICATION_STRATEGY_NOT_FOUND , UserProfileFactory , USER_PROFILE_NOT_FOUND , } from '@loopback/authentication' ; import { addExtension , CoreTags , inject , Provider } from '@loopback/core' ; import { anOpenApiSpec } from '@loopback/openapi-spec-builder' ; import { api , FindRoute , get , InvokeMethod , ParseParams , Reject , RequestContext , RestApplication , RestBindings , RestServer , Send , SequenceHandler , } from '@loopback/rest' ; import { SecurityBindings , securityId , UserProfile } from '@loopback/security' ; import { Client , createClientForHandler } from '@loopback/testlab' ; import { BasicStrategy , BasicVerifyFunction } from 'passport-http' ; import { StrategyAdapter } from '../../strategy-adapter' ; const SequenceActions = RestBindings . SequenceActions ; const AUTH_STRATEGY_NAME = 'basic' ; const INVALID_USER_CREDENTIALS_MESSAGE = 'Invalid credentials' ; const INVALID_USER_CREDENTIALS_CODE = 'INVALID_USER_CREDENTIALS' ; const USERS_REPOSITORY_BINDING_KEY = 'repositories.users' ; const VERIFY_FUNCTION_BASIC_AUTHENTICATION_BINDING_KEY = 'authentication.basic.verify' ; const BASIC_STRATEGY_BINDING_KEY = 'authentication.strategies.basicAuthStrategy' ; enum ScenarioEnum { WithoutProviders , WithProviders } describe ( 'Basic Authentication' , ( ) => { let app ; let server ; let users ; beforeEach ( givenAnApp ) ; beforeEach ( givenUserRepository ) ; beforeEach ( givenControllerInApp ) ; it ( 'authenticates successfully for correct credentials' , async ( ) => { await remainingSetup ( ScenarioEnum . WithoutProviders ) ; const client = whenIMakeRequestTo ( server ) ; const credential = users . list . Joseph . profile . username + ':' + users . list . Joseph . password ; const hash = Buffer . from ( credential ) . toString ( 'base64' ) ; await client . get ( '/whoAmI' ) . set ( 'Authorization' , 'Basic ' + hash ) . expect ( users . list . Joseph . profile . id ) ; } ) ; it ( 'returns error for invalid credentials' , async ( ) => { await remainingSetup ( ScenarioEnum . WithoutProviders ) ; const client = whenIMakeRequestTo ( server ) ; const credential = users . list . Simon . profile . username + ':' + '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 'authenticate' decorator` , async ( ) => { class InfoController { @ get ( '/status' ) status ( ) { return { running : true } ; } } app . controller ( InfoController ) ; await remainingSetup ( ScenarioEnum . WithoutProviders ) ; await whenIMakeRequestTo ( server ) . get ( '/status' ) . expect ( 200 , { running : true } ) ; } ) ; it ( 'Using provider - authenticates successfully for correct credentials' , async ( ) => { await remainingSetup ( ScenarioEnum . WithProviders ) ; const client = whenIMakeRequestTo ( server ) ; const credential = users . list . Joseph . profile . username + ':' + users . list . Joseph . password ; const hash = Buffer . from ( credential ) . toString ( 'base64' ) ; await client . get ( '/whoAmI' ) . set ( 'Authorization' , 'Basic ' + hash ) . expect ( users . list . Joseph . profile . id ) ; } ) ; it ( 'Using provider - returns error for invalid credentials' , async ( ) => { await remainingSetup ( ScenarioEnum . WithProviders ) ; const client = whenIMakeRequestTo ( server ) ; const credential = users . list . Simon . profile . username + ':' + 'invalid' ; const hash = Buffer . from ( credential ) . toString ( 'base64' ) ; await client . get ( '/whoAmI' ) . set ( 'Authorization' , 'Basic ' + hash ) . expect ( 401 ) ; } ) ; it ( `Using provider - allows anonymous requests to methods with no 'authenticate' decorator` , async ( ) => { class InfoController { @ get ( '/status' ) status ( ) { return { running : true } ; } } app . controller ( InfoController ) ; await remainingSetup ( ScenarioEnum . WithProviders ) ; await whenIMakeRequestTo ( server ) . get ( '/status' ) . expect ( 200 , { running : true } ) ; } ) ; async function givenAnApp ( ) { app = new RestApplication ( ) ; app . component ( AuthenticationComponent ) ; } function givenUserRepository ( ) { users = new UserRepository ( { Joseph : { profile : { id : '1' , username : 'joesmith71' , firstName : 'Joseph' , lastName : 'Smith' , } , password : '12345' , } , Simon : { profile : { id : '2' , username : 'simonsmith71' , firstName : 'Simon' , lastName : 'Smith' , } , password : 'alpha' , } , Flint : { profile : { id : '3' , username : 'flintsmith71' , firstName : 'Flint' , lastName : 'Smith' , } , password : 'beta' , } , Curious : { profile : { id : '4' , username : 'curioussmith71' , firstName : 'Curious' , lastName : 'Smith' , } , password : 'gamma' , } , } ) ; } function verify ( username , password , cb ) { users . find ( username , password , cb ) ; } const myUserProfileFactory = function ( user ) : UserProfile { const userProfile = { [ securityId ] : user . id } ; return userProfile ; } ; const basicStrategy = new BasicStrategy ( verify ) ; const basicAuthStrategy = new StrategyAdapter ( basicStrategy , AUTH_STRATEGY_NAME , myUserProfileFactory ) ; async function setupBindings1 ( ) { app . bind ( BASIC_STRATEGY_BINDING_KEY ) . to ( basicAuthStrategy ) . tag ( { [ CoreTags . EXTENSION_FOR ] : AuthenticationBindings . AUTHENTICATION_STRATEGY_EXTENSION_POINT_NAME , } ) ; } async function setupBindings2 ( ) { addExtension ( app , AuthenticationBindings . AUTHENTICATION_STRATEGY_EXTENSION_POINT_NAME , PassportBasicAuthProvider , { namespace : AuthenticationBindings . AUTHENTICATION_STRATEGY_EXTENSION_POINT_NAME , } ) ; app . bind ( USERS_REPOSITORY_BINDING_KEY ) . to ( users ) ; app . bind ( VERIFY_FUNCTION_BASIC_AUTHENTICATION_BINDING_KEY ) . toProvider ( VerifyFunctionProvider ) ; app . bind ( AuthenticationBindings . USER_PROFILE_FACTORY ) . to ( myUserProfileFactory ) ; } async function remainingSetup ( scenario ) { if ( scenario === ScenarioEnum . WithoutProviders ) { await setupBindings1 ( ) ; } else if ( scenario === ScenarioEnum . WithProviders ) await setupBindings2 ( ) ; await givenAServer ( ) ; } async function givenAServer ( ) { server = await app . getServer ( RestServer ) ; givenAuthenticatedSequence ( ) ; } 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 ( SecurityBindings . USER ) private user ) { } @ authenticate ( AUTH_STRATEGY_NAME ) async whoAmI ( ) { return this . user [ securityId ] ; } } app . controller ( MyController ) ; } function givenAuthenticatedSequence ( ) { class MySequence implements SequenceHandler { constructor ( @ inject ( SequenceActions . FIND_ROUTE ) protected findRoute , @ inject ( SequenceActions . PARSE_PARAMS ) protected parseParams , @ inject ( SequenceActions . INVOKE_METHOD ) protected invoke , @ inject ( SequenceActions . SEND ) protected send , @ inject ( SequenceActions . REJECT ) protected reject , @ inject ( AuthenticationBindings . AUTH_ACTION ) protected authenticateRequest ) { } async handle ( context ) { try { const { request , response } = context ; const route = this . findRoute ( request ) ; await this . authenticateRequest ( request ) ; const args = await this . parseParams ( request , route ) ; const result = await this . invoke ( route , args ) ; this . send ( response , result ) ; } catch ( error ) { if ( error . message . message === INVALID_USER_CREDENTIALS_MESSAGE ) { Object . assign ( error , { code : INVALID_USER_CREDENTIALS_CODE } ) ; Object . assign ( error , { message : INVALID_USER_CREDENTIALS_MESSAGE } ) ; } if ( error . code === AUTHENTICATION_STRATEGY_NOT_FOUND || error . code === USER_PROFILE_NOT_FOUND || error . code === INVALID_USER_CREDENTIALS_CODE ) { Object . assign ( error , { statusCode : 401 } ) ; } this . reject ( context , error ) ; return ; } } } server . sequence ( MySequence ) ; } function whenIMakeRequestTo ( restServer ) { return createClientForHandler ( restServer . requestHandler ) ; } } ) ; interface MyUser { id : string ; username : string ; firstName ? : string ; lastName ? : string ; } class UserRepository { constructor ( readonly list ) { } find ( username , password , cb ) { const userList = this . list ; function search ( key ) { return userList [ key ] . profile . username === 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 ) ; } findWithoutCallBack ( username , password ) { let foundUser ; const userList = this . list ; function search ( key ) { return userList [ key ] . profile . username === username ; } const found = Object . keys ( userList ) . find ( search ) ; if ( ! found ) { foundUser = undefined ; } else { if ( userList [ found ] . password !== password ) { foundUser = undefined ; } else { foundUser = userList [ found ] . profile ; } } return foundUser ; } } class PassportBasicAuthProvider implements Provider < AuthenticationStrategy > { constructor ( @ inject ( VERIFY_FUNCTION_BASIC_AUTHENTICATION_BINDING_KEY ) private verifyFn , @ inject ( AuthenticationBindings . USER_PROFILE_FACTORY ) private userProfileFactory ) { } value ( ) { const basicStrategy = this . configuredBasicStrategy ( this . verifyFn ) ; return this . convertToAuthStrategy ( basicStrategy ) ; } configuredBasicStrategy ( verifyFn ) { return new BasicStrategy ( verifyFn ) ; } convertToAuthStrategy ( basic ) { return new StrategyAdapter ( basic , AUTH_STRATEGY_NAME , this . userProfileFactory ) ; } } class VerifyFunctionProvider implements Provider < BasicVerifyFunction > { constructor ( @ inject ( USERS_REPOSITORY_BINDING_KEY ) private userRepo ) { } value ( ) { const myThis = this ; return async function ( username , password , cb ) { let user ; try { user = myThis . userRepo . findWithoutCallBack ( username , password ) ; if ( ! user ) { const error = new Error ( INVALID_USER_CREDENTIALS_MESSAGE ) ; throw error ; } cb ( null , user ) ; } catch ( error ) { cb ( error , null ) ; } } ; } }