-
Notifications
You must be signed in to change notification settings - Fork 117
First cut for authentication middleware #305
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 }); | ||
}); | ||
}); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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.create(data.payload); | ||
} | ||
|
||
remove(id, params) { | ||
const token = id !== null ? id : params.token; | ||
|
||
return this.authentication.verify({ token }); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Doesn't this need to be There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. indeed, |
||
} | ||
|
||
setup(app) { | ||
this.authentication = app.authentication; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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()]; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How is 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
|
||
// 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); | ||
}; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
]; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
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 = app.service(user.service); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Possibly we want to support passing an actual service as well, instead of just the path name. |
||
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(!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 }); | ||
}); | ||
}; | ||
} |
There was a problem hiding this comment.
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?There was a problem hiding this comment.
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
andtoken
sections might change as well.