wildduck/lib/api/auth.js

400 lines
12 KiB
JavaScript
Raw Normal View History

2017-07-26 16:52:55 +08:00
'use strict';
2020-07-20 01:51:06 +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');
2018-08-29 18:15:38 +08:00
const roles = require('../roles');
2020-07-20 01:51:06 +08:00
const { nextPageCursorSchema, previousPageCursorSchema, pageNrSchema, sessSchema, sessIPSchema, booleanSchema } = require('../schemas');
2017-07-26 16:52:55 +08:00
module.exports = (db, server, userHandler) => {
2018-08-29 18:15:38 +08:00
server.post(
'/authenticate',
tools.asyncifyJson(async (req, res, next) => {
res.charSet('utf-8');
2017-07-26 16:52:55 +08:00
2018-08-29 18:15:38 +08:00
const schema = Joi.object().keys({
username: Joi.alternatives()
.try(
Joi.string()
.lowercase()
.regex(/^[a-z0-9][a-z0-9.]+[a-z0-9]$/, 'username')
.min(3)
.max(30),
2020-07-20 01:51:06 +08:00
Joi.string().email({ tlds: false })
2018-08-29 18:15:38 +08:00
)
.required(),
2020-07-16 16:15:04 +08:00
password: Joi.string().max(256).required(),
2018-08-29 18:15:38 +08:00
protocol: Joi.string().default('API'),
2019-03-21 16:30:32 +08:00
scope: Joi.string()
.default('master')
// token can be true only if scope is master
.when('token', { is: true, then: Joi.valid('master') }),
2017-07-26 16:52:55 +08:00
2020-07-16 16:15:04 +08:00
appId: Joi.string().empty('').uri(),
2018-08-29 18:15:38 +08:00
2020-07-20 01:51:06 +08:00
token: booleanSchema.default(false),
2020-07-20 01:51:06 +08:00
sess: sessSchema,
ip: sessIPSchema
2017-07-26 16:52:55 +08:00
});
2020-10-08 13:55:56 +08:00
2020-07-20 01:51:06 +08:00
const result = schema.validate(req.params, {
2018-08-29 18:15:38 +08:00
abortEarly: false,
convert: true
});
if (result.error) {
res.status(400);
2018-08-29 18:15:38 +08:00
res.json({
error: result.error.message,
code: 'InputValidationError',
details: tools.validationErrors(result)
2018-08-29 18:15:38 +08:00
});
return next();
}
let permission = roles.can(req.role).createAny('authentication');
2018-08-29 18:15:38 +08:00
// permissions check
req.validate(permission);
// filter out unallowed fields
result.value = permission.filter(result.value);
2017-07-26 16:52:55 +08:00
2018-08-29 18:15:38 +08:00
let meta = {
protocol: result.value.protocol,
sess: result.value.sess,
ip: result.value.ip
};
2018-06-28 14:12:31 +08:00
2018-08-29 18:15:38 +08:00
if (result.value.appId) {
meta.appId = result.value.appId;
}
let authData, user;
try {
2019-07-12 15:21:48 +08:00
[authData, user] = await userHandler.asyncAuthenticate(result.value.username, result.value.password, result.value.scope, meta);
2018-08-29 18:15:38 +08:00
} catch (err) {
2018-08-23 16:32:21 +08:00
let response = {
2019-01-13 19:13:22 +08:00
error: err.message,
code: 'AuthFailed' || err.code
2018-08-23 16:32:21 +08:00
};
if (user) {
response.id = user.toString();
}
res.status(403);
2018-08-23 16:32:21 +08:00
res.json(response);
2017-07-26 16:52:55 +08:00
return next();
}
if (!authData) {
2018-08-23 16:32:21 +08:00
let response = {
error: 'Authentication failed',
code: 'AuthFailed'
};
if (user) {
response.id = user.toString();
}
res.status(403);
2018-08-23 16:32:21 +08:00
res.json(response);
2017-07-26 16:52:55 +08:00
return next();
}
2017-10-10 16:19:10 +08:00
let authResponse = {
2017-07-26 16:52:55 +08:00
success: true,
2019-03-21 05:51:35 +08:00
id: authData.user.toString(),
2017-07-26 16:52:55 +08:00
username: authData.username,
scope: authData.scope,
require2fa: authData.require2fa,
requirePasswordChange: authData.requirePasswordChange
2017-10-10 16:19:10 +08:00
};
if (result.value.token) {
try {
authResponse.token = await userHandler.generateAuthToken(authData.user);
} catch (err) {
let response = {
error: err.message,
2019-04-05 20:08:46 +08:00
code: err.code || 'AuthFailed',
id: user.toString()
};
res.status(500);
res.json(response);
return next();
}
}
2017-10-10 16:19:10 +08:00
if (authData.u2fAuthRequest) {
authResponse.u2fAuthRequest = authData.u2fAuthRequest;
}
res.status(200);
res.json(permission.filter(authResponse));
2017-07-26 16:52:55 +08:00
return next();
2018-08-29 18:15:38 +08:00
})
);
2017-07-26 16:52:55 +08:00
2019-04-05 20:08:46 +08:00
server.del(
'/authenticate',
tools.asyncifyJson(async (req, res, next) => {
res.charSet('utf-8');
if (req.accessToken) {
try {
await db.redis
.multi()
.del('tn:token:' + req.accessToken.hash)
.exec();
} catch (err) {
// ignore
}
}
res.json({ success: true });
return next();
})
);
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({
2020-07-16 16:15:04 +08:00
user: Joi.string().hex().lowercase().length(24).required(),
action: Joi.string().trim().lowercase().empty('').max(100),
limit: Joi.number().default(20).min(1).max(250),
2020-07-20 01:51:06 +08:00
next: nextPageCursorSchema,
previous: previousPageCursorSchema,
page: pageNrSchema,
filterip: sessIPSchema,
sess: sessSchema,
ip: sessIPSchema
2018-08-03 20:44:03 +08:00
});
2017-07-26 16:52:55 +08:00
2020-07-20 01:51:06 +08:00
const result = schema.validate(req.params, {
2018-08-03 20:44:03 +08:00
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.status(400);
2018-08-03 20:44:03 +08:00
res.json({
error: result.error.message,
code: 'InputValidationError',
details: tools.validationErrors(result)
2018-08-03 20:44:03 +08:00
});
return next();
}
2018-08-29 18:15:38 +08:00
// permissions check
if (req.user && req.user === result.value.user) {
req.validate(roles.can(req.role).readOwn('authentication'));
} else {
req.validate(roles.can(req.role).readAny('authentication'));
}
2018-08-03 20:44:03 +08:00
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;
2018-11-12 20:36:34 +08:00
let ip = result.value.filterIp;
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-11-12 20:39:11 +08:00
let total = await db.users.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-09-20 16:28:43 +08:00
} else if ((!page || 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 = {
2018-11-14 19:32:40 +08:00
id: (resultData._id || '').toString()
2018-08-03 20:44:03 +08:00
};
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
2018-08-29 18:15:38 +08:00
server.get(
'/users/:user/authlog/:event',
tools.asyncifyJson(async (req, res, next) => {
res.charSet('utf-8');
2018-01-12 16:16:16 +08:00
2018-08-29 18:15:38 +08:00
const schema = Joi.object().keys({
2020-07-16 16:15:04 +08:00
user: Joi.string().hex().lowercase().length(24).required(),
event: Joi.string().hex().lowercase().length(24).required(),
2020-07-20 01:51:06 +08:00
sess: sessSchema,
ip: sessIPSchema
2018-08-29 18:15:38 +08:00
});
2018-01-12 16:16:16 +08:00
2020-07-20 01:51:06 +08:00
const result = schema.validate(req.params, {
2018-08-29 18:15:38 +08:00
abortEarly: false,
convert: true,
allowUnknown: false
2018-01-12 16:16:16 +08:00
});
2018-08-29 18:15:38 +08:00
if (result.error) {
res.status(400);
2018-08-29 18:15:38 +08:00
res.json({
error: result.error.message,
code: 'InputValidationError',
details: tools.validationErrors(result)
2018-08-29 18:15:38 +08:00
});
return next();
}
2018-01-12 16:16:16 +08:00
2018-08-29 18:15:38 +08:00
// permissions check
if (req.user && req.user === result.value.user) {
req.validate(roles.can(req.role).readOwn('authentication'));
} else {
req.validate(roles.can(req.role).readAny('authentication'));
}
2018-01-12 16:16:16 +08:00
2018-08-29 18:15:38 +08:00
let user = new ObjectID(result.value.user);
let event = new ObjectID(result.value.event);
2018-01-12 16:16:16 +08:00
2018-08-29 18:15:38 +08:00
let userData;
try {
userData = await db.users.collection('users').findOne(
{
_id: user
},
{
projection: {
_id: true
2018-01-12 16:16:16 +08:00
}
2018-08-29 18:15:38 +08:00
}
);
} catch (err) {
res.json({
error: 'MongoDB Error: ' + err.message,
code: 'InternalDatabaseError'
});
return next();
}
2018-01-12 16:16:16 +08:00
2018-08-29 18:15:38 +08:00
if (!userData) {
res.json({
error: 'This user does not exist',
code: 'UserNotFound'
2018-01-12 16:16:16 +08:00
});
2018-08-29 18:15:38 +08:00
return next();
2018-01-12 16:16:16 +08:00
}
2018-08-29 18:15:38 +08:00
let filter = { _id: event, user };
let eventData;
try {
2018-11-12 20:39:11 +08:00
eventData = await db.users.collection('authlog').findOne(filter);
2018-08-29 18:15:38 +08:00
} catch (err) {
res.json({
error: 'MongoDB Error: ' + err.message,
code: 'InternalDatabaseError'
});
return next();
}
if (!eventData) {
res.json({
error: 'Event was not found',
code: 'EventNotFound'
});
return next();
}
let response = {
success: true,
2021-01-07 15:41:48 +08:00
id: eventData._id.toString()
2018-08-29 18:15:38 +08:00
};
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
};