From b72ba01ddd3496d3fa06adf5b726c6a5d8a1bbe9 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Sun, 24 May 2020 12:55:04 -0700 Subject: [PATCH] feat(authentication): add a middleware for authentication --- ...ic-auth-extension.middleware.acceptance.ts | 272 ++++++++++ ...wt-auth-extension.middleware.acceptance.ts | 504 ++++++++++++++++++ .../authentication.middleware.sequence.ts | 8 + .../src/authentication.component.ts | 19 +- packages/authentication/src/keys.ts | 8 + .../src/providers/auth-action.provider.ts | 42 +- packages/rest/src/sequence.ts | 5 +- 7 files changed, 844 insertions(+), 14 deletions(-) create mode 100644 packages/authentication/src/__tests__/acceptance/basic-auth-extension.middleware.acceptance.ts create mode 100644 packages/authentication/src/__tests__/acceptance/jwt-auth-extension.middleware.acceptance.ts create mode 100644 packages/authentication/src/__tests__/fixtures/sequences/authentication.middleware.sequence.ts diff --git a/packages/authentication/src/__tests__/acceptance/basic-auth-extension.middleware.acceptance.ts b/packages/authentication/src/__tests__/acceptance/basic-auth-extension.middleware.acceptance.ts new file mode 100644 index 000000000000..a0227fce7872 --- /dev/null +++ b/packages/authentication/src/__tests__/acceptance/basic-auth-extension.middleware.acceptance.ts @@ -0,0 +1,272 @@ +// Copyright IBM Corp. 2019,2020. 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 {Application, inject} from '@loopback/core'; +import {anOpenApiSpec} from '@loopback/openapi-spec-builder'; +import {api, get} from '@loopback/openapi-v3'; +import {Request, RestServer} from '@loopback/rest'; +import {SecurityBindings, securityId, UserProfile} from '@loopback/security'; +import {Client, createClientForHandler, expect} from '@loopback/testlab'; +import { + authenticate, + AuthenticationBindings, + registerAuthenticationStrategy, +} from '../..'; +import {AuthenticationStrategy} from '../../types'; +import { + createBasicAuthorizationHeaderValue, + getApp, + getUserRepository, + myUserProfileFactory, +} from '../fixtures/helper'; +import {BasicAuthenticationStrategyBindings, USER_REPO} from '../fixtures/keys'; +import {AuthenticationMiddlewareSequence} from '../fixtures/sequences/authentication.middleware.sequence'; +import {BasicAuthenticationUserService} from '../fixtures/services/basic-auth-user-service'; +import {BasicAuthenticationStrategy} from '../fixtures/strategies/basic-strategy'; +import {User} from '../fixtures/users/user'; +import {UserRepository} from '../fixtures/users/user.repository'; + +describe('Basic Authentication', () => { + let app: Application; + let server: RestServer; + let users: UserRepository; + let joeUser: User; + beforeEach(givenAServer); + beforeEach(givenControllerInApp); + beforeEach(givenAuthenticatedSequence); + beforeEach(givenProviders); + + it(`authenticates successfully for correct credentials of user 'jack'`, async () => { + const client = whenIMakeRequestTo(server); + await client + .get('/whoAmI') + .set('Authorization', createBasicAuthorizationHeaderValue(joeUser)) + .expect(joeUser.id); + }); + + it('returns error for missing Authorization header', async () => { + const client = whenIMakeRequestTo(server); + + await client.get('/whoAmI').expect({ + error: { + message: 'Authorization header not found.', + name: 'UnauthorizedError', + statusCode: 401, + }, + }); + }); + + it(`returns error for missing 'Basic ' portion of Authorization header value`, async () => { + const client = whenIMakeRequestTo(server); + await client + .get('/whoAmI') + .set( + 'Authorization', + createBasicAuthorizationHeaderValue(joeUser, {prefix: 'NotB@sic '}), + ) + .expect({ + error: { + message: `Authorization header is not of type 'Basic'.`, + name: 'UnauthorizedError', + statusCode: 401, + }, + }); + }); + + it(`returns error for too many parts in Authorization header value`, async () => { + const client = whenIMakeRequestTo(server); + await client + .get('/whoAmI') + .set( + 'Authorization', + createBasicAuthorizationHeaderValue(joeUser) + ' someOtherValue', + ) + .expect({ + error: { + message: `Authorization header value has too many parts. It must follow the pattern: 'Basic xxyyzz' where xxyyzz is a base64 string.`, + name: 'UnauthorizedError', + statusCode: 401, + }, + }); + }); + + it(`returns error for missing ':' in decrypted Authorization header credentials value`, async () => { + const client = whenIMakeRequestTo(server); + await client + .get('/whoAmI') + .set( + 'Authorization', + createBasicAuthorizationHeaderValue(joeUser, {separator: '|'}), + ) + .expect({ + error: { + message: `Authorization header 'Basic' value does not contain two parts separated by ':'.`, + name: 'UnauthorizedError', + statusCode: 401, + }, + }); + }); + + it(`returns error for too many parts in decrypted Authorization header credentials value`, async () => { + const client = whenIMakeRequestTo(server); + await client + .get('/whoAmI') + .set( + 'Authorization', + createBasicAuthorizationHeaderValue(joeUser, { + extraSegment: 'extraPart', + }), + ) + .expect({ + error: { + message: `Authorization header 'Basic' value does not contain two parts separated by ':'.`, + name: 'UnauthorizedError', + statusCode: 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}); + }); + + it('returns error for unknown authentication strategy', async () => { + class InfoController { + @get('/status') + @authenticate('doesnotexist') + status() { + return {running: true}; + } + } + + app.controller(InfoController); + await whenIMakeRequestTo(server) + .get('/status') + .expect({ + error: { + message: `The strategy 'doesnotexist' is not available.`, + name: 'Error', + statusCode: 401, + code: 'AUTHENTICATION_STRATEGY_NOT_FOUND', + }, + }); + }); + + it('returns error when undefined user profile returned from authentication strategy', async () => { + class BadBasicStrategy implements AuthenticationStrategy { + name = 'badbasic'; + async authenticate(request: Request): Promise { + return undefined; + } + } + registerAuthenticationStrategy(server, BadBasicStrategy); + + class InfoController { + @get('/status') + @authenticate('badbasic') + status() { + return {running: true}; + } + } + + app.controller(InfoController); + await whenIMakeRequestTo(server) + .get('/status') + .expect({ + error: { + message: `User profile not returned from strategy's authenticate function`, + name: 'Error', + statusCode: 401, + code: 'USER_PROFILE_NOT_FOUND', + }, + }); + }); + + it('adds security scheme component to apiSpec', async () => { + const EXPECTED_SPEC = { + components: { + securitySchemes: { + basic: { + type: 'http', + scheme: 'basic', + }, + }, + }, + }; + const spec = await server.getApiSpec(); + expect(spec).to.containDeep(EXPECTED_SPEC); + }); + + async function givenAServer() { + app = getApp(); + 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() {} + + @authenticate('basic') + async whoAmI( + @inject(SecurityBindings.USER) userProfile: UserProfile, + ): Promise { + if (!userProfile) return 'userProfile is undefined'; + if (!userProfile[securityId]) return 'userProfile id is undefined'; + return userProfile[securityId]; + } + } + app.controller(MyController); + } + + function givenAuthenticatedSequence() { + // bind user defined sequence + server.sequence(AuthenticationMiddlewareSequence); + } + + function givenProviders() { + registerAuthenticationStrategy(server, BasicAuthenticationStrategy); + + server + .bind(BasicAuthenticationStrategyBindings.USER_SERVICE) + .toClass(BasicAuthenticationUserService); + + users = getUserRepository(); + joeUser = users.list['joe888']; + server.bind(USER_REPO).to(users); + + server + .bind(AuthenticationBindings.USER_PROFILE_FACTORY) + .to(myUserProfileFactory); + } + + function whenIMakeRequestTo(restServer: RestServer): Client { + return createClientForHandler(restServer.requestHandler); + } +}); diff --git a/packages/authentication/src/__tests__/acceptance/jwt-auth-extension.middleware.acceptance.ts b/packages/authentication/src/__tests__/acceptance/jwt-auth-extension.middleware.acceptance.ts new file mode 100644 index 000000000000..39cef1a798a6 --- /dev/null +++ b/packages/authentication/src/__tests__/acceptance/jwt-auth-extension.middleware.acceptance.ts @@ -0,0 +1,504 @@ +// Copyright IBM Corp. 2019,2020. 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 {Application, inject} from '@loopback/core'; +import {get, post} from '@loopback/openapi-v3'; +import {Request, RestServer} from '@loopback/rest'; +import {SecurityBindings, securityId, UserProfile} from '@loopback/security'; +import {Client, createClientForHandler, expect} from '@loopback/testlab'; +import { + authenticate, + AuthenticationBindings, + AuthenticationStrategy, + registerAuthenticationStrategy, +} from '../..'; +import {UserProfileFactory} from '../../types'; +import { + createBearerAuthorizationHeaderValue, + getApp, + getUserRepository, + myUserProfileFactory, +} from '../fixtures/helper'; +import {JWTAuthenticationStrategyBindings, USER_REPO} from '../fixtures/keys'; +import {AuthenticationMiddlewareSequence} from '../fixtures/sequences/authentication.middleware.sequence'; +import {JWTService} from '../fixtures/services/jwt-service'; +import {JWTAuthenticationStrategy} from '../fixtures/strategies/jwt-strategy'; +import {User} from '../fixtures/users/user'; +import {UserRepository} from '../fixtures/users/user.repository'; + +describe('JWT Authentication', () => { + let app: Application; + let server: RestServer; + let testUsers: UserRepository; + let joeUser: User; + let token: string; + const TOKEN_SECRET_VALUE = 'myjwts3cr3t'; + const TOKEN_EXPIRES_IN_VALUE = '600'; + + beforeEach(givenAServer); + beforeEach(givenAuthenticatedSequence); + beforeEach(givenProviders); + + it('authenticates successfully with valid token', async () => { + class InfoController { + constructor( + @inject(JWTAuthenticationStrategyBindings.TOKEN_SERVICE) + public tokenService: JWTService, + @inject(USER_REPO) + public users: UserRepository, + @inject(AuthenticationBindings.USER_PROFILE_FACTORY) + public userProfileFactory: UserProfileFactory, + ) {} + + @post('/login') + async logIn() { + // + // ...Other code for verifying a valid user (e.g. basic or local strategy)... + // + + // Now with a valid userProfile, let's create a JSON web token + return this.tokenService.generateToken( + this.userProfileFactory(joeUser), + ); + } + + @get('/whoAmI') + @authenticate('jwt') + whoAmI(@inject(SecurityBindings.USER) userProfile: UserProfile) { + if (!userProfile) return 'userProfile is undefined'; + if (!userProfile[securityId]) + return 'userProfile[securityId] is undefined'; + return userProfile[securityId]; + } + } + + app.controller(InfoController); + + token = (await whenIMakeRequestTo(server).post('/login').expect(200)).text; + + expect(token).to.be.not.null(); + expect(token).to.be.String(); + + const id = ( + await whenIMakeRequestTo(server) + .get('/whoAmI') + .set('Authorization', createBearerAuthorizationHeaderValue(token)) + .expect(200) + ).text; + + expect(id).to.equal(joeUser.id); + }); + + it('returns error for missing Authorization header', async () => { + class InfoController { + constructor( + @inject(JWTAuthenticationStrategyBindings.TOKEN_SERVICE) + public tokenService: JWTService, + @inject(USER_REPO) + public users: UserRepository, + @inject(AuthenticationBindings.USER_PROFILE_FACTORY) + public userProfileFactory: UserProfileFactory, + ) {} + + @post('/login') + async logIn() { + // + // ...Other code for verifying a valid user (e.g. basic or local strategy)... + // + + // Now with a valid userProfile, let's create a JSON web token + return this.tokenService.generateToken( + this.userProfileFactory(joeUser), + ); + } + + @get('/whoAmI') + @authenticate('jwt') + whoAmI(@inject(SecurityBindings.USER) userProfile: UserProfile) { + if (!userProfile) return 'userProfile is undefined'; + if (!userProfile[securityId]) + return 'userProfile[securityId] is undefined'; + return userProfile[securityId]; + } + } + + app.controller(InfoController); + + token = (await whenIMakeRequestTo(server).post('/login').expect(200)).text; + + expect(token).to.be.not.null(); + expect(token).to.be.String(); + + await whenIMakeRequestTo(server) + .get('/whoAmI') + .expect({ + error: { + message: 'Authorization header not found.', + name: 'UnauthorizedError', + statusCode: 401, + }, + }); + }); + + it(`returns error for invalid 'Bearer ' portion of Authorization header value`, async () => { + class InfoController { + constructor( + @inject(JWTAuthenticationStrategyBindings.TOKEN_SERVICE) + public tokenService: JWTService, + @inject(USER_REPO) + public users: UserRepository, + @inject(AuthenticationBindings.USER_PROFILE_FACTORY) + public userProfileFactory: UserProfileFactory, + ) {} + + @post('/login') + async logIn() { + // + // ...Other code for verifying a valid user (e.g. basic or local strategy)... + // + + // Now with a valid userProfile, let's create a JSON web token + return this.tokenService.generateToken( + this.userProfileFactory(joeUser), + ); + } + + @get('/whoAmI') + @authenticate('jwt') + whoAmI(@inject(SecurityBindings.USER) userProfile: UserProfile) { + if (!userProfile) return 'userProfile is undefined'; + if (!userProfile[securityId]) + return 'userProfile[securityId] is undefined'; + return userProfile[securityId]; + } + } + + app.controller(InfoController); + + token = (await whenIMakeRequestTo(server).post('/login').expect(200)).text; + + expect(token).to.be.not.null(); + expect(token).to.be.String(); + + await whenIMakeRequestTo(server) + .get('/whoAmI') + .set( + 'Authorization', + createBearerAuthorizationHeaderValue(token, 'NotB3ar3r '), + ) + .expect({ + error: { + message: `Authorization header is not of type 'Bearer'.`, + name: 'UnauthorizedError', + statusCode: 401, + }, + }); + }); + + it(`returns error for too many parts in Authorization header value`, async () => { + class InfoController { + constructor( + @inject(JWTAuthenticationStrategyBindings.TOKEN_SERVICE) + public tokenService: JWTService, + @inject(USER_REPO) + public users: UserRepository, + @inject(AuthenticationBindings.USER_PROFILE_FACTORY) + public userProfileFactory: UserProfileFactory, + ) {} + + @post('/login') + async logIn() { + // + // ...Other code for verifying a valid user (e.g. basic or local strategy)... + // + + return this.tokenService.generateToken( + this.userProfileFactory(joeUser), + ); + } + + @get('/whoAmI') + @authenticate('jwt') + whoAmI(@inject(SecurityBindings.USER) userProfile: UserProfile) { + if (!userProfile) return 'userProfile is undefined'; + if (!userProfile[securityId]) + return 'userProfile[securityId] is undefined'; + return userProfile[securityId]; + } + } + + app.controller(InfoController); + + token = (await whenIMakeRequestTo(server).post('/login').expect(200)).text; + + expect(token).to.be.not.null(); + expect(token).to.be.String(); + + await whenIMakeRequestTo(server) + .get('/whoAmI') + .set( + 'Authorization', + createBearerAuthorizationHeaderValue(token) + ' someOtherValue', + ) + .expect({ + error: { + message: `Authorization header value has too many parts. It must follow the pattern: 'Bearer xx.yy.zz' where xx.yy.zz is a valid JWT token.`, + name: 'UnauthorizedError', + statusCode: 401, + }, + }); + }); + + it('returns error due to expired token', async () => { + class InfoController { + constructor() {} + + @get('/whoAmI') + @authenticate('jwt') + whoAmI(@inject(SecurityBindings.USER) userProfile: UserProfile) { + if (!userProfile) return 'userProfile is undefined'; + if (!userProfile[securityId]) + return 'userProfile[securityId] is undefined'; + return userProfile[securityId]; + } + } + + app.controller(InfoController); + + const expiredToken = await getExpiredToken(); + + await whenIMakeRequestTo(server) + .get('/whoAmI') + .set('Authorization', createBearerAuthorizationHeaderValue(expiredToken)) + .expect({ + error: { + message: `Error verifying token : jwt expired`, + name: 'UnauthorizedError', + statusCode: 401, + }, + }); + }); + + it('returns error due to invalid token #1', async () => { + class InfoController { + constructor() {} + + @get('/whoAmI') + @authenticate('jwt') + whoAmI(@inject(SecurityBindings.USER) userProfile: UserProfile) { + if (!userProfile) return 'userProfile is undefined'; + if (!userProfile[securityId]) + return 'userProfile[securityId] is undefined'; + return userProfile[securityId]; + } + } + + app.controller(InfoController); + + const invalidToken = 'aaa.bbb.ccc'; + + await whenIMakeRequestTo(server) + .get('/whoAmI') + .set('Authorization', createBearerAuthorizationHeaderValue(invalidToken)) + .expect({ + error: { + message: 'Error verifying token : invalid token', + name: 'UnauthorizedError', + statusCode: 401, + }, + }); + }); + + it('returns error due to invalid token #2', async () => { + class InfoController { + constructor() {} + + @get('/whoAmI') + @authenticate('jwt') + whoAmI(@inject(SecurityBindings.USER) userProfile: UserProfile) { + if (!userProfile) return 'userProfile is undefined'; + if (!userProfile[securityId]) + return 'userProfile[securityId] is undefined'; + return userProfile[securityId]; + } + } + + app.controller(InfoController); + + const invalidToken = 'aaa.bbb.ccc.ddd'; + + await whenIMakeRequestTo(server) + .get('/whoAmI') + .set('Authorization', createBearerAuthorizationHeaderValue(invalidToken)) + .expect({ + error: { + message: 'Error verifying token : jwt malformed', + name: 'UnauthorizedError', + statusCode: 401, + }, + }); + }); + + it('creates a json web token and throws error for userProfile that is undefined', async () => { + class InfoController { + constructor( + @inject(JWTAuthenticationStrategyBindings.TOKEN_SERVICE) + public tokenService: JWTService, + @inject(USER_REPO) + public users: UserRepository, + @inject(AuthenticationBindings.USER_PROFILE_FACTORY) + public userProfileFactory: UserProfileFactory, + ) {} + + @get('/createtoken') + async createToken() { + return this.tokenService.generateToken(undefined); + } + } + + app.controller(InfoController); + + await whenIMakeRequestTo(server) + .get('/createtoken') + .expect({ + error: { + message: `Error generating token : userProfile is null`, + name: 'UnauthorizedError', + statusCode: 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}); + }); + + it('returns error for unknown authentication strategy', async () => { + class InfoController { + @get('/status') + @authenticate('doesnotexist') + status() { + return {running: true}; + } + } + + app.controller(InfoController); + await whenIMakeRequestTo(server) + .get('/status') + .expect({ + error: { + message: `The strategy 'doesnotexist' is not available.`, + name: 'Error', + statusCode: 401, + code: 'AUTHENTICATION_STRATEGY_NOT_FOUND', + }, + }); + }); + + it('returns error when undefined user profile returned from authentication strategy', async () => { + class BadJWTStrategy implements AuthenticationStrategy { + name = 'badjwt'; + async authenticate(request: Request): Promise { + return undefined; + } + } + registerAuthenticationStrategy(server, BadJWTStrategy); + + class InfoController { + @get('/status') + @authenticate('badjwt') + status() { + return {running: true}; + } + } + + app.controller(InfoController); + await whenIMakeRequestTo(server) + .get('/status') + .expect({ + error: { + message: `User profile not returned from strategy's authenticate function`, + name: 'Error', + statusCode: 401, + code: 'USER_PROFILE_NOT_FOUND', + }, + }); + }); + + it('adds security scheme component to apiSpec', async () => { + const EXPECTED_SPEC = { + components: { + securitySchemes: { + jwt: { + type: 'http', + scheme: 'bearer', + bearerFormat: 'JWT', + }, + }, + }, + }; + const spec = await server.getApiSpec(); + expect(spec).to.containDeep(EXPECTED_SPEC); + }); + + async function givenAServer() { + app = getApp(); + server = await app.getServer(RestServer); + } + + /** + * Creates an expired token + * + * Specifying a negative value for 'expiresIn' so the + * token is automatically expired + */ + async function getExpiredToken() { + const userProfile = myUserProfileFactory(joeUser); + const tokenService = new JWTService(TOKEN_SECRET_VALUE, '-10'); + return tokenService.generateToken(userProfile); + } + + function givenAuthenticatedSequence() { + // bind user defined sequence + server.sequence(AuthenticationMiddlewareSequence); + } + + function givenProviders() { + registerAuthenticationStrategy(server, JWTAuthenticationStrategy); + + server + .bind(JWTAuthenticationStrategyBindings.TOKEN_SECRET) + .to(TOKEN_SECRET_VALUE); + + server + .bind(JWTAuthenticationStrategyBindings.TOKEN_EXPIRES_IN) + .to(TOKEN_EXPIRES_IN_VALUE); + + server + .bind(JWTAuthenticationStrategyBindings.TOKEN_SERVICE) + .toClass(JWTService); + + testUsers = getUserRepository(); + joeUser = testUsers.list['joe888']; + server.bind(USER_REPO).to(testUsers); + server + .bind(AuthenticationBindings.USER_PROFILE_FACTORY) + .to(myUserProfileFactory); + } + + function whenIMakeRequestTo(restServer: RestServer): Client { + return createClientForHandler(restServer.requestHandler); + } +}); diff --git a/packages/authentication/src/__tests__/fixtures/sequences/authentication.middleware.sequence.ts b/packages/authentication/src/__tests__/fixtures/sequences/authentication.middleware.sequence.ts new file mode 100644 index 000000000000..ba7b9b99d21e --- /dev/null +++ b/packages/authentication/src/__tests__/fixtures/sequences/authentication.middleware.sequence.ts @@ -0,0 +1,8 @@ +// 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 {MiddlewareSequence} from '@loopback/rest'; + +export class AuthenticationMiddlewareSequence extends MiddlewareSequence {} diff --git a/packages/authentication/src/authentication.component.ts b/packages/authentication/src/authentication.component.ts index df158aaf35bd..f3638c17e987 100644 --- a/packages/authentication/src/authentication.component.ts +++ b/packages/authentication/src/authentication.component.ts @@ -3,23 +3,22 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {bind, Component, ContextTags, ProviderMap} from '@loopback/core'; +import {bind, Component, ContextTags} from '@loopback/core'; import {AuthenticationBindings} from './keys'; import { AuthenticateActionProvider, + AuthenticationMiddlewareProvider, AuthenticationStrategyProvider, AuthMetadataProvider, } from './providers'; @bind({tags: {[ContextTags.KEY]: AuthenticationBindings.COMPONENT}}) export class AuthenticationComponent implements Component { - providers?: ProviderMap; - - constructor() { - this.providers = { - [AuthenticationBindings.AUTH_ACTION.key]: AuthenticateActionProvider, - [AuthenticationBindings.STRATEGY.key]: AuthenticationStrategyProvider, - [AuthenticationBindings.METADATA.key]: AuthMetadataProvider, - }; - } + providers = { + [AuthenticationBindings.AUTH_ACTION.key]: AuthenticateActionProvider, + [AuthenticationBindings.STRATEGY.key]: AuthenticationStrategyProvider, + [AuthenticationBindings.METADATA.key]: AuthMetadataProvider, + [AuthenticationBindings.AUTHENTICATION_MIDDLEWARE + .key]: AuthenticationMiddlewareProvider, + }; } diff --git a/packages/authentication/src/keys.ts b/packages/authentication/src/keys.ts index 28f1bb7e55fa..615e0dfdb608 100644 --- a/packages/authentication/src/keys.ts +++ b/packages/authentication/src/keys.ts @@ -5,6 +5,7 @@ import {BindingKey, MetadataAccessor} from '@loopback/core'; import {SecurityBindings, UserProfile} from '@loopback/security'; +import {Middleware} from '@loopback/rest'; import {AuthenticationComponent} from './authentication.component'; import { AuthenticateFn, @@ -88,6 +89,13 @@ export namespace AuthenticationBindings { 'authentication.actions.authenticate', ); + /** + * Binding key for AUTHENTICATION_MIDDLEWARE + */ + export const AUTHENTICATION_MIDDLEWARE = BindingKey.create( + 'middleware.authentication', + ); + /** * Key used to inject authentication metadata, which is used to determine * whether a request requires authentication or not. diff --git a/packages/authentication/src/providers/auth-action.provider.ts b/packages/authentication/src/providers/auth-action.provider.ts index 40e54f0fbbfe..c004bd431fb8 100644 --- a/packages/authentication/src/providers/auth-action.provider.ts +++ b/packages/authentication/src/providers/auth-action.provider.ts @@ -3,13 +3,20 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {Getter, inject, Provider, Setter} from '@loopback/core'; -import {Request, RedirectRoute} from '@loopback/rest'; +import {bind, Getter, inject, Provider, Setter} from '@loopback/core'; +import { + asMiddleware, + Middleware, + RedirectRoute, + Request, + RestTags, +} from '@loopback/rest'; import {SecurityBindings, UserProfile} from '@loopback/security'; import {AuthenticationBindings} from '../keys'; import { AuthenticateFn, AuthenticationStrategy, + AUTHENTICATION_STRATEGY_NOT_FOUND, USER_PROFILE_NOT_FOUND, } from '../types'; /** @@ -80,3 +87,34 @@ export class AuthenticateActionProvider implements Provider { } } } + +@bind( + asMiddleware({ + chain: RestTags.REST_MIDDLEWARE_CHAIN, + group: 'authentication', + upstreamGroups: ['cors', 'findRoute'], + }), +) +export class AuthenticationMiddlewareProvider implements Provider { + constructor( + @inject(AuthenticationBindings.AUTH_ACTION) + private authenticate: AuthenticateFn, + ) {} + + value(): Middleware { + return async (ctx, next) => { + try { + await this.authenticate(ctx.request); + } catch (error) { + if ( + error.code === AUTHENTICATION_STRATEGY_NOT_FOUND || + error.code === USER_PROFILE_NOT_FOUND + ) { + error.statusCode = 401; + } + throw error; + } + return next(); + }; + } +} diff --git a/packages/rest/src/sequence.ts b/packages/rest/src/sequence.ts index 2565b25a0158..d9a0554cac7e 100644 --- a/packages/rest/src/sequence.ts +++ b/packages/rest/src/sequence.ts @@ -141,11 +141,12 @@ export class MiddlewareSequence implements SequenceHandler { // default 'middleware', + // rest + 'findRoute', + // auth 'authentication', - // rest - 'findRoute', 'parseParams', 'invokeMethod', ],