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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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);
+ });
+
+ });
+}