Skip to content

Commit

Permalink
feat: Authentication v3 client (#1240)
Browse files Browse the repository at this point in the history
BREAKING CHANGE: Rewrite for authentication v3
  • Loading branch information
daffl committed Mar 10, 2019
1 parent 82bcfbe commit 65b43bd
Show file tree
Hide file tree
Showing 27 changed files with 1,025 additions and 1,916 deletions.
989 changes: 504 additions & 485 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"install": "lerna bootstrap",
"publish": "lerna publish",
"lint": "semistandard \"packages/**/lib/**/*.js\" \"packages/**/test/**/*.js\" --fix",
"test": "npm run lint && nyc lerna run test --ignore @feathersjs/authentication-client",
"test": "npm run lint && nyc lerna run test",
"test:client": "grunt"
},
"semistandard": {
Expand Down
127 changes: 127 additions & 0 deletions packages/authentication-client/lib/core.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
const { NotAuthenticated } = require('@feathersjs/errors');

exports.Storage = class Storage {
constructor () {
this.store = {};
}

getItem (key) {
return this.store[key];
}

setItem (key, value) {
return (this.store[key] = value);
}

removeItem (key) {
delete this.store[key];
return this;
}
};

exports.AuthenticationClient = class AuthenticationClient {
constructor (app, options) {
const socket = app.io || app.primus;

this.app = app;
this.app.set('storage', this.app.get('storage') || options.storage);
this.options = options;
this.authenticated = false;

if (socket) {
this.handleSocket(socket);
}
}

get service () {
return this.app.service(this.options.path);
}

get storage () {
return this.app.get('storage');
}

handleSocket (socket) {
// Connection events happen on every reconnect
const connected = this.app.io ? 'connect' : 'open';

socket.on(connected, () => {
// Only reconnect when `reAuthenticate()` or `authenticate()`
// has been called explicitly first
if (this.authenticated) {
// Force reauthentication with the server
this.reauthenticate(true);
}
});
}

setJwt (accessToken) {
return Promise.resolve(this.storage.setItem(this.options.storageKey, accessToken));
}

getJwt () {
return Promise.resolve(this.storage.getItem(this.options.storageKey));
}

removeJwt () {
return Promise.resolve(this.storage.removeItem(this.options.storageKey));
}

reset () {
// Reset the internal authentication state
// but not the accessToken from storage
const authResult = this.app.get('authentication');

this.app.set('authentication', null);
this.authenticated = false;

return authResult;
}

reauthenticate (force = false) {
// Either returns the authentication state or
// tries to re-authenticate with the stored JWT and strategy
const authPromise = this.app.get('authentication');

if (!authPromise || force === true) {
return this.getJwt().then(accessToken => {
if (!accessToken) {
throw new NotAuthenticated('No accessToken found in storage');
}

return this.authenticate({
strategy: this.options.jwtStrategy,
accessToken
});
});
}

return authPromise;
}

authenticate (authentication) {
if (!authentication) {
return this.reauthenticate();
}

const promise = this.service.create(authentication)
.then(authResult => {
const { accessToken } = authResult;

this.authenticated = true;

return this.setJwt(accessToken).then(() => authResult);
});

this.app.set('authentication', promise);

return promise;
}

logout () {
return this.app.get('authentication')
.then(() => this.service.remove(null))
.then(() => this.removeJwt())
.then(() => this.reset());
}
};
19 changes: 19 additions & 0 deletions packages/authentication-client/lib/hooks/authentication.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
const { stripSlashes } = require('@feathersjs/commons');

module.exports = () => {
return context => {
const { app, params, path, method, app: { authentication } } = context;

if (stripSlashes(authentication.options.path) === path && method === 'create') {
return context;
}

return Promise.resolve(app.get('authentication')).then(authResult => {
if (authResult) {
context.params = Object.assign({}, authResult, params);
}

return context;
});
};
};
12 changes: 3 additions & 9 deletions packages/authentication-client/lib/hooks/index.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,5 @@
const authentication = require('./authentication');
const populateHeader = require('./populate-header');
const populateAccessToken = require('./populate-access-token');
const populateEntity = require('./populate-entity');

let hooks = {
populateHeader,
populateAccessToken,
populateEntity
};

module.exports = hooks;
exports.authentication = authentication;
exports.populateHeader = populateHeader;
13 changes: 0 additions & 13 deletions packages/authentication-client/lib/hooks/populate-access-token.js

This file was deleted.

39 changes: 0 additions & 39 deletions packages/authentication-client/lib/hooks/populate-entity.js

This file was deleted.

25 changes: 12 additions & 13 deletions packages/authentication-client/lib/hooks/populate-header.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
module.exports = function populateHeader (options = {}) {
if (!options.header) {
throw new Error(`You need to pass 'options.header' to the populateHeader() hook.`);
}
module.exports = () => {
return context => {
const { app, params: { accessToken } } = context;
const authentication = app.authentication;

return function (hook) {
if (hook.type !== 'before') {
return Promise.reject(new Error(`The 'populateHeader' hook should only be used as a 'before' hook.`));
}
// Set REST header if necessary
if (app.rest && accessToken) {
const { scheme, header } = authentication.options;
const authHeader = `${scheme} ${accessToken}`;

if (hook.params.accessToken) {
hook.params.headers = Object.assign({}, {
[options.header]: options.prefix ? `${options.prefix} ${hook.params.accessToken}` : hook.params.accessToken
}, hook.params.headers);
context.params.headers = Object.assign({}, {
[header]: authHeader
}, context.params.headers);
}

return Promise.resolve(hook);
return context;
};
};
69 changes: 27 additions & 42 deletions packages/authentication-client/lib/index.js
Original file line number Diff line number Diff line change
@@ -1,52 +1,37 @@
const hooks = require('./hooks/index');
const Passport = require('./passport');

const { AuthenticationClient, Storage } = require('./core');
const hooks = require('./hooks');
const defaults = {
header: 'Authorization',
cookie: 'feathers-jwt',
scheme: 'Bearer',
storageKey: 'feathers-jwt',
jwtStrategy: 'jwt',
path: '/authentication',
entity: 'user',
service: 'users',
timeout: 5000
Authentication: AuthenticationClient
};

function init (config = {}) {
const options = Object.assign({}, defaults, config);

return function () {
const app = this;

app.passport = new Passport(app, options);
app.authenticate = app.passport.authenticate.bind(app.passport);
app.logout = app.passport.logout.bind(app.passport);

// Set up hook that adds token and user to params so that
// it they can be accessed by client side hooks and services
app.mixins.push(function (service) {
// if (typeof service.hooks !== 'function') {
if (app.version < '3.0.0') {
throw new Error(`This version of @feathersjs/authentication-client only works with @feathersjs/feathers v3.0.0 or later.`);
}

service.hooks({
before: hooks.populateAccessToken(options)
});
module.exports = _options => {
const options = Object.assign({}, {
storage: new Storage()
}, defaults, _options);
const { Authentication } = options;

return app => {
const authentication = new Authentication(app, options);

app.authentication = authentication;
app.authenticate = authentication.authenticate.bind(authentication);
app.reauthenticate = authentication.reauthenticate.bind(authentication);
app.logout = authentication.logout.bind(authentication);

app.hooks({
before: [
hooks.authentication(),
hooks.populateHeader()
]
});

// Set up hook that adds authorization header for REST provider
if (app.rest) {
app.mixins.push(function (service) {
service.hooks({
before: hooks.populateHeader(options)
});
});
}
};
}

module.exports = init;
};

module.exports.default = init;
module.exports.defaults = defaults;
Object.assign(module.exports, {
AuthenticationClient, Storage, hooks, defaults
});
Loading

0 comments on commit 65b43bd

Please sign in to comment.