This repository has been archived by the owner on Mar 22, 2022. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 118
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
First cut for authentication middleware (#305)
* First cut for authentication middleware * Fix service methods * Allow passing user service and service name
- Loading branch information
1 parent
364caf2
commit b4caf91
Showing
13 changed files
with
532 additions
and
64 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }); | ||
}); | ||
}); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.createJWT(data.payload); | ||
} | ||
|
||
remove(id, params) { | ||
const token = id !== null ? id : params.token; | ||
|
||
return this.authentication.verifyJWT({ token }); | ||
} | ||
|
||
setup(app) { | ||
this.authentication = app.authentication; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()]; | ||
|
||
// 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); | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
]; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 }); | ||
}); | ||
}; | ||
} |
Oops, something went wrong.