Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(authentication): add support for multiple strategies on same method #5735

Merged
merged 2 commits into from
Aug 18, 2020
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
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,49 +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('adds authenticate metadata to target class', () => {
@authenticate('my-strategy', {option1: 'value1', option2: 'value2'})
it('can add authenticate metadata to target method with multiple strategies', () => {
class TestClass {
@authenticate('my-strategy', 'my-strategy2')
whoAmI() {}
}

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

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 @@ -96,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
@@ -0,0 +1,13 @@
import {AuthenticationMetadata} from '../../..';

const options = {option1: 'value1', option2: 'value2'};

export const mockAuthenticationMetadata: AuthenticationMetadata = {
strategy: 'MockStrategy',
options,
};

export const mockAuthenticationMetadata2: AuthenticationMetadata = {
strategy: 'MockStrategy2',
options,
};
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
// License text available at https://opensource.org/licenses/MIT

import {Request} from '@loopback/rest';
import {UserProfile} from '@loopback/security';
import {securityId, UserProfile} from '@loopback/security';
import {AuthenticationStrategy} from '../../../types';

class AuthenticationError extends Error {
Expand All @@ -15,7 +15,7 @@ class AuthenticationError extends Error {
* Test fixture for a mock asynchronous authentication strategy
*/
export class MockStrategy implements AuthenticationStrategy {
name: 'MockStrategy';
name = 'MockStrategy';
// user to return for successful authentication
private mockUser: UserProfile;

Expand Down Expand Up @@ -51,3 +51,14 @@ export class MockStrategy implements AuthenticationStrategy {
return this.returnMockUser();
}
}

export class MockStrategy2 implements AuthenticationStrategy {
name = 'MockStrategy2';

async authenticate(request: Request): Promise<UserProfile | undefined> {
if (request.headers?.testState2 === 'fail') {
throw new AuthenticationError();
}
return {[securityId]: 'mock-id'};
}
}
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
Loading