Skip to content

Commit

Permalink
feat: Add AuthenticationBaseStrategy and make authentication option h…
Browse files Browse the repository at this point in the history
…andling more explicit (#1284)
  • Loading branch information
daffl committed Apr 11, 2019
1 parent 9dd6713 commit 2667d92
Show file tree
Hide file tree
Showing 18 changed files with 123 additions and 140 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"scripts": {
"install": "lerna bootstrap",
"publish": "lerna publish",
"lint": "tslint 'packages/**/*.js' 'packages/**/*.ts' -c tslint.json",
"lint": "tslint 'packages/**/*.js' 'packages/**/*.ts' -c tslint.json --fix",
"test": "npm run lint && nyc lerna run test",
"test:client": "grunt"
},
Expand Down
4 changes: 3 additions & 1 deletion packages/authentication-client/test/integration/fixture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ export default (app: Application) => {
const authentication = new AuthenticationService(app);

app.set('authentication', {
entity: 'user',
service: 'users',
secret: 'supersecret',
strategies: [ 'local', 'jwt' ],
jwtStrategies: [ 'local', 'jwt' ],
local: {
usernameField: 'email',
passwordField: 'password'
Expand Down
33 changes: 10 additions & 23 deletions packages/authentication-local/src/strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,14 @@ import bcrypt from 'bcryptjs';
import { get, omit } from 'lodash';
import Debug from 'debug';
import { NotAuthenticated } from '@feathersjs/errors';
import { AuthenticationStrategy, AuthenticationBase, AuthenticationRequest } from '@feathersjs/authentication';
import { Application, Query, Params } from '@feathersjs/feathers';
import { Query, Params } from '@feathersjs/feathers';
import {
AuthenticationRequest, AuthenticationBaseStrategy
} from '@feathersjs/authentication';

const debug = Debug('@feathersjs/authentication-local/strategy');

export class LocalStrategy implements AuthenticationStrategy {
authentication?: AuthenticationBase;
app?: Application;
name?: string;

setAuthentication (auth: AuthenticationBase) {
this.authentication = auth;
}

setApplication (app: Application) {
this.app = app;
}

setName (name: string) {
this.name = name;
}

export class LocalStrategy extends AuthenticationBaseStrategy {
verifyConfiguration () {
const config = this.configuration;

Expand All @@ -36,16 +22,17 @@ export class LocalStrategy implements AuthenticationStrategy {

get configuration () {
const authConfig = this.authentication.configuration;
const config = authConfig[this.name] || {};
const config = super.configuration || {};

return Object.assign({}, {
return {
hashSize: 10,
service: authConfig.service,
entity: authConfig.entity,
errorMessage: 'Invalid login',
entityPasswordField: config.passwordField,
entityUsernameField: config.usernameField
}, config);
entityUsernameField: config.usernameField,
...config
};
}

getEntityQuery (query: Query, _params: Params) {
Expand Down
4 changes: 3 additions & 1 deletion packages/authentication-local/test/fixture.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ module.exports = (app = feathers()) => {
const authentication = new AuthenticationService(app);

app.set('authentication', {
entity: 'user',
service: 'users',
secret: 'supersecret',
strategies: [ 'local', 'jwt' ],
jwtStrategies: [ 'local', 'jwt' ],
local: {
usernameField: 'email',
passwordField: 'password'
Expand Down
2 changes: 1 addition & 1 deletion packages/authentication/src/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export interface AuthenticationStrategy {
* @param authentication The authentication request
* @param params The service call parameters
*/
authenticate (authentication: AuthenticationRequest, params: Params): Promise<AuthenticationResult>;
authenticate? (authentication: AuthenticationRequest, params: Params): Promise<AuthenticationResult>;
/**
* Parse a basic HTTP request and response for authentication request information.
* @param req The HTTP request
Expand Down
1 change: 1 addition & 0 deletions packages/authentication/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@ export {
AuthenticationResult,
AuthenticationStrategy
} from './core';
export { AuthenticationBaseStrategy } from './strategy';
export { AuthenticationService } from './service';
export { JWTStrategy } from './jwt';
32 changes: 8 additions & 24 deletions packages/authentication/src/jwt.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,15 @@
import { NotAuthenticated } from '@feathersjs/errors';
import { AuthenticationStrategy, AuthenticationBase, AuthenticationRequest } from './core';
import { Application, Params } from '@feathersjs/feathers';
import { AuthenticationRequest } from './core';
import { Params } from '@feathersjs/feathers';
import { IncomingMessage } from 'http';
import { AuthenticationBaseStrategy } from './strategy';

const SPLIT_HEADER = /(\S+)\s+(\S+)/;

export class JWTStrategy implements AuthenticationStrategy {
authentication?: AuthenticationBase;
app?: Application;
name?: string;

setAuthentication (auth: AuthenticationBase): void {
this.authentication = auth;
}

setApplication (app: Application) {
this.app = app;
}

setName (name: string) {
this.name = name;
}

export class JWTStrategy extends AuthenticationBaseStrategy {
get configuration () {
const authConfig = this.authentication.configuration;
const config = authConfig[this.name];
const config = super.configuration;

return {
...config,
Expand All @@ -51,11 +36,10 @@ export class JWTStrategy implements AuthenticationStrategy {
* @param params Service call parameters
*/
async getEntity (id: string, params: Params) {
const { service } = this.configuration;
const entityService = this.app.service(service);
const entityService = this.entityService;

if (!entityService) {
throw new NotAuthenticated(`Could not find entity service '${service}'`);
if (entityService === null) {
throw new NotAuthenticated(`Could not find entity service`);
}

// @ts-ignore
Expand Down
5 changes: 2 additions & 3 deletions packages/authentication/src/options.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
export default {
entity: 'user',
service: 'users',
strategies: [],
httpStrategies: [ 'jwt' ],
jwtStrategies: [],
jwtOptions: {
header: { typ: 'access' }, // by default is an access token but can be any type
audience: 'https://yourdomain.com', // The resource server where the token is processed
Expand Down
22 changes: 11 additions & 11 deletions packages/authentication/src/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,13 @@ export class AuthenticationService extends AuthenticationBase implements Service
* @param params Service call parameters
*/
async create (data: AuthenticationRequest, params?: Params) {
const { strategies } = this.configuration;
const { jwtStrategies } = this.configuration;

if (!strategies.length) {
throw new NotAuthenticated('No authentication strategies allowed for creating a JWT');
if (!jwtStrategies.length) {
throw new NotAuthenticated('No authentication strategies allowed for creating a JWT (`jwtStrategies`)');
}

const authResult = await this.authenticate(data, params, ...strategies);
const authResult = await this.authenticate(data, params, ...jwtStrategies);

debug('Got authentication result', authResult);

Expand Down Expand Up @@ -88,7 +88,7 @@ export class AuthenticationService extends AuthenticationBase implements Service
*/
async remove (id: null|string, params?: Params) {
const { authentication } = params;
const { strategies } = this.configuration;
const { jwtStrategies } = this.configuration;

// When an id is passed it is expected to be the authentication `accessToken`
if (id !== null && id !== authentication.accessToken) {
Expand All @@ -97,7 +97,7 @@ export class AuthenticationService extends AuthenticationBase implements Service

debug('Verifying authentication strategy in remove');

return this.authenticate(authentication, params, ...strategies);
return this.authenticate(authentication, params, ...jwtStrategies);
}

/**
Expand All @@ -106,13 +106,17 @@ export class AuthenticationService extends AuthenticationBase implements Service
setup () {
// The setup method checks for valid settings and registers the
// connection and event (login, logout) hooks
const { secret, service, entity, entityId, strategies } = this.configuration;
const { secret, service, entity, entityId } = this.configuration;

if (typeof secret !== 'string') {
throw new Error(`A 'secret' must be provided in your authentication configuration`);
}

if (entity !== null) {
if (service === undefined) {
throw new Error(`The 'service' options is not set in the authentication configuration`);
}

if (this.app.service(service) === undefined) {
throw new Error(`The '${service}' entity service does not exist (set to 'null' if it is not required)`);
}
Expand All @@ -122,10 +126,6 @@ export class AuthenticationService extends AuthenticationBase implements Service
}
}

if (strategies.length === 0) {
throw new Error(`At least one valid authentication strategy required in '${this.configKey}.strategies'`);
}

// @ts-ignore
this.hooks({ after: [ connection(), events() ] });
}
Expand Down
34 changes: 34 additions & 0 deletions packages/authentication/src/strategy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { AuthenticationStrategy, AuthenticationBase } from './core';
import { Application, Service } from '@feathersjs/feathers';

export class AuthenticationBaseStrategy implements AuthenticationStrategy {
authentication?: AuthenticationBase;
app?: Application;
name?: string;

setAuthentication (auth: AuthenticationBase) {
this.authentication = auth;
}

setApplication (app: Application) {
this.app = app;
}

setName (name: string) {
this.name = name;
}

get configuration () {
return this.authentication.configuration[this.name];
}

get entityService (): Service<any> {
const { service } = this.configuration;

if (!service) {
return null;
}

return this.app.service(service) || null;
}
}
11 changes: 10 additions & 1 deletion packages/authentication/test/core.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ describe('authentication/core', () => {
beforeEach(() => {
app = feathers();
auth = new AuthenticationBase(app, 'authentication', {
secret: 'supersecret'
entity: 'user',
service: 'users',
secret: 'supersecret',
first: { hello: 'test' }
});

auth.register('first', new Strategy1());
Expand Down Expand Up @@ -95,6 +98,12 @@ describe('authentication/core', () => {
assert.strictEqual(first.app, app);
assert.strictEqual(first.authentication, auth);
});

it('strategy configuration getter', () => {
const [ first ] = auth.getStrategies('first') as [ Strategy1 ];

assert.deepStrictEqual(first.configuration, { hello: 'test' });
});
});

describe('authenticate', () => {
Expand Down
26 changes: 5 additions & 21 deletions packages/authentication/test/fixtures.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,23 @@
import { NotAuthenticated } from '@feathersjs/errors';
import { Application, Params } from '@feathersjs/feathers';
import { Params } from '@feathersjs/feathers';

import { AuthenticationStrategy, AuthenticationRequest } from '../src/core';
import { AuthenticationService } from '../src/service';
import { AuthenticationRequest } from '../src/core';
import { IncomingMessage } from 'http';
import { AuthenticationBaseStrategy } from '../src/strategy';

export interface MockRequest extends IncomingMessage {
isDave?: boolean;
isV2?: boolean;
}

export class Strategy1 implements AuthenticationStrategy {
export class Strategy1 extends AuthenticationBaseStrategy {
static result = {
user: {
id: 123,
name: 'Dave'
}
};

name?: string;
app?: Application;
authentication?: AuthenticationService;

setName (name: string) {
this.name = name;
}

setApplication (app: Application) {
this.app = app;
}

setAuthentication (authentication: AuthenticationService) {
this.authentication = authentication;
}

async authenticate (authentication: AuthenticationRequest) {
if (authentication.username === 'David' || authentication.both) {
return Strategy1.result;
Expand All @@ -51,7 +35,7 @@ export class Strategy1 implements AuthenticationStrategy {
}
}

export class Strategy2 implements AuthenticationStrategy {
export class Strategy2 extends AuthenticationBaseStrategy {
static result = {
user: {
name: 'V2',
Expand Down
8 changes: 6 additions & 2 deletions packages/authentication/test/hooks/authenticate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,16 @@ describe('authentication/hooks/authenticate', () => {
beforeEach(() => {
app = feathers();
app.use('/authentication', new AuthenticationService(app, 'authentication', {
entity: 'user',
service: 'users',
secret: 'supersecret',
strategies: [ 'first' ]
jwtStrategies: [ 'first' ]
}));
app.use('/auth-v2', new AuthenticationService(app, 'auth-v2', {
entity: 'user',
service: 'users',
secret: 'supersecret',
strategies: [ 'test' ]
jwtStrategies: [ 'test' ]
}));
app.use('/users', {
async find () {
Expand Down
6 changes: 4 additions & 2 deletions packages/authentication/test/jwt.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,10 @@ describe('authentication/jwt', () => {
app = feathers();

const authService = new AuthenticationService(app, 'authentication', {
entity: 'user',
service: 'users',
secret: 'supersecret',
strategies: [ 'jwt' ]
jwtStrategies: [ 'jwt' ]
});

authService.register('jwt', new JWTStrategy());
Expand Down Expand Up @@ -101,7 +103,7 @@ describe('authentication/jwt', () => {
assert.fail('Should never get here');
} catch (error) {
assert.strictEqual(error.name, 'NotAuthenticated');
assert.strictEqual(error.message, `Could not find entity service 'users'`);
assert.strictEqual(error.message, `Could not find entity service`);
}
});

Expand Down
Loading

0 comments on commit 2667d92

Please sign in to comment.