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

A bunch of bug fixes #349

Merged
merged 1 commit into from
Nov 23, 2016
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -147,8 +147,7 @@ app.service('authentication').hooks({
before: {
create: [
// You can chain multiple strategies
auth.hooks.authenticate(['jwt', 'local']),
customizeJWTPayload()
auth.hooks.authenticate(['jwt', 'local'])
],
remove: [
auth.hooks.authenticate('jwt')
53 changes: 35 additions & 18 deletions docs/migrating.md
Original file line number Diff line number Diff line change
@@ -91,29 +91,13 @@ app.use('/users', memory())
Strategy: FacebookStrategy
}));

// This hook customizes your payload.
function customizeJWTPayload() {
return function(hook) {
console.log('Customizing JWT Payload');
hook.data.payload = {
// You need to make sure you have the right id.
// You can put whatever you want to be encoded in
// the JWT access token.
id: hook.params.user.id
};

return Promise.resolve(hook);
};
}

// Authenticate the user using the a JWT or
// email/password strategy and if successful
// return a new JWT access token.
app.service('authentication').hooks({
before: {
create: [
auth.hooks.authenticate(['jwt', 'local']),
customizeJWTPayload()
auth.hooks.authenticate(['jwt', 'local'])
]
}
});
@@ -125,7 +109,7 @@ There are a number of breaking changes since the services have been removed:

- Change `auth.token` -> `auth.jwt` in your config
- Move `auth.token.secret` -> `auth.secret`
- `auth.token.payload` option has been removed. See [customizing JWT payload]() for how to do this.
- `auth.token.payload` option has been removed. See [customizing JWT payload](#customizing-jwt-payload) for how to do this.
- `auth.idField` has been removed. It is now included in all services so we can pull it internally without you needing to specify it.
- `auth.shouldSetupSuccessRoute` has been removed. Success redirect middleware is registered automatically but only triggers if you explicitly set a redirect. [See redirecting]() for more details.
- `auth.shouldSetupFailureRoute` has been removed. Failure redirect middleware is registered automatically but only triggers if you explicitly set a redirect. [See redirecting]() for more details.
@@ -233,6 +217,39 @@ app.authenticate({

We previously made the poor assumption that you are always authenticating a user. This is not always the case, or your app may not care about the current user as you already have their id in the accessToken payload or can encode some additional details in the JWT accessToken. Therefore, if you need to get the current user you need to request it explicitly after authentication or populate it yourself in an after hook server side. See the new usage above for how to fetch your user.

## Customizing JWT Payload

By default the payload for your JWT is simply your entity id (ie. `{ userId }`). However, you can customize your JWT payloads however you wish by adding a `before` hook to the authentication service. For example:

```js
// This hook customizes your payload.
function customizeJWTPayload() {
return function(hook) {
console.log('Customizing JWT Payload');
hook.data.payload = {
// You need to make sure you have the right id.
// You can put whatever you want to be encoded in
// the JWT access token.
customId: hook.params.user.id
};

return Promise.resolve(hook);
};
}

// Authenticate the user using the a JWT or
// email/password strategy and if successful
// return a new JWT access token.
app.service('authentication').hooks({
before: {
create: [
auth.hooks.authenticate(['jwt', 'local']),
customizeJWTPayload()
]
}
});
```

## JWT Parsing

The JWT is only parsed from the header and body by default now. It is no longer pulled from the query string unless you explicitly tell `feathers-authentication-jwt` to do so.
13 changes: 1 addition & 12 deletions example/app.js
Original file line number Diff line number Diff line change
@@ -10,16 +10,6 @@ const local = require('feathers-authentication-local');
const jwt = require('feathers-authentication-jwt');
const auth = require('../lib/index');

function customizeJWTPayload() {
return function(hook) {
hook.data.payload = {
id: hook.params.user.id
};

return Promise.resolve(hook);
};
}

const app = feathers();
app.configure(rest())
.configure(socketio())
@@ -36,8 +26,7 @@ app.service('authentication').hooks({
before: {
create: [
// You can chain multiple strategies
auth.hooks.authenticate(['jwt', 'local']),
customizeJWTPayload()
auth.hooks.authenticate(['jwt', 'local'])
],
remove: [
auth.hooks.authenticate('jwt')
19 changes: 10 additions & 9 deletions src/passport/authenticate.js
Original file line number Diff line number Diff line change
@@ -13,9 +13,7 @@ export default function authenticate (options = {}) {
debug('Initializing custom passport authenticate', options);

// This function is bound by passport and called by passport.authenticate()
return function (passport, name, strategyOptions = {}, callback = () => {}) {
debug('passport.authenticate called with the following options', passport, name, strategyOptions, callback);

return function (passport, name, strategyOptions = {}, callback = () => {}) {
// This is called by the feathers middleware, hook or socket. The request object
// is a mock request derived from an http request, socket object, or hook.
return function (request = {}) {
@@ -26,7 +24,7 @@ export default function authenticate (options = {}) {
// Default is hook.params.user, req.user and socket.user.
const entity = strategyOptions.entity || strategyOptions.assignProperty || options.entity;
let failures = [];
let strategies = [name];
let strategies;

// Cast `name` to an array, allowing authentication to pass through a chain of
// strategies. The first name to succeed, redirect, or error will halt
@@ -38,8 +36,12 @@ export default function authenticate (options = {}) {
// It is not feasible to construct a chain of multiple strategies that involve
// redirection (for example both Facebook and Twitter), since the first one to
// redirect will halt the chain.
if (Array.isArray(name)) {
if (request.strategy) {
strategies = [request.strategy];
} else if (Array.isArray(name)) {
strategies = name;
} else {
strategies = [name];
}

function attempt(index) {
@@ -60,7 +62,6 @@ export default function authenticate (options = {}) {
if (!prototype) {
return reject(new Error(`Unknown authentication strategy '${layer}'`));
}


// Implement required passport methods that
// can be called by a passport strategy.
@@ -88,13 +89,13 @@ export default function authenticate (options = {}) {
reject(error);
};

strategy.success = (data, info) => {
debug(`'${layer}' authentication strategy succeeded`, data, info);
strategy.success = (data, payload) => {
debug(`'${layer}' authentication strategy succeeded`, data, payload);
resolve({
success: true,
data: {
[entity]: data,
info
payload
}
});
};
15 changes: 15 additions & 0 deletions src/passport/initialize.js
Original file line number Diff line number Diff line change
@@ -15,7 +15,22 @@ export default function initialize (options = {}) {
// app.configure(authentication()).

// Expose our JWT util functions globally
passport._feathers = {};
passport.createJWT = createJWT;
passport.verifyJWT = verifyJWT;
passport.options = function(name, strategyOptions) {
if (!name) {
return passport._feathers;
}

if (typeof name === 'string' && !strategyOptions) {
return passport._feathers[name];
}

if (typeof name === 'string' && strategyOptions) {
debug(`Setting ${name} strategy options`, strategyOptions);
passport._feathers[name] = Object.assign({}, strategyOptions);
}
};
};
}
5 changes: 3 additions & 2 deletions src/service.js
Original file line number Diff line number Diff line change
@@ -10,15 +10,16 @@ class Service {
this.passport = app.passport;
}

create(data, params) {
create(data = {}, params = {}) {
const defaults = this.app.get('auth');
const payload = merge(data.payload, params.payload);

// create accessToken
// TODO (EK): Support refresh tokens
// TODO (EK): This should likely be a hook
// TODO (EK): This service can be datastore backed to support blacklists :)
return this.passport
.createJWT(data.payload, merge(defaults, params))
.createJWT(payload, merge(defaults, params))
.then(accessToken => {
return { accessToken };
});
14 changes: 8 additions & 6 deletions src/socket/handler.js
Original file line number Diff line number Diff line change
@@ -76,17 +76,19 @@ export default function setupSocketHandler(app, options, { feathersParams, provi
return callback(normalizeError(error));
}

const promise = app.authenticate(strategy, options[strategy])(socket._feathers)
const stategyOptions = app.passport.options(strategy);

const promise = app.authenticate(strategy, stategyOptions)(socket._feathers)
.then(result => {
if (result.success) {
// NOTE (EK): I don't think we need to support
// custom redirects. We can emit this to the client
// and let the client redirect.
// if (options.successRedirect) {
// if (stategyOptions.successRedirect) {
// return {
// redirect: true,
// status: 302,
// url: options.successRedirect
// url: stategyOptions.successRedirect
// };
// }
return Promise.resolve(result.data);
@@ -96,16 +98,16 @@ export default function setupSocketHandler(app, options, { feathersParams, provi
// NOTE (EK): I don't think we need to support
// custom redirects. We can emit this to the client
// and let the client redirect.
// if (options.failureRedirect) {
// if (stategyOptions.failureRedirect) {
// return {
// redirect: true,
// status: 302,
// url: options.failureRedirect
// url: stategyOptions.failureRedirect
// };
// }

const { challenge } = result;
const message = options.failureMessage || (challenge && challenge.message);
const message = stategyOptions.failureMessage || (challenge && challenge.message);

return Promise.reject(new errors[401](message, challenge));
}
3 changes: 2 additions & 1 deletion src/utils.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Debug from 'debug';
import pick from 'lodash.pick';
import omit from 'lodash.omit';
import jwt from 'jsonwebtoken';

const debug = Debug('feathers-authentication:authentication:utils');
@@ -32,7 +33,7 @@ export function createJWT (payload = {}, options = {}) {
}

// TODO (EK): Support jwtids. Maybe auto-generate a uuid
jwt.sign(payload, secret, pick(settings, VALID_KEYS), function(error, token) {
jwt.sign(omit(payload, VALID_KEYS), secret, pick(settings, VALID_KEYS), function(error, token) {
if (error) {
debug('Error signing JWT', error);
return reject(error);
24 changes: 14 additions & 10 deletions test/fixtures/server.js
Original file line number Diff line number Diff line change
@@ -17,15 +17,15 @@ const User = {
permissions: ['*']
};

function customizeJWTPayload() {
return function(hook) {
hook.data.payload = {
id: hook.params.user.id
};
// function customizeJWTPayload() {
// return function(hook) {
// hook.data.payload = {
// id: hook.params.user.id
// };

return Promise.resolve(hook);
};
}
// return Promise.resolve(hook);
// };
// }

export default function(settings, socketProvider) {
const app = feathers();
@@ -39,15 +39,19 @@ export default function(settings, socketProvider) {
.use(bodyParser.urlencoded({ extended: true }))
.configure(auth(settings))
.configure(local())
.configure(local({
name: 'org-local',
entity: 'org'
}))
.configure(jwt())
.use('/users', memory())
.use('/', feathers.static(__dirname + '/public'));

app.service('authentication').hooks({
before: {
create: [
auth.hooks.authenticate(['jwt', 'local']),
customizeJWTPayload()
auth.hooks.authenticate(['jwt', 'local', 'org-local']),
// customizeJWTPayload()
],
remove: [
auth.hooks.authenticate('jwt')
27 changes: 23 additions & 4 deletions test/integration/primus.test.js
Original file line number Diff line number Diff line change
@@ -19,6 +19,7 @@ describe('Primus authentication', function() {
let server;
let socket;
let Socket;
let serverSocket;
let ExpiringSocket;
let expiringServer;
let expiringSocket;
@@ -30,7 +31,7 @@ describe('Primus authentication', function() {
app.passport.createJWT({}, options)
.then(token => {
expiredToken = token;
return app.passport.createJWT({ id: 0 }, app.get('auth'));
return app.passport.createJWT({ userId: 0 }, app.get('auth'));
})
.then(token => {
accessToken = token;
@@ -40,6 +41,7 @@ describe('Primus authentication', function() {
server = app.listen(port);
server.once('listening', () => {
Socket = app.primus.Socket;
app.primus.on('connection', s => serverSocket = s);
done();
});
});
@@ -78,11 +80,28 @@ describe('Primus authentication', function() {
app.passport.verifyJWT(response.accessToken, app.get('auth')).then(payload => {
expect(payload).to.exist;
expect(payload.iss).to.equal('feathers');
expect(payload.id).to.equal(0);
expect(payload.userId).to.equal(0);
done();
});
});
});

it('sets the user on the socket', done => {
socket.send('authenticate', data, (error, response) => {
expect(response.accessToken).to.exist;
expect(serverSocket.request.feathers.user).to.not.equal(undefined);
done();
});
});

it('sets entity specified in strategy', done => {
data.strategy = 'org-local';
socket.send('authenticate', data, (error, response) => {
expect(response.accessToken).to.exist;
expect(serverSocket.request.feathers.org).to.not.equal(undefined);
done();
});
});
});

describe('when using invalid credentials', () => {
@@ -132,7 +151,7 @@ describe('Primus authentication', function() {
app.passport.verifyJWT(response.accessToken, app.get('auth')).then(payload => {
expect(payload).to.exist;
expect(payload.iss).to.equal('feathers');
expect(payload.id).to.equal(0);
expect(payload.userId).to.equal(0);
done();
});
});
@@ -148,7 +167,7 @@ describe('Primus authentication', function() {
app.passport.verifyJWT(response.accessToken, app.get('auth')).then(payload => {
expect(payload).to.exist;
expect(payload.iss).to.equal('feathers');
expect(payload.id).to.equal(0);
expect(payload.userId).to.equal(0);
done();
});
});
Loading