2017-07-26 16:52:55 +08:00
|
|
|
'use strict';
|
|
|
|
|
2017-11-27 20:20:57 +08:00
|
|
|
const Joi = require('../joi');
|
2018-08-03 20:44:03 +08:00
|
|
|
const MongoPaging = require('mongo-cursor-pagination');
|
2017-07-26 16:52:55 +08:00
|
|
|
const ObjectID = require('mongodb').ObjectID;
|
2018-08-03 20:44:03 +08:00
|
|
|
const tools = require('../tools');
|
2017-07-26 16:52:55 +08:00
|
|
|
|
|
|
|
module.exports = (db, server, userHandler) => {
|
2017-11-28 21:45:43 +08:00
|
|
|
/**
|
|
|
|
* @api {post} /authenticate Authenticate an User
|
|
|
|
* @apiName PostAuth
|
|
|
|
* @apiGroup Authentication
|
|
|
|
* @apiHeader {String} X-Access-Token Optional access token if authentication is enabled
|
|
|
|
* @apiHeaderExample {json} Header-Example:
|
|
|
|
* {
|
|
|
|
* "X-Access-Token": "59fc66a03e54454869460e45"
|
|
|
|
* }
|
|
|
|
*
|
|
|
|
* @apiParam {String} username Username or E-mail address
|
|
|
|
* @apiParam {String} password Password
|
|
|
|
* @apiParam {String} [protocol] Application identifier for security logs
|
|
|
|
* @apiParam {String} [scope="master"] Required scope. One of <code>master</code>, <code>imap</code>, <code>smtp</code>, <code>pop3</code>
|
|
|
|
* @apiParam {String} [sess] Session identifier for the logs
|
|
|
|
* @apiParam {String} [ip] IP address for the logs
|
|
|
|
*
|
|
|
|
* @apiSuccess {Boolean} success Indicates successful response
|
|
|
|
* @apiSuccess {String} id ID of authenticated User
|
|
|
|
* @apiSuccess {String} username Username of authenticated User
|
|
|
|
* @apiSuccess {String} scope The scope this authentication is valid for
|
|
|
|
* @apiSuccess {String[]} require2fa List of enabled 2FA mechanisms
|
|
|
|
* @apiSuccess {Boolean} requirePasswordChange Indicates if account hassword has been reset and should be replaced
|
|
|
|
*
|
|
|
|
* @apiError error Description of the error
|
|
|
|
*
|
|
|
|
* @apiExample {curl} Example usage:
|
|
|
|
* curl -i -XPOST http://localhost:8080/authenticate \
|
|
|
|
* -H 'Content-type: application/json' \
|
|
|
|
* -d '{
|
|
|
|
* "username": "myuser",
|
|
|
|
* "password": "secretpass",
|
|
|
|
* "scope": "master"
|
|
|
|
* }'
|
|
|
|
*
|
|
|
|
* @apiSuccessExample {json} Success-Response:
|
|
|
|
* HTTP/1.1 200 OK
|
|
|
|
* {
|
|
|
|
* "success": true,
|
|
|
|
* "id": "5a12914c350c183bd0d331f0",
|
|
|
|
* "username": "myuser",
|
|
|
|
* "scope": "master",
|
|
|
|
* "require2fa": [
|
|
|
|
* "totp"
|
|
|
|
* ],
|
|
|
|
* "requirePasswordChange": false
|
|
|
|
* }
|
|
|
|
*
|
|
|
|
* @apiErrorExample {json} Error-Response:
|
|
|
|
* HTTP/1.1 200 OK
|
|
|
|
* {
|
|
|
|
* "error": "Authentication failed. Invalid scope"
|
|
|
|
* }
|
|
|
|
*/
|
2017-07-26 16:52:55 +08:00
|
|
|
server.post('/authenticate', (req, res, next) => {
|
|
|
|
res.charSet('utf-8');
|
|
|
|
|
|
|
|
const schema = Joi.object().keys({
|
2017-09-01 19:50:53 +08:00
|
|
|
username: Joi.alternatives()
|
|
|
|
.try(
|
|
|
|
Joi.string()
|
|
|
|
.lowercase()
|
2018-02-22 02:26:07 +08:00
|
|
|
.regex(/^[a-z0-9][a-z0-9.]+[a-z0-9]$/, 'username')
|
|
|
|
.min(3)
|
2017-09-01 19:50:53 +08:00
|
|
|
.max(30),
|
|
|
|
Joi.string().email()
|
|
|
|
)
|
|
|
|
.required(),
|
|
|
|
password: Joi.string()
|
|
|
|
.max(256)
|
|
|
|
.required(),
|
2017-07-26 16:52:55 +08:00
|
|
|
|
|
|
|
protocol: Joi.string().default('API'),
|
|
|
|
scope: Joi.string().default('master'),
|
|
|
|
|
2018-06-28 14:12:31 +08:00
|
|
|
appId: Joi.string()
|
|
|
|
.empty('')
|
|
|
|
.uri(),
|
|
|
|
|
2017-10-30 19:41:53 +08:00
|
|
|
sess: Joi.string().max(255),
|
2017-07-26 16:52:55 +08:00
|
|
|
ip: Joi.string().ip({
|
|
|
|
version: ['ipv4', 'ipv6'],
|
|
|
|
cidr: 'forbidden'
|
|
|
|
})
|
|
|
|
});
|
|
|
|
|
|
|
|
const result = Joi.validate(req.params, schema, {
|
|
|
|
abortEarly: false,
|
|
|
|
convert: true
|
|
|
|
});
|
|
|
|
|
|
|
|
if (result.error) {
|
|
|
|
res.json({
|
2017-12-21 16:31:34 +08:00
|
|
|
error: result.error.message,
|
|
|
|
code: 'InputValidationError'
|
2017-07-26 16:52:55 +08:00
|
|
|
});
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
|
|
|
|
let meta = {
|
|
|
|
protocol: result.value.protocol,
|
2017-10-30 19:41:53 +08:00
|
|
|
sess: result.value.sess,
|
2017-07-26 16:52:55 +08:00
|
|
|
ip: result.value.ip
|
|
|
|
};
|
|
|
|
|
2018-06-28 14:12:31 +08:00
|
|
|
if (result.value.appId) {
|
|
|
|
meta.appId = result.value.appId;
|
|
|
|
}
|
|
|
|
|
2017-07-26 16:52:55 +08:00
|
|
|
userHandler.authenticate(result.value.username, result.value.password, result.value.scope, meta, (err, authData) => {
|
|
|
|
if (err) {
|
|
|
|
res.json({
|
|
|
|
error: err.message
|
|
|
|
});
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!authData) {
|
|
|
|
res.json({
|
|
|
|
error: 'Authentication failed'
|
|
|
|
});
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
|
2017-10-10 16:19:10 +08:00
|
|
|
let authResponse = {
|
2017-07-26 16:52:55 +08:00
|
|
|
success: true,
|
|
|
|
id: authData.user,
|
|
|
|
username: authData.username,
|
|
|
|
scope: authData.scope,
|
2017-08-05 20:55:29 +08:00
|
|
|
require2fa: authData.require2fa,
|
|
|
|
requirePasswordChange: authData.requirePasswordChange
|
2017-10-10 16:19:10 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
if (authData.u2fAuthRequest) {
|
|
|
|
authResponse.u2fAuthRequest = authData.u2fAuthRequest;
|
|
|
|
}
|
|
|
|
|
|
|
|
res.json(authResponse);
|
2017-07-26 16:52:55 +08:00
|
|
|
|
|
|
|
return next();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2017-11-28 21:45:43 +08:00
|
|
|
/**
|
|
|
|
* @api {get} /users/:user/authlog List authentication Events
|
|
|
|
* @apiName GetAuthlog
|
|
|
|
* @apiGroup Authentication
|
|
|
|
* @apiHeader {String} X-Access-Token Optional access token if authentication is enabled
|
|
|
|
* @apiHeaderExample {json} Header-Example:
|
|
|
|
* {
|
|
|
|
* "X-Access-Token": "59fc66a03e54454869460e45"
|
|
|
|
* }
|
|
|
|
*
|
|
|
|
* @apiParam {String} user ID of the User
|
|
|
|
* @apiParam {String} [action] Limit listing only to values with specific action value
|
|
|
|
* @apiParam {String} [sess] Limit listing only to values with specific session identifier
|
|
|
|
* @apiParam {String} [ip Limit listing only to values with specific IP address
|
|
|
|
* @apiParam {Number} [limit=20] How many records to return
|
|
|
|
* @apiParam {Number} [page=1] Current page number. Informational only, page numbers start from 1
|
|
|
|
* @apiParam {Number} [next] Cursor value for next page, retrieved from <code>nextCursor</code> response value
|
|
|
|
* @apiParam {Number} [previous] Cursor value for previous page, retrieved from <code>previousCursor</code> response value
|
|
|
|
*
|
|
|
|
* @apiSuccess {Boolean} success Indicates successful response
|
|
|
|
* @apiSuccess {Number} total How many results were found
|
|
|
|
* @apiSuccess {Number} page Current page number. Derived from <code>page</code> query argument
|
|
|
|
* @apiSuccess {String} previousCursor Either a cursor string or false if there are not any previous results
|
|
|
|
* @apiSuccess {String} nextCursor Either a cursor string or false if there are not any next results
|
|
|
|
* @apiSuccess {Object[]} results Event listing
|
|
|
|
* @apiSuccess {String} results.id ID of the Event
|
|
|
|
* @apiSuccess {String} results.action Action identifier
|
|
|
|
* @apiSuccess {String} results.result Did the action succeed
|
|
|
|
* @apiSuccess {String} results.sess Session identifier
|
|
|
|
* @apiSuccess {String} results.ip IP address of the Event
|
|
|
|
* @apiSuccess {String} results.created Datestring of the Event time
|
|
|
|
*
|
|
|
|
* @apiError error Description of the error
|
|
|
|
*
|
|
|
|
* @apiExample {curl} Example usage:
|
|
|
|
* curl -i "http://localhost:8080/users/59fc66a03e54454869460e45/authlog?action=account+created"
|
|
|
|
*
|
|
|
|
* @apiSuccessExample {json} Success-Response:
|
|
|
|
* HTTP/1.1 200 OK
|
|
|
|
* {
|
|
|
|
* "success": true,
|
|
|
|
* "action": "account created",
|
|
|
|
* "total": 1,
|
|
|
|
* "page": 1,
|
|
|
|
* "previousCursor": false,
|
|
|
|
* "nextCursor": false,
|
|
|
|
* "results": [
|
|
|
|
* {
|
|
|
|
* "id": "59fc66a03e54454869460e4d",
|
|
|
|
* "action": "account created",
|
|
|
|
* "result": "success",
|
|
|
|
* "sess": null,
|
|
|
|
* "ip": null,
|
|
|
|
* "created": "2017-11-03T12:52:48.792Z",
|
|
|
|
* "expires": "2017-12-03T12:52:48.792Z"
|
|
|
|
* }
|
|
|
|
* ]
|
|
|
|
* }
|
|
|
|
*
|
|
|
|
* @apiErrorExample {json} Error-Response:
|
|
|
|
* HTTP/1.1 200 OK
|
|
|
|
* {
|
|
|
|
* "error": "Database error"
|
|
|
|
* }
|
|
|
|
*/
|
2018-08-03 20:44:03 +08:00
|
|
|
server.get(
|
|
|
|
{ name: 'authlog', path: '/users/:user/authlog' },
|
|
|
|
tools.asyncifyJson(async (req, res, next) => {
|
|
|
|
res.charSet('utf-8');
|
|
|
|
|
|
|
|
const schema = Joi.object().keys({
|
|
|
|
user: Joi.string()
|
|
|
|
.hex()
|
|
|
|
.lowercase()
|
|
|
|
.length(24)
|
|
|
|
.required(),
|
|
|
|
action: Joi.string()
|
|
|
|
.trim()
|
|
|
|
.lowercase()
|
|
|
|
.empty('')
|
|
|
|
.max(100),
|
|
|
|
ip: Joi.string().ip({
|
|
|
|
version: ['ipv4', 'ipv6'],
|
|
|
|
cidr: 'forbidden'
|
|
|
|
}),
|
|
|
|
limit: Joi.number()
|
|
|
|
.default(20)
|
|
|
|
.min(1)
|
|
|
|
.max(250),
|
|
|
|
next: Joi.string()
|
|
|
|
.empty('')
|
|
|
|
.mongoCursor()
|
|
|
|
.max(1024),
|
|
|
|
previous: Joi.string()
|
|
|
|
.empty('')
|
|
|
|
.mongoCursor()
|
|
|
|
.max(1024),
|
|
|
|
page: Joi.number()
|
|
|
|
.empty('')
|
|
|
|
.default(1)
|
|
|
|
});
|
2017-07-26 16:52:55 +08:00
|
|
|
|
2018-08-03 20:44:03 +08:00
|
|
|
req.query.user = req.params.user;
|
2017-07-26 16:52:55 +08:00
|
|
|
|
2018-08-03 20:44:03 +08:00
|
|
|
const result = Joi.validate(req.query, schema, {
|
|
|
|
abortEarly: false,
|
|
|
|
convert: true,
|
|
|
|
allowUnknown: false
|
2017-07-26 16:52:55 +08:00
|
|
|
});
|
|
|
|
|
2018-08-03 20:44:03 +08:00
|
|
|
if (result.error) {
|
|
|
|
res.json({
|
|
|
|
error: result.error.message,
|
|
|
|
code: 'InputValidationError'
|
|
|
|
});
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
|
|
|
|
let user = new ObjectID(result.value.user);
|
|
|
|
let limit = result.value.limit;
|
2018-08-15 04:03:12 +08:00
|
|
|
|
2018-08-03 20:44:03 +08:00
|
|
|
let action = result.value.action;
|
|
|
|
let ip = result.value.ip;
|
2018-08-15 04:03:12 +08:00
|
|
|
|
2018-08-03 20:44:03 +08:00
|
|
|
let page = result.value.page;
|
|
|
|
let pageNext = result.value.next;
|
|
|
|
let pagePrevious = result.value.previous;
|
|
|
|
|
|
|
|
let userData;
|
|
|
|
try {
|
|
|
|
userData = await db.users.collection('users').findOne(
|
|
|
|
{
|
|
|
|
_id: user
|
|
|
|
},
|
|
|
|
{
|
2018-08-15 04:45:45 +08:00
|
|
|
projection: {
|
2018-08-03 20:44:03 +08:00
|
|
|
_id: true
|
|
|
|
}
|
2017-07-26 16:52:55 +08:00
|
|
|
}
|
2018-08-03 20:44:03 +08:00
|
|
|
);
|
|
|
|
} catch (err) {
|
|
|
|
res.json({
|
|
|
|
error: 'MongoDB Error: ' + err.message,
|
|
|
|
code: 'InternalDatabaseError'
|
|
|
|
});
|
|
|
|
return next();
|
|
|
|
}
|
2017-07-26 16:52:55 +08:00
|
|
|
|
2018-08-03 20:44:03 +08:00
|
|
|
if (!userData) {
|
|
|
|
res.json({
|
|
|
|
error: 'This user does not exist',
|
|
|
|
code: 'UserNotFound'
|
|
|
|
});
|
|
|
|
return next();
|
|
|
|
}
|
2017-11-23 17:51:37 +08:00
|
|
|
|
2018-08-03 20:44:03 +08:00
|
|
|
let filter = { user };
|
|
|
|
if (ip) {
|
|
|
|
filter.ip = ip;
|
|
|
|
}
|
2017-07-26 16:52:55 +08:00
|
|
|
|
2018-08-03 21:15:35 +08:00
|
|
|
let total = await db.database.collection('authlog').countDocuments(filter);
|
2017-11-23 17:51:37 +08:00
|
|
|
|
2018-08-03 20:44:03 +08:00
|
|
|
let opts = {
|
|
|
|
limit,
|
|
|
|
query: filter,
|
|
|
|
sortAscending: false
|
|
|
|
};
|
2017-11-23 17:51:37 +08:00
|
|
|
|
2018-08-03 20:44:03 +08:00
|
|
|
if (pageNext) {
|
|
|
|
opts.next = pageNext;
|
2018-08-03 20:59:33 +08:00
|
|
|
} else if (page > 1 && pagePrevious) {
|
2018-08-03 20:44:03 +08:00
|
|
|
opts.previous = pagePrevious;
|
|
|
|
}
|
|
|
|
|
|
|
|
let listing;
|
|
|
|
try {
|
|
|
|
listing = await MongoPaging.find(db.users.collection('authlog'), opts);
|
|
|
|
} catch (err) {
|
|
|
|
res.json({
|
|
|
|
error: 'MongoDB Error: ' + err.message,
|
|
|
|
code: 'InternalDatabaseError'
|
2017-07-26 16:52:55 +08:00
|
|
|
});
|
2018-08-03 20:44:03 +08:00
|
|
|
return next();
|
2017-11-23 17:51:37 +08:00
|
|
|
}
|
2018-08-03 20:44:03 +08:00
|
|
|
|
|
|
|
if (!listing.hasPrevious) {
|
|
|
|
page = 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
let response = {
|
|
|
|
success: true,
|
|
|
|
action,
|
|
|
|
total,
|
|
|
|
page,
|
|
|
|
previousCursor: listing.hasPrevious ? listing.previous : false,
|
|
|
|
nextCursor: listing.hasNext ? listing.next : false,
|
|
|
|
results: (listing.results || []).map(resultData => {
|
|
|
|
let response = {
|
|
|
|
id: resultData._id
|
|
|
|
};
|
|
|
|
Object.keys(resultData).forEach(key => {
|
|
|
|
if (!['_id', 'user'].includes(key)) {
|
|
|
|
response[key] = resultData[key];
|
|
|
|
}
|
|
|
|
});
|
|
|
|
return response;
|
|
|
|
})
|
|
|
|
};
|
|
|
|
|
|
|
|
res.json(response);
|
|
|
|
return next();
|
|
|
|
})
|
|
|
|
);
|
2018-01-12 16:16:16 +08:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @api {get} /users/:user/authlog/:event Request Event information
|
|
|
|
* @apiName GetAuthlogEvent
|
|
|
|
* @apiGroup Authentication
|
|
|
|
* @apiHeader {String} X-Access-Token Optional access token if authentication is enabled
|
|
|
|
* @apiHeaderExample {json} Header-Example:
|
|
|
|
* {
|
|
|
|
* "X-Access-Token": "59fc66a03e54454869460e45"
|
|
|
|
* }
|
|
|
|
*
|
|
|
|
* @apiParam {String} user ID of the User
|
|
|
|
* @apiParam {String} event ID of the Event
|
|
|
|
*
|
|
|
|
* @apiSuccess {Boolean} success Indicates successful response
|
|
|
|
* @apiSuccess {String} id ID of the Event
|
|
|
|
* @apiSuccess {String} action Action identifier
|
|
|
|
* @apiSuccess {String} result Did the action succeed
|
|
|
|
* @apiSuccess {String} sess Session identifier
|
|
|
|
* @apiSuccess {String} ip IP address of the Event
|
|
|
|
* @apiSuccess {String} created Datestring of the Event time
|
|
|
|
*
|
|
|
|
* @apiError error Description of the error
|
|
|
|
*
|
|
|
|
* @apiExample {curl} Example usage:
|
|
|
|
* curl -i "http://localhost:8080/users/59fc66a03e54454869460e45/authlog/59fc66a03e54454869460e4d"
|
|
|
|
*
|
|
|
|
* @apiSuccessExample {json} Success-Response:
|
|
|
|
* HTTP/1.1 200 OK
|
|
|
|
* {
|
|
|
|
* "id": "59fc66a03e54454869460e4d",
|
|
|
|
* "action": "account created",
|
|
|
|
* "result": "success",
|
|
|
|
* "sess": null,
|
|
|
|
* "ip": null,
|
|
|
|
* "created": "2017-11-03T12:52:48.792Z",
|
|
|
|
* "expires": "2017-12-03T12:52:48.792Z"
|
|
|
|
* }
|
|
|
|
*
|
|
|
|
* @apiErrorExample {json} Error-Response:
|
|
|
|
* HTTP/1.1 200 OK
|
|
|
|
* {
|
|
|
|
* "error": "Database error"
|
|
|
|
* }
|
|
|
|
*/
|
|
|
|
server.get('/users/:user/authlog/:event', (req, res, next) => {
|
|
|
|
res.charSet('utf-8');
|
|
|
|
|
|
|
|
const schema = Joi.object().keys({
|
|
|
|
user: Joi.string()
|
|
|
|
.hex()
|
|
|
|
.lowercase()
|
|
|
|
.length(24)
|
|
|
|
.required(),
|
|
|
|
event: Joi.string()
|
|
|
|
.hex()
|
|
|
|
.lowercase()
|
|
|
|
.length(24)
|
|
|
|
.required()
|
|
|
|
});
|
|
|
|
|
|
|
|
const result = Joi.validate(req.params, schema, {
|
|
|
|
abortEarly: false,
|
|
|
|
convert: true,
|
|
|
|
allowUnknown: false
|
|
|
|
});
|
|
|
|
|
|
|
|
if (result.error) {
|
|
|
|
res.json({
|
|
|
|
error: result.error.message,
|
|
|
|
code: 'InputValidationError'
|
|
|
|
});
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
|
|
|
|
let user = new ObjectID(result.value.user);
|
|
|
|
let event = new ObjectID(result.value.event);
|
|
|
|
|
|
|
|
db.users.collection('users').findOne(
|
|
|
|
{
|
|
|
|
_id: user
|
|
|
|
},
|
|
|
|
{
|
2018-08-15 04:45:45 +08:00
|
|
|
projection: {
|
2018-01-12 16:16:16 +08:00
|
|
|
_id: true
|
|
|
|
}
|
|
|
|
},
|
|
|
|
(err, userData) => {
|
|
|
|
if (err) {
|
|
|
|
res.json({
|
|
|
|
error: 'MongoDB Error: ' + err.message,
|
|
|
|
code: 'InternalDatabaseError'
|
|
|
|
});
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
if (!userData) {
|
|
|
|
res.json({
|
|
|
|
error: 'This user does not exist',
|
|
|
|
code: 'UserNotFound'
|
|
|
|
});
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
|
|
|
|
let filter = { _id: event, user };
|
|
|
|
|
|
|
|
db.database.collection('authlog').findOne(filter, (err, eventData) => {
|
|
|
|
if (err) {
|
|
|
|
res.json({
|
|
|
|
error: err.message
|
|
|
|
});
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!eventData) {
|
|
|
|
res.json({
|
|
|
|
error: 'Event was not found',
|
|
|
|
code: 'EventNotFound'
|
|
|
|
});
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
|
|
|
|
let response = {
|
|
|
|
success: true,
|
|
|
|
id: eventData._id
|
|
|
|
};
|
|
|
|
Object.keys(eventData).forEach(key => {
|
|
|
|
if (!['_id', 'user'].includes(key)) {
|
|
|
|
response[key] = eventData[key];
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
res.json(response);
|
|
|
|
return next();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
);
|
|
|
|
});
|
2017-07-26 16:52:55 +08:00
|
|
|
};
|