-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: adapter that wraps passport middleware
- Loading branch information
Showing
9 changed files
with
468 additions
and
50 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
201 changes: 201 additions & 0 deletions
201
...sport-adapter/src/__tests__/acceptance/authentication-with-passport-adapter.acceptance.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.