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

First cut for authentication middleware #305

Merged
merged 3 commits into from Oct 11, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
87 changes: 87 additions & 0 deletions src/base.js
@@ -0,0 +1,87 @@
import Debug from 'debug';
import jwt from 'jsonwebtoken';

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

export default class Authentication {
constructor(app, options) {
this.options = options;
this.app = app;
this._middleware = [];
}

use(... middleware) {
const mapped = middleware.map(current =>
current.call(this.app, this.options)
);

this._middleware = this._middleware.concat(mapped);

return this;
}

authenticate(data) {
let promise = Promise.resolve(data);

debug('Authenticating', data);

this._middleware.forEach(middleware =>
promise = promise.then(data =>
middleware.call(this.app, data, this.options)
)
);

return promise;
}

verifyJWT(data, params) {
const settings = Object.assign({}, this.options.jwt, params);
const token = typeof data === 'string' ? data : data.token;
const { secret } = this.options;

debug('Verifying token', token);

return new Promise((resolve, reject) => {
jwt.verify(token, secret, settings, (error, payload) => {
if(error) {
debug('Error verifying token', error);
return reject(error);
}

debug('Verified token with payload', payload);
resolve({ token, payload });
});
});
}

createJWT(data, params) {
const settings = Object.assign({}, this.options.jwt, params);
const { secret } = this.options;

if(data.iss) {
delete settings.issuer;
}

if(data.sub) {
delete settings.subject;
}

if(data.exp) {
delete settings.expiresIn;
}

return new Promise((resolve, reject) => {
debug('Creating JWT using options', settings);

jwt.sign(data, secret, settings, (error, token) => {
if (error) {
debug('Error signing JWT', error);
return reject(error);
}

debug('New JWT issued with payload', data);
return resolve({ token, payload: data });
});
});
}
}
86 changes: 26 additions & 60 deletions src/index.js
@@ -1,77 +1,32 @@
import Debug from 'debug';
import passport from 'passport';
import merge from 'lodash.merge';

// Exposed modules
import hooks from './hooks';
import token from './services/token';
import local from './services/local';
import oauth2 from './services/oauth2';
import * as mw from './middleware';
import getOptions from './options';
import Authentication from './base';

const debug = Debug('feathers-authentication:main');

// Options that apply to any provider
const defaults = {
header: 'Authorization',
setupMiddleware: true, // optional - to setup middleware yourself set to false.
cookie: { // Used for redirects, server side rendering and OAuth
enabled: false, // Set to true to enable all cookies
name: 'feathers-jwt',
httpOnly: true,
maxAge: '1d',
secure: true
},
token: {
name: 'token', // optional
service: '/auth/token', // optional string or Service
subject: 'auth', // optional
issuer: 'feathers', // optional
algorithm: 'HS256', // optional
expiresIn: '1d', // optional
secret: null, // required
successRedirect: null, // optional - no default. If set the default success handler will redirect to location
failureRedirect: null, // optional - no default. If set the default success handler will redirect to location
successHandler: null // optional - a middleware to handle things once authentication succeeds
},
local: {
service: '/auth/local', // optional string or Service
successRedirect: null, // optional - no default. If set the default success handler will redirect to location
failureRedirect: null, // optional - no default. If set the default success handler will redirect to location
successHandler: null, // optional - a middleware to handle things once authentication succeeds
passReqToCallback: true, // optional - whether request should be passed to callback
session: false // optional - whether we should use a session
},
user: {
service: '/users', // optional string or Service
idField: '_id', // optional
usernameField: 'email', // optional
passwordField: 'password' // optional
},
oauth2: {
// service: '/auth/facebook', // required - the service path or initialized service
passReqToCallback: true, // optional - whether request should be passed to callback
// callbackUrl: 'callback', // optional - the callback url, by default this gets set to /<service>/callback
permissions: {
state: true,
session: false
}
}
};
export default function init(config = {}) {
const middleware = [];

export default function auth(config = {}) {
return function() {
function authentication() {
const app = this;
let _super = app.setup;

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

// 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 (!authOptions.token.secret) {
throw new Error (`You must provide a token secret in your config via 'auth.token.secret'.`);
if (!authOptions.secret && !authOptions.token.secret) {
throw new Error (`You must provide a 'secret' in your authentication configuration`);
}

// Make sure cookies don't have to be sent over HTTPS
Expand All @@ -86,7 +41,7 @@ export default function auth(config = {}) {
// REST middleware
if (app.rest && authOptions.setupMiddleware) {
debug('registering REST authentication middleware');

// Be able to parse cookies it they are enabled
if (authOptions.cookie.enable) {
app.use(mw.cookieParser());
Expand All @@ -96,7 +51,7 @@ export default function auth(config = {}) {
app.use(mw.exposeRequestResponse(authOptions));
// Parse token from header, cookie, or request objects
app.use(mw.tokenParser(authOptions));
// Verify and decode a JWT if it is present
// Verify and decode a JWT if it is present
app.use(mw.verifyToken(authOptions));
// Make the Passport user available for REST services.
app.use(mw.populateUser(authOptions));
Expand Down Expand Up @@ -133,12 +88,23 @@ export default function auth(config = {}) {

return result;
};

app.authentication = new Authentication(app, authOptions);
app.authentication.use(... middleware);
}

authentication.use = function(... mw) {
middleware.push(... mw);

return authentication;
};

return authentication;
}

// Exposed Modules
auth.hooks = hooks;
auth.middleware = mw;
auth.LocalService = local;
auth.TokenService = token;
auth.OAuth2Service = oauth2;
init.hooks = hooks;
init.middleware = mw;
init.LocalService = local;
init.TokenService = token;
init.OAuth2Service = oauth2;
59 changes: 59 additions & 0 deletions src/options.js
@@ -0,0 +1,59 @@
import merge from 'lodash.merge';

// Options that apply to any provider
export const defaults = {
header: 'Authorization',
setupMiddleware: true, // optional - to setup middleware yourself set to false.
cookie: { // Used for redirects, server side rendering and OAuth
enabled: false, // Set to true to enable all cookies
name: 'feathers-jwt',
httpOnly: true,
maxAge: '1d',
secure: true
},
jwt: {
issuer: 'feathers',
algorithm: 'HS256',
expiresIn: '1d'
},
token: {
name: 'token', // optional
service: '/auth/token', // optional string or Service
subject: 'auth', // optional
Copy link
Member

Choose a reason for hiding this comment

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

Good call moving this into a separate file and setting up jwt namespace. I guess we'll de-dupe these options later?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yep, we'll clean it up later. De-duping and the local and token sections might change as well.

issuer: 'feathers', // optional
algorithm: 'HS256', // optional
expiresIn: '1d', // optional
secret: null, // required
successRedirect: null, // optional - no default. If set the default success handler will redirect to location
failureRedirect: null, // optional - no default. If set the default success handler will redirect to location
successHandler: null // optional - a middleware to handle things once authentication succeeds
},
local: {
service: '/auth/local', // optional string or Service
successRedirect: null, // optional - no default. If set the default success handler will redirect to location
failureRedirect: null, // optional - no default. If set the default success handler will redirect to location
successHandler: null, // optional - a middleware to handle things once authentication succeeds
passReqToCallback: true, // optional - whether request should be passed to callback
session: false // optional - whether we should use a session
},
user: {
service: '/users', // optional string or Service
idField: '_id', // optional
usernameField: 'email', // optional
passwordField: 'password', // optional
crypto: 'bcryptjs'
},
oauth2: {
// service: '/auth/facebook', // required - the service path or initialized service
passReqToCallback: true, // optional - whether request should be passed to callback
// callbackUrl: 'callback', // optional - the callback url, by default this gets set to /<service>/callback
permissions: {
state: true,
session: false
}
}
};

export default function(... otherOptions) {
return merge({}, defaults, ... otherOptions);
}
19 changes: 19 additions & 0 deletions src/services/authentication.js
@@ -0,0 +1,19 @@
export class Authentication {
create(data, params) {
if(params.provider && !params.authentication) {
return Promise.reject(new Error(`External ${params.provider} requests need to run through an authentication provider`));
}

return this.authentication.createJWT(data.payload);
}

remove(id, params) {
const token = id !== null ? id : params.token;

return this.authentication.verifyJWT({ token });
}

setup(app) {
this.authentication = app.authentication;
}
}
36 changes: 36 additions & 0 deletions src/token/from-request.js
@@ -0,0 +1,36 @@
import Debug from 'debug';

const debug = Debug('feathers-authentication:token:from-request');

export default function(options) {
const header = options.header;

if (!header) {
throw new Error(`'header' property must be set in authentication options`);
}

return function fromRequest(data) {
if (typeof data === 'object' && data.headers) {
const req = data;

debug('Parsing token from request');

// Normalize header capitalization the same way Node.js does
let token = req.headers && req.headers[header.toLowerCase()];
Copy link
Member

@ekryski ekryski Oct 9, 2016

Choose a reason for hiding this comment

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

Do we want to support body and query string as well or just header? If it's not going to cause us problems maybe we should (at least body). I personally use querystring a lot in dev mode because I'm lazy but really we shouldn't support that officially.

Copy link
Member Author

Choose a reason for hiding this comment

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

How is body being used? I couldn't think of a case where it made much sense (even might cause issues with token being a legitimate property when calling create, update and patch).

Do you have use cases where query string is easier than setting the header? We can add it back in but I don't think it was documented so far with more than a brief mention. Removing it would be less to document and test and we don't risk promoting not a best practise.

Copy link
Member

Choose a reason for hiding this comment

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

I would vote to nix both and only potentially add support for body if it is requested after release.

Copy link
Member

Choose a reason for hiding this comment

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

I agree actually. You're totally right @daffl we shouldn't support body. Querystring might still be a thing actually if we are supporting short lived JWTs for things like pw reset however, let's just go with this for now. I'm going to tinker with something regarding the password reset tokens, possibly by only allowing querystring verification if the token is an authentication subject.


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

debug('Token found in header');
}

return Promise.resolve({ token, req });
}

return Promise.resolve(data);
};
}
11 changes: 11 additions & 0 deletions src/token/index.js
@@ -0,0 +1,11 @@
import fromRequest from './from-request';
import verifyToken from './verify-token';
import populateUser from './populate-user';

export default {
fromRequest, verifyToken, populateUser
};

export const authMiddleware = [
fromRequest, verifyToken, populateUser
];
39 changes: 39 additions & 0 deletions src/token/populate-user.js
@@ -0,0 +1,39 @@
import Debug from 'debug';

const debug = Debug('feathers-authentication:token:populate-user');

export default function(options) {
const app = this;
const { user } = options;

if(!user.service) {
throw new Error(`'user.service' needs to be set in authentication options`);
}

return function populateUser(data) {
const service = typeof user.service === 'string' ? app.service(user.service) : user.service;
const idField = user.idField || service.id;

if(typeof idField !== 'string') {
throw new Error(`'user.idField' needs to be set in authentication options or the '${user.service}' service needs to provide an 'id' field.`);
}

if(typeof service.get !== 'function') {
throw new Error(`'user.service' does not support a 'get' method necessary for populateUser.`);
}

if(!data || !data.payload || data.payload[idField] === undefined) {
return Promise.resolve(data);
}

const id = data.payload[idField];

debug(`Populating user ${id}`);

return service.get(id).then(result => {
const user = result.toJSON ? result.toJSON() : result;

return Object.assign({}, data, { user });
});
};
}