Skip to content

Commit

Permalink
Merge pull request #101 from feathersjs/client-auth-polish
Browse files Browse the repository at this point in the history
Finalizing client side authentication module
  • Loading branch information
ekryski authored and daffl committed Aug 29, 2018
1 parent 481973c commit 9106bd9
Show file tree
Hide file tree
Showing 7 changed files with 353 additions and 178 deletions.
17 changes: 9 additions & 8 deletions packages/authentication/.jshintrc
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,14 @@
"white": false,
"node": true,
"globals": {
"it": true,
"describe": true,
"before": true,
"beforeEach": true,
"after": true,
"afterEach": true,
"document": true,
"localStorage": true
"window": true,
"it": true,
"describe": true,
"before": true,
"beforeEach": true,
"after": true,
"afterEach": true,
"document": true,
"localStorage": true
}
}
4 changes: 3 additions & 1 deletion packages/authentication/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,16 +62,18 @@
"body-parser": "^1.9.0",
"feathers": "2.0.0-pre.4",
"feathers-hooks": "^1.0.0-pre.4",
"feathers-localstorage": "^0.2.0",
"feathers-memory": "^0.6.1",
"feathers-primus": "^1.3.2",
"feathers-rest": "^1.2.2",
"feathers-socketio": "^1.3.2",
"jshint": "^2.8.0",
"localstorage-memory": "^1.0.2",
"mocha": "^2.3.3",
"nsp": "^2.2.0",
"passport-facebook": "^2.1.0",
"passport-github": "^1.0.0",
"primus": "^5.0.1",
"primus-emitter": "^3.1.1",
"request": "^2.69.0",
"socket.io-client": "^1.1.0",
"ws": "^1.0.1",
Expand Down
22 changes: 8 additions & 14 deletions packages/authentication/src/client/hooks.js
Original file line number Diff line number Diff line change
@@ -1,24 +1,18 @@
export function populateParams(options) {
export function populateParams() {
return function(hook) {
const storage = hook.app.service(options.storage);

// We can not run this hook on the storage service itself
if(this !== storage) {
return Promise.all([
storage.get('user'),
storage.get('token')
]).then(([ user, token ]) => {
Object.assign(hook.params, { user, token });
return hook;
});
}
const app = hook.app;

Object.assign(hook.params, {
user: app.get('user'),
token: app.get('token')
});
};
}

export function populateHeader(options = {}) {
return function(hook) {
if (hook.params.token) {
hook.params.headers = Object.assign({}, {
hook.params.headers = Object.assign({}, {
[options.header || 'Authorization']: hook.params.token
}, hook.params.headers);
}
Expand Down
149 changes: 52 additions & 97 deletions packages/authentication/src/client/index.js
Original file line number Diff line number Diff line change
@@ -1,137 +1,92 @@
import * as hooks from './hooks';
import { connected, authenticateSocket, getJWT, getStorage } from './utils';

const defaults = {
storage: '/storage',
tokenKey: 'feathers-jwt',
localEndpoint: '/auth/local',
tokenEndpoint: '/auth/token'
};

export default function(opts = {}) {
const authOptions = Object.assign({}, defaults, opts);
const config = Object.assign({}, defaults, opts);

return function() {
const app = this;
const storage = () => app.service(authOptions.storage);

if (!storage) {
throw new Error(`You need register a local store before you can use feathers-authentication. Did you call app.use('storage', localstorage())`);

if(!app.get('storage')) {
app.set('storage', getStorage(config.storage));
}

const handleResponse = function (response) {
return storage().create([{
id: 'token',
value: response.token
}, {
id: 'user',
value: response.data
}]).then(() => response);
};
app.authenticate = function(options = {}) {
const storage = this.get('storage');
let getOptions = Promise.resolve(options);

app.authenticate = function(options) {
// If no type was given let's try to authenticate with a stored JWT
if (!options.type) {
throw new Error('You need to provide a `type` attribute when calling app.authenticate()');
getOptions = getJWT(config.tokenKey, this.get('storage')).then(token => {
if(!token) {
return Promise.reject(new Error(`Could not find stored JWT and no authentication type was given`));
}

return { type: 'token', token };
});
}

let endPoint;
const handleResponse = function (response) {
app.set('token', response.token);
app.set('user', response.data);

if (options.type === 'local') {
endPoint = authOptions.localEndpoint;
} else if (options.type === 'token') {
endPoint = authOptions.tokenEndpoint;
} else {
throw new Error(`Unsupported authentication 'type': ${options.type}`);
}
return Promise.resolve(storage.setItem(config.tokenKey, response.token))
.then(() => response);
};

return new Promise(function(resolve, reject) {
// TODO (EK): Handle OAuth logins
// If we are using a REST client
if (app.rest) {
return app.service(endPoint).create(options).then(handleResponse);
}

if (app.io || app.primus) {
const socket = app.io || app.primus;
const handleUnauthorized = function(error) {
// Unleak event handlers
this.off('disconnect', reject);
this.off('close', reject);

reject(error);
};
const handleAuthenticated = function(response) {
// We need to bind and unbind the event handlers that didn't run
// so that they don't leak around
this.off('unauthorized', handleUnauthorized);
this.off('disconnect', reject);
this.off('close', reject);

handleResponse(response).then(reponse => resolve(reponse)).catch(error => {
throw error;
});
};

// Also, binding to events that aren't fired (like `close`)
// for Socket.io doesn't hurt if we unbind once we're done
socket.once('disconnect', reject);
socket.once('close', reject);
socket.once('unauthorized', handleUnauthorized);
socket.once('authenticated', handleAuthenticated);
}
return getOptions.then(options => {
let endPoint;

// If we are using socket.io
if (app.io) {
const socket = app.io;

// If we aren't already connected then throw an error
if (!socket.connected) {
throw new Error('Socket not connected');
}

socket.emit('authenticate', options);
if (options.type === 'local') {
endPoint = config.localEndpoint;
} else if (options.type === 'token') {
endPoint = config.tokenEndpoint;
} else {
throw new Error(`Unsupported authentication 'type': ${options.type}`);
}

// If we are using primus
if (app.primus) {
const socket = app.primus;

// If we aren't already connected then throw an error
if (socket.readyState !== 3) {
throw new Error('Socket not connected');
return connected(app).then(socket => {
// TODO (EK): Handle OAuth logins
// If we are using a REST client
if (app.rest) {
return app.service(endPoint).create(options).then(handleResponse);
}

socket.send('authenticate', options);
}
});
};

app.user = function() {
return storage().get('user').then(data => data.value);
};

app.token = function() {
return storage().get('token').then(data => data.value);
const method = app.io ? 'emit' : 'send';

return authenticateSocket(options, socket, method).then(handleResponse);
});
});
};

app.logout = function() {
return storage().remove(null, { id: { $in: ['user', 'token' ] } });
app.set('user', null);
app.set('token', null);

// TODO (EK): invalidate token with server
return Promise.resolve(app.get('storage').setItem(config.tokenKey, null));
};

// Set up hook that adds 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 (!service.before || !service.after) {
throw new Error(`It looks like feathers-hooks isn't configured. It is required before you configure feathers-authentication.`);
if (typeof service.before !== 'function' || typeof service.after !== 'function') {
throw new Error(`It looks like feathers-hooks isn't configured. It is required before running feathers-authentication.`);
}
service.before(hooks.populateParams(authOptions));

service.before(hooks.populateParams(config));
});

// Set up hook that adds authorization header for REST provider
if (app.rest) {
app.mixins.push(function(service) {
if (!service.before || !service.after) {
throw new Error(`It looks like feathers-hooks isn't configured. It is required before you configure feathers-authentication.`);
}
service.before(hooks.populateHeader(authOptions));
service.before(hooks.populateHeader(config));
});
}
};
Expand Down
81 changes: 81 additions & 0 deletions packages/authentication/src/client/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// Returns a promise that resolves when the socket is connected
export function connected(app) {
return new Promise((resolve, reject) => {
if(app.rest) {
return resolve();
}

const socket = app.io || app.primus;

if(!socket) {
return reject(new Error(`It looks like no client connection has been configured.`));
}

// If one of those events happens before `connect` the promise will be rejected
// If it happens after, it will do nothing (since Promises can only resolve once)
socket.once('disconnect', reject);
socket.once('close', reject);

// If the socket is not connected yet we have to wait for the `connect` event
if( (app.io && !socket.connected) || (app.primus && socket.readyState !== 3)) {
socket.once('connect', () => resolve(socket));
} else {
resolve(socket);
}
});
}

// Returns a promise that authenticates a socket
export function authenticateSocket(options, socket, method) {
return new Promise((resolve, reject) => {
socket.once('unauthorized', reject);
socket.once('authenticated', resolve);

socket[method]('authenticate', options);
});
}

// Returns the value for a cookie
export function getCookie(name) {
if(typeof document !== 'undefined') {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);

if(parts.length === 2) {
return parts.pop().split(';').shift();
}
}

return null;
}

// Tries the JWT from the given key either from a storage or the cookie
export function getJWT(key, storage) {
return Promise.resolve(storage.getItem(key)).then(jwt => {
const cookieKey = getCookie(key);

if(cookieKey) {
return cookieKey;
}

return jwt;
});
}

// Returns a storage implementation
export function getStorage(storage) {
if(storage) {
return storage;
}

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

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

0 comments on commit 9106bd9

Please sign in to comment.