Skip to content
This repository has been archived by the owner on Mar 22, 2022. It is now read-only.

0.7 Release #139

Merged
merged 29 commits into from
Mar 30, 2016
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
c8475e3
Move to bcryptjs by default and allow to pass bcrypt
daffl Mar 23, 2016
79f24db
Remove unncessary Travis CI boilerplate
daffl Mar 23, 2016
d3655f0
adding ability to use custom cookie name on client. Clears cookie on …
ekryski Mar 24, 2016
f7fa8cc
removing default name from util function
ekryski Mar 24, 2016
b3bd9a2
adding support for redirects that are the same route but have a custo…
ekryski Mar 24, 2016
72891cc
Merge branch '0.6.1' into 0.7
ekryski Mar 28, 2016
2e35da0
Removing the cookie authentication fallback. Refs #132.
ekryski Mar 28, 2016
36cc00f
Setting default algorithm to HS512 for stronger encryption
ekryski Mar 28, 2016
6736a07
Adding ability to configure cookie and locking the cookie down. Close…
ekryski Mar 28, 2016
a0b2ed4
De-authenticating sockets on logout. Closes #136
ekryski Mar 28, 2016
8405986
merging PR to switch to bcryptjs and resolving conflicts
ekryski Mar 28, 2016
0dc0ede
Adding tests for success and fail redirect middleware
ekryski Mar 30, 2016
44aa4c3
Prevent auth services from emitting default service events. Closes #126.
ekryski Mar 30, 2016
72be2f8
OAuth user gets updated now. Closes #124
ekryski Mar 30, 2016
2363314
Making hooks optional if called internally. Closes #138
ekryski Mar 30, 2016
c73fea5
Making restrictToOwner throw an error if condition fails. Closes #127
ekryski Mar 30, 2016
1905e0d
restrictToRoles now throws an error if user doesn't have roles or isn…
ekryski Mar 30, 2016
1df214c
Fixing typos and removing console.log
ekryski Mar 30, 2016
633775a
Passing hook params as well.
ekryski Mar 30, 2016
8148f8e
Checking existence of filter function instead. Moving code to setup m…
ekryski Mar 30, 2016
e80a1ba
Being able to set cookie: false to disable cookies
ekryski Mar 30, 2016
944d8d2
Using callback for logout confirmation instead of second event
ekryski Mar 30, 2016
d17adbf
Adding tests to verify that service events are not sent
ekryski Mar 30, 2016
24c67e0
default cookie expiration needs to be set in middleware
ekryski Mar 30, 2016
b92eace
adding some debug statements to OAuth2 service
ekryski Mar 30, 2016
ab53e19
remove console.log
ekryski Mar 30, 2016
0c4f9cd
Making sure remove password hook is always invoked
ekryski Mar 30, 2016
01a4a6b
Adding test to ensure password is removed
ekryski Mar 30, 2016
96ab0d1
Supporting model responses and avoiding infinite loops in restrictToR…
ekryski Mar 30, 2016
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 0 additions & 8 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,3 @@ node_js:
- 'node'
- 'iojs'
- '0.12'
before_install:
- sudo apt-get install python-software-properties
- sudo add-apt-repository ppa:ubuntu-toolchain-r/test -y
- sudo apt-get update -y
- sudo apt-get install gcc-4.9 g++-4.9 -y
- sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-4.9 20
- sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-4.9 20
- sudo update-alternatives --install /usr/bin/gcov gcov /usr/bin/gcov-4.9 20
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
"./lib/index": "./lib/client/index"
},
"dependencies": {
"bcrypt": "^0.8.5",
"bcryptjs": "^2.3.0",
"debug": "^2.2.0",
"feathers-errors": "^2.0.1",
"feathers-hooks": "^1.5.0",
Expand Down
29 changes: 24 additions & 5 deletions src/client/index.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
import * as hooks from './hooks';
import { connected, authenticateSocket, getJWT, getStorage } from './utils';
import errors from 'feathers-errors';
import * as hooks from './hooks';
import {
connected,
authenticateSocket,
logoutSocket,
getJWT,
getStorage,
clearCookie
} from './utils';

const defaults = {
cookie: 'fathers-jwt',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's like your dad's JWT 😉

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep. JWT on punch cards.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LOL. Man I always make that typo. Cracks me up every time.

tokenKey: 'feathers-jwt',
localEndpoint: '/auth/local',
tokenEndpoint: '/auth/token'
Expand All @@ -24,7 +32,7 @@ export default function(opts = {}) {

// If no type was given let's try to authenticate with a stored JWT
if (!options.type) {
getOptions = getJWT(config.tokenKey, this.get('storage')).then(token => {
getOptions = getJWT(config.tokenKey, config.cookie, this.get('storage')).then(token => {
if (!token) {
return Promise.reject(new errors.NotAuthenticated(`Could not find stored JWT and no authentication type was given`));
}
Expand Down Expand Up @@ -66,12 +74,23 @@ export default function(opts = {}) {
});
};

// Set our logout method with the correct socket context
app.logout = function() {
app.set('user', null);
app.set('token', null);

clearCookie(config.cookie);

// TODO (EK): invalidate token with server
return Promise.resolve(app.get('storage').setItem(config.tokenKey, ''));
// remove the token from localStorage
return Promise.resolve(app.get('storage').setItem(config.tokenKey, '')).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 logoutSocket(socket, method);
}
});
};

// Set up hook that adds adds token and user to params so that
Expand Down
39 changes: 33 additions & 6 deletions src/client/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,27 @@ export function authenticateSocket(options, socket, method) {
});
}

// Returns a promise that de-authenticates a socket
export function logoutSocket(socket, method) {
return new Promise((resolve, reject) => {
// If we don't get a logged out message within 10 seconds
// consider it a failure.
const timeout = setTimeout(function() {
reject(new Error('Could not logout over socket'));
}, 10000);

socket.once('logged out', function() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couldn't this just be the logout event?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no, but we could use a callback instead of two events.

clearTimeout(timeout);
resolve();
});

socket[method]('logout');
});
}

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

Expand All @@ -50,13 +68,22 @@ export function getCookie(name) {
return null;
}

// Returns the value for a cookie
export function clearCookie(name) {
if (typeof document !== 'undefined') {
document.cookie = `${name}=;expires=Thu, 01 Jan 1970 00:00:01 GMT;`;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a standard default?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It just needs to be set in the past, so it can be whatever.

}

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);
export function getJWT(tokenKey, cookieKey, storage) {
return Promise.resolve(storage.getItem(tokenKey)).then(jwt => {
const cookieToken = getCookie(cookieKey);

if(cookieKey) {
return cookieKey;
if (cookieToken) {
return cookieToken;
}

return jwt;
Expand Down
4 changes: 4 additions & 0 deletions src/hooks/associate-current-user.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ export default function(options = {}){
}

if (!hook.params.user) {
if (!hook.params.provider) {
return hook;
}

throw new Error('There is no current user to associate.');
}

Expand Down
19 changes: 13 additions & 6 deletions src/hooks/hash-password.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import bcrypt from 'bcrypt';
import errors from 'feathers-errors';
import bcrypt from 'bcryptjs';


const defaults = { passwordField: 'password' };

Expand All @@ -11,23 +12,29 @@ export default function(options = {}){

options = Object.assign({}, defaults, hook.app.get('auth'), options);

const crypto = options.bcrypt || bcrypt;

if (hook.data === undefined) {
return hook;
}

const password = hook.data[options.passwordField];

if (password === undefined) {
if (!hook.params.provider) {
return hook;
}

throw new errors.BadRequest(`'${options.passwordField}' field is missing.`);
}

return new Promise(function(resolve, reject){
bcrypt.genSalt(10, function(err, salt) {
bcrypt.hash(password, salt, function(err, hash) {
if (err) {
return reject(err);
crypto.genSalt(10, function(error, salt) {
crypto.hash(password, salt, function(error, hash) {
if (error) {
return reject(error);
}

hook.data[options.passwordField] = hash;
resolve(hook);
});
Expand Down
4 changes: 4 additions & 0 deletions src/hooks/query-with-current-user.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ export default function(options = {}) {
}

if (!hook.params.user) {
if (!hook.params.provider) {
return hook;
}

throw new Error('There is no current user to associate.');
}

Expand Down
24 changes: 16 additions & 8 deletions src/hooks/restrict-to-owner.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ export default function(options = {}){
throw new Error(`The 'restrictToOwner' hook should only be used as a 'before' hook.`);
}

if (!hook.id) {
throw new Error(`The 'restrictToOwner' hook should only be used on the 'get', 'update', 'patch' and 'remove' service methods.`);
}

// If it was an internal call then skip this hook
if (!hook.params.provider) {
return hook;
Expand All @@ -30,13 +34,17 @@ export default function(options = {}){
throw new Error(`'${options.idField} is missing from current user.'`);
}

// NOTE (EK): This just scopes the query for the resource requested to the
// current user, which will result in a 404 if they are not the owner.
hook.params.query[options.ownerField] = id;

// TODO (EK): Maybe look up the actual document in this hook and throw a Forbidden error
// if (field && id && field.toString() !== id.toString()) {
// throw new errors.Forbidden('You do not have valid permissions to access this.');
// }
// look up the document and throw a Forbidden error if the user is not an owner
return new Promise((resolve, reject) => {
this.get(hook.id).then(data => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to add params here? this.get(hook.id, hook.params)?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably ya. Good catch! Going to have the same in restrictToRoles

const field = data[options.ownerField];

if ( field === undefined || field.toString() !== id.toString() ) {
reject(new errors.Forbidden('You do not have the permissions to access this.'));
}

resolve(hook);
}).catch(reject);
});
};
}
25 changes: 16 additions & 9 deletions src/hooks/restrict-to-roles.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ export default function(options = {}){
throw new Error(`The 'restrictToRoles' hook should only be used as a 'before' hook.`);
}

if (!hook.id) {
throw new Error(`The 'restrictToRoles' hook should only be used on the 'get', 'update', 'patch' and 'remove' service methods.`);
}

// If it was an internal call then skip this hook
if (!hook.params.provider) {
return hook;
Expand Down Expand Up @@ -57,15 +61,18 @@ export default function(options = {}){
// If we should allow users that own the resource and they don't already have
// the permitted roles check to see if they are the owner of the requested resource
if (options.owner && !authorized) {
// NOTE (EK): This just scopes the query for the resource requested to the
// current user, which will result in a 404 if they are not the owner.
hook.params.query[options.ownerField] = id;
authorized = true;

// TODO (EK): Maybe look up the actual document in this hook and throw a Forbidden error
// if (field && id && field.toString() !== id.toString()) {
// throw new errors.Forbidden('You do not have valid permissions to access this.');
// }
// look up the document and throw a Forbidden error if the user is not an owner
return new Promise((resolve, reject) => {
this.get(hook.id).then(data => {
const field = data[options.ownerField];

if ( field === undefined || field.toString() !== id.toString() ) {
reject(new errors.Forbidden('You do not have the permissions to access this.'));
}

resolve(hook);
}).catch(reject);
});
}

if (!authorized) {
Expand Down
58 changes: 37 additions & 21 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,21 @@ const PROVIDERS = {
// Options that apply to any provider
const defaults = {
idField: '_id',
shouldSetupSuccessRoute: true,
shouldSetupFailureRoute: true,
successRedirect: '/auth/success',
failureRedirect: '/auth/failure',
tokenEndpoint: '/auth/token',
localEndpoint: '/auth/local',
userEndpoint: '/users',
header: 'authorization',
cookie: 'feathers-jwt'
cookie: {
enabled: true,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this just be considered disabled when someone set cookie: false?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can probably do that.

name: 'feathers-jwt',
httpOnly: false,
secure: process.env.NODE_ENV === 'production' ? true : false,
expires: new Date()
}
};

export default function auth(config = {}) {
Expand All @@ -52,27 +60,17 @@ export default function auth(config = {}) {

// Merge and flatten options
const authOptions = Object.assign({}, defaults, app.get('auth'), config);

// If we should redirect on success and the redirect route is the same as the
// default then we'll set up a route handler. Otherwise we'll leave it to the developer
// to set up their own custom route handler.
if (authOptions.successRedirect === defaults.successRedirect) {
debug(`Setting up successRedirect route: ${authOptions.successRedirect}`);

app.get(authOptions.successRedirect, function(req, res){
res.sendFile(path.resolve(__dirname, 'public', 'auth-success.html'));
});

// If a custom success redirect is passed in or it is disabled then we
// won't setup the default route handler.
if (authOptions.successRedirect !== defaults.successRedirect) {
authOptions.shouldSetupSuccessRoute = false;
}

// If we should redirect on failure and the redirect route is the same as the
// default then we'll set up a route handler. Otherwise we'll leave it to the developer
// to set up their own custom route handler.
if (authOptions.failureRedirect === defaults.failureRedirect) {
debug(`Setting up failureRedirect route: ${authOptions.failureRedirect}`);

app.get(authOptions.failureRedirect, function(req, res){
res.sendFile(path.resolve(__dirname, 'public', 'auth-fail.html'));
});
// If a custom failure redirect is passed in or it is disabled then we
// won't setup the default route handler.
if (authOptions.failureRedirect !== defaults.failureRedirect) {
authOptions.shouldSetupFailureRoute = false;
}

// Set the options on the app
Expand Down Expand Up @@ -115,7 +113,7 @@ export default function auth(config = {}) {
// be dealing with a config param and not a provider config
// If that's the case we don't need to merge params and we
// shouldn't try to set up a service for this key.
if (!isObject(config[key])) {
if (!isObject(config[key]) || key === 'cookie') {
return;
}

Expand Down Expand Up @@ -145,6 +143,24 @@ export default function auth(config = {}) {
// Register error handling middleware for redirecting to support
// redirecting on authentication failure.
app.use(middleware.failedLogin(authOptions));

// Setup route handler for default success redirect
if (authOptions.shouldSetupSuccessRoute) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What was the reason again for adding those? I mean if you do a redirect it will almost certainly be to your own success page no?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem is if someone wants to use the same route name but a different handler. Which, I think is actually probably the most common use case. You can't rely on just inspecting whether the successRedirect is different than the default.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What I'm saying is why are there default auth redirect HTML pages at all? I might be missing something but they don't seem particularly useful. If you do a browser redirect it'll pretty much always be to your app right? Couldn't we make it required and throw an error if it isn't set (or disabled)?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For web apps you are correct, you would pretty much always override them. However, if you use the pop-up method or are on mobile, with the defaults you don't need to do anything to the server. You check that you navigated to /auth/success, grab the JWT from the cookie, and close the pop-up window or the webview on mobile.

debug(`Setting up successRedirect route: ${authOptions.successRedirect}`);

app.get(authOptions.successRedirect, function(req, res){
res.sendFile(path.resolve(__dirname, 'public', 'auth-success.html'));
});
}

// Setup route handler for default failure redirect
if (authOptions.shouldSetupFailureRoute) {
debug(`Setting up failureRedirect route: ${authOptions.failureRedirect}`);

app.get(authOptions.failureRedirect, function(req, res){
res.sendFile(path.resolve(__dirname, 'public', 'auth-fail.html'));
});
}
};
}

Expand Down
Loading