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

Allow user info to not require username, refactor affected functionalities #226

Open
wants to merge 3 commits into
base: 221-XHN-dev
Choose a base branch
from
Open
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
3 changes: 3 additions & 0 deletions config.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,9 @@ const config = {
address: '[email protected]'
}
},
loginInfo: {
usernameRequired: true
},
passwordComplexity: {
min: 8,
max: 32,
Expand Down
53 changes: 41 additions & 12 deletions server/api/login.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,13 @@ const register = function (server, options) {
method: 'POST',
path: '/api/login',
options: {
tags: ['api','auth'],
tags: ['api', 'auth'],
description: 'Log in with username and password.',
auth: false,
validate: {
payload: {
username: Joi.string().lowercase().required(),
username: Joi.string().lowercase().optional(),
email: Joi.string().lowercase().optional(),
password: Joi.string().required()
}
},
Expand All @@ -34,10 +35,15 @@ const register = function (server, options) {
method: async function (request, h) {

const ip = request.info.remoteAddress;
const username = request.payload.username;

const detected = await AuthAttempt.abuseDetected(ip,username);

let detected;
if (Config.get('/loginInfo/usernameRequired')) {
const username = request.payload.username;
detected = await AuthAttempt.abuseDetected(ip, username);
}
else {
const email = request.payload.email;
detected = await AuthAttempt.abuseDetected(ip, email);
}
if (detected) {
throw Boom.badRequest('Maximum number of auth attempts reached.');
}
Expand All @@ -48,15 +54,27 @@ const register = function (server, options) {
assign: 'user',
method: async function (request, h) {

const username = request.payload.username;
let userInfo;
if (Config.get('/loginInfo/usernameRequired')) {
userInfo = request.payload.username;
if (userInfo.indexOf('@') > -1) {
throw Boom.badRequest('Email cannot be used as username');
}
}
else {
userInfo = request.payload.email;
if (userInfo.indexOf('@') === -1) {
throw Boom.badRequest('Email required');
}
}
const password = request.payload.password;
const ip = request.info.remoteAddress;
const user = await User.findByCredentials(username, password);
const user = await User.findByCredentials(userInfo, password);
const userAgent = request.headers['user-agent'];

if (!user) {

await AuthAttempt.create(ip, username, userAgent);
await AuthAttempt.create(ip, userInfo, userAgent);

throw Boom.badRequest('Credentials are invalid or account is inactive.');
}
Expand Down Expand Up @@ -87,11 +105,23 @@ const register = function (server, options) {
const authHeader = `Basic ${Buffer.from(credentials).toString('base64')}`;

request.cookieAuth.set(request.pre.session);
if (Config.get('/loginInfo/usernameRequired')) {
return ({
user: {
_id: request.pre.user._id,
name: request.pre.user.name,
username: request.pre.user.username,
email: request.pre.user.email,
roles: request.pre.user.roles
},
session: request.pre.session,
authHeader
});
}
return ({
user: {
_id: request.pre.user._id,
name: request.pre.user.name,
username: request.pre.user.username,
email: request.pre.user.email,
roles: request.pre.user.roles
},
Expand All @@ -102,7 +132,6 @@ const register = function (server, options) {
});



server.route({
method: 'POST',
path: '/api/login/forgot',
Expand Down Expand Up @@ -259,7 +288,7 @@ const register = function (server, options) {
method: 'POST',
path: '/api/login/reset',
options: {
tags: ['api','auth'],
tags: ['api', 'auth'],
description: 'Verify Key to reset new password',
auth: false,
validate: {
Expand Down
13 changes: 8 additions & 5 deletions server/api/signup.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,14 @@ const register = function (server, options) {
assign: 'usernameCheck',
method: async function (request, h) {

const user = await User.findByUsername(request.payload.username);
if (Config.get('/loginInfo/usernameRequired')) {
const user = await User.findByUsername(request.payload.username);

if (user) {
if (user) {

throw Boom.conflict('Username already in use.');
throw Boom.conflict('Username already in use.');
}
}

return h.continue;
}
}, {
Expand Down Expand Up @@ -104,13 +105,15 @@ const register = function (server, options) {
const result = {
user: {
_id: user._id,
username: user.username,
email: user.email,
roles: user.roles
},
session,
authHeader
};
if (Config.get('/loginInfo/usernameRequired')) {
result.user.username = user.username;
};

return result;
}
Expand Down
24 changes: 14 additions & 10 deletions server/api/users.js
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,10 @@ const register = function (server, options) {
},
handler: async function (request, h) {

const username = request.payload.username;
let username = null;
if (Config.get('/loginInfo/usernameRequired')) {
username = request.payload.username;
}
const password = request.payload.password;
const email = request.payload.email;
const name = request.payload.name;
Expand Down Expand Up @@ -249,7 +252,7 @@ const register = function (server, options) {
},
validate: {
payload: {
username: Joi.string().token().lowercase().required(),
username: Joi.string().token().lowercase().optional(),
email: Joi.string().email().lowercase().required(),
name: Joi.string().required(),
gender: Joi.string().allow('male', 'female'),
Expand All @@ -265,17 +268,18 @@ const register = function (server, options) {
assign: 'usernameCheck',
method: async function (request, h) {

const conditions = {
username: request.payload.username,
_id: { $ne: request.auth.credentials.user._id }
};
if (Config.get('/loginInfo/usernameRequired')) {
const conditions = {
username: request.payload.username,
_id: { $ne: request.auth.credentials.user._id }
};

const user = await User.findOne(conditions);
const user = await User.findOne(conditions);

if (user) {
throw Boom.conflict('Username already in use.');
if (user) {
throw Boom.conflict('Username already in use.');
}
}

return h.continue;
}
}, {
Expand Down
27 changes: 13 additions & 14 deletions server/models/auth-attempt.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ const Hoek = require('hoek');

class AuthAttempt extends AnchorModel {

static async create(ip, username, userAgent) {
static async create(ip, userInfo, userAgent) {

Assert.ok(ip, 'Missing ip argument.');
Assert.ok(username, 'Missing username argument.');
Assert.ok(userInfo, 'Missing username or email argument.');
Assert.ok(userAgent, 'Missing userAgent argument.');

const agentInfo = UserAgent.lookup(userAgent);
Expand All @@ -21,22 +21,22 @@ class AuthAttempt extends AnchorModel {
browser,
ip,
os: agentInfo.os.toString(),
username
userInfo
});

const authAttempts = await this.insertOne(document);

return authAttempts[0];
}

static async abuseDetected(ip, username) {
static async abuseDetected(ip, userInfo) {

Assert.ok(ip, 'Missing ip argument.');
Assert.ok(username, 'Missing username argument.');
Assert.ok(userInfo, 'Missing username or email argument.');

const [countByIp, countByIpAndUser] = await Promise.all([
this.count({ ip }),
this.count({ ip, username })
this.count({ ip, userInfo })
]);

const config = Config.get('/authAttempts');
Expand All @@ -49,13 +49,12 @@ class AuthAttempt extends AnchorModel {


AuthAttempt.collectionName = 'authAttempts';

AuthAttempt.schema = Joi.object({
_id: Joi.object(),
browser: Joi.string(),
ip: Joi.string().required(),
os: Joi.string().required(),
username: Joi.string().lowercase().required(),
userInfo: Joi.string().lowercase().required(),
createdAt: Joi.date().default(new Date(), 'time of creation')
});

Expand All @@ -64,14 +63,14 @@ AuthAttempt.routes = Hoek.applyToDefaults(AnchorModel.routes, {
disabled: true
},
update: Joi.object({
username: Joi.string().lowercase().required()
userInfo: Joi.string().lowercase().required()
}),
tableView: {
outputDataFields: {
username: { label: 'Username' },
userInfo: { label: 'User Info' },
ip: { label: 'IP' },
createdAt: { label: 'Time' },
_id: { label: 'ID', accessRoles: ['admin', 'researcher','root'], invisible: true },
_id: { label: 'ID', accessRoles: ['admin', 'researcher', 'root'], invisible: true },
os: { label: 'OS', invisible: true },
browser: { label: 'browser', invisible: true }

Expand All @@ -82,16 +81,16 @@ AuthAttempt.routes = Hoek.applyToDefaults(AnchorModel.routes, {
},
editView: {
editSchema: Joi.object({
username: Joi.string().lowercase().required()
userInfo: Joi.string().lowercase().required()
})
}
});

AuthAttempt.lookups = [];

AuthAttempt.indexes = [
{ key: { ip: 1, username: 1 } },
{ key: { username: 1 } }
{ key: { ip: 1, userInfo: 1 } },
{ key: { userInfo: 1 } }
];

module.exports = AuthAttempt;
Loading