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

Commit

Permalink
First cut for authentication middleware (#305)
Browse files Browse the repository at this point in the history
* First cut for authentication middleware

* Fix service methods

* Allow passing user service and service name
  • Loading branch information
daffl authored and ekryski committed Oct 11, 2016
1 parent c2a44b6 commit 6f0308f
Show file tree
Hide file tree
Showing 13 changed files with 532 additions and 65 deletions.
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
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()];

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

0 comments on commit 6f0308f

Please sign in to comment.