Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Authentication v3 local authentication #1211

Merged
merged 4 commits into from
Feb 13, 2019
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"install": "lerna bootstrap",
"publish": "lerna publish",
"lint": "semistandard \"packages/**/lib/**/*.js\" \"packages/**/test/**/*.js\" --fix",
"test": "npm run lint && nyc lerna run test --ignore @feathersjs/authentication-*",
"test": "npm run lint && nyc lerna run test --ignore @feathersjs/authentication-client",
"test:client": "grunt"
},
"semistandard": {
Expand Down
71 changes: 34 additions & 37 deletions packages/authentication-local/lib/hooks/hash-password.js
Original file line number Diff line number Diff line change
@@ -1,54 +1,51 @@
const hasher = require('../utils/hash');
const { merge, get, set, cloneDeep } = require('lodash');
const Debug = require('debug');
const { get, set, cloneDeep } = require('lodash');
const { BadRequest } = require('@feathersjs/errors');

const debug = Debug('@feathersjs/authentication-local:hooks:hash-password');
const debug = require('debug')('@feathersjs/authentication-local/hooks/hash-password');

module.exports = function hashPassword (field, options = {}) {
if (!field) {
throw new Error('The hashPassword hook requires a field name option');
}

module.exports = function hashPassword (options = {}) {
return function (context) {
if (context.type !== 'before') {
return Promise.reject(new Error(`The 'hashPassword' hook should only be used as a 'before' hook.`));
return Promise.reject(
new Error(`The 'hashPassword' hook should only be used as a 'before' hook`)
);
}

const app = context.app;
const authOptions = app.get('authentication') || {};

options = merge({ passwordField: 'password' }, authOptions.local, options);

debug('Running hashPassword hook with options:', options);

const field = options.passwordField;
const hashPw = options.hash || hasher;
const { app, data, params } = context;
const password = get(data, field);

if (typeof field !== 'string') {
return Promise.reject(new Error(`You must provide a 'passwordField' in your authentication configuration or pass one explicitly`));
if (data === undefined || password === undefined) {
debug(`hook.data or hook.data.${field} is undefined. Skipping hashPassword hook.`);
return Promise.resolve(context);
}

if (typeof hashPw !== 'function') {
return Promise.reject(new Error(`'hash' must be a function that takes a password and returns Promise that resolves with a hashed password.`));
}
const serviceName = options.authentication || app.get('defaultAuthentication');
const authService = app.service(serviceName);
const { strategy = 'local' } = options;

if (context.data === undefined) {
debug(`hook.data is undefined. Skipping hashPassword hook.`);
return Promise.resolve(context);
if (!authService || typeof authService.getStrategies !== 'function') {
return Promise.reject(
new BadRequest(`Could not find '${serviceName}' service to hash password`)
);
}

const dataIsArray = Array.isArray(context.data);
const data = dataIsArray ? context.data : [ context.data ];
const [ localStrategy ] = authService.getStrategies(strategy);

return Promise.all(data.map(item => {
const password = get(item, field);
if (password) {
return hashPw(password).then(hashedPassword =>
set(cloneDeep(item), field, hashedPassword)
);
}
if (!localStrategy || typeof localStrategy.hashPassword !== 'function') {
return Promise.reject(
new BadRequest(`Could not find '${strategy}' strategy to hash password`)
);
}

return item;
})).then(results => {
context.data = dataIsArray ? results : results[0];
return localStrategy.hashPassword(password, params)
.then(hashedPassword => {
context.data = set(cloneDeep(data), field, hashedPassword);

return context;
});
return context;
});
};
};
7 changes: 0 additions & 7 deletions packages/authentication-local/lib/hooks/index.js

This file was deleted.

74 changes: 6 additions & 68 deletions packages/authentication-local/lib/index.js
Original file line number Diff line number Diff line change
@@ -1,69 +1,7 @@
const Debug = require('debug');
const { merge, omit, pick } = require('lodash');
const hooks = require('./hooks');
const DefaultVerifier = require('./verifier');
const LocalStrategy = require('./strategy');
const hashPassword = require('./hooks/hash-password');
const protect = require('./hooks/protect');

const passportLocal = require('passport-local');

const debug = Debug('@feathersjs/authentication-local');
const defaults = {
name: 'local',
usernameField: 'email',
passwordField: 'password'
};

const KEYS = [
'entity',
'service',
'passReqToCallback',
'session'
];

function init (options = {}) {
return function localAuth () {
const app = this;
const _super = app.setup;

if (!app.passport) {
throw new Error(`Can not find app.passport. Did you initialize feathers-authentication before @feathersjs/authentication-local?`);
}

let name = options.name || defaults.name;
let authOptions = app.get('authentication') || {};
let localOptions = authOptions[name] || {};

// NOTE (EK): Pull from global auth config to support legacy auth for an easier transition.
const localSettings = merge({}, defaults, pick(authOptions, KEYS), localOptions, omit(options, ['Verifier']));
let Verifier = DefaultVerifier;

if (options.Verifier) {
Verifier = options.Verifier;
}

app.setup = function () {
let result = _super.apply(this, arguments);
let verifier = new Verifier(app, localSettings);

if (!verifier.verify) {
throw new Error(`Your verifier must implement a 'verify' function. It should have the same signature as a local passport verify callback.`);
}

// Register 'local' strategy with passport
debug('Registering local authentication strategy with options:', localSettings);
app.passport.use(localSettings.name, new passportLocal.Strategy(localSettings, verifier.verify.bind(verifier)));
app.passport.options(localSettings.name, localSettings);

return result;
};
};
}

module.exports = init;

// Exposed Modules
Object.assign(module.exports, {
default: init,
defaults,
hooks,
Verifier: DefaultVerifier
});
exports.LocalStrategy = LocalStrategy;
exports.protect = protect;
exports.hashPassword = hashPassword;
113 changes: 113 additions & 0 deletions packages/authentication-local/lib/strategy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
const bcrypt = require('bcryptjs');
const { NotAuthenticated } = require('@feathersjs/errors');
const { get, omit } = require('lodash');

const debug = require('debug')('@feathersjs/authentication-local/strategy');

module.exports = class LocalStrategy {
setAuthentication (auth) {
this.authentication = auth;
}

setApplication (app) {
this.app = app;
}

setName (name) {
this.name = name;
}

get configuration () {
const authConfig = this.authentication.configuration;
const config = authConfig[this.name];

return Object.assign({}, {
hashSize: 10,
service: authConfig.service,
entity: authConfig.entity,
errorMessage: 'Invalid login',
entityPasswordField: config.passwordField,
entityUsernameField: config.usernameField
}, config);
}

getEntityQuery (query) {
return Promise.resolve(Object.assign({
$limit: 1
}, query));
}

findEntity (username, params) {
const { entityUsernameField, service, errorMessage } = this.configuration;

return this.getEntityQuery({
[entityUsernameField]: username
}, params).then(query => {
const findParams = Object.assign({}, params, { query });
const entityService = this.app.service(service);

debug('Finding entity with query', params.query);

return entityService.find(findParams);
}).then(result => {
const list = Array.isArray(result) ? result : result.data;

if (!Array.isArray(list) || list.length === 0) {
debug(`No entity found`);

return Promise.reject(new NotAuthenticated(errorMessage));
}

const [ entity ] = list;

return entity;
});
}

comparePassword (entity, password) {
const { entityPasswordField, errorMessage } = this.configuration;
// find password in entity, this allows for dot notation
const hash = get(entity, entityPasswordField);

if (!hash) {
debug(`Record is missing the '${entityPasswordField}' password field`);

return Promise.reject(new NotAuthenticated(errorMessage));
}

debug('Verifying password');

return bcrypt.compare(password, hash).then(result => {
if (result) {
return entity;
}

throw new NotAuthenticated(errorMessage);
});
}

hashPassword (password) {
return bcrypt.hash(password, this.configuration.hashSize);
}

authenticate (data, params) {
const { passwordField, usernameField, entity, errorMessage } = this.configuration;
const username = data[usernameField];
const password = data[passwordField];

if (data.strategy && data.strategy !== this.name) {
return Promise.reject(new NotAuthenticated(errorMessage));
}

return this.findEntity(username, omit(params, 'provider'))
.then(entity => this.comparePassword(entity, password))
.then(entity => params.provider
? this.findEntity(username, params) : entity
).then(authEntity => {
return {
authentication: { strategy: this.name },
[entity]: authEntity
};
});
}
};
27 changes: 0 additions & 27 deletions packages/authentication-local/lib/utils/hash.js

This file was deleted.

Loading