Skip to content

Commit

Permalink
feat(authentication): update signature of authenticate decorator
Browse files Browse the repository at this point in the history
It is now possible to provide multiple strategy names and/or metadata objects

BREAKING CHANGE: The `@authenticate` signature changed, options are no longer
a separate input parameter but instead have to be provided in the metadata object.
The metadata value is now `AuthenticationMetadata[]`.

Signed-off-by: nflaig <[email protected]>
  • Loading branch information
nflaig committed Jul 19, 2020
1 parent 4d9207a commit 657b89d
Show file tree
Hide file tree
Showing 11 changed files with 210 additions and 120 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,15 @@ import {authenticate, getAuthenticateMetadata} from '../../..';

describe('Authentication', () => {
describe('@authenticate decorator', () => {
it('can add authenticate metadata to target method with options', () => {
it('can add authenticate metadata to target method', () => {
class TestClass {
@authenticate('my-strategy', {option1: 'value1', option2: 'value2'})
@authenticate('my-strategy')
whoAmI() {}
}

const metaData = getAuthenticateMetadata(TestClass, 'whoAmI');
expect(metaData).to.eql({
expect(metaData?.[0]).to.eql({
strategy: 'my-strategy',
options: {option1: 'value1', option2: 'value2'},
});
});

Expand All @@ -31,7 +30,7 @@ describe('Authentication', () => {
}

const metaData = getAuthenticateMetadata(TestClass, 'whoAmI');
expect(metaData).to.eql({
expect(metaData?.[0]).to.eql({
strategy: 'my-strategy',
options: {option1: 'value1', option2: 'value2'},
});
Expand All @@ -44,62 +43,97 @@ describe('Authentication', () => {
}

const metaData = getAuthenticateMetadata(TestClass, 'whoAmI');
expect(metaData).to.eql({strategy: 'my-strategy', options: {}});
expect(metaData?.[0]).to.eql({
strategy: 'my-strategy',
});
});

it('can add authenticate metadata to target method with strategies as array', () => {
it('can add authenticate metadata to target method with multiple strategies', () => {
class TestClass {
@authenticate(['my-strategy', 'my-strategy2'])
@authenticate('my-strategy', 'my-strategy2')
whoAmI() {}
}

const metaData = getAuthenticateMetadata(TestClass, 'whoAmI');
expect(metaData).to.eql({
strategy: ['my-strategy', 'my-strategy2'],
options: {},
expect(metaData?.[0]).to.eql({
strategy: 'my-strategy',
});
expect(metaData?.[1]).to.eql({
strategy: 'my-strategy2',
});
});

it('adds authenticate metadata to target class', () => {
@authenticate('my-strategy', {option1: 'value1', option2: 'value2'})
it('can add authenticate metadata to target method with multiple objects', () => {
class TestClass {
@authenticate(
{
strategy: 'my-strategy',
options: {option1: 'value1', option2: 'value2'},
},
{
strategy: 'my-strategy2',
options: {option1: 'value1', option2: 'value2'},
},
)
whoAmI() {}
}

const metaData = getAuthenticateMetadata(TestClass, 'whoAmI');
expect(metaData).to.eql({
expect(metaData?.[0]).to.eql({
strategy: 'my-strategy',
options: {option1: 'value1', option2: 'value2'},
});
expect(metaData?.[1]).to.eql({
strategy: 'my-strategy2',
options: {option1: 'value1', option2: 'value2'},
});
});

it('adds authenticate metadata to target class', () => {
@authenticate('my-strategy')
class TestClass {
whoAmI() {}
}

const metaData = getAuthenticateMetadata(TestClass, 'whoAmI');
expect(metaData?.[0]).to.eql({
strategy: 'my-strategy',
});
});

it('overrides class level metadata by method level', () => {
@authenticate('my-strategy', {option1: 'value1', option2: 'value2'})
@authenticate({
strategy: 'my-strategy',
options: {option1: 'value1', option2: 'value2'},
})
class TestClass {
@authenticate('another-strategy', {
option1: 'valueA',
option2: 'value2',
@authenticate({
strategy: 'another-strategy',
options: {
option1: 'valueA',
option2: 'value2',
},
})
whoAmI() {}
}

const metaData = getAuthenticateMetadata(TestClass, 'whoAmI');
expect(metaData).to.eql({
expect(metaData?.[0]).to.eql({
strategy: 'another-strategy',
options: {option1: 'valueA', option2: 'value2'},
});
});
});

it('can skip authentication', () => {
@authenticate('my-strategy', {option1: 'value1', option2: 'value2'})
@authenticate('my-strategy')
class TestClass {
@authenticate.skip()
whoAmI() {}
}

const metaData = getAuthenticateMetadata(TestClass, 'whoAmI');
expect(metaData).to.containEql({skip: true});
expect(metaData?.[0]).to.containEql({skip: true});
});

it('can skip authentication at class level', () => {
Expand All @@ -109,6 +143,6 @@ describe('Authentication', () => {
}

const metaData = getAuthenticateMetadata(TestClass, 'whoAmI');
expect(metaData).to.containEql({skip: true});
expect(metaData?.[0]).to.containEql({skip: true});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@ export const mockAuthenticationMetadata: AuthenticationMetadata = {
};

export const mockAuthenticationMetadata2: AuthenticationMetadata = {
strategy: ['MockStrategy', 'MockStrategy2'],
strategy: 'MockStrategy2',
options,
};
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,17 @@
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import {Context, Provider, CoreBindings} from '@loopback/core';
import {Context, CoreBindings, Provider} from '@loopback/core';
import {expect} from '@loopback/testlab';
import {authenticate, AuthenticationMetadata} from '../../..';
import {AuthenticationBindings} from '../../../keys';
import {AuthMetadataProvider} from '../../../providers';

describe('AuthMetadataProvider', () => {
let provider: Provider<AuthenticationMetadata | undefined>;
let provider: Provider<AuthenticationMetadata[] | undefined>;

class TestController {
@authenticate('my-strategy', {option1: 'value1', option2: 'value2'})
@authenticate('my-strategy')
whoAmI() {}

@authenticate.skip()
Expand All @@ -29,11 +29,10 @@ describe('AuthMetadataProvider', () => {
describe('value()', () => {
it('returns the auth metadata of a controller method', async () => {
const authMetadata:
| AuthenticationMetadata
| AuthenticationMetadata[]
| undefined = await provider.value();
expect(authMetadata).to.be.eql({
expect(authMetadata?.[0]).to.be.eql({
strategy: 'my-strategy',
options: {option1: 'value1', option2: 'value2'},
});
});

Expand All @@ -45,12 +44,11 @@ describe('AuthMetadataProvider', () => {
context
.bind(CoreBindings.CONTROLLER_METHOD_META)
.toProvider(AuthMetadataProvider);
const authMetadata = await context.get(
CoreBindings.CONTROLLER_METHOD_META,
);
expect(authMetadata).to.be.eql({
const authMetadata:
| AuthenticationMetadata[]
| undefined = await context.get(CoreBindings.CONTROLLER_METHOD_META);
expect(authMetadata?.[0]).to.be.eql({
strategy: 'my-strategy',
options: {option1: 'value1', option2: 'value2'},
});
});

Expand All @@ -61,10 +59,10 @@ describe('AuthMetadataProvider', () => {
context
.bind(CoreBindings.CONTROLLER_METHOD_META)
.toProvider(AuthMetadataProvider);
const authMetadata = await context.get(
CoreBindings.CONTROLLER_METHOD_META,
);
expect(authMetadata).to.be.undefined();
const authMetadata:
| AuthenticationMetadata[]
| undefined = await context.get(CoreBindings.CONTROLLER_METHOD_META);
expect(authMetadata?.[0]).to.be.undefined();
});

it('returns undefined for a method decorated with @authenticate.skip even with default metadata', async () => {
Expand All @@ -76,11 +74,11 @@ describe('AuthMetadataProvider', () => {
.toProvider(AuthMetadataProvider);
context
.configure(AuthenticationBindings.COMPONENT)
.to({defaultMetadata: {strategy: 'xyz'}});
const authMetadata = await context.get(
CoreBindings.CONTROLLER_METHOD_META,
);
expect(authMetadata).to.be.undefined();
.to({defaultMetadata: [{strategy: 'xyz'}]});
const authMetadata:
| AuthenticationMetadata[]
| undefined = await context.get(CoreBindings.CONTROLLER_METHOD_META);
expect(authMetadata?.[0]).to.be.undefined();
});

it('returns undefined if no auth metadata is defined', async () => {
Expand All @@ -92,10 +90,10 @@ describe('AuthMetadataProvider', () => {
context
.bind(CoreBindings.CONTROLLER_METHOD_META)
.toProvider(AuthMetadataProvider);
const authMetadata = await context.get(
CoreBindings.CONTROLLER_METHOD_META,
);
expect(authMetadata).to.be.undefined();
const authMetadata:
| AuthenticationMetadata[]
| undefined = await context.get(CoreBindings.CONTROLLER_METHOD_META);
expect(authMetadata?.[0]).to.be.undefined();
});

it('returns default metadata if no auth metadata is defined', async () => {
Expand All @@ -106,25 +104,25 @@ describe('AuthMetadataProvider', () => {
context.bind(CoreBindings.CONTROLLER_METHOD_NAME).to('whoAmI');
context
.configure(AuthenticationBindings.COMPONENT)
.to({defaultMetadata: {strategy: 'xyz'}});
.to({defaultMetadata: [{strategy: 'xyz'}]});
context
.bind(CoreBindings.CONTROLLER_METHOD_META)
.toProvider(AuthMetadataProvider);
const authMetadata = await context.get(
CoreBindings.CONTROLLER_METHOD_META,
);
expect(authMetadata).to.be.eql({strategy: 'xyz'});
const authMetadata:
| AuthenticationMetadata[]
| undefined = await context.get(CoreBindings.CONTROLLER_METHOD_META);
expect(authMetadata?.[0]).to.be.eql({strategy: 'xyz'});
});

it('returns undefined when the class or method is missing', async () => {
const context: Context = new Context();
context
.bind(CoreBindings.CONTROLLER_METHOD_META)
.toProvider(AuthMetadataProvider);
const authMetadata = await context.get(
CoreBindings.CONTROLLER_METHOD_META,
);
expect(authMetadata).to.be.undefined();
const authMetadata:
| AuthenticationMetadata[]
| undefined = await context.get(CoreBindings.CONTROLLER_METHOD_META);
expect(authMetadata?.[0]).to.be.undefined();
});
});
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
// Copyright IBM Corp. 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 {Context} from '@loopback/core';
import {expect} from '@loopback/testlab';
import {
Expand All @@ -20,7 +25,7 @@ describe('AuthStrategyProvider', () => {
beforeEach(() => {
givenAuthenticationStrategyProvider(
[mockStrategy, mockStrategy2],
mockAuthenticationMetadata2,
[mockAuthenticationMetadata, mockAuthenticationMetadata2],
);
});

Expand All @@ -29,20 +34,20 @@ describe('AuthStrategyProvider', () => {
const strategies = await strategyProvider.value();

expect(strategies).to.not.be.undefined();
expect(strategies![0]).to.be.equal(mockStrategy);
expect(strategies![1]).to.be.equal(mockStrategy2);
expect(strategies?.[0]).to.be.equal(mockStrategy);
expect(strategies?.[1]).to.be.equal(mockStrategy2);
});

it('should only return the authentication strategy specified in the authentication metadata', async () => {
givenAuthenticationStrategyProvider(
[mockStrategy, mockStrategy2],
mockAuthenticationMetadata,
[mockAuthenticationMetadata],
);

const strategies = await strategyProvider.value();

expect(strategies?.length).to.be.equal(1);
expect(strategies![0]).to.be.equal(mockStrategy);
expect(strategies?.[0]).to.be.equal(mockStrategy);
});

it('should return undefined if the authentication metadata is not available', async () => {
Expand All @@ -54,11 +59,11 @@ describe('AuthStrategyProvider', () => {
});

it('should throw an error if the authentication strategy is not available', async () => {
givenAuthenticationStrategyProvider([], mockAuthenticationMetadata);
givenAuthenticationStrategyProvider([], [mockAuthenticationMetadata]);

await expect(strategyProvider.value()).to.be.rejected();

givenAuthenticationStrategyProvider([], mockAuthenticationMetadata2);
givenAuthenticationStrategyProvider([], [mockAuthenticationMetadata2]);

await expect(strategyProvider.value()).to.be.rejected();
});
Expand Down Expand Up @@ -91,7 +96,7 @@ describe('AuthStrategyProvider', () => {

function givenAuthenticationStrategyProvider(
strategies: AuthenticationStrategy[],
metadata: AuthenticationMetadata | undefined,
metadata: AuthenticationMetadata[] | undefined,
) {
strategyProvider = new AuthenticationStrategyProvider(
() => Promise.resolve(strategies),
Expand Down
Loading

0 comments on commit 657b89d

Please sign in to comment.