Skip to content

Commit

Permalink
Add support for socket timeouts and some refactoring (#30)
Browse files Browse the repository at this point in the history
  • Loading branch information
daffl committed Aug 29, 2018
1 parent 7d4a7e0 commit e2890b9
Show file tree
Hide file tree
Showing 5 changed files with 117 additions and 63 deletions.
3 changes: 2 additions & 1 deletion packages/authentication-client/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ const defaults = {
jwtStrategy: 'jwt',
path: '/authentication',
entity: 'user',
service: 'users'
service: 'users',
timeout: 5000
};

export default function init (config = {}) {
Expand Down
111 changes: 49 additions & 62 deletions packages/authentication-client/src/passport.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import errors from 'feathers-errors';
import decode from 'jwt-decode';
import Debug from 'debug';
import { Storage, payloadIsValid, getCookie, clearCookie } from './utils';

const debug = Debug('feathers-authentication-client');

Expand All @@ -10,9 +11,14 @@ export default class Passport {
throw new Error('You have already registered authentication on this client app instance. You only need to do it once.');
}

this.options = options;
this.app = app;
this.storage = app.get('storage') || this.getStorage(options.storage);
Object.assign(this, {
options,
app,
payloadIsValid,
getCookie,
clearCookie,
storage: app.get('storage') || this.getStorage(options.storage)
});

this.setJWT = this.setJWT.bind(this);

Expand Down Expand Up @@ -103,6 +109,11 @@ export default class Passport {
return new Promise((resolve, reject) => {
const connected = app.primus ? 'open' : 'connect';
const disconnect = app.io ? 'disconnect' : 'end';
const timeout = setTimeout(() => {
debug('Socket connection timed out');
reject(new Error('Socket connection timed out'));
}, this.options.timeout);

debug('Waiting for socket connection');

const handleDisconnect = () => {
Expand All @@ -116,6 +127,7 @@ export default class Passport {
debug('Socket connected');
debug(`Removing ${disconnect} listener`);
socket.removeListener(disconnect, handleDisconnect);
clearTimeout(timeout);
resolve(socket);
});
});
Expand Down Expand Up @@ -154,12 +166,18 @@ export default class Passport {
// Returns a promise that authenticates a socket
authenticateSocket (credentials, socket, emit) {
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
debug('authenticateSocket timed out');
reject(new Error('Authentication timed out'));
}, this.options.timeout);

debug('Attempting to authenticate socket');
socket[emit]('authenticate', credentials, (error, data) => {
if (error) {
return reject(error);
}

clearTimeout(timeout);
socket.authenticated = true;
debug('Socket authenticated!');

Expand All @@ -170,12 +188,19 @@ export default class Passport {

logoutSocket (socket, emit) {
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
debug('logoutSocket timed out');
reject(new Error('Logout timed out'));
}, this.options.timeout);

socket[emit]('logout', error => {
clearTimeout(timeout);
socket.authenticated = false;

if (error) {
reject(error);
return reject(error);
}

socket.authenticated = false;
resolve();
});
});
Expand All @@ -188,15 +213,16 @@ export default class Passport {
this.clearCookie(this.options.cookie);

// remove the accessToken from localStorage
return Promise.resolve(app.get('storage').removeItem(this.options.storageKey)).then(() => {
// If using sockets de-authenticate the socket
if (app.io || app.primus) {
const method = app.io ? 'emit' : 'send';
const socket = app.io ? app.io : app.primus;

return this.logoutSocket(socket, method);
}
});
return Promise.resolve(app.get('storage')
.removeItem(this.options.storageKey)).then(() => {
// If using sockets de-authenticate the socket
if (app.io || app.primus) {
const method = app.io ? 'emit' : 'send';
const socket = app.io ? app.io : app.primus;

return this.logoutSocket(socket, method);
}
});
}

setJWT (data) {
Expand All @@ -219,15 +245,16 @@ export default class Passport {
return resolve(accessToken);
}

return Promise.resolve(this.storage.getItem(this.options.storageKey)).then(jwt => {
let token = jwt || this.getCookie(this.options.cookie);
return Promise.resolve(this.storage.getItem(this.options.storageKey))
.then(jwt => {
let token = jwt || this.getCookie(this.options.cookie);

if (token && token !== 'null' && !this.payloadIsValid(decode(token))) {
token = undefined;
}
if (token && token !== 'null' && !this.payloadIsValid(decode(token))) {
token = undefined;
}

return resolve(token);
});
return resolve(token);
});
});
}

Expand All @@ -250,52 +277,12 @@ export default class Passport {
}
}

// Pass a decoded payload and it will return a boolean based on if it hasn't expired.
payloadIsValid (payload) {
return payload && payload.exp * 1000 > new Date().getTime();
}

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;
}

clearCookie (name) {
if (typeof document !== 'undefined') {
document.cookie = `${name}=;expires=Thu, 01 Jan 1970 00:00:01 GMT;`;
}

return null;
}

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

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

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

removeItem (key) {
delete this.store[key];
return this;
}
};
return new Storage();
}
}
44 changes: 44 additions & 0 deletions packages/authentication-client/src/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
export 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;
}
}

// Pass a decoded payload and it will return a boolean based on if it hasn't expired.
export function payloadIsValid (payload) {
return payload && payload.exp * 1000 > new Date().getTime();
}

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;
}

export function clearCookie (name) {
if (typeof document !== 'undefined') {
document.cookie = `${name}=;expires=Thu, 01 Jan 1970 00:00:01 GMT;`;
}

return null;
}
11 changes: 11 additions & 0 deletions packages/authentication-client/test/integration/primus.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,17 @@ describe('Primus client authentication', function () {
});
});

it('supports socket timeouts', () => {
return client.passport.connected().then(() => {
client.passport.options.timeout = 0;

return client.authenticate(options).catch(error => {
client.passport.options.timeout = 5000;
expect(error.message).to.equal('Authentication timed out');
});
});
});

it('local username password authentication and access to protected service', () => {
return client.authenticate(options).then(response => {
expect(response.accessToken).to.not.equal(undefined);
Expand Down
11 changes: 11 additions & 0 deletions packages/authentication-client/test/integration/socketio.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,17 @@ describe('Socket.io client authentication', function () {
});
});

it('supports socket timeouts', () => {
return client.passport.connected().then(() => {
client.passport.options.timeout = 0;

return client.authenticate(options).catch(error => {
client.passport.options.timeout = 5000;
expect(error.message).to.equal('Authentication timed out');
});
});
});

it('local username password authentication and access to protected service', () => {
return client.authenticate(options).then(response => {
expect(response.accessToken).to.not.equal(undefined);
Expand Down

0 comments on commit e2890b9

Please sign in to comment.