From 5535135ddb55cff455d8bc20713f9fae240aa028 Mon Sep 17 00:00:00 2001 From: Douglas McConnachie Date: Tue, 10 Mar 2020 16:27:31 +0000 Subject: [PATCH] feat: enable authStrategy to provide OASEnhancer add asAuthStrategy decorator for AuthenticationStrategy. Decorate and extend AuthenticationStrategy as follows: ```ts @bind(asAuthStrategy, asSpecEnhancer) class MyAuthenticationStrategy implements AuthenticationStrategy, OASEnhancer { // ... } ``` Bind to application: ```ts this.add(createBindingFromClass(MyAuthenticationStrategy)); ``` Signed-off-by: Douglas McConnachie --- .../basic-auth-extension.acceptance.ts | 21 ++++++++++- .../jwt-auth-extension.acceptance.ts | 18 ++++++++- .../fixtures/strategies/basic-strategy.ts | 23 ++++++++++-- .../fixtures/strategies/jwt-strategy.ts | 24 ++++++++++-- .../register-authentication-strategy.unit.ts | 37 +++++++++++++++++-- packages/authentication/src/types.ts | 24 +++++++++++- .../unit/merge-security-scheme.unit.ts | 29 +++++++++++++++ .../src/enhancers/spec-enhancer.service.ts | 28 +++++++++++++- 8 files changed, 186 insertions(+), 18 deletions(-) create mode 100644 packages/openapi-v3/src/__tests__/unit/merge-security-scheme.unit.ts diff --git a/packages/authentication/src/__tests__/acceptance/basic-auth-extension.acceptance.ts b/packages/authentication/src/__tests__/acceptance/basic-auth-extension.acceptance.ts index e110c110358f..730c4e8f67dc 100644 --- a/packages/authentication/src/__tests__/acceptance/basic-auth-extension.acceptance.ts +++ b/packages/authentication/src/__tests__/acceptance/basic-auth-extension.acceptance.ts @@ -1,4 +1,4 @@ -// Copyright IBM Corp. 2019. All Rights Reserved. +// 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 @@ -9,7 +9,7 @@ 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} from '@loopback/testlab'; +import {Client, createClientForHandler, expect} from '@loopback/testlab'; import { authenticate, AuthenticationBindings, @@ -164,6 +164,7 @@ describe('Basic Authentication', () => { }, }); }); + it('returns error when undefined user profile returned from authentication strategy', async () => { class BadBasicStrategy implements AuthenticationStrategy { name = 'badbasic'; @@ -193,6 +194,22 @@ describe('Basic Authentication', () => { }, }); }); + + 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); diff --git a/packages/authentication/src/__tests__/acceptance/jwt-auth-extension.acceptance.ts b/packages/authentication/src/__tests__/acceptance/jwt-auth-extension.acceptance.ts index 0b660a40b27a..c73bd24c54fb 100644 --- a/packages/authentication/src/__tests__/acceptance/jwt-auth-extension.acceptance.ts +++ b/packages/authentication/src/__tests__/acceptance/jwt-auth-extension.acceptance.ts @@ -1,4 +1,4 @@ -// Copyright IBM Corp. 2019. All Rights Reserved. +// 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 @@ -454,6 +454,22 @@ describe('JWT Authentication', () => { }); }); + 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); diff --git a/packages/authentication/src/__tests__/fixtures/strategies/basic-strategy.ts b/packages/authentication/src/__tests__/fixtures/strategies/basic-strategy.ts index e52287680b9c..63ce2bac2a1c 100644 --- a/packages/authentication/src/__tests__/fixtures/strategies/basic-strategy.ts +++ b/packages/authentication/src/__tests__/fixtures/strategies/basic-strategy.ts @@ -1,12 +1,18 @@ -// Copyright IBM Corp. 2019. All Rights Reserved. +// 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 {inject} from '@loopback/context'; +import {bind, inject} from '@loopback/context'; +import { + asSpecEnhancer, + mergeSecuritySchemeToSpec, + OASEnhancer, + OpenApiSpec, +} from '@loopback/openapi-v3'; import {HttpErrors, Request} from '@loopback/rest'; import {UserProfile} from '@loopback/security'; -import {AuthenticationStrategy} from '../../../types'; +import {asAuthStrategy, AuthenticationStrategy} from '../../../types'; import {BasicAuthenticationStrategyBindings} from '../keys'; import {BasicAuthenticationUserService} from '../services/basic-auth-user-service'; @@ -15,7 +21,9 @@ export interface BasicAuthenticationStrategyCredentials { password: string; } -export class BasicAuthenticationStrategy implements AuthenticationStrategy { +@bind(asAuthStrategy, asSpecEnhancer) +export class BasicAuthenticationStrategy + implements AuthenticationStrategy, OASEnhancer { name = 'basic'; constructor( @@ -77,4 +85,11 @@ export class BasicAuthenticationStrategy implements AuthenticationStrategy { return creds; } + + modifySpec(spec: OpenApiSpec): OpenApiSpec { + return mergeSecuritySchemeToSpec(spec, this.name, { + type: 'http', + scheme: 'basic', + }); + } } diff --git a/packages/authentication/src/__tests__/fixtures/strategies/jwt-strategy.ts b/packages/authentication/src/__tests__/fixtures/strategies/jwt-strategy.ts index 6e56fe91a3ff..5b0db60adfde 100644 --- a/packages/authentication/src/__tests__/fixtures/strategies/jwt-strategy.ts +++ b/packages/authentication/src/__tests__/fixtures/strategies/jwt-strategy.ts @@ -1,16 +1,24 @@ -// Copyright IBM Corp. 2019. All Rights Reserved. +// 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 {inject} from '@loopback/context'; +import {bind, inject} from '@loopback/context'; +import { + asSpecEnhancer, + mergeSecuritySchemeToSpec, + OASEnhancer, + OpenApiSpec, +} from '@loopback/openapi-v3'; import {HttpErrors, Request} from '@loopback/rest'; import {UserProfile} from '@loopback/security'; -import {AuthenticationStrategy} from '../../../types'; +import {asAuthStrategy, AuthenticationStrategy} from '../../../types'; import {JWTAuthenticationStrategyBindings} from '../keys'; import {JWTService} from '../services/jwt-service'; -export class JWTAuthenticationStrategy implements AuthenticationStrategy { +@bind(asAuthStrategy, asSpecEnhancer) +export class JWTAuthenticationStrategy + implements AuthenticationStrategy, OASEnhancer { name = 'jwt'; constructor( @@ -48,4 +56,12 @@ export class JWTAuthenticationStrategy implements AuthenticationStrategy { return token; } + + modifySpec(spec: OpenApiSpec): OpenApiSpec { + return mergeSecuritySchemeToSpec(spec, this.name, { + type: 'http', + scheme: 'bearer', + bearerFormat: 'JWT', + }); + } } diff --git a/packages/authentication/src/__tests__/unit/types/register-authentication-strategy.unit.ts b/packages/authentication/src/__tests__/unit/types/register-authentication-strategy.unit.ts index 3bae54c19097..3f5a2953763c 100644 --- a/packages/authentication/src/__tests__/unit/types/register-authentication-strategy.unit.ts +++ b/packages/authentication/src/__tests__/unit/types/register-authentication-strategy.unit.ts @@ -3,11 +3,18 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {Context} from '@loopback/context'; +import {bind, Context, createBindingFromClass} from '@loopback/context'; +import { + asSpecEnhancer, + OASEnhancer, + OAS_ENHANCER_EXTENSION_POINT_NAME, + OpenApiSpec, +} from '@loopback/openapi-v3'; import {Request} from '@loopback/rest'; import {securityId, UserProfile} from '@loopback/security'; import {expect} from '@loopback/testlab'; import { + asAuthStrategy, AuthenticationBindings, AuthenticationStrategy, registerAuthenticationStrategy, @@ -24,21 +31,45 @@ describe('registerAuthenticationStrategy', () => { MyAuthenticationStrategy, ); expect(binding.tagMap).to.containEql({ - extensionFor: + extensionFor: [ AuthenticationBindings.AUTHENTICATION_STRATEGY_EXTENSION_POINT_NAME, + OAS_ENHANCER_EXTENSION_POINT_NAME, + ], }); expect(binding.key).to.eql( `${AuthenticationBindings.AUTHENTICATION_STRATEGY_EXTENSION_POINT_NAME}.MyAuthenticationStrategy`, ); }); - class MyAuthenticationStrategy implements AuthenticationStrategy { + it('adds a binding for the strategy and security spec', () => { + const binding = createBindingFromClass(MyAuthenticationStrategy); + expect(binding.tagMap).to.containEql({ + extensionFor: [ + AuthenticationBindings.AUTHENTICATION_STRATEGY_EXTENSION_POINT_NAME, + OAS_ENHANCER_EXTENSION_POINT_NAME, + ], + }); + expect(binding.key).to.eql( + `${OAS_ENHANCER_EXTENSION_POINT_NAME}.MyAuthenticationStrategy`, + ); + }); + + @bind(asAuthStrategy, asSpecEnhancer) + class MyAuthenticationStrategy + implements AuthenticationStrategy, OASEnhancer { name: 'my-auth'; async authenticate(request: Request): Promise { return { [securityId]: 'somebody', }; } + modifySpec(spec: OpenApiSpec): OpenApiSpec { + return { + openapi: '3.0.0', + info: {title: 'Test', version: '1.0.0'}, + paths: {}, + }; + } } function givenContext() { diff --git a/packages/authentication/src/types.ts b/packages/authentication/src/types.ts index d1562947c19a..c810b0644908 100644 --- a/packages/authentication/src/types.ts +++ b/packages/authentication/src/types.ts @@ -1,9 +1,15 @@ -// Copyright IBM Corp. 2018,2019. All Rights Reserved. +// Copyright IBM Corp. 2018,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 {addExtension, Constructor, Context} from '@loopback/core'; +import { + addExtension, + BindingTemplate, + Constructor, + Context, + extensionFor, +} from '@loopback/core'; import {Request} from '@loopback/rest'; import {UserProfile} from '@loopback/security'; import {AuthenticationBindings} from './keys'; @@ -93,6 +99,7 @@ export const USER_PROFILE_NOT_FOUND = 'USER_PROFILE_NOT_FOUND'; * Registers an authentication strategy as an extension of the * AuthenticationBindings.AUTHENTICATION_STRATEGY_EXTENSION_POINT_NAME extension * point. + * * @param context - Context object * @param strategyClass - Class for the authentication strategy */ @@ -110,3 +117,16 @@ export function registerAuthenticationStrategy( }, ); } + +/** + * A binding template for auth strategy contributor extensions + */ +export const asAuthStrategy: BindingTemplate = binding => { + extensionFor( + AuthenticationBindings.AUTHENTICATION_STRATEGY_EXTENSION_POINT_NAME, + )(binding); + binding.tag({ + namespace: + AuthenticationBindings.AUTHENTICATION_STRATEGY_EXTENSION_POINT_NAME, + }); +}; diff --git a/packages/openapi-v3/src/__tests__/unit/merge-security-scheme.unit.ts b/packages/openapi-v3/src/__tests__/unit/merge-security-scheme.unit.ts new file mode 100644 index 000000000000..f30458c4f6b7 --- /dev/null +++ b/packages/openapi-v3/src/__tests__/unit/merge-security-scheme.unit.ts @@ -0,0 +1,29 @@ +// Copyright IBM Corp. 2020. All Rights Reserved. +// Node module: @loopback/openapi-v3 +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {expect} from '@loopback/testlab'; +import {mergeSecuritySchemeToSpec} from '../..'; +import {createEmptyApiSpec, SecuritySchemeObject} from '../../types'; + +describe('mergeSecuritySchemeToSpec', () => { + it('adds security scheme to spec', () => { + const spec = createEmptyApiSpec(); + const schemeName = 'basic'; + const schemeSpec: SecuritySchemeObject = { + type: 'http', + scheme: 'basic', + }; + + const newSpec = mergeSecuritySchemeToSpec(spec, schemeName, schemeSpec); + expect(newSpec.components).to.deepEqual({ + securitySchemes: { + basic: { + type: 'http', + scheme: 'basic', + }, + }, + }); + }); +}); diff --git a/packages/openapi-v3/src/enhancers/spec-enhancer.service.ts b/packages/openapi-v3/src/enhancers/spec-enhancer.service.ts index 4a5a09e213f1..90912a1e8f6c 100644 --- a/packages/openapi-v3/src/enhancers/spec-enhancer.service.ts +++ b/packages/openapi-v3/src/enhancers/spec-enhancer.service.ts @@ -1,4 +1,4 @@ -// Copyright IBM Corp. 2019. All Rights Reserved. +// Copyright IBM Corp. 2019,2020. All Rights Reserved. // Node module: @loopback/openapi-v3 // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT @@ -7,7 +7,7 @@ import {config, extensionPoint, extensions, Getter} from '@loopback/core'; import debugModule from 'debug'; import * as _ from 'lodash'; import {inspect} from 'util'; -import {OpenApiSpec} from '../types'; +import {OpenApiSpec, SecuritySchemeObject} from '../types'; import {OASEnhancer, OAS_ENHANCER_EXTENSION_POINT_NAME} from './types'; const jsonmergepatch = require('json-merge-patch'); @@ -119,3 +119,27 @@ export function mergeOpenAPISpec( const mergedSpec = jsonmergepatch.merge(currentSpec, patchSpec); return mergedSpec; } + +/** + * Security scheme merge helper function to patch the current OpenAPI spec. + * It provides a direct route to add a security schema to the specs components. + * It returns a new merged object without modifying the original one. + * + * @param currentSpec The original spec + * @param schemeName The name of the security scheme to be added + * @param schemeSpec The security scheme spec body to be added, + */ +export function mergeSecuritySchemeToSpec( + spec: OpenApiSpec, + schemeName: string, + schemeSpec: SecuritySchemeObject, +): OpenApiSpec { + const patchSpec = { + components: { + securitySchemes: {[schemeName]: schemeSpec}, + }, + }; + + const mergedSpec = mergeOpenAPISpec(spec, patchSpec); + return mergedSpec; +}