Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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(
Expand Down Expand Up @@ -77,4 +85,11 @@ export class BasicAuthenticationStrategy implements AuthenticationStrategy {

return creds;
}

modifySpec(spec: OpenApiSpec): OpenApiSpec {
return mergeSecuritySchemeToSpec(spec, this.name, {
type: 'http',
scheme: 'basic',
});
}
}
Original file line number Diff line number Diff line change
@@ -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(
Expand Down Expand Up @@ -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',
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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<UserProfile | undefined> {
return {
[securityId]: 'somebody',
};
}
modifySpec(spec: OpenApiSpec): OpenApiSpec {
return {
openapi: '3.0.0',
info: {title: 'Test', version: '1.0.0'},
paths: {},
};
}
}

function givenContext() {
Expand Down
24 changes: 22 additions & 2 deletions packages/authentication/src/types.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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
*/
Expand All @@ -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,
});
};
Original file line number Diff line number Diff line change
@@ -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',
},
},
});
});
});
28 changes: 26 additions & 2 deletions packages/openapi-v3/src/enhancers/spec-enhancer.service.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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');

Expand Down Expand Up @@ -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;
}