diff --git a/packages/authentication/src/__tests__/acceptance/basic-auth.acceptance.ts b/packages/authentication/src/__tests__/acceptance/basic-auth.acceptance.ts deleted file mode 100644 index 748403667441..000000000000 --- a/packages/authentication/src/__tests__/acceptance/basic-auth.acceptance.ts +++ /dev/null @@ -1,208 +0,0 @@ -// 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 - -import {inject} from '@loopback/context'; -import {Application} 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 { - authenticate, - AuthenticateFn, - AuthenticationBindings, - AuthenticationComponent, - UserProfile, -} from '../..'; - -const SequenceActions = RestBindings.SequenceActions; - -describe.skip('Basic Authentication', () => { - let app: Application; - let server: RestServer; - let users: UserRepository; - beforeEach(givenAServer); - beforeEach(givenUserRepository); - beforeEach(givenControllerInApp); - beforeEach(givenAuthenticatedSequence); - beforeEach(givenProviders); - - 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'}, - }); - } - - async function givenAServer() { - app = new Application(); - app.component(AuthenticationComponent); - app.component(RestComponent); - 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('BasicStrategy') - async whoAmI(): Promise { - 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 givenProviders() { - /** - class MyPassportStrategyProvider implements Provider { - constructor( - @inject(AuthenticationBindings.METADATA) - private metadata: AuthenticationMetadata, - ) {} - value(): ValueOrPromise { - if (!this.metadata) { - return undefined; - } - const name = this.metadata.strategy; - if (name === 'BasicStrategy') { - return new BasicStrategy(this.verify); - } else { - return Promise.reject(`The strategy ${name} is not available.`); - } - } - // callback method for BasicStrategy - verify(username: string, password: string, cb: Function) { - process.nextTick(() => { - users.find(username, password, cb); - }); - } - } - server - .bind(AuthenticationBindings.STRATEGY) - .toProvider(MyPassportStrategyProvider); - */ - } - - 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); - } -} diff --git a/packages/authentication/src/__tests__/unit/fixtures/mock-strategy-passport.ts b/packages/authentication/src/__tests__/unit/fixtures/mock-strategy-passport.ts new file mode 100644 index 000000000000..b8aaa77ea34f --- /dev/null +++ b/packages/authentication/src/__tests__/unit/fixtures/mock-strategy-passport.ts @@ -0,0 +1,67 @@ +// 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 + +import {Request} from 'express'; +import {AuthenticateOptions, Strategy} from 'passport'; +import {UserProfile} from '../../../types'; + +/** + * Test fixture for a mock asynchronous passport-strategy + */ +export class MockPassportStrategy extends Strategy { + // user to return for successful authentication + private mockUser: UserProfile; + + setMockUser(userObj: UserProfile) { + this.mockUser = userObj; + } + + /** + * authenticate() function similar to passport-strategy packages + * @param req + */ + async authenticate(req: Request, options?: AuthenticateOptions) { + await this.verify(req); + } + /** + * @param req + * mock verification function; usually passed in as constructor argument for + * passport-strategy + * + * For the purpose of mock tests we have this here + * pass req.query.testState = 'fail' to mock failed authorization + * pass req.query.testState = 'error' to mock unexpected error + */ + async verify(request: Request) { + if ( + request.headers && + request.headers.testState && + request.headers.testState === 'fail' + ) { + this.returnUnauthorized('authorization failed'); + return; + } else if ( + request.headers && + request.headers.testState && + request.headers.testState === 'error' + ) { + this.returnError('unexpected error'); + return; + } + process.nextTick(this.returnMockUser.bind(this)); + } + + returnMockUser() { + this.success(this.mockUser); + } + + returnUnauthorized(challenge?: string | number, status?: number) { + this.fail(challenge, status); + } + + returnError(err: string) { + this.error(err); + } +} diff --git a/packages/authentication/src/__tests__/unit/fixtures/mock-strategy.ts b/packages/authentication/src/__tests__/unit/fixtures/mock-strategy.ts index 5cd2b1849308..d4f1881e8f39 100644 --- a/packages/authentication/src/__tests__/unit/fixtures/mock-strategy.ts +++ b/packages/authentication/src/__tests__/unit/fixtures/mock-strategy.ts @@ -3,26 +3,35 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {Strategy, AuthenticateOptions} from 'passport'; -import {Request} from 'express'; +import {Request} from '@loopback/rest'; +import {AuthenticationStrategy, UserProfile} from '../../../types'; + +class AuthenticationError extends Error { + statusCode?: number; +} /** * Test fixture for a mock asynchronous passport-strategy */ -export class MockStrategy extends Strategy { +export class MockStrategy implements AuthenticationStrategy { + name: 'MockStrategy'; // user to return for successful authentication - private mockUser: Object; + private mockUser: UserProfile; - setMockUser(userObj: Object) { + setMockUser(userObj: UserProfile) { this.mockUser = userObj; } + returnMockUser(): UserProfile { + return this.mockUser; + } + /** * authenticate() function similar to passport-strategy packages * @param req */ - async authenticate(req: Request, options?: AuthenticateOptions) { - await this.verify(req); + async authenticate(req: Request): Promise { + return await this.verify(req); } /** * @param req @@ -39,28 +48,16 @@ export class MockStrategy extends Strategy { request.headers.testState && request.headers.testState === 'fail' ) { - this.returnUnauthorized('authorization failed'); - return; + const err = new AuthenticationError('authorization failed'); + err.statusCode = 401; + throw err; } else if ( request.headers && request.headers.testState && request.headers.testState === 'error' ) { - this.returnError('unexpected error'); - return; + throw new Error('unexpected error'); } - process.nextTick(this.returnMockUser.bind(this)); - } - - returnMockUser() { - this.success(this.mockUser); - } - - returnUnauthorized(challenge?: string | number, status?: number) { - this.fail(challenge, status); - } - - returnError(err: string) { - this.error(err); + return this.returnMockUser(); } } diff --git a/packages/authentication/src/__tests__/unit/providers/authentication.provider.unit.ts b/packages/authentication/src/__tests__/unit/providers/authentication.provider.unit.ts index e0012ea2f7af..84e9cf9f54eb 100644 --- a/packages/authentication/src/__tests__/unit/providers/authentication.provider.unit.ts +++ b/packages/authentication/src/__tests__/unit/providers/authentication.provider.unit.ts @@ -8,14 +8,15 @@ import {Request} from '@loopback/rest'; import {expect} from '@loopback/testlab'; import {AuthenticateFn, AuthenticationBindings, UserProfile} from '../../..'; import {AuthenticateActionProvider} from '../../../providers'; +import {AuthenticationStrategy} from '../../../types'; import {MockStrategy} from '../fixtures/mock-strategy'; -describe.skip('AuthenticateActionProvider', () => { +describe('AuthenticateActionProvider', () => { describe('constructor()', () => { it('instantiateClass injects authentication.strategy in the constructor', async () => { const context = new Context(); const strategy = new MockStrategy(); - //context.bind(AuthenticationBindings.STRATEGY).to(strategy); + context.bind(AuthenticationBindings.STRATEGY).to(strategy); const provider = await instantiateClass( AuthenticateActionProvider, context, @@ -52,7 +53,7 @@ describe.skip('AuthenticateActionProvider', () => { describe('context.get(provider_key)', () => { it('returns a function which authenticates a request and returns a user', async () => { const context: Context = new Context(); - //context.bind(AuthenticationBindings.STRATEGY).to(strategy); + context.bind(AuthenticationBindings.STRATEGY).to(strategy); context .bind(AuthenticationBindings.AUTH_ACTION) .toProvider(AuthenticateActionProvider); @@ -64,9 +65,11 @@ describe.skip('AuthenticateActionProvider', () => { expect(user).to.be.equal(mockUser); }); - it('throws an error if the injected passport strategy is not valid', async () => { + it('throws an error if the injected strategy is not valid', async () => { const context: Context = new Context(); - //context.bind(AuthenticationBindings.STRATEGY).to({} as Strategy); + context + .bind(AuthenticationBindings.STRATEGY) + .to({} as AuthenticationStrategy); context .bind(AuthenticationBindings.AUTH_ACTION) .toProvider(AuthenticateActionProvider); @@ -80,12 +83,15 @@ describe.skip('AuthenticateActionProvider', () => { } catch (exception) { error = exception; } - expect(error).to.have.property('message', 'invalid strategy parameter'); + expect(error).to.have.property( + 'message', + 'strategy.authenticate is not a function', + ); }); it('throws Unauthorized error when authentication fails', async () => { const context: Context = new Context(); - //context.bind(AuthenticationBindings.STRATEGY).to(strategy); + context.bind(AuthenticationBindings.STRATEGY).to(strategy); context .bind(AuthenticationBindings.AUTH_ACTION) .toProvider(AuthenticateActionProvider); @@ -107,10 +113,10 @@ describe.skip('AuthenticateActionProvider', () => { function givenAuthenticateActionProvider() { strategy = new MockStrategy(); strategy.setMockUser(mockUser); - // provider = new AuthenticateActionProvider( - // () => Promise.resolve(strategy), - // u => (currentUser = u), - // ); + provider = new AuthenticateActionProvider( + () => Promise.resolve(strategy), + u => (currentUser = u), + ); currentUser = undefined; } }); diff --git a/packages/authentication/src/__tests__/unit/strategy-adapter.unit.ts b/packages/authentication/src/__tests__/unit/strategy-adapter.unit.ts index 8b3cc2587205..f42f0a2557c0 100644 --- a/packages/authentication/src/__tests__/unit/strategy-adapter.unit.ts +++ b/packages/authentication/src/__tests__/unit/strategy-adapter.unit.ts @@ -6,7 +6,7 @@ import {expect} from '@loopback/testlab'; import {StrategyAdapter, UserProfile} from '../..'; import {Request, HttpErrors} from '@loopback/rest'; -import {MockStrategy} from './fixtures/mock-strategy'; +import {MockPassportStrategy} from './fixtures/mock-strategy-passport'; import {AuthenticateOptions} from 'passport'; describe('Strategy Adapter', () => { @@ -16,11 +16,11 @@ describe('Strategy Adapter', () => { it('calls the authenticate method of the strategy', async () => { let calledFlag = false; // TODO: (as suggested by @bajtos) use sinon spy - class Strategy extends MockStrategy { + class Strategy extends MockPassportStrategy { // override authenticate method to set calledFlag async authenticate(req: Request, options?: AuthenticateOptions) { calledFlag = true; - await MockStrategy.prototype.authenticate.call(this, req, options); + await super.authenticate(req, options); } } const strategy = new Strategy(); @@ -31,7 +31,7 @@ describe('Strategy Adapter', () => { }); it('returns a promise which resolves to an object', async () => { - const strategy = new MockStrategy(); + const strategy = new MockPassportStrategy(); strategy.setMockUser(mockUser); const adapter = new StrategyAdapter(strategy); const request = {}; @@ -40,7 +40,7 @@ describe('Strategy Adapter', () => { }); it('throws Unauthorized error when authentication fails', async () => { - const strategy = new MockStrategy(); + const strategy = new MockPassportStrategy(); strategy.setMockUser(mockUser); const adapter = new StrategyAdapter(strategy); const request = {}; @@ -55,7 +55,7 @@ describe('Strategy Adapter', () => { }); it('throws InternalServerError when strategy returns error', async () => { - const strategy = new MockStrategy(); + const strategy = new MockPassportStrategy(); strategy.setMockUser(mockUser); const adapter = new StrategyAdapter(strategy); const request = {}; diff --git a/packages/authentication/src/strategy-adapter.ts b/packages/authentication/src/strategy-adapter.ts index aa9542d6d03a..665baf01df6e 100644 --- a/packages/authentication/src/strategy-adapter.ts +++ b/packages/authentication/src/strategy-adapter.ts @@ -10,6 +10,12 @@ import {UserProfile} from './types'; const passportRequestMixin = require('passport/lib/http/request'); /** + * @deprecated + * + * THIS CLASS IS DEPRECATED. + * We will be adding a new package to implement one or more new passport + * strategy adapters. + * * Adapter class to invoke passport-strategy * 1. provides express dependencies to the passport strategies * 2. provides shimming of requests for passport authentication