Skip to content

Commit

Permalink
feat: Let strategies handle the connection (#1510)
Browse files Browse the repository at this point in the history
  • Loading branch information
daffl committed Aug 17, 2019
1 parent 64807e3 commit 4329feb
Show file tree
Hide file tree
Showing 10 changed files with 123 additions and 124 deletions.
2 changes: 1 addition & 1 deletion packages/authentication-client/test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ describe('@feathersjs/authentication-client', () => {
if (!app.get('authentication')) {
throw new Error('Not logged in');
}

return Promise.resolve({ id });
}
});
Expand Down
9 changes: 8 additions & 1 deletion packages/authentication/src/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import jsonwebtoken, { SignOptions, Secret, VerifyOptions } from 'jsonwebtoken';
import uuidv4 from 'uuid/v4';
import { NotAuthenticated } from '@feathersjs/errors';
import Debug from 'debug';
import { Application, Params } from '@feathersjs/feathers';
import { Application, Params, HookContext } from '@feathersjs/feathers';
import { IncomingMessage, ServerResponse } from 'http';
import defaultOptions from './options';

Expand Down Expand Up @@ -49,6 +49,13 @@ export interface AuthenticationStrategy {
* @param params The service call parameters
*/
authenticate? (authentication: AuthenticationRequest, params: Params): Promise<AuthenticationResult>;
/**
* Update a real-time connection according to this strategy.
*
* @param connection The real-time connection
* @param context The hook context
*/
handleConnection? (connection: any, context: HookContext): Promise<HookContext>;
/**
* Parse a basic HTTP request and response for authentication request information.
* @param req The HTTP request
Expand Down
25 changes: 11 additions & 14 deletions packages/authentication/src/hooks/connection.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,23 @@
import { HookContext } from '@feathersjs/feathers';
import Debug from 'debug';
import { omit } from 'lodash';

const debug = Debug('@feathersjs/authentication/hooks/connection');
import { AuthenticationBase } from '../core';

export default (strategy = 'jwt') => (context: HookContext) => {
const { method, result, params: { connection } } = context;
const { accessToken, ...rest } = result;
export default () => async (context: HookContext) => {
const { result, params: { connection } } = context;

if (!connection) {
return context;
}

const { authentication = {} } = connection;
const service = context.service as unknown as AuthenticationBase;
const strategies = service.getStrategies(...Object.keys(service.strategies))
.filter(current => typeof current.handleConnection === 'function');

if (method === 'remove' && accessToken === authentication.accessToken) {
debug('Removing authentication information from real-time connection');
delete connection.authentication;
} else if (method === 'create' && accessToken) {
debug('Adding authentication information to real-time connection');
Object.assign(connection, rest, {
authentication: { strategy, accessToken }
});
Object.assign(connection, omit(result, 'accessToken', 'authentication'));

for (const strategy of strategies) {
await strategy.handleConnection(connection, context);
}

return context;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,11 @@ import Debug from 'debug';
import { HookContext } from '@feathersjs/feathers';

const debug = Debug('@feathersjs/authentication/hooks/connection');
const EVENTS: { [key: string]: string } = {
create: 'login',
remove: 'logout'
};

export default () => (context: HookContext) => {
const { method, app, result, params } = context;
const event = EVENTS[method];
export default (event: string) => (context: HookContext) => {
const { type, app, result, params } = context;

if (event && params.provider && result) {
if (type === 'after' && params.provider && result) {
debug(`Sending authentication event '${event}'`);
app.emit(event, result, params, context);
}
Expand Down
2 changes: 1 addition & 1 deletion packages/authentication/src/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export { default as authenticate } from './authenticate';
export { default as connection } from './connection';
export { default as events } from './events';
export { default as event } from './event';
32 changes: 29 additions & 3 deletions packages/authentication/src/jwt.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { NotAuthenticated } from '@feathersjs/errors';
import { omit } from 'lodash';
import { AuthenticationRequest, AuthenticationResult } from './core';
import { Params } from '@feathersjs/feathers';
import { IncomingMessage } from 'http';
import { omit } from 'lodash';
import Debug from 'debug';
import { Params, HookContext } from '@feathersjs/feathers';

import { AuthenticationBaseStrategy } from './strategy';
import { AuthenticationRequest, AuthenticationResult } from './core';

const debug = Debug('@feathersjs/authentication/jwt');
const SPLIT_HEADER = /(\S+)\s+(\S+)/;

export class JWTStrategy extends AuthenticationBaseStrategy {
Expand All @@ -21,6 +24,25 @@ export class JWTStrategy extends AuthenticationBaseStrategy {
};
}

async handleConnection (connection: any, context: HookContext) {
const { result: { accessToken }, method } = context;

if (accessToken) {
if (method === 'create') {
debug('Adding authentication information to connection');
connection.authentication = {
strategy: this.name,
accessToken
};
} else if (method === 'remove' && accessToken === connection.authentication.accessToken) {
debug('Removing authentication information from connection');
delete connection.authentication;
}
}

return context;
}

verifyConfiguration () {
const allowedKeys = [ 'entity', 'service', 'header', 'schemes' ];

Expand All @@ -40,6 +62,8 @@ export class JWTStrategy extends AuthenticationBaseStrategy {
const { entity } = this.configuration;
const entityService = this.entityService;

debug('Getting entity', id);

if (entityService === null) {
throw new NotAuthenticated(`Could not find entity service`);
}
Expand Down Expand Up @@ -96,6 +120,8 @@ export class JWTStrategy extends AuthenticationBaseStrategy {
return null;
}

debug('Found parsed header value');

const [ , scheme = null, schemeValue = null ] = headerValue.match(SPLIT_HEADER) || [];
const hasScheme = scheme && schemes.some(
current => new RegExp(current, 'i').test(scheme)
Expand Down
10 changes: 8 additions & 2 deletions packages/authentication/src/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import Debug from 'debug';
import { merge, get } from 'lodash';
import { NotAuthenticated } from '@feathersjs/errors';
import { AuthenticationBase, AuthenticationResult, AuthenticationRequest } from './core';
import { connection, events } from './hooks';
import { connection, event } from './hooks';
import { Application, Params, ServiceMethods } from '@feathersjs/feathers';

const debug = Debug('@feathersjs/authentication/service');
Expand Down Expand Up @@ -154,6 +154,12 @@ export class AuthenticationService extends AuthenticationBase implements Partial
}

// @ts-ignore
this.hooks({ after: [ connection(), events() ] });
this.hooks({
after: [ connection() ],
finally: {
create: [ event('login') ],
remove: [ event('logout') ]
}
});
}
}
92 changes: 0 additions & 92 deletions packages/authentication/test/hooks/connection.test.ts

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import assert from 'assert';
import feathers, { Params, HookContext } from '@feathersjs/feathers';

import hook from '../../src/hooks/events';
import hook from '../../src/hooks/event';
import { AuthenticationRequest, AuthenticationResult } from '../../src/core';

describe('authentication/hooks/events', () => {
Expand All @@ -19,7 +19,8 @@ describe('authentication/hooks/events', () => {

service.hooks({
after: {
all: [ hook() ]
create: [ hook('login') ],
remove: [ hook('logout') ]
}
});

Expand Down
59 changes: 59 additions & 0 deletions packages/authentication/test/jwt.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ describe('authentication/jwt', () => {
});

payload = await service.verifyAccessToken(accessToken);
app.setup();
});

it('getEntity', async () => {
Expand All @@ -91,6 +92,64 @@ describe('authentication/jwt', () => {
});
});

describe('updateConnection', () => {
it('adds authentication information on create', async () => {
const connection: any = {};

await app.service('authentication').create({
strategy: 'jwt',
accessToken
}, { connection });

assert.deepStrictEqual(connection.user, user);
assert.deepStrictEqual(connection.authentication, {
strategy: 'jwt',
accessToken
});
});

it('deletes authentication information on remove', async () => {
const connection: any = {};

await app.service('authentication').create({
strategy: 'jwt',
accessToken
}, { connection });

assert.ok(connection.authentication);

await app.service('authentication').remove(null, {
authentication: connection.authentication,
connection
});

assert.ok(!connection.authentication);
});

it('does not remove if accessToken does not match', async () => {
const connection: any = {};

await app.service('authentication').create({
strategy: 'jwt',
accessToken
}, { connection });

assert.ok(connection.authentication);

await app.service('authentication').remove(null, {
authentication: {
strategy: 'jwt',
accessToken: await app.service('authentication').createAccessToken({}, {
subject: `${user.id}`
})
},
connection
});

assert.ok(connection.authentication);
});
});

describe('with authenticate hook', () => {
it('fails for protected service and external call when not set', async () => {
try {
Expand Down

0 comments on commit 4329feb

Please sign in to comment.