diff --git a/packages/authentication/.jshintrc b/packages/authentication/.jshintrc index 741f1e44d4..6b23be1298 100644 --- a/packages/authentication/.jshintrc +++ b/packages/authentication/.jshintrc @@ -25,6 +25,8 @@ "before": true, "beforeEach": true, "after": true, - "afterEach": true + "afterEach": true, + "document": true, + "localStorage": true } } diff --git a/packages/authentication/.travis.yml b/packages/authentication/.travis.yml index 4cc633fdd7..15d686825a 100644 --- a/packages/authentication/.travis.yml +++ b/packages/authentication/.travis.yml @@ -1,7 +1,8 @@ language: node_js node_js: - - '4.2.6' + - 'node' - 'iojs' + - '0.12' before_install: - sudo apt-get install python-software-properties - sudo add-apt-repository ppa:ubuntu-toolchain-r/test -y diff --git a/packages/authentication/README.md b/packages/authentication/README.md index 51f55c1efa..bda7dd82db 100644 --- a/packages/authentication/README.md +++ b/packages/authentication/README.md @@ -26,59 +26,89 @@ Please refer to the [Authentication documentation](http://docs.feathersjs.com/au Here's an example of a Feathers server that uses `feathers-authentication` for local auth. It includes a `users` service that uses `feathers-mongoose`. *Note that it does NOT implement any authorization.* ```js -/* * * Import Feathers and Plugins * * */ -var feathers = require('feathers'); -var hooks = require('feathers-hooks'); -var bodyParser = require('body-parser'); -var feathersAuth = require('feathers-authentication').default; -var authHooks = require('feathers-authentication').hooks; - -/* * * Prepare the Mongoose service * * */ -var mongooseService = require('feathers-mongoose'); -var mongoose = require('mongoose'); -var Schema = mongoose.Schema; -var UserSchema = new Schema({ +import feathers from 'feathers'; +import hooks from 'feathers-hooks'; +import bodyParser from 'body-parser'; +import authentication from 'feathers-authentication'; +import { hooks as authHooks } from 'feathers-authentication'; +import mongoose from 'mongoose'; +import service from 'feathers-mongoose'; + +const port = 3030; +const Schema = mongoose.Schema; +const UserSchema = new Schema({ username: {type: String, required: true, unique: true}, password: {type: String, required: true }, createdAt: {type: Date, 'default': Date.now}, updatedAt: {type: Date, 'default': Date.now} }); -var UserModel = mongoose.model('User', UserSchema); +let UserModel = mongoose.model('User', UserSchema); -/* * * Connect the MongoDB Server * * */ mongoose.Promise = global.Promise; mongoose.connect('mongodb://localhost:27017/feathers'); -/* * * Initialize the App and Plugins * * */ -var app = feathers() +let app = feathers() .configure(feathers.rest()) .configure(feathers.socketio()) .configure(hooks()) .use(bodyParser.json()) .use(bodyParser.urlencoded({ extended: true })) - // Configure feathers-authentication - .configure(feathersAuth({ - secret: 'feathers-rocks' + .configure(authentication({ + token: { + secret: 'feathers-rocks' + }, + local: { + usernameField: 'username' + }, + facebook: { + clientID: '', + clientSecret: '' + } })); -/* * * Setup the User Service and hashPassword Hook * * */ -app.use('/api/users', new mongooseService('user', UserModel)) -var service = app.service('/api/users'); -service.before({ +app.use('/users', new service('user', {Model: UserModel})) + +let userService = app.service('users'); +userService.before({ create: [authHooks.hashPassword('password')] }); -/* * * Start the Server * * */ -var port = 3030; -var server = app.listen(port); +let server = app.listen(port); server.on('listening', function() { - console.log(`Feathers application started on localhost:3030); + console.log(`Feathers application started on localhost:${port}`); }); ``` +## Client use +You can use the client in the Browser, in NodeJS and in React Native. +```js +import io from 'socket.io-client'; +import feathers from 'feathers/client'; +import hooks from `feathers-hooks`; +import socketio from 'feathers-socketio/client'; +import authentication from 'feathers-authentication/client'; + +const socket = io('http://path/to/api'); +const app = feathers() + .configure(socketio(socket)) // you could use Primus or REST instead + .configure(hooks()) + .configure(authentication()); + +app.io.on('connect', function(){ + app.authenticate({ + type: 'local', + 'email': 'admin@feathersjs.com', + 'password': 'admin' + }).then(function(result){ + console.log('Authenticated!', result); + }).catch(function(error){ + console.error('Error authenticating!', error); + }); +}); +``` ## Changelog diff --git a/packages/authentication/client.js b/packages/authentication/client.js new file mode 100644 index 0000000000..19d52462d8 --- /dev/null +++ b/packages/authentication/client.js @@ -0,0 +1 @@ +module.exports = require('./lib/client'); \ No newline at end of file diff --git a/packages/authentication/example/README.md b/packages/authentication/example/README.md deleted file mode 100644 index 8305ac6363..0000000000 --- a/packages/authentication/example/README.md +++ /dev/null @@ -1 +0,0 @@ -Open this folder in a terminal and run `node example-server` to start the example server. \ No newline at end of file diff --git a/packages/authentication/example/app.js b/packages/authentication/example/app.js new file mode 100644 index 0000000000..c591a7aa45 --- /dev/null +++ b/packages/authentication/example/app.js @@ -0,0 +1,90 @@ +var feathers = require('feathers'); +var rest = require('feathers-rest'); +var socketio = require('feathers-socketio'); +var primus = require('feathers-primus'); +var hooks = require('feathers-hooks'); +var memory = require('feathers-memory'); +var bodyParser = require('body-parser'); +var authentication = require('../lib/index').default; +var authHooks = require('../lib/index').hooks; + +// Passport Auth Strategies +var FacebookStrategy = require('passport-facebook').Strategy; +var GithubStrategy = require('passport-github').Strategy; + +// Initialize the application +var app = feathers() + .configure(rest()) + // .configure(primus({ + // transformer: 'websockets' + // })) + .configure(socketio()) + .configure(hooks()) + // Needed for parsing bodies (login) + .use(bodyParser.json()) + .use(bodyParser.urlencoded({ extended: true })) + // Configure feathers-authentication + .configure(authentication({ + token: { + secret: 'feathers-rocks' + }, + local: {}, + facebook: { + strategy: FacebookStrategy, + "clientID": "your-facebook-client-id", + "clientSecret": "your-facebook-client-secret", + "permissions": { + authType: 'rerequest', + "scope": ["public_profile", "email"] + } + }, + github: { + strategy: GithubStrategy, + "clientID": "your-github-client-id", + "clientSecret": "your-github-client-secret" + } + })) + // Initialize a user service + .use('/users', memory()) + // A simple Message service that we can used for testing + .use('/messages', memory()) + .use('/', feathers.static(__dirname + '/public')) + .use(function(error, req, res, next){ + res.status(error.code); + res.json(error); + }); + +var messageService = app.service('/messages'); +messageService.create({text: 'A million people walk into a Silicon Valley bar'}, {}, function(){}); +messageService.create({text: 'Nobody buys anything'}, {}, function(){}); +messageService.create({text: 'Bar declared massive success'}, {}, function(){}); + +messageService.before({ + all: [ + authHooks.verifyToken({secret: 'feathers-rocks'}), + authHooks.populateUser(), + authHooks.requireAuth() + ] +}) + +var userService = app.service('/users'); + +// Add a hook to the user service that automatically replaces +// the password with a hash of the password before saving it. +userService.before({ + create: authHooks.hashPassword() +}); + +// Create a user that we can use to log in +var User = { + email: 'admin@feathersjs.com', + password: 'admin' +}; + +userService.create(User, {}).then(function(user) { + console.log('Created default user', user); +}); + +app.listen(3030); + +console.log('Feathers authentication app started on 127.0.0.1:3030'); diff --git a/packages/authentication/example/example-server.js b/packages/authentication/example/example-server.js deleted file mode 100644 index 4aaed503b9..0000000000 --- a/packages/authentication/example/example-server.js +++ /dev/null @@ -1,47 +0,0 @@ -var feathers = require('feathers'); -var hooks = require('feathers-hooks'); -var memory = require('feathers-memory'); -var bodyParser = require('body-parser'); -var feathersAuth = require('../lib/index'); -var hashPassword = feathersAuth.hooks.hashPassword; - -// Initialize the application -var app = feathers() - .configure(feathers.rest()) - .configure(feathers.socketio()) - .configure(hooks()) - // Needed for parsing bodies (login) - .use(bodyParser.urlencoded({ extended: true })) - // Configure feathers-authentication - .configure(feathersAuth({ - secret: 'feathers-rocks' - })) - // Initialize a user service - .use('/api/users', memory()) - // A simple Todos service that we can used for testing - .use('/api/todos', memory()) - .use('/', feathers.static(__dirname + '/public')); - -var todoService = app.service('/api/todos'); -todoService.create({name: 'Do the dishes'}, {}, function(){}); -todoService.create({name: 'Buy a guitar'}, {}, function(){}); -todoService.create({name: 'Exercise for 30 minutes.'}, {}, function(){}); - -var userService = app.service('/api/users'); - -// Add a hook to the user service that automatically replaces -// the password with a hash of the password before saving it. -userService.before({ - create: hashPassword() -}); - -// Create a user that we can use to log in -userService.create({ - username: 'feathers', - password: 'test' -}, {}, function(error, user) { - console.log('Created default user', user); - console.log('Open http://localhost:4000'); -}); - -app.listen(4000); diff --git a/packages/authentication/example/public/index.html b/packages/authentication/example/public/index.html index fd73fd045e..91b7c91527 100644 --- a/packages/authentication/example/public/index.html +++ b/packages/authentication/example/public/index.html @@ -2,30 +2,409 @@ - + Feathers Authentication + + -

feathers-passport

- REST endpoint /todos/rest

- Test sockets -

-  
-  

Login

-

feathers / test

-
-
- - -
-
- - -
-
- -
-
- Logout + + +
+

Authentication Examples

+ +
+

Email/Password authentication via REST

+ +
+
+ + +
+
+ + +
+ + +
+
+ +
+

Token based authentication via REST

+ +
+
+ + +
+ + +
+
+ +
+

Basic email/password authentication via socket

+
+
+ + +
+
+ + +
+ +
+
+ +
+

Token based authentication via socket

+
+
+ + +
+ + +
+
+ +
+

Basic email/password authentication via primus

+
+
+ + +
+
+ + +
+ +
+
+ +
+

Token based authentication via primus

+
+
+ + +
+ + +
+
+ +
+

Authenticate via Github

+ Login with Github +
+ +
+

Authenticate via Facebook

+ Login with Facebook +
+ +

+  
+ + + + + + + \ No newline at end of file diff --git a/packages/authentication/example/public/logout.html b/packages/authentication/example/public/logout.html deleted file mode 100644 index b248cd1c5a..0000000000 --- a/packages/authentication/example/public/logout.html +++ /dev/null @@ -1,10 +0,0 @@ - - - - - Logout - - - There's no need to logout because the server isn't stateful (no sessions). Just destroy the auth token and you're done. - - \ No newline at end of file diff --git a/packages/authentication/example/public/sockets.html b/packages/authentication/example/public/sockets.html deleted file mode 100644 index 9caeb6bb2c..0000000000 --- a/packages/authentication/example/public/sockets.html +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - - -

feathers-passport

- REST endpoint /todos/rest

- Go Back - -

This page features hard-coded login. View the source.

- -

-  
-  
-
-  Logout
-
-
-
\ No newline at end of file
diff --git a/packages/authentication/package.json b/packages/authentication/package.json
index 3bc217f4f9..a6744bd91a 100644
--- a/packages/authentication/package.json
+++ b/packages/authentication/package.json
@@ -26,26 +26,29 @@
     "node": ">= 0.12.0"
   },
   "scripts": {
+    "start": "node example/app",
     "prepublish": "npm run compile",
     "publish": "git push origin && git push origin --tags",
     "release:patch": "npm version patch && npm publish",
     "release:minor": "npm version minor && npm publish",
     "release:major": "npm version major && npm publish",
-    "compile": "rm -rf lib/ && babel -d lib/ src/",
+    "compile": "rm -rf lib/ && babel -d lib/ src/ && mkdir lib/public/ && cp src/public/* lib/public/",
     "watch": "babel --watch -d lib/ src/",
     "jshint": "jshint src/. test/. --config",
-    "mocha": "mocha test/ --compilers js:babel-core/register",
+    "mocha": "mocha test/* --compilers js:babel-core/register",
     "test": "npm run compile && npm run jshint && npm run mocha && nsp check"
   },
   "directories": {
     "lib": "lib"
   },
+  "browser": {
+    "./lib/index": "./lib/client/index"
+  },
   "dependencies": {
     "bcrypt": "^0.8.5",
     "debug": "^2.2.0",
     "feathers-errors": "^1.1.5",
     "jsonwebtoken": "^5.4.0",
-    "lodash": "^2.4.1",
     "passport": "^0.3.0",
     "passport-local": "^1.0.0"
   },
@@ -57,14 +60,18 @@
     "body-parser": "^1.9.0",
     "feathers": "2.0.0-pre.4",
     "feathers-hooks": "^1.0.0-pre.4",
-    "feathers-memory": "^0.5.3",
-    "feathers-rest": "^1.1.1",
-    "feathers-socketio": "^1.2.0",
+    "feathers-memory": "^0.6.0",
+    "feathers-primus": "^1.3.2",
+    "feathers-rest": "^1.2.2",
+    "feathers-socketio": "^1.3.2",
     "jshint": "^2.8.0",
     "mocha": "^2.3.3",
     "nsp": "^2.2.0",
-    "request": "^2.44.0",
+    "passport-facebook": "^2.1.0",
+    "passport-github": "^1.0.0",
+    "request": "^2.69.0",
     "socket.io-client": "^1.1.0",
+    "ws": "^1.0.1",
     "xmlhttprequest": "^1.6.0"
   }
 }
diff --git a/packages/authentication/src/client/hooks.js b/packages/authentication/src/client/hooks.js
new file mode 100644
index 0000000000..d8a2ec81e8
--- /dev/null
+++ b/packages/authentication/src/client/hooks.js
@@ -0,0 +1,40 @@
+import utils from './utils';
+
+export let populateParams = function() {
+  return function(hook) {
+    hook.params.user = utils.getUser();
+    hook.params.token = utils.getToken();
+  };
+};
+
+export let populateHeader = function(options = {}) {
+  const defaults = {
+    header: 'Authorization'
+  };
+  
+  options = Object.assign({}, defaults, options);
+
+  return function(hook) {
+    if (hook.params.token) {
+      hook.params.headers = {
+        [options.header]: hook.params.token
+      }; 
+    }
+  };
+};
+
+export let populateSocketParams = function() {
+  return function(hook) {
+    if (hook.params.token) {
+      hook.params.query = {
+        token: hook.params.token
+      };
+    }
+  };
+};
+
+export default {
+  populateParams,
+  populateHeader,
+  populateSocketParams
+};
\ No newline at end of file
diff --git a/packages/authentication/src/client/index.js b/packages/authentication/src/client/index.js
new file mode 100644
index 0000000000..3385a3e946
--- /dev/null
+++ b/packages/authentication/src/client/index.js
@@ -0,0 +1,188 @@
+import hooks from './hooks';
+import utils from './utils';
+
+const defaults = {
+  usernameField: 'email',
+  passwordField: 'password',
+  userEndpoint: '/users',
+  localEndpoint: '/auth/local',
+  tokenEndpoint: '/auth/token'
+};
+
+export default function(options = {}) {
+  const authOptions = Object.assign({}, defaults, options);
+
+  return function() {
+    const app = this;
+
+    app.authenticate = function(options) {
+      if (!options.type) {
+        throw new Error('You need to provide a `type` attribute when calling app.authenticate()');
+      }
+      
+      let endPoint;
+
+      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 new Promise(function(resolve, reject) {
+
+      //   // If we are using a REST client
+      //   if (app.rest) {
+      //     return app.service(endPoint).create(options).then(response => {
+      //       utils.setToken(response.token);
+      //       utils.setUser(response.data);
+
+      //       return resolve(response);
+      //     }).catch(reject);
+      //   }
+
+      //   // If we are using sockets
+      //   function connected(socket, event) {
+      //     if(socket.connected) {
+      //       return Promise.resolve(socket);
+      //     }
+          
+      //     return new Promise((resolve, reject) => {
+      //       socket.on(event, () => resolve(socket));
+      //     });
+      //   }
+
+      //   function handleAuth(method) {
+      //     return new Promise((resolve, reject) => {
+      //       return function(socket) {
+      //         socket.on('unauthorized', function(error) {
+      //           console.error('Unauthorized', error);
+      //           return reject(error);
+      //         });
+
+      //         socket.on('disconnect', function(error) {
+      //           console.error('Socket disconnected', error);
+      //           return reject(error);
+      //         });
+
+      //         socket.on('authenticated', function (response) {
+      //           console.log('authenticated', response);
+      //           utils.setToken(response.token);
+      //           utils.setUser(response.data);
+
+      //           return resolve(response);
+      //         });
+
+      //         socket[method]('authenticate', options);
+      //       };
+      //     });
+      //   }
+
+      //   if (app.io) {
+      //     connected(app.io, 'connected').then(handleAuth('emit')).then(function(response){
+
+      //     }).catch(function(error){
+      //       console.log('Errrr', error);
+      //     });
+      //   }
+
+      //   if (app.primus) {
+      //     connected(app.primus, 'open').then(handleAuth('send'));
+      //   }
+      // });
+
+      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(response => {
+            utils.setToken(response.token);
+            utils.setUser(response.data);
+
+            return resolve(response);
+          }).catch(reject);
+        }
+
+        if (app.io || app.primus) {
+          const transport = app.io ? 'io' : 'primus';
+          
+          app[transport].on('unauthorized', function(error) {
+            // console.error('Unauthorized', error);
+            return reject(error);
+          });
+
+          app[transport].on('authenticated', function (response) {
+            // console.log('authenticated', response);
+            utils.setToken(response.token);
+            utils.setUser(response.data);
+
+            return resolve(response);
+          });
+        }
+
+        // If we are using socket.io
+        if (app.io) {
+          // If we aren't already connected then throw an error
+          if (!app.io.connected) {
+            throw new Error('Socket not connected');
+          }
+          
+          app.io.on('disconnect', function(error) {
+            // console.error('Socket disconnected', error);
+            return reject(error);
+          });
+
+          app.io.emit('authenticate', options);
+        }
+
+        // If we are using primus
+        if (app.primus) {
+          // If we aren't already connected then throw an error
+          if (app.primus.readyState !== 3) {
+            throw new Error('Socket not connected');
+          }
+
+          app.primus.on('close', function(error) {
+            console.error('Socket disconnected', error);
+            return reject(error);
+          });
+          
+          app.primus.send('authenticate', options);
+        }
+      });
+    };
+
+    app.user = function() {
+      return utils.getUser();
+    };
+
+    app.logout = function() {
+      // remove user and token from localstorage
+      // on React native it's async storage
+      utils.clearToken();
+      utils.clearUser();
+    };
+
+    // Set up hook that adds adds token to data sent to server over sockets
+    app.mixins.push(function(service) {
+      service.before(hooks.populateParams());
+    });
+    
+    // Set up hook that adds authorization header
+    if (app.rest) {
+      app.mixins.push(function(service) {
+        service.before(hooks.populateHeader());
+      });
+    }
+
+    // Set up hook that adds adds token to data sent to server over sockets
+    if (app.io || app.primus) {
+      app.mixins.push(function(service) {
+        service.before(hooks.populateSocketParams());
+      });
+    }
+  };
+}
diff --git a/packages/authentication/src/client/utils.js b/packages/authentication/src/client/utils.js
new file mode 100644
index 0000000000..8ca755069b
--- /dev/null
+++ b/packages/authentication/src/client/utils.js
@@ -0,0 +1,91 @@
+export let getCookie = function(name) {
+  var value = '; ' + document.cookie;
+  var parts = value.split('; ' + name + '=');
+  
+  if (parts.length === 2) {
+    return parts.pop().split(';').shift();
+  }
+
+  return null;
+};
+
+export let getUser = function() {
+  // TODO (EK): Maybe make this configurable
+  const key = 'feathers-user';
+  let user = localStorage.getItem(key);
+
+  return JSON.parse(user);
+};
+
+export let setToken = function(token) {
+  // TODO (EK): Maybe make this configurable
+  const key = 'feathers-jwt';
+  localStorage.setItem(key, token);
+
+  // TODO (EK): Support async storage for react native  
+
+  return true;
+};
+
+export let setUser = function(user) {
+  // TODO (EK): Maybe make this configurable
+  const key = 'feathers-user';
+  localStorage.setItem(key, JSON.stringify(user));
+
+  // TODO (EK): Support async storage for react native  
+
+  return true;
+};
+
+export let getToken = function() {
+  // TODO (EK): Maybe make this configurable
+  const key = 'feathers-jwt';
+  let token = localStorage.getItem(key);
+
+  if (token) {
+    return token;
+  }
+
+  // TODO (EK): Support async storage for react native  
+
+  // We don't have the token so try and fetch it from the cookie
+  // and store it in local storage.
+  // TODO (EK): Maybe we should clear the cookie
+  token = getCookie(key);
+
+  if (token) {
+    localStorage.setItem(key, token);
+  }
+
+  return token;
+};
+
+export let clearToken = function() {
+  // TODO (EK): Maybe make this configurable
+  const key = 'feathers-jwt';
+
+  // TODO (EK): Support async storage for react native
+  localStorage.removeItem(key);
+
+  return true;
+};
+
+export let clearUser = function() {
+  // TODO (EK): Maybe make this configurable
+  const key = 'feathers-user';
+
+  // TODO (EK): Support async storage for react native
+  localStorage.removeItem(key);
+
+  return true;
+};
+
+export default {
+  getUser,
+  setUser,
+  clearUser,
+  getToken,
+  setToken,
+  clearToken,
+  getCookie
+};
\ No newline at end of file
diff --git a/packages/authentication/src/hooks.js b/packages/authentication/src/hooks.js
deleted file mode 100644
index adc81fb83e..0000000000
--- a/packages/authentication/src/hooks.js
+++ /dev/null
@@ -1,212 +0,0 @@
-import bcrypt from 'bcrypt';
-import errors from 'feathers-errors';
-
-/**
- * A function that generates a feathers hook that replaces a password located
- * at the provided passwordField with a hash of the password.
- * @param  {String} passwordField  The field containing the password.
- * @return {function}   The hashPassword feathers hook.
- */
-exports.hashPassword = function(passwordField){
-  // If it's called directly as a hook, assume the passwordField was 'password'.
-  if(typeof arguments[0] === 'object'){
-    console.log('Running hashPassword hook assuming passwordField of "password"');
-    var hook = arguments[0];
-    var next = arguments[1];
-    bcrypt.genSalt(10, function(err, salt) {
-      bcrypt.hash(hook.data.password, salt, function(err, hash) {
-        hook.data.password = hash;
-        return next();
-      });
-    });
-
-  // otherwise it was run as a function at execution.
-  } else {
-    passwordField = passwordField || 'password';
-    return function(hook, next) {
-      bcrypt.genSalt(10, function(err, salt) {
-        bcrypt.hash(hook.data[passwordField], salt, function(err, hash) {
-          hook.data[passwordField] = hash;
-          return next();
-        });
-      });
-    };  
-  }
-};
-
-/**
- * Only authenticated users allowed, period!
- *
- * find, get, create, update, remove
- */
-exports.requireAuth = function (hook, next) {
-  // Allow user to view records without a userId.
-  if (!hook.params.user) {
-    return next(new errors.NotAuthenticated('Please include a valid auth token in the Authorization header.'));
-  } else {
-    return next(null, hook);
-  }
-};
-
-
-/**
- * Add the current user's id to the query params.
- *
- * find, get
- */
-exports.queryWithUserId = function (idInDB, userId) {
-  // If it's called directly as a hook, use defaults of query.userId and user._id.
-  if(typeof arguments[0] === 'object'){
-    console.log('Running setOwner hook with defaults of query.userId and user._id');
-    var hook = arguments[0];
-    var next = arguments[1];
-
-    hook.params.query.userId = hook.params.user._id;
-    return next(null, hook);
-
-  // otherwise it was run as a function at execution.
-  } else {
-    return function(hook, next) {
-      hook.params.query[idInDB] = hook.params.user[userId];
-      return next(null, hook);      
-    };  
-  }
-};
-
-
-/**
- * Checks that the action is performed by an admin or owner of the userId.
- * // TODO: Fix this.
- *
- * find, get, create, update, remove
- */
-exports.verifyOwnership = function (hook, next) {
-  if (hook.params.user.admin) {
-    hook.params.query.userId = hook.params.user._id;
-  }
-  return next(null, hook);
-};
-
-
-/**
- * Set the userId as the owner.
- *
- * find, get, create, update, remove
- */
-exports.setOwnerIfNotAdmin = function (hook, next) {
-  if (!hook.params.user.admin) {
-    hook.params.query.userId = hook.params.user._id;
-  }
-  return next(null, hook);
-};
-
-
-/**
- * restrictToSelf - non-admins can't see other users.
- * USER service only!
- *
- * find, get, create, update, remove
- */
-exports.restrictToSelf = function (hook, next) {
-  if (!hook.params.user.admin) {
-    hook.params.query._id = hook.params.user._id;
-  }
-  return next(null, hook);
-};
-
-
-/**
- * Stop
- *
- * find, get, create, update, remove
- */
-exports.stop = function (hook, next) {
-  return next(new errors.Forbidden('Safety check. We just stopped you from blowing things up.'));
-};
-
-/**
- * lowercaseEmail
- * If email is passed in, lowercase it for consistent logins.
- *
- * update
- */
-exports.lowercaseEmail = function (hook, next) {
-
-  // Allow user to view records without a userId.
-  if (hook.data.email) {
-    hook.data.email = hook.data.email.toLowerCase();
-  }
-  return next(null, hook);
-};
-
-
-/**
- * Authenticated users can have their own records (with their userId),
- * and non-authenticated users can view records that have no userId (public).
- *
- * find, get, create, update, remove
- */
-exports.requireAuthForPrivate = function(hook, next){
-
-  // If no user, limit to public records (no userId)
-  if (!hook.params.user) {
-    hook.params.query.userId = null;
-    return next();
-  }
-
-  return next(null, hook);
-};
-
-
-/**
- * Set up the userId on data.
- *
- * create
- */
-exports.setUserID = function(hook, next){
-
-  // If a user is logged in, set up the userId on the data.
-  if (hook.params && hook.params.user && !hook.data.userId) {
-    hook.data.userId = hook.params.user._id;
-  }
-  return next(null, hook);
-};
-
-
-/**
- * If the user is not an admin, remove any admin attribute.  This prevents
- * unauthorized users from setting other users up as administrators.
- * This typically would be used on a user-type service.
- *
- * create, update
- */
-exports.requireAdminToSetAdmin = function(hook, next){
-
-  // If not logged in or logged in but not an admin,
-  if (hook.params.user && !hook.params.user.admin) {
-
-    // delete admin before save.
-    delete hook.data.admin;
-  }
-
-  return next(null, hook);
-};
-
-/**
- * Log a hook to the console for debugging.
- * before or after
- *
- * find, get, create, update, delete
- */
-exports.log = function(hook, next){
-  console.log(hook);
-  return next(null, hook);
-};
-exports.logData = function(hook, next){
-  console.log(hook.data);
-  return next(null, hook);
-};
-exports.logParams = function(hook, next){
-  console.log(hook.params);
-  return next(null, hook);
-};
diff --git a/packages/authentication/src/hooks/hash-password.js b/packages/authentication/src/hooks/hash-password.js
index ad11e1d349..a5eb1ddfd5 100644
--- a/packages/authentication/src/hooks/hash-password.js
+++ b/packages/authentication/src/hooks/hash-password.js
@@ -5,21 +5,26 @@ import bcrypt from 'bcrypt';
  * of the password.
  * @param  {String} passwordField  The field containing the password.
  */
-export default function(passwordField = 'password'){
+export default function(options = {}){
+  const defaults = {passwordField: 'password'};
+  options = Object.assign({}, defaults, options);
+
   return function(hook) {
+    if (!hook.data || !hook.data[options.passwordField]) {
+      return hook;
+    }
 
-    return new Promise(function(resolve, reject) {
+    return new Promise(function(resolve, reject){
       bcrypt.genSalt(10, function(err, salt) {
-        bcrypt.hash(hook.data[passwordField], salt, function(err, hash) {
+        bcrypt.hash(hook.data[options.passwordField], salt, function(err, hash) {
           if (err) {
-            reject(err);
-          } else {
-            hook.data[passwordField] = hash;
-            resolve(hook);
+            return reject(err);
           }
+          
+          hook.data[options.passwordField] = hash;
+          resolve(hook);
         });
       });
     });
-
   };
 }
diff --git a/packages/authentication/src/hooks/index.js b/packages/authentication/src/hooks/index.js
index 3e8817669c..6b19587f3f 100644
--- a/packages/authentication/src/hooks/index.js
+++ b/packages/authentication/src/hooks/index.js
@@ -5,6 +5,9 @@ import requireAuth from './require-auth';
 import restrictToSelf from './restrict-to-self';
 import setUserId from './set-user-id';
 import toLowerCase from './to-lower-case';
+import verifyToken from './verify-token';
+import populateUser from './populate-user';
+import normalizeAuthToken from './normalize-auth-token';
 
 let hooks = {
   hashPassword,
@@ -13,7 +16,10 @@ let hooks = {
   requireAuth,
   restrictToSelf,
   setUserId,
-  toLowerCase
+  toLowerCase,
+  verifyToken,
+  populateUser,
+  normalizeAuthToken
 };
 
 export default hooks;
diff --git a/packages/authentication/src/hooks/normalize-auth-token.js b/packages/authentication/src/hooks/normalize-auth-token.js
new file mode 100644
index 0000000000..981f1191bf
--- /dev/null
+++ b/packages/authentication/src/hooks/normalize-auth-token.js
@@ -0,0 +1,23 @@
+export default function() {
+  return function(hook) {
+    const hasDataToken = hook.data && hook.data.token;
+    const hasQueryToken = hook.params.query && hook.params.query.token;
+
+    if (!hook.params.token) {
+      if (hasDataToken) {
+        hook.params.token = hook.data.token;
+      }
+      else if (hasQueryToken) {
+        hook.params.token = hook.params.query.token;
+      }
+    }
+
+    if (hasDataToken) {
+      delete hook.data.token;
+    }
+    
+    if (hasQueryToken) {
+      delete hook.params.query.token;
+    }
+  };
+}
\ No newline at end of file
diff --git a/packages/authentication/src/hooks/populate-user.js b/packages/authentication/src/hooks/populate-user.js
new file mode 100644
index 0000000000..a6a4d5ef22
--- /dev/null
+++ b/packages/authentication/src/hooks/populate-user.js
@@ -0,0 +1,53 @@
+/**
+ * Populate the current user associated with the JWT
+ */
+const defaults = {
+  userEndpoint: '/users',
+  passwordField: 'password',
+  idField: 'id'
+};
+
+export default function(options = {}){
+  options = Object.assign({}, defaults, options);
+
+  return function(hook) {
+    // If we already have a current user just pass through
+    if (hook.params.user) {
+      return Promise.resolve(hook);
+    }
+
+    let id;
+    
+    // If it's an after hook grab the id from the result
+    if (hook.result) {
+      id = hook.result[options.idField];
+    }
+    // Check to see if we have an id from a decoded JWT
+    else if (hook.params.payload) {
+      id = hook.params.payload.id;
+    }
+
+    // If we didn't find an id then just pass through
+    if (id === undefined) {
+      return Promise.resolve(hook);
+    }
+
+    return new Promise(function(resolve, reject){
+      hook.app.service(options.userEndpoint).get(id, {}).then(user => {
+        // attach the user to the hook for use in other hooks or services
+        hook.params.user = user;
+
+        // If it's an after hook attach the user to the response
+        if (hook.result) {
+          hook.result.data = Object.assign({}, user = !user.toJSON ? user : user.toJSON());
+          
+          // format response
+          delete hook.result.id;
+          delete hook.result.data[options.passwordField];
+        }
+        
+        return resolve(hook);
+      }).catch(reject);
+    });
+  };
+}
diff --git a/packages/authentication/src/hooks/restrict-to-self.js b/packages/authentication/src/hooks/restrict-to-self.js
index 19904f4924..f95d3f7a0c 100644
--- a/packages/authentication/src/hooks/restrict-to-self.js
+++ b/packages/authentication/src/hooks/restrict-to-self.js
@@ -6,12 +6,15 @@
  *
  * find, get, create, update, remove
  */
-export default function restrictToSelf(idProp = '_id') {
-  return function(hook){
+export default function restrictToSelf(options = {}) {
+  const defaults = {idField: '_id'};
+  options = Object.assign({}, defaults, options);
 
+  return function(hook){
     if (hook.params.user) {
-      hook.params.query[idProp] = hook.params.user[idProp];
+      hook.params.query[options.idField] = hook.params.user[options.idField];
+    } else {
+      throw new Error(`Could not find the user\'s ${options.idField} for the restrictToSelf hook.`);
     }
-
   };
 }
diff --git a/packages/authentication/src/hooks/set-user-id.js b/packages/authentication/src/hooks/set-user-id.js
index 45cc223c6e..37a261ee65 100644
--- a/packages/authentication/src/hooks/set-user-id.js
+++ b/packages/authentication/src/hooks/set-user-id.js
@@ -8,27 +8,27 @@
  * before
  * all, find, get, create, update, patch, remove
  */
-export default function setUserId(sourceProp = '_id', destProp = 'userId'){
-  return function(hook) {
+export default function setUserId(options = {}){
+  const defaults = { sourceProp: '_id', destProp: 'userId' };
+  options = Object.assign({}, defaults, options);
 
+  return function(hook) {
     function setId(obj){
-      obj[destProp] = hook.params.user[sourceProp];
+      obj[options.destProp] = hook.params.user[options.sourceProp];
     }
 
     if (hook.params.user) {
-
       // Handle arrays.
       if (Array.isArray(hook.data)) {
         hook.data.forEach(item => {
           setId(item);
         });
-
+      
+      }
       // Handle single objects.
-      } else {
+      else {
         setId(hook.data);
       }
-
     }
-
   };
 }
diff --git a/packages/authentication/src/hooks/to-lower-case.js b/packages/authentication/src/hooks/to-lower-case.js
index 8268d6a1e9..a0844f8403 100644
--- a/packages/authentication/src/hooks/to-lower-case.js
+++ b/packages/authentication/src/hooks/to-lower-case.js
@@ -4,7 +4,9 @@
  * looks for the key on the data object for before hooks and the result object for
  * the after hooks.
  */
-export default function toLowercase(fieldName) {
+export default function toLowercase(options = {}) {
+  const fieldName = options.fieldName;
+  
   if (!fieldName) {
     throw new Error('You must provide the name of the field to use in the toLowerCase hook.');
   }
diff --git a/packages/authentication/src/hooks/verify-token.js b/packages/authentication/src/hooks/verify-token.js
new file mode 100644
index 0000000000..fd5a42e291
--- /dev/null
+++ b/packages/authentication/src/hooks/verify-token.js
@@ -0,0 +1,34 @@
+import jwt from 'jsonwebtoken';
+import errors from 'feathers-errors';
+
+/**
+ * Verifies that a JWT token is valid
+ * 
+ * @param  {Object} options - An options object
+ * @param {String} options.secret - The JWT secret
+ */
+export default function(options = {}){
+  const secret = options.secret;
+
+  return function(hook) {
+    const token = hook.params.token;
+
+    if (!token) {
+      return Promise.resolve(hook);
+    }
+
+    return new Promise(function(resolve, reject){
+      jwt.verify(token, secret, options, function (error, payload) {
+        if (error) {
+          // Return a 401 if the token has expired or is invalid.
+          return reject(new errors.NotAuthenticated(error));
+        }
+        
+        // Attach our decoded token payload to the params
+        hook.params.payload = payload;
+
+        resolve(hook);
+      });
+    });
+  };
+}
diff --git a/packages/authentication/src/index.js b/packages/authentication/src/index.js
index 931670bb2a..e96c8b5ebc 100644
--- a/packages/authentication/src/index.js
+++ b/packages/authentication/src/index.js
@@ -1,243 +1,106 @@
-import makeDebug from 'debug';
-import _ from 'lodash';
-import jwt from 'jsonwebtoken';
+import Debug from 'debug';
+import path from 'path';
+import crypto from 'crypto';
 import passport from 'passport';
-import passportLocal from 'passport-local';
-var LocalStrategy = passportLocal.Strategy;
-import bcrypt from 'bcrypt';
-
-var defaults = {
-  userEndpoint: '/api/users',
-  usernameField: 'username',
-  passwordField: 'password',
-  userProperty: passport._userProperty || 'user',
-  loginEndpoint: '/api/login',
-  loginError: 'Invalid login.',
-  jwtOptions: {
-    expiresIn: 36000, // seconds to expiration. Default is 10 hours.
-  },
-  passport: passport,
+import hooks from './hooks';
+import token from './services/token';
+import local from './services/local';
+import oauth2 from './services/oauth2';
+import * as middleware from './middleware';
+
+const debug = Debug('feathers-authentication:main');
+const PROVIDERS = {
+  token,
+  local
 };
-const debug = makeDebug('feathers-authentication');
-
-export default function(config) {
-  var settings = _.merge(defaults, config);
-
-  if(!settings.secret) {
-    throw new Error('A JWT secret must be provided!');
-  }
 
+export default function(providers) {
   return function() {
-    var app = this;
-    var oldSetup = app.setup;
-
-    app.use(settings.passport.initialize());
-    var strategy = settings.strategy || getDefaultStrategy(app, settings);
-    passport.use(strategy);
-
-    debug('setting up feathers-authentication');
-
-    // Route for token refresh
-    app.post(settings.loginEndpoint + '/refresh', verifyToken, function(req, res) {
-      var data = req.authData;
-      delete data.password;
-      var token = jwt.sign(data, settings.secret, settings.jwtOptions);
-      return res.json({
-        token: token,
-        data: data
-      });
-    });
+    const app = this;
+    let _super = app.setup;
 
-    // Add a route for passport login and token refresh.
-    app.post(settings.loginEndpoint, verifyToken, function(req, res, next) {
-      // Non-expired token was passed in and refreshed
-      if (req.authData) {
-        var data = req.authData;
-        delete req.authData.password;
-        var token = jwt.sign(req.authData, settings.secret, settings.jwtOptions);
-        return res.json({
-          token: token,
-          data: data
-        });
-
-      // Otherwise, authenticate the user and return a token
-      } else {
-        passport.authenticate('local', { session: false }, function(err, user) {
-          if (err) {
-            return res.status(500).json(err);
-          }
-
-          // Login was successful. Generate and send token.
-          if (user) {
-            user = !user.toJSON ? user : user.toJSON();
-            delete user.password;
-            var token = jwt.sign(user, settings.secret, settings.jwtOptions);
-            return res.json({
-              token: token,
-              data: user
-            });
-
-          // Login failed.
-          } else {
-            return res.status(401).json({
-              code: 401,
-              name: 'NotAuthenticated',
-              message: settings.loginError
-            });
-          }
-        })(req, res, next);
-      }
-    })
-
-    // Make the Passport user available for REST services.
-    .use(function(req, res, next) {
-      if (req.headers.authorization) {
-        var token = req.headers.authorization.split(' ')[1];
-        debug('Got an Authorization token', token);
-        // TODO: Move token verification into its own middleware. See line ~44.
-        jwt.verify(token, settings.secret, function(err, data) {
-          if (err) {
-            // Return a 401 Unauthorized if the token has expired.
-            if (err.name === 'TokenExpiredError') {
-              return res.status(401).json(err);
-            }
-            return next(err);
-          }
-          // A valid token's data is set up on feathers.user.
-          req.feathers = _.extend({ user: data }, req.feathers);
-          return next();
-        });
-      } else {
-        return next();
-      }
+    // Add mixin to normalize the auth token on the params
+    app.mixins.push(function(service){
+      service.before(hooks.normalizeAuthToken());
     });
 
-    app.setup = function() {
-      var result = oldSetup.apply(this, arguments);
-      var io = app.io;
-      var primus = app.primus;
+    // REST middleware
+    if (app.rest) {
+      debug('registering REST authentication middleware');
+      // Make the Passport user available for REST services.
+      // app.use( middleware.exposeAuthenticatedUser() );
+      // Get the token and expose it to REST services.
+      // TODO (EK): Maybe make header key configurable
+      app.use( middleware.normalizeAuthToken() );
+    }
 
-      debug('running app.setup');
+    // NOTE (EK): Currently we require token based auth so
+    // if the developer didn't provide a config for our token
+    // provider then we'll set up a sane default for them.
+    if (providers.token === undefined) {
+      providers.token = {
+        secret: crypto.randomBytes(64).toString('base64')
+      };
+    }
 
-      function setUserData(socket, data) {
-        socket.feathers = _.extend({ user: data }, socket.feathers);
-      }
+    // If they didn't pass in a local provider let's set one up
+    // for them with the default options.
+    if (providers.local === undefined) {
+      providers.local = {};
+    }
 
-      function checkToken(token, socket, callback) {
-        if (!token) {
-          return callback(null, true);
-        }
-        jwt.verify(token, settings.secret, function(err, data) {
-          if (err) {
-            return callback(err);
-          }
-          setUserData(socket, data);
-          callback(null, data);
-        });
-      }
+    const authOptions = Object.assign({ successRedirect: '/auth/success' }, providers.local, providers.token);
+    
+    app.use(passport.initialize());
+
+    app.setup = function() {
+      let result = _super.apply(this, arguments);
 
       // Socket.io middleware
-      if (io) {
-        debug('intializing SocketIO middleware');
-        io.use(function (socket, next) {
-
-          // If there's a token in place, decode it and set up the feathers.user
-          checkToken(socket.handshake.query.token, socket, function(err, data){
-            if(err) {
-              return next(err);
-            }
-
-            // If no token was passed, still allow the websocket. Service hooks can take care of Auth.
-            if(data === true) {
-              return next(null, true);
-            }
-
-            socket.on('authenticate', function (data) {
-              checkToken(data.token, socket, function (err, data) {
-                delete data.password;
-                if (data) {
-                  socket.emit('authenticated', data);
-                }
-              });
-            });
-
-            return next(null, data);
-          });
-        });
+      if (app.io) {
+        debug('registering Socket.io authentication middleware');
+        app.io.on('connection', middleware.setupSocketIOAuthentication(app, authOptions));
       }
 
       // Primus middleware
-      if(primus) {
-        debug('intializing Primus middleware');
-        primus.authorize(function(req, done) {
-          checkToken(req.handshake.query.token, req, done);
-        });
+      if (app.primus) {
+        debug('registering Primus authentication middleware');
+        app.primus.on('connection', middleware.setupPrimusAuthentication(app, authOptions));
       }
 
       return result;
     };
-  };
-  function verifyToken(req, res, next) {
-    if(req.body.token) {
-      jwt.verify(req.body.token, settings.secret, function (err, data) {
-        if (err) {
-          // Return a 401 Unauthorized if the token has expired.
-          if (err.name === 'TokenExpiredError') {
-            return res.status(401).json(err);
-          }
-          return next(err);
-        }
-        req.authData = data;
-        next();
-      });
-    } else {
-      next();
-    }
-  }
-}
 
-function getDefaultStrategy(app, settings){
-  var strategySetup = {
-    usernameField: settings.usernameField,
-    passwordField: settings.passwordField
-  };
-  return new LocalStrategy(strategySetup, function(username, password, done) {
-    var findParams = {
-      internal: true,
-      query: {}
-    };
-    findParams.query[settings.usernameField] = username;
-    app.service(settings.userEndpoint).find(findParams, function(error, users) {
-      // Handle any 500 server errors.
-      if(error) {
-        return done(error);
+    // Merge all of our options and configure the appropriate service
+    Object.keys(providers).forEach(function (key) {
+      // Check to see if the key is a local or token provider
+      let provider = PROVIDERS[key];
+      let providerOptions = providers[key];
+
+      // If it's not one of our own providers then determine whether it is oauth1 or oauth2
+      if (!provider) {
+        // Check to see if it is an oauth2 provider
+        if (providerOptions.clientID && providerOptions.clientSecret) {
+          provider = oauth2;
+        } 
+        // Check to see if it is an oauth1 provider
+        else if (providerOptions.consumerKey && providerOptions.consumerSecret){
+          throw new Error(`Sorry we don't support OAuth1 providers right now. Try using a ${key} OAuth2 provider.`);
+        }
+        else if (!provider) {
+          throw new Error(`Invalid '${key}' provider configuration.\nYou need to provide your 'clientID' and 'clientSecret' if using an OAuth2 provider or your 'consumerKey' and 'consumerSecret' if using an OAuth1 provider.`);
+        }
       }
 
-      // Paginated services return the array of results in the data attribute.
-      var user = users[0] || users.data && users.data[0];
+      const options = Object.assign({ provider: key, endPoint: `/auth/${key}` }, providerOptions, authOptions);
 
-      // Handle bad username.
-      if(!user) {
-        return done(null, false);
-      }
-      // Check password
-      bcrypt.compare(password, user[settings.passwordField], function(err, res) {
-        // Handle 500 server error.
-        if (err) {
-          return done(err);
-        }
-        // Successful login.
-        if (res) {
-          return done(null, user);
-        // Handle bad password.
-        } else {
-          return done(null, false);
-        }
-      });
+      app.configure( provider(options) );
     });
-  });
+
+    app.get(authOptions.successRedirect, function(req, res){
+      res.sendFile(path.resolve(__dirname, 'public', 'auth-success.html'));
+    });
+  };
 }
 
-import hooks from './hooks/index';
-// Make the password hashing hook available separately.
-export {hooks};
+export { hooks };
diff --git a/packages/authentication/src/middleware/index.js b/packages/authentication/src/middleware/index.js
new file mode 100644
index 0000000000..9a93efd77f
--- /dev/null
+++ b/packages/authentication/src/middleware/index.js
@@ -0,0 +1,210 @@
+import Debug from 'debug';
+import errors from 'feathers-errors';
+
+const debug = Debug('feathers-authentication:middleware');
+const FIVE_SECONDS = 5000;
+const TEN_HOURS = 36000;
+const defaults = {
+  timeout: FIVE_SECONDS,
+  tokenEndpoint: '/auth/token',
+  localEndpoint: '/auth/local'
+};
+
+// Usually this is a big no no but passport requires the 
+// request object to inspect req.body and req.query so we
+// need to miss behave a bit. Don't do this in your own code!
+export let exposeConnectMiddleware = function(req, res, next) {
+  req.feathers.req = req;
+  req.feathers.res = res;
+  next();
+};
+
+// Make the authenticated passport user also available for REST services
+// export let exposeAuthenticatedUser = function(options = {}) {
+//   return function(req, res, next) {
+//     req.feathers.user = req.user;
+//     next();
+//   };
+// };
+
+// Make the authenticated passport user also available for REST services
+export let normalizeAuthToken = function(options = {}) {
+  const defaults = {
+    header: 'authorization',
+    cookie: 'feathers-jwt'
+  };
+
+  options = Object.assign({}, defaults, options);
+
+  return function(req, res, next) {
+    let token = req.headers[options.header];
+    
+    // Check the header for the token (preferred method)
+    if (token) {
+      // if the value contains "bearer" or "Bearer" then cut that part out
+      if ( /bearer/i.test(token) ) {
+        token = token.split(' ')[1];
+      }
+    }
+
+    // If we don't already have token in the header check for a cookie
+    if (!token && req.cookies && req.cookies[options.cookie]) {
+      token = req.cookies[options.cookie];
+    }
+    // Check the body next if we still don't have a token
+    else if (req.body.token) {
+      token = req.body.token;
+      delete req.body.token;
+    }
+    // Finally, check the query string. (worst method)
+    else if (req.query.token) {
+      token = req.query.token;
+      delete req.query.token;
+    }
+
+    // Tack it on to our feathers object so that it is passed to services
+    req.feathers.token = token;
+
+    next();
+  };
+};
+
+export let successfulLogin = function(options = {}) {
+  return function(req, res, next) {
+    // NOTE (EK): If we are not dealing with a browser or it was an
+    // XHR request then just skip this. This is primarily for
+    // handling the oauth redirects and for us to securely send the
+    // JWT to the client.
+    if (req.xhr || !req.accepts('html')) {
+      return next();
+    }
+
+    // clear any previous JWT cookie
+    res.clearCookie('feathers-jwt');
+
+    // Set a our JWT in a cookie.
+    // TODO (EK): Look into hardening this cookie a bit.
+    let expiration = new Date();
+    expiration.setTime(expiration.getTime() + TEN_HOURS);
+
+    res.cookie('feathers-jwt', res.data.token, { expires: expiration});
+
+    // Redirect to our success route
+    res.redirect(options.successRedirect);
+  };
+};
+
+export let setupSocketIOAuthentication = function(app, options = {}) {
+  options = Object.assign(options, defaults);
+  
+  debug('Setting up Socket.io authentication middleware with options:', options);
+
+  return function(socket) {
+    let errorHandler = function(error) {
+      socket.emit('unauthorized', error, function(){
+        // TODO (EK): Maybe we support disconnecting the socket
+        // if a certain number of authorization attempts have failed
+        // for brute force protection
+        // socket.disconnect('unauthorized');
+      });
+
+      throw error;
+    };
+
+    // Expose the request object to services and hooks
+    // for Passport. This is normally a big no no.
+    socket.feathers.req = socket.request;
+
+    socket.on('authenticate', function(data) {
+      // Authenticate the user using token strategy
+      if (data.token) {
+        if (typeof data.token !== 'string') {
+          return errorHandler(new errors.BadRequest('Invalid token data type.'));
+        }
+
+        // The token gets normalized in hook.params for REST so we'll stay with
+        // convention and pass it as params using sockets.
+        app.service(options.tokenEndpoint).create({}, data).then(response => {
+          socket.feathers.user = response.data;
+          socket.emit('authenticated', response);
+        }).catch(errorHandler);
+      }
+      // Authenticate the user using local auth strategy
+      else {
+        // Put our data in a fake req.body object to get local auth
+        // with Passport to work because it checks res.body for the 
+        // username and password.
+        let params = {
+          req: socket.request
+        };
+
+        params.req.body = data;
+
+        app.service(options.localEndpoint).create(data, params).then(response => {
+          socket.feathers.user = response.data;
+          socket.emit('authenticated', response);
+        }).catch(errorHandler);
+      }
+    });
+  };
+};
+
+// TODO (EK): DRY this up along with the code in setupSocketIOAuthentication
+export let setupPrimusAuthentication = function(app, options = {}) {
+  options = Object.assign(options, defaults);
+
+  debug('Setting up Primus authentication middleware with options:', options);
+
+  return function(socket) {
+    let errorHandler = function(error) {
+      socket.send('unauthorized', error);
+      // TODO (EK): Maybe we support disconnecting the socket
+      // if a certain number of authorization attempts have failed
+      // for brute force protection
+      // socket.end('unauthorized', error);
+      throw error;
+    };
+
+    socket.request.feathers.req = socket.request;
+
+    socket.on('authenticate', function(data) {
+      // Authenticate the user using token strategy
+      if (data.token) {
+        if (typeof data.token !== 'string') {
+          return errorHandler(new errors.BadRequest('Invalid token data type.'));
+        }
+
+        // The token gets normalized in hook.params for REST so we'll stay with
+        // convention and pass it as params using sockets.
+        app.service(options.tokenEndpoint).create({}, data).then(response => {
+          socket.request.feathers.user = response.data;
+          socket.send('authenticated', response);
+        }).catch(errorHandler);
+      }
+      // Authenticate the user using local auth strategy
+      else {
+        // Put our data in a fake req.body object to get local auth
+        // with Passport to work because it checks res.body for the 
+        // username and password.
+        let params = {
+          req: socket.request
+        };
+
+        params.req.body = data;
+
+        app.service(options.localEndpoint).create(data, params).then(response => {
+          socket.request.feathers.user = response.data;
+          socket.send('authenticated', response);
+        }).catch(errorHandler);
+      }
+    });
+  };
+};
+
+export default {
+  exposeConnectMiddleware,
+  normalizeAuthToken,
+  successfulLogin,
+  setupSocketIOAuthentication,
+  setupPrimusAuthentication
+};
\ No newline at end of file
diff --git a/packages/authentication/src/public/auth-success.html b/packages/authentication/src/public/auth-success.html
new file mode 100644
index 0000000000..c1cc030d4c
--- /dev/null
+++ b/packages/authentication/src/public/auth-success.html
@@ -0,0 +1,86 @@
+
+
+
+  
+  Feathers Authentication Success
+  
+  
+
+
+  
+
+  
+

Success

+

You are now logged in. We've stored your JWT in a cookie with the name "feathers-jwt" for you. It is:

+

+  
+ + + \ No newline at end of file diff --git a/packages/authentication/src/services/local/index.js b/packages/authentication/src/services/local/index.js new file mode 100644 index 0000000000..8dabe04842 --- /dev/null +++ b/packages/authentication/src/services/local/index.js @@ -0,0 +1,121 @@ +import Debug from 'debug'; +import errors from 'feathers-errors'; +import bcrypt from 'bcrypt'; +import passport from 'passport'; +import { Strategy } from 'passport-local'; +import { exposeConnectMiddleware } from '../../middleware'; +import { successfulLogin } from '../../middleware'; + +const debug = Debug('feathers-authentication:local'); +const defaults = { + userEndpoint: '/users', + usernameField: 'email', + passwordField: 'password', + userProperty: passport._userProperty || 'user', + localEndpoint: '/auth/local', + tokenEndpoint: '/auth/token' +}; + +export class Service { + constructor(options = {}) { + this.options = options; + } + + checkCredentials(username, password, done) { + const params = { + internal: true, + query: { + [this.options.usernameField]: username + } + }; + + // Look up the user + this.app.service(this.options.userEndpoint) + .find(params) + .then(users => { + // Paginated services return the array of results in the data attribute. + let user = users[0] || users.data && users.data[0]; + + // Handle bad username. + if (!user) { + return done(null, false); + } + + return user; + }) + .then(user => { + // Check password + bcrypt.compare(password, user[this.options.passwordField], function(error, result) { + // Handle 500 server error. + if (error) { + return done(error); + } + // Successful login. + if (result) { + return done(null, user); + } + // Handle bad password. + return done(null, false); + }); + }) + .catch(done); + } + + // POST /auth/local + create(data, params) { + const options = this.options; + let app = this.app; + + // Validate username and password, then generate a JWT and return it + return new Promise(function(resolve, reject){ + let middleware = passport.authenticate('local', { session: false }, function(error, user) { + if (error) { + return reject(error); + } + + // Login failed. + if (!user) { + return reject(new errors.NotAuthenticated('Invalid login.')); + } + + // Login was successful. Generate and send token. + // TODO (EK): Maybe the id field should be configurable + const payload = { + id: user.id !== undefined ? user.id : user._id + }; + + // Get a new JWT and the associated user from the Auth token service and send it back to the client. + return app.service(options.tokenEndpoint) + .create(payload, { internal: true }) + .then(resolve) + .catch(reject); + }); + + middleware(params.req); + }); + } + + setup(app) { + // attach the app object to the service context + // so that we can call other services + this.app = app; + } +} + +export default function(options){ + options = Object.assign({}, defaults, options); + debug('configuring local authentication service with options', options); + + return function() { + const app = this; + + // Initialize our service with any options it requires + app.use(options.localEndpoint, exposeConnectMiddleware, new Service(options), successfulLogin(options)); + + // Get our initialize service to that we can bind hooks + const localService = app.service(options.localEndpoint); + + // Register our local auth strategy and get it to use the passport callback function + passport.use(new Strategy(options, localService.checkCredentials.bind(localService))); + }; +} diff --git a/packages/authentication/src/services/oauth2/index.js b/packages/authentication/src/services/oauth2/index.js new file mode 100644 index 0000000000..e1e86dfd28 --- /dev/null +++ b/packages/authentication/src/services/oauth2/index.js @@ -0,0 +1,186 @@ +import Debug from 'debug'; +import errors from 'feathers-errors'; +import passport from 'passport'; +import { exposeConnectMiddleware } from '../../middleware'; +import { successfulLogin } from '../../middleware'; + +const debug = Debug('feathers-authentication:oauth2'); +const defaults = { + successRedirect: '/auth/success', + passwordField: 'password', + userEndpoint: '/users', + tokenEndpoint: '/auth/token', + passReqToCallback: true, + callbackSuffix: 'callback', + permissions: {} +}; + +export class Service { + constructor(options = {}) { + this.options = options; + } + + oauthCallback(req, accessToken, refreshToken, profile, done) { + let app = this.app; + const options = this.options; + const params = { + internal: true, + query: { + // facebookId: profile.id + [`${options.provider}Id`]: profile.id + } + }; + + // console.log('Authenticating', accessToken, refreshToken, profile); + + // Find or create the user since they could have signed up via facebook. + app.service(options.userEndpoint) + .find(params) + .then(users => { + // Paginated services return the array of results in the data attribute. + let user = users[0] || users.data && users.data[0]; + + // If user found return them + if (user) { + return done(null, user); + } + + // No user found so we need to create one. + // + // TODO (EK): This is where we should look at req.user and see if we + // can consolidate profiles. We might want to give the developer a hook + // so that they can control the consolidation strategy. + profile._json.accessToken = accessToken; + + let data = Object.assign({ + [`${options.provider}Id`]: profile.id, + [`${options.provider}`]: profile._json + }); + + return app.service(options.userEndpoint).create(data, { internal: true }).then(user => { + return done(null, user); + }).catch(done); + }).catch(done); + } + + // GET /auth/facebook + find(params) { + // Authenticate via your provider. This will redirect you to authorize the application. + const authOptions = Object.assign({session: false}, this.options.permissions); + return passport.authenticate(this.options.provider, authOptions)(params.req, params.res); + } + + // For GET /auth/facebook/callback + get(id, params) { + const options = this.options; + const authOptions = Object.assign({session: false}, options.permissions); + let app = this.app; + + // TODO (EK): Make this configurable + if (id !== 'callback') { + return Promise.reject(new errors.NotFound()); + } + + return new Promise(function(resolve, reject){ + + let middleware = passport.authenticate(options.provider, authOptions, function(error, user) { + if (error) { + return reject(error); + } + + // Login failed. + if (!user) { + return reject(new errors.NotAuthenticated(`An error occurred logging in with ${options.provider}`)); + } + + // Login was successful. Clean up the user object for the response. + // TODO (EK): Maybe the id field should be configurable + const payload = { + id: user.id !== undefined ? user.id : user._id + }; + + // Get a new JWT and the associated user from the Auth token service and send it back to the client. + return app.service(options.tokenEndpoint) + .create(payload, { internal: true }) + .then(resolve) + .catch(reject); + }); + + middleware(params.req, params.res); + }); + } + + // // POST /auth/facebook /auth/facebook:: + // create(data, params) { + // // TODO (EK): This should be for token based auth + // const options = this.options; + + // // Authenticate via facebook, then generate a JWT and return it + // return new Promise(function(resolve, reject){ + // let middleware = passport.authenticate('facebook-token', { session: false }, function(error, user) { + // if (error) { + // return reject(error); + // } + + // // Login failed. + // if (!user) { + // return reject(new errors.NotAuthenticated(options.loginError)); + // } + + // // Login was successful. Generate and send token. + // user = Object.assign({}, user = !user.toJSON ? user : user.toJSON()); + // delete user[options.passwordField]; + + // // TODO (EK): call this.app.service('/auth/token').create() instead + // const token = jwt.sign(user, options.secret, options); + + // return resolve({ + // token: token, + // data: user + // }); + // }); + + // middleware(params.req); + // }); + // } + + setup(app) { + // attach the app object to the service context + // so that we can call other services + this.app = app; + } +} + +export default function(options){ + options = Object.assign({}, defaults, options); + + if (!options.provider) { + throw new Error('You need to pass a `provider` for your authentication provider'); + } + + if (!options.endPoint) { + throw new Error(`You need to provide an 'endPoint' for your ${options.provider} provider`); + } + + if (!options.strategy) { + throw new Error(`You need to provide a Passport 'strategy' for your ${options.provider} provider`); + } + + options.callbackURL = options.callbackURL || `${options.endPoint}/${options.callbackSuffix}`; + + debug(`configuring ${options.provider} OAuth2 service with options`, options); + + return function() { + const app = this; + const Strategy = options.strategy; + + // Initialize our service with any options it requires + app.use(options.endPoint, exposeConnectMiddleware, new Service(options), successfulLogin(options)); + + // Get our initialized service + const service = app.service(options.endPoint); + + // Register our Passport auth strategy and get it to use our passport callback function + passport.use(new Strategy(options, service.oauthCallback.bind(service))); + }; +} diff --git a/packages/authentication/src/services/token/index.js b/packages/authentication/src/services/token/index.js new file mode 100644 index 0000000000..74488b9efb --- /dev/null +++ b/packages/authentication/src/services/token/index.js @@ -0,0 +1,134 @@ +import Debug from 'debug'; +import jwt from 'jsonwebtoken'; +import hooks from '../../hooks'; +import errors from 'feathers-errors'; + +const debug = Debug('feathers-authentication:token'); +const defaults = { + userEndpoint: '/users', + passwordField: 'password', + tokenEndpoint: '/auth/token', + issuer: 'feathers', + algorithms: ['HS256'], + expiresIn: '1d', // 1 day +}; + +/** + * Verifies that a JWT token is valid. This is a private hook. + * + * @param {Object} options - An options object + * @param {String} options.secret - The JWT secret + */ +let _verifyToken = function(options = {}){ + const secret = options.secret; + + return function(hook) { + return new Promise(function(resolve, reject){ + if (hook.params.internal) { + hook.params.data = hook.data; + return resolve(hook); + } + + const token = hook.params.token; + + jwt.verify(token, secret, options, function (error, payload) { + if (error) { + // Return a 401 if the token has expired. + return reject(new errors.NotAuthenticated(error)); + } + + // Normalize our params with the token in it. + hook.data = { id: payload.id }; + hook.params.data = Object.assign({}, hook.data, payload, { token }); + hook.params.query = Object.assign({}, hook.params.query, { token }); + resolve(hook); + }); + }); + }; +}; + +export class Service { + constructor(options = {}) { + this.options = options; + } + + // GET /auth/token + // This is sort of a dummy route that we are using just to verify + // that our token is correct by running our verifyToken hook. It + // doesn't refresh our token it just returns our existing one with + // our user data. + // find(params) { + // if (params.data && params.data.token) { + // const token = params.data.token; + // delete params.data.token; + + // return Promise.resolve({ + // token: token, + // data: params.data + // }); + // } + + // return Promise.reject(new errors.GeneralError('Something weird happened')); + // } + + // GET /auth/token/refresh + get(id, params) { + if (id !== 'refresh') { + return Promise.reject(new errors.NotFound()); + } + + const options = this.options; + const data = params.data; + // Our before hook determined that we had a valid token or that this + // was internally called so let's generate a new token with the user + // id and return both the ID and the token. + return new Promise(function(resolve){ + jwt.sign(data, options.secret, options, token => { + return resolve( Object.assign(data, { token }) ); + }); + }); + } + + // POST /auth/token + create(data) { + const options = this.options; + + // Our before hook determined that we had a valid token or that this + // was internally called so let's generate a new token with the user + // id and return both the ID and the token. + return new Promise(function(resolve){ + jwt.sign(data, options.secret, options, token => { + return resolve( Object.assign(data, { token }) ); + }); + }); + } +} + +export default function(options){ + options = Object.assign({}, defaults, options); + + debug('configuring token authentication service with options', options); + + return function() { + const app = this; + + // Initialize our service with any options it requires + app.use(options.tokenEndpoint, new Service(options)); + + // Get our initialize service to that we can bind hooks + const tokenService = app.service('/auth/token'); + + // Set up our before hooks + tokenService.before({ + create: [_verifyToken(options)], + find: [_verifyToken(options)], + get: [_verifyToken(options)] + }); + + tokenService.after({ + create: [hooks.populateUser(options)], + find: [hooks.populateUser(options)], + get: [hooks.populateUser(options)] + }); + }; +} \ No newline at end of file diff --git a/packages/authentication/test/client/hooks.test.js b/packages/authentication/test/client/hooks.test.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/authentication/test/client/index.test.js b/packages/authentication/test/client/index.test.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/authentication/test/client/storage.test.js b/packages/authentication/test/client/storage.test.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/authentication/test/hooks.test.js b/packages/authentication/test/hooks.test.js deleted file mode 100644 index 20c06fdf04..0000000000 --- a/packages/authentication/test/hooks.test.js +++ /dev/null @@ -1,139 +0,0 @@ -import assert from 'assert'; -import feathers from 'feathers'; -import feathersHooks from 'feathers-hooks'; -import feathersAuth from '../src/index'; -import memory from 'feathers-memory'; -import {hooks} from '../src/index'; - -console.log(require('../lib/index')); - -const app = feathers(); - -app.configure(feathersHooks()) - .configure(feathersAuth({ - secret: 'feathers-rocks' - })); - -// A hook that simulates a logged-in user. -let setUser = function(){ - return function(hook){ - hook.params.user = { - username: 'steinway', - _id: 0, - id: 0 - }; - }; -}; - - -describe('Auth hooks', () => { - it('are available at module.hooks', () => { - assert.equal(typeof require('../lib/index').hooks, 'object'); - }); -}); - -describe('The hashPassword() hook', () => { - it('is found at module.hooks.hashPassword', () => { - assert.equal(typeof require('../lib/index').hooks.hashPassword, 'function'); - }); - - it('hashes passwords at default location "password"', (done) => { - app.use('todos', memory()); - const todos = app.service('todos'); - todos.before({ - create: [hooks.hashPassword()] - }); - - todos.create({username: 'bosendorfer', password: 'feathers'}, function(err, data){ - assert.notEqual(data.password, 'feathers'); - done(); - }); - }); - - it('hashes passwords at custom location "pwd"', (done) => { - app.use('todos', memory()); - const todos = app.service('todos'); - todos.before({ - create: [hooks.hashPassword('pwd')] - }); - - todos.create({username: 'bosendorfer', pwd: 'feathers'}, function(err, data){ - assert.notEqual(data.pwd, 'feathers'); - done(); - }); - }); -}); - - -describe('The queryWithUserId() hook', () => { - it('is found at module.hooks.queryWithUserId', () => { - assert.equal(typeof require('../lib/index').hooks.queryWithUserId, 'function'); - }); - - it('adds the user\'s _id at the default location "userId"', (done) => { - app.use('todos', memory({idField: '_id'})); - const todos = app.service('todos'); - - let setUser = function(){ - return function(hook){ - hook.params.user = { - username: 'steinway', - _id: 0 - }; - }; - }; - - let customHook = function(){ - return function(hook){ - assert.equal(hook.params.query.userId, 0, 'The userId was added to the query params.'); - }; - }; - - todos.before({ - find: [setUser(), hooks.queryWithUserId(), customHook()] - }); - - todos.create({username: 'bosendorfer', password: 'feathers'}, function(){ - todos.find({query: {username: 'bosendorfer'}}, function(){ - done(); - }); - }); - }); - - it('adds the user\'s id at the custom location "smurfId"', (done) => { - app.use('todos', memory({idField: '_id'})); - const todos = app.service('todos'); - - let customHook = function(){ - return function(hook){ - assert.equal(hook.params.query.smurf, 0, 'The smurfId was added to the query params.'); - }; - }; - - todos.before({ - find: [setUser(), hooks.queryWithUserId('id', 'smurfId'), customHook()] - }); - - todos.create({username: 'bosendorfer', password: 'feathers'}, function(){ - todos.find({query: {username: 'bosendorfer'}}, function(){ - done(); - }); - }); - }); - - it('returns an error if no user is logged in', (done) => { - app.use('todos', memory({idField: '_id'})); - const todos = app.service('todos'); - - todos.before({ - find: [hooks.queryWithUserId()] - }); - - todos.create({username: 'bosendorfer', password: 'feathers'}, function(){ - todos.find({query:{}}, function(err){ - assert.equal(typeof err, 'object'); - done(); - }); - }); - }); -}); diff --git a/packages/authentication/test/integration/primus.test.js b/packages/authentication/test/integration/primus.test.js new file mode 100644 index 0000000000..afb143a90a --- /dev/null +++ b/packages/authentication/test/integration/primus.test.js @@ -0,0 +1,204 @@ +import assert from 'assert'; +import createApplication from '../test-server'; +import jwt from 'jsonwebtoken'; + +describe('Primus authentication', function() { + this.timeout(10000); + const host = 'http://localhost:8888'; + + let server, app, primus, Socket; + let email = 'test@feathersjs.com'; + let password = 'test'; + let settings = { + token: { + secret: 'feathers-rocks' + } + }; + let jwtOptions = { + issuer: 'feathers', + algorithms: ['HS256'], + expiresIn: '1h' // 1 hour + }; + + // create a valid JWT + let validToken = jwt.sign({ id: 0 }, settings.token.secret, jwtOptions); + + // create an expired JWT + jwtOptions.expiresIn = 1; // 1 ms + let expiredToken = jwt.sign({ id: 0 }, settings.token.secret, jwtOptions); + + before((done) => { + createApplication(settings, email, password, false, (err, obj) =>{ + app = obj.app; + server = obj.server; + Socket = app.primus.Socket; + + // Add a quick timeout to make sure that our token is expired + setTimeout(done, 10); + }); + }); + + after(function(done) { + server.close(done); + }); + + beforeEach(done => { + primus = new Socket(host); + primus.on('open', function() { + done(); + }); + }); + + afterEach(() => { + primus.end(); + }); + + describe('Local authentication', () => { + describe('when login unsuccessful', () => { + it('returns a 401 when user not found', function(done) { + const data = { + email: 'not-found@feathersjs.com', + password + }; + + primus.on('unauthorized', function(error) { + assert.equal(error.code, 401); + done(); + }); + + primus.send('authenticate', data); + }); + + it('returns a 401 when password is invalid', function(done) { + const data = { + email: 'testd@feathersjs.com', + password: 'invalid' + }; + + primus.on('unauthorized', function(error) { + assert.equal(error.code, 401); + done(); + }); + + primus.send('authenticate', data); + }); + + it.skip('disconnects the socket', function(done) { + const data = { + token: expiredToken + }; + + primus.on('close', function() { + done(); + }); + + primus.send('authenticate', data); + }); + }); + + describe('when login succeeds', () => { + it('returns a JWT', function(done) { + const data = { + email, + password + }; + + primus.on('authenticated', function(response) { + assert.ok(response.token); + done(); + }); + + primus.send('authenticate', data); + }); + + it('returns the logged in user', function(done) { + const data = { + email, + password + }; + + primus.on('authenticated', function(response) { + assert.equal(response.data.email, 'test@feathersjs.com'); + done(); + }); + + primus.send('authenticate', data); + }); + }); + }); + + describe('Token authentication', () => { + describe('when login unsuccessful', () => { + + it('returns a 401 when token is invalid', function(done) { + const data = { + token: 'invalid' + }; + + primus.on('unauthorized', function(error) { + assert.equal(error.code, 401); + done(); + }); + + primus.send('authenticate', data); + }); + + it('returns a 401 when token is expired', function(done) { + const data = { + token: expiredToken + }; + + primus.on('unauthorized', function(error) { + assert.equal(error.code, 401); + done(); + }); + + primus.send('authenticate', data); + }); + + it.skip('disconnects the socket', function(done) { + const data = { + token: expiredToken + }; + + primus.on('close', function() { + done(); + }); + + primus.send('authenticate', data); + }); + }); + + describe('when login succeeds', () => { + const data = { token: validToken }; + + it('returns a JWT', function(done) { + primus.on('authenticated', function(response) { + assert.ok(response.token); + done(); + }); + + primus.send('authenticate', data); + }); + + it('returns the logged in user', function(done) { + primus.on('authenticated', function(response) { + assert.equal(response.data.email, 'test@feathersjs.com'); + done(); + }); + + primus.send('authenticate', data); + }); + }); + }); + + describe('OAuth1 authentication', () => { + // TODO (EK): This isn't really possible with primus unless + // you are sending auth_tokens from your OAuth1 provider + }); + + describe('OAuth2 authentication', () => { + // TODO (EK): This isn't really possible with primus unless + // you are sending auth_tokens from your OAuth2 provider + }); +}); diff --git a/packages/authentication/test/integration/rest.test.js b/packages/authentication/test/integration/rest.test.js new file mode 100644 index 0000000000..151c6d715f --- /dev/null +++ b/packages/authentication/test/integration/rest.test.js @@ -0,0 +1,238 @@ +import assert from 'assert'; +import request from 'request'; +import createApplication from '../test-server'; +import jwt from 'jsonwebtoken'; + +describe('REST authentication', function() { + this.timeout(10000); + const host = 'http://localhost:8888'; + + let server, app; + let email = 'test@feathersjs.com'; + let password = 'test'; + let settings = { + token: { + secret: 'feathers-rocks' + } + }; + let jwtOptions = { + issuer: 'feathers', + algorithms: ['HS256'], + expiresIn: '1h' // 1 hour + }; + + // create a valid JWT + let validToken = jwt.sign({ id: 0 }, settings.token.secret, jwtOptions); + + // create an expired JWT + jwtOptions.expiresIn = 1; // 1 ms + let expiredToken = jwt.sign({ id: 0 }, settings.token.secret, jwtOptions); + + before((done) => { + createApplication(settings, email, password, true, (err, obj) =>{ + app = obj.app; + server = obj.server; + + setTimeout(done, 10); + }); + }); + + after(function(done) { + server.close(done); + }); + + describe('Local authentication', () => { + describe('when login unsuccessful', () => { + const options = { + url: `${host}/auth/local`, + method: 'POST', + form: {}, + json: true + }; + + it('returns a 401 when user not found', function(done) { + options.form = { + email: 'not-found@feathersjs.com', + password + }; + + request(options, function(err, response) { + assert.equal(response.statusCode, 401); + done(); + }); + }); + + it('returns a 401 when password is invalid', function(done) { + options.form = { + email: 'testd@feathersjs.com', + password: 'invalid' + }; + + request(options, function(err, response) { + assert.equal(response.statusCode, 401); + done(); + }); + }); + }); + + describe('when login succeeds', () => { + const options = { + url: `${host}/auth/local`, + method: 'POST', + form: { + email, + password + }, + json: true + }; + + it('returns a 201', function(done) { + request(options, function(err, response) { + assert.equal(response.statusCode, 201); + done(); + }); + }); + + it('returns a JWT', function(done) { + request(options, function(err, response, body) { + assert.ok(body.token, 'POST to /auth/local gave us back a token.'); + done(); + }); + }); + + it('returns the logged in user', function(done) { + request(options, function(err, response, body) { + assert.equal(body.data.email, 'test@feathersjs.com'); + done(); + }); + }); + }); + }); + + describe('Token authentication', () => { + describe('when login unsuccessful', () => { + const options = { + url: `${host}/auth/token`, + method: 'POST', + form: {}, + json: true + }; + + it('returns a 401 when token is invalid', function(done) { + options.form = { + token: 'invalid' + }; + + request(options, function(err, response) { + assert.equal(response.statusCode, 401); + done(); + }); + }); + + it('returns a 401 when token is expired', function(done) { + options.form = { + token: expiredToken + }; + + request(options, function(err, response) { + assert.equal(response.statusCode, 401); + done(); + }); + }); + }); + + describe('when login succeeds', () => { + const options = { + url: `${host}/auth/token`, + method: 'POST', + form: { + token: validToken + }, + json: true + }; + + it('returns a 201', function(done) { + request(options, function(err, response) { + assert.equal(response.statusCode, 201); + done(); + }); + }); + + it('returns a JWT', function(done) { + request(options, function(err, response, body) { + assert.ok(body.token, 'POST to /auth/token gave us back a token.'); + done(); + }); + }); + + it('returns the logged in user', function(done) { + request(options, function(err, response, body) { + assert.equal(body.data.email, 'test@feathersjs.com'); + done(); + }); + }); + }); + }); + + describe('OAuth1 authentication', () => { + // TODO (EK): This is hard to test + }); + + describe('OAuth2 authentication', () => { + // TODO (EK): This is hard to test + }); + + // it('Requests without auth to an unprotected service will return data.', function(done) { + // request({ + // url: 'http://localhost:8888/api/tasks', + // method: 'GET', + // json: true + // }, function(err, res, tasks) { + // assert.equal(tasks.length, 3, 'Got tasks'); + + // request({ + // url: 'http://localhost:8888/api/tasks/1', + // json: true + // }, function(err, res, task) { + // assert.deepEqual(task, { + // id: '1', + // name: 'Make Pizza.' + // }); + // done(); + // }); + // }); + // }); + + // it('Requests without auth to a protected service will return an error.', function(done) { + // request({ + // url: 'http://localhost:8888/api/todos', + // method: 'GET', + // json: true + // }, function(err, res, body) { + // assert.equal(typeof body, 'string', 'Got an error string back, not an object/array'); + + // request({ + // url: 'http://localhost:8888/api/todos/1', + // json: true + // }, function(err, res, body) { + // assert.equal(typeof body, 'string', 'Got an error string back, not an object/array'); + // done(); + // }); + // }); + // }); + + // it('Requests with a broken token will return a JWT error', function(done) { + // request({ + // url: 'http://localhost:8888/api/todos', + // method: 'GET', + // json: true, + // headers: { + // 'Authorization': 'Bearer abcd' + // } + // }, function(err, res, body) { + // assert.equal(typeof body, 'string', 'Got an error string back, not an object/array'); + // assert.ok(body.indexOf('JsonWebTokenError' > -1), 'Got a JsonWebTokenError'); + // done(); + // }); + // }); +}); diff --git a/packages/authentication/test/integration/socket-io.test.js b/packages/authentication/test/integration/socket-io.test.js new file mode 100644 index 0000000000..62a361febc --- /dev/null +++ b/packages/authentication/test/integration/socket-io.test.js @@ -0,0 +1,206 @@ +import assert from 'assert'; +import io from 'socket.io-client'; +import createApplication from '../test-server'; +import jwt from 'jsonwebtoken'; + +describe('Socket.io authentication', function() { + this.timeout(15000); + const host = 'http://localhost:8888'; + + let server, app, socket; + let email = 'test@feathersjs.com'; + let password = 'test'; + let settings = { + token: { + secret: 'feathers-rocks' + } + }; + let jwtOptions = { + issuer: 'feathers', + algorithms: ['HS256'], + expiresIn: '1h' // 1 hour + }; + + // create a valid JWT + let validToken = jwt.sign({ id: 0 }, settings.token.secret, jwtOptions); + + // create an expired JWT + jwtOptions.expiresIn = 1; // 1 ms + let expiredToken = jwt.sign({ id: 0 }, settings.token.secret, jwtOptions); + + before((done) => { + createApplication(settings, email, password, true, (err, obj) =>{ + app = obj.app; + server = obj.server; + + setTimeout(done, 10); + }); + }); + + after(function(done) { + server.close(done); + }); + + beforeEach(done => { + socket = io(host, { transport: ['websockets'] }); + socket.on('connect', function() { + done(); + }); + }); + + afterEach(() => { + socket.disconnect(); + }); + + describe('Local authentication', () => { + describe('when login unsuccessful', () => { + it('returns a 401 when user not found', function(done) { + const data = { + email: 'not-found@feathersjs.com', + password + }; + + socket.on('unauthorized', function(error) { + assert.equal(error.code, 401); + done(); + }); + + socket.emit('authenticate', data); + }); + + it('returns a 401 when password is invalid', function(done) { + const data = { + email: 'testd@feathersjs.com', + password: 'invalid' + }; + + socket.on('unauthorized', function(error) { + assert.equal(error.code, 401); + done(); + }); + + socket.emit('authenticate', data); + }); + + it.skip('disconnects the socket', function(done) { + const data = { + email: 'testd@feathersjs.com', + password: 'invalid' + }; + + socket.on('disconnect', function(error) { + assert.equal(error.code, 401); + done(); + }); + + socket.emit('authenticate', data); + }); + }); + + describe('when login succeeds', () => { + it('returns a JWT', function(done) { + const data = { + email, + password + }; + + socket.on('authenticated', function(response) { + assert.ok(response.token); + done(); + }); + + socket.emit('authenticate', data); + }); + + it('returns the logged in user', function(done) { + const data = { + email, + password + }; + + socket.on('authenticated', function(response) { + assert.equal(response.data.email, 'test@feathersjs.com'); + done(); + }); + + socket.emit('authenticate', data); + }); + }); + }); + + describe('Token authentication', () => { + describe('when login unsuccessful', () => { + + it('returns a 401 when token is invalid', function(done) { + const data = { + token: 'invalid' + }; + + socket.on('unauthorized', function(error) { + assert.equal(error.code, 401); + done(); + }); + + socket.emit('authenticate', data); + }); + + it('returns a 401 when token is expired', function(done) { + const data = { + token: expiredToken + }; + + socket.on('unauthorized', function(error) { + assert.equal(error.code, 401); + done(); + }); + + socket.emit('authenticate', data); + }); + + it.skip('disconnects the socket', function(done) { + const data = { + token: expiredToken + }; + + socket.on('disconnect', function(error) { + assert.equal(error.code, 401); + done(); + }); + + socket.emit('authenticate', data); + }); + }); + + describe('when login succeeds', () => { + const data = { token: validToken }; + + it('returns a JWT', function(done) { + socket.on('authenticated', function(response) { + assert.ok(response.token); + done(); + }); + + socket.emit('authenticate', data); + }); + + it('returns the logged in user', function(done) { + socket.on('authenticated', function(response) { + assert.equal(response.data.email, 'test@feathersjs.com'); + done(); + }); + + socket.emit('authenticate', data); + }); + }); + }); + + describe('OAuth1 authentication', () => { + // TODO (EK): This isn't really possible with sockets unless + // you are sending auth_tokens from your OAuth1 provider + }); + + describe('OAuth2 authentication', () => { + // TODO (EK): This isn't really possible with sockets unless + // you are sending auth_tokens from your OAuth2 provider + }); +}); diff --git a/packages/authentication/test/rest.expired-token.test.js b/packages/authentication/test/rest.expired-token.test.js deleted file mode 100644 index e42882e7a8..0000000000 --- a/packages/authentication/test/rest.expired-token.test.js +++ /dev/null @@ -1,94 +0,0 @@ -import assert from 'assert'; -import request from 'request'; -import createApplication from './server-fixtures'; - -describe('Test using an expired token', function() { - this.timeout(10000); - let server, - app, - username = 'feathers', - password = 'test', - token, - settings = { - secret: 'feathers-rocks', - jwtOptions: { - expiresIn: 1 // Testing token expiration after 1 second. - } - }; - - before((done) => { - createApplication(settings, username, password, function(err, obj){ - app = obj.app; - server = obj.server; - done(); - }); - }); - - after(function(done) { - server.close(done); - }); - - it('Login works.', function(done) { - request({ - url: 'http://localhost:8888/api/login', - method: 'POST', - form: { - username: username, - password: password - }, - json: true - }, function(err, res, body) { - token = body.token; - assert.ok(body.token, 'POST to /api/login gave us back a token.'); - done(); - }); - }); - - it('Requests with an expired token will return an error.', function(done) { - setTimeout(function(){ - request({ - url: 'http://localhost:8888/api/todos', - method: 'GET', - headers: { - 'Authorization': 'Bearer ' + token, - 'Accept' : 'application/json', - 'Content-Type': 'application/json' - }, - json: true - }, function(err, res, body) { - assert.equal(body.name, 'TokenExpiredError', 'Got an error string back, not an object/array'); - done(); - }); - }, 2500); - }); - - it('Requests to refresh an expired token will fail', function (done) { - setTimeout(function() { - request({ - url: 'http://localhost:8888/api/login/refresh', - method: 'POST', - form: { - token: token - }, - json: true - }, function (err, res, body) { - assert.equal(body.name, 'TokenExpiredError', 'Got an error string back, not an object/array'); - done(); - }); - }, 2500); - }); - - it('Requests to refresh with no token will throw an error', function (done) { - request({ - url: 'http://localhost:8888/api/login/refresh', - method: 'POST', - form: { - - }, - json: true - }, function (err, res) { - assert.equal(res.statusCode, 500, 'Throws error'); - done(); - }); - }); -}); diff --git a/packages/authentication/test/rest.test.js b/packages/authentication/test/rest.test.js deleted file mode 100644 index 31a4a463bd..0000000000 --- a/packages/authentication/test/rest.test.js +++ /dev/null @@ -1,109 +0,0 @@ -import assert from 'assert'; -import request from 'request'; -import createApplication from './server-fixtures'; - -describe('REST API authentication', function() { - this.timeout(10000); - let server, app, - username = 'feathers', - token, - password = 'test', - settings = { - secret: 'feathers-rocks' - }; - - before((done) => { - createApplication(settings, username, password, function(err, obj){ - app = obj.app; - server = obj.server; - done(); - }); - }); - - after(function(done) { - server.close(done); - }); - - it('Posting no data to login returns a 401.', function(done) { - request({ - url: 'http://localhost:8888/api/login', - method: 'POST', - form: {}, - json: true - }, function(err, response, body) { - assert.equal(body.code, 401, 'POST to /api/login with no params returns an error.'); - done(); - }); - }); - - it('Login works.', function(done) { - request({ - url: 'http://localhost:8888/api/login', - method: 'POST', - form: { - username: username, - password: password - }, - json: true - }, function(err, res, body) { - token = body.token; - assert.ok(body.token, 'POST to /api/login gave us back a token.'); - assert.equal(body.data.password, undefined, 'The returned token data did not include a password.'); - done(); - }); - }); - - it('Requests without auth to an unprotected service will return data.', function(done) { - request({ - url: 'http://localhost:8888/api/tasks', - method: 'GET', - json: true - }, function(err, res, tasks) { - assert.equal(tasks.length, 3, 'Got tasks'); - - request({ - url: 'http://localhost:8888/api/tasks/1', - json: true - }, function(err, res, task) { - assert.deepEqual(task, { - id: '1', - name: 'Make Pizza.' - }); - done(); - }); - }); - }); - - it('Requests without auth to a protected service will return an error.', function(done) { - request({ - url: 'http://localhost:8888/api/todos', - method: 'GET', - json: true - }, function(err, res, body) { - assert.equal(typeof body, 'string', 'Got an error string back, not an object/array'); - - request({ - url: 'http://localhost:8888/api/todos/1', - json: true - }, function(err, res, body) { - assert.equal(typeof body, 'string', 'Got an error string back, not an object/array'); - done(); - }); - }); - }); - - it('Requests with a broken token will return a JWT error', function(done) { - request({ - url: 'http://localhost:8888/api/todos', - method: 'GET', - json: true, - headers: { - 'Authorization': 'Bearer abcd' - } - }, function(err, res, body) { - assert.equal(typeof body, 'string', 'Got an error string back, not an object/array'); - assert.ok(body.indexOf('JsonWebTokenError' > -1), 'Got a JsonWebTokenError'); - done(); - }); - }); -}); diff --git a/packages/authentication/test/rest.valid-token.test.js b/packages/authentication/test/rest.valid-token.test.js deleted file mode 100644 index f0f2f1ab84..0000000000 --- a/packages/authentication/test/rest.valid-token.test.js +++ /dev/null @@ -1,87 +0,0 @@ -import assert from 'assert'; -import request from 'request'; -import createApplication from './server-fixtures'; - -describe('REST API authentication with valid auth token', function () { - this.timeout(10000); - let server, - app, - username = 'feathers', - token, - password = 'test', - settings = { - secret: 'feathers-rocks' - }; - - before(function (done) { - - createApplication(settings, username, password, function (err, obj) { - app = obj.app; - server = obj.server; - - request({ - url: 'http://localhost:8888/api/login', - method: 'POST', - form: { - username: username, - password: password - }, - json: true - }, function(err, res, body) { - token = body.token; - done(); - }); - }); - }); - - after(function (done) { - server.close(done); - }); - - - it('Requests with valid auth to protected services will return data', function (done) { - request({ - url: 'http://localhost:8888/api/todos', - method: 'GET', - json: true, - headers: { - 'Authorization': 'Bearer ' + token - } - }, function (err, res, body) { - assert.equal(body.length, 3, 'Got data back'); - assert.equal(body[0].name, 'Do the dishes', 'Got todos back'); - done(); - }); - }); - - it('Requests with valid auth to unprotected services will return data', function (done) { - request({ - url: 'http://localhost:8888/api/tasks', - method: 'GET', - json: true, - headers: { - 'Authorization': 'Bearer ' + token - } - }, function (err, res, body) { - assert.equal(body.length, 3, 'Got data back'); - assert.equal(body[0].name, 'Feed the pigs', 'Got tasks back'); - done(); - }); - }); - - it('Requests to refresh a valid token will return data', function (done) { - request({ - url: 'http://localhost:8888/api/login/refresh', - method: 'POST', - form: { - token: token - }, - json: true - }, function (err, res, body) { - assert.ok(body.token, 'POST to /api/login gave us back a token.'); - assert.equal(body.token, token, 'Token is the same'); - done(); - }); - }); - -}); diff --git a/packages/authentication/test/server-fixtures.js b/packages/authentication/test/server-fixtures.js deleted file mode 100644 index 1e60d48a68..0000000000 --- a/packages/authentication/test/server-fixtures.js +++ /dev/null @@ -1,69 +0,0 @@ -import feathers from 'feathers'; -import socketio from 'feathers-socketio'; -import rest from 'feathers-rest'; -import feathersHooks from 'feathers-hooks'; -import feathersAuth from '../src/'; -import {hooks} from '../src/'; -import bodyParser from 'body-parser'; -import memory from 'feathers-memory'; -import async from 'async'; - -export default function(settings, username, password, next) { - - const app = feathers(); - - app.configure(rest()) - .configure(socketio()) - .configure(feathersHooks()) - .use(bodyParser.json()) - .use(bodyParser.urlencoded({ extended: true })) - .configure(feathersAuth(settings)) - .use('/api/users', memory()) - .use('/api/todos', memory()) - .use('/api/tasks', memory()) - .use('/', feathers.static(__dirname)); - - let server = app.listen(8888); - - let userService = app.service('/api/users'); - userService.before({ - create: [hooks.hashPassword()] - }); - - - // Todos will require auth. - let todoService = app.service('/api/todos'); - - // Tasks service won't require auth. - let taskService = app.service('/api/tasks'); - - server.on('listening', () => { - console.log('server listening'); - - async.series([ - function(cb){ - userService.create({username: username, password: password}, {}, cb); - }, - function(cb){ - todoService.create({name: 'Do the dishes'}, {}, function(){}); - todoService.create({name: 'Buy a guitar'}, {}, function(){}); - todoService.create({name: 'Exercise for 30 minutes.'}, {}, function(){}); - taskService.create({name: 'Feed the pigs'}, {}, function(){}); - taskService.create({name: 'Make Pizza.'}, {}, function(){}); - taskService.create({name: 'Write a book.'}, {}, cb); - } - ], function(){ - todoService.before({ - find: [hooks.requireAuth()], - get: [hooks.requireAuth()] - }); - - var obj = { - app: app, - server: server - }; - next(null, obj); - }); - - }); -} diff --git a/packages/authentication/test/socket-io.test.js b/packages/authentication/test/socket-io.test.js deleted file mode 100644 index 183dd92b3f..0000000000 --- a/packages/authentication/test/socket-io.test.js +++ /dev/null @@ -1,83 +0,0 @@ -import assert from 'assert'; -import request from 'request'; -import io from 'socket.io-client'; -import createApplication from './server-fixtures'; - -describe('Socket.io authentication', function() { - this.timeout(10000); - let server, - app, - token, - username = 'feathers', - password = 'test', - settings = { - secret: 'sockets-rock' - }; - - before(function(done) { - createApplication(settings, username, password, function(err, obj){ - app = obj.app; - server = obj.server; - done(); - }); - }); - - after(function(done) { - server.close(done); - }); - - it('Login works.', function(done) { - request({ - url: 'http://localhost:8888/api/login', - method: 'POST', - form: { - username: username, - password: password - }, - json: true - }, function(err, res, body) { - token = body.token; - assert.ok(body.token, 'POST to /api/login gave us back a token.'); - done(); - }); - }); - - it('Can connect without an auth token and get no todos.', function(done) { - var socket = io('http://localhost:8888'); - socket.on('connect', function() { - socket.emit('api/todos::find', {}, function(error, todos) { - assert.ok(error.message, 'Got an error message back'); - assert.equal(todos, undefined, 'No todos were returned.'); - socket.disconnect(); - done(); - }); - }); - }); - - it('Can connect with an auth token and get todos.', function(done) { - var socket = io('http://localhost:8888', { - query: 'token=' + token, - forceNew: true - }); - socket.on('connect', function() { - socket.emit('api/todos::find', {}, function(error, todos) { - assert.equal(todos[1].name, 'Buy a guitar'); - socket.disconnect(); - done(); - }); - }); - }); - - it('Can connect without an auth token and get data from an unprotected service.', function(done) { - var socket = io('http://localhost:8888', { - forceNew: true - }); - socket.on('connect', function() { - socket.emit('api/tasks::find', {}, function(error, todos) { - assert.equal(todos[0].name, 'Feed the pigs'); - socket.disconnect(); - done(); - }); - }); - }); -}); diff --git a/packages/authentication/test/src/hooks.test.js b/packages/authentication/test/src/hooks.test.js new file mode 100644 index 0000000000..5d34f30937 --- /dev/null +++ b/packages/authentication/test/src/hooks.test.js @@ -0,0 +1,124 @@ +import assert from 'assert'; +import {hooks} from '../../src/index'; + +describe('Auth hooks', () => { + it('accessible at module.hooks', () => { + assert.equal(typeof require('../../lib/index').hooks, 'object'); + }); + + describe('hashPassword() hook', () => { + it('accessible at module.hooks.hashPassword', () => { + assert.equal(typeof require('../../lib/index').hooks.hashPassword, 'function'); + }); + + describe('when hook.data does not exist', () => { + it('does not do anything', () => { + let hook = { + foo: { password: 'password' } + }; + + hook = hooks.hashPassword()(hook); + assert.equal(hook.data, undefined); + }); + }); + + describe('when hook.data does exist', () => { + it('attaches hashed password to hook.data', (done) => { + let hook = { + data: { password: 'password' } + }; + + hooks.hashPassword()(hook).then(hook => { + assert.ok(hook.data.password); + assert.notEqual(hook.data.custom, 'password'); + done(); + }); + }); + + it('supports custom password fields', (done) => { + let hook = { + data: { custom: 'password' } + }; + + hooks.hashPassword({ passwordField: 'custom'})(hook).then(hook => { + assert.ok(hook.data.custom); + assert.notEqual(hook.data.custom, 'password'); + done(); + }); + }); + }); + }); +}); + + +// describe('The queryWithUserId() hook', () => { +// it('is found at module.hooks.queryWithUserId', () => { +// assert.equal(typeof require('../../lib/index').hooks.queryWithUserId, 'function'); +// }); + +// it('adds the user\'s _id at the default location "userId"', (done) => { +// app.use('todos', memory({idField: '_id'})); +// const todos = app.service('todos'); + +// let setUser = function(){ +// return function(hook){ +// hook.params.user = { +// username: 'steinway', +// _id: 0 +// }; +// }; +// }; + +// let customHook = function(){ +// return function(hook){ +// assert.equal(hook.params.query.userId, 0, 'The userId was added to the query params.'); +// }; +// }; + +// todos.before({ +// find: [setUser(), hooks.queryWithUserId(), customHook()] +// }); + +// todos.create({username: 'bosendorfer', password: 'feathers'}, function(){ +// todos.find({query: {username: 'bosendorfer'}}, function(){ +// done(); +// }); +// }); +// }); + +// it('adds the user\'s id at the custom location "smurfId"', (done) => { +// app.use('todos', memory({idField: '_id'})); +// const todos = app.service('todos'); + +// let customHook = function(){ +// return function(hook){ +// assert.equal(hook.params.query.smurf, 0, 'The smurfId was added to the query params.'); +// }; +// }; + +// todos.before({ +// find: [setUser(), hooks.queryWithUserId('id', 'smurfId'), customHook()] +// }); + +// todos.create({username: 'bosendorfer', password: 'feathers'}, function(){ +// todos.find({query: {username: 'bosendorfer'}}, function(){ +// done(); +// }); +// }); +// }); + +// it('returns an error if no user is logged in', (done) => { +// app.use('todos', memory({idField: '_id'})); +// const todos = app.service('todos'); + +// todos.before({ +// find: [hooks.queryWithUserId()] +// }); + +// todos.create({username: 'bosendorfer', password: 'feathers'}, function(){ +// todos.find({query:{}}, function(err){ +// assert.equal(typeof err, 'object'); +// done(); +// }); +// }); +// }); diff --git a/packages/authentication/test/src/index.test.js b/packages/authentication/test/src/index.test.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/authentication/test/src/middleware.test.js b/packages/authentication/test/src/middleware.test.js new file mode 100644 index 0000000000..a82389806a --- /dev/null +++ b/packages/authentication/test/src/middleware.test.js @@ -0,0 +1,118 @@ +import assert from 'assert'; +import middleware from '../../src/middleware'; + +const MockRequest = { + feathers: {}, + params: {}, + body: {}, + query: {}, + headers: {}, + cookies: {} +}; + +const MockResponse = { + json: function(){} +}; + +const MockNext = function(){}; + +describe('Middleware', () => { + describe('Expose connect middleware', () => { + it('adds the request object to req.feathers', () => { + middleware.exposeConnectMiddleware(MockRequest, MockResponse, MockNext); + assert.deepEqual(MockRequest.feathers.req, MockRequest); + }); + + it('adds the response object to req.feathers', () => { + middleware.exposeConnectMiddleware(MockRequest, MockResponse, MockNext); + assert.deepEqual(MockRequest.feathers.res, MockResponse); + }); + }); + + describe('Normalize Auth Token', () => { + describe('Auth token passed via header', () => { + it('grabs the token', () => { + const req = Object.assign({}, MockRequest, { + headers: { + authorization: 'Bearer my-token' + } + }); + + middleware.normalizeAuthToken()(req, MockResponse, MockNext); + assert.deepEqual(req.feathers.token, 'my-token'); + }); + + it('supports a custom header', () => { + const req = Object.assign({}, MockRequest, { + headers: { + 'x-authorization': 'Bearer my-token' + } + }); + + middleware.normalizeAuthToken({header: 'x-authorization'})(req, MockResponse, MockNext); + assert.deepEqual(req.feathers.token, 'my-token'); + }); + }); + + describe('Auth token passed via cookie', () => { + it('grabs the token', () => { + const req = Object.assign({}, MockRequest, { + cookies: { + 'feathers-jwt': 'my-token' + } + }); + + middleware.normalizeAuthToken()(req, MockResponse, MockNext); + assert.deepEqual(req.feathers.token, 'my-token'); + }); + }); + + describe('Auth token passed via body', () => { + it('grabs the token', () => { + const req = Object.assign({}, MockRequest, { + body: { + token: 'my-token' + } + }); + + middleware.normalizeAuthToken()(req, MockResponse, MockNext); + assert.deepEqual(req.feathers.token, 'my-token'); + }); + + it('deletes the token from the body', () => { + const req = Object.assign({}, MockRequest, { + body: { + token: 'my-token' + } + }); + + middleware.normalizeAuthToken()(req, MockResponse, MockNext); + assert.equal(req.body.token, undefined); + }); + }); + + describe('Auth token passed via query', () => { + it('grabs the token', () => { + const req = Object.assign({}, MockRequest, { + query: { + token: 'my-token' + } + }); + + middleware.normalizeAuthToken()(req, MockResponse, MockNext); + assert.deepEqual(req.feathers.token, 'my-token'); + }); + + it('removes the token from the query string', () => { + const req = Object.assign({}, MockRequest, { + query: { + token: 'my-token' + } + }); + + middleware.normalizeAuthToken()(req, MockResponse, MockNext); + assert.equal(req.query.token, undefined); + }); + }); + }); +}); \ No newline at end of file diff --git a/packages/authentication/test/test-server.js b/packages/authentication/test/test-server.js new file mode 100644 index 0000000000..8947aae614 --- /dev/null +++ b/packages/authentication/test/test-server.js @@ -0,0 +1,73 @@ +import feathers from 'feathers'; +import primus from 'feathers-primus'; +import socketio from 'feathers-socketio'; +import rest from 'feathers-rest'; +import feathersHooks from 'feathers-hooks'; +import authentication from '../src/'; +import {hooks} from '../src/'; +import bodyParser from 'body-parser'; +import memory from 'feathers-memory'; +import async from 'async'; + +export default function(settings, username, password, useSocketio, next) { + + const app = feathers(); + + app.configure(rest()) + .configure(useSocketio ? socketio() : primus({ transformer: 'websockets' })) + .configure(feathersHooks()) + .use(bodyParser.json()) + .use(bodyParser.urlencoded({ extended: true })) + .configure(authentication(settings)) + .use('/users', memory()) + .use('/messages', memory()) + .use('/tasks', memory()) + .use('/', feathers.static(__dirname)) + /*jshint unused: false*/ + .use(function(error, req, res, next){ + res.status(error.code); + res.json(error); + }); + + let server = app.listen(8888); + + let userService = app.service('/users'); + userService.before({ + create: [hooks.hashPassword()] + }); + + + // Messages will require auth. + let messageService = app.service('/messages'); + + // Tasks service won't require auth. + let taskService = app.service('/tasks'); + + server.on('listening', () => { + async.series([ + function(cb){ + userService.create({email: username, password: password}, {}, cb); + }, + function(cb){ + messageService.create({text: 'A million people walk into a Silicon Valley bar'}, {}, function(){}); + messageService.create({text: 'Nobody buys anything'}, {}, function(){}); + messageService.create({text: 'Bar declared massive success'}, {}, function(){}); + taskService.create({text: 'Feed the pigs'}, {}, function(){}); + taskService.create({text: 'Make Pizza.'}, {}, function(){}); + taskService.create({text: 'Write a book.'}, {}, cb); + } + ], function(){ + messageService.before({ + find: [hooks.requireAuth()], + get: [hooks.requireAuth()] + }); + + var obj = { + app: app, + server: server + }; + next(null, obj); + }); + + }); +}