experimental user token generation on auth

This commit is contained in:
Andris Reinman 2019-03-20 23:19:15 +02:00
parent bc59ac1451
commit 5fffe6fb79
8 changed files with 95 additions and 6 deletions

19
api.js
View file

@ -247,6 +247,25 @@ server.use(
} catch (err) {
// ignore
}
} else if (tokenData.ttl && isNaN(tokenData.ttl) && Number(tokenData.ttl) > 0) {
let tokenTTL = Number(tokenData.ttl);
let tokenLifetime = config.api.accessControl.tokenLifetime || 30 * 24 * 3600;
// check if token is not too old
if (tokenLifetime < (Date.now() - Number(tokenData.created)) / 1000) {
// token is still usable, increase session length
try {
await db.redis
.multi()
.expire('tn:token:' + tokenHash, tokenTTL)
.expire('tn:user:' + tokenData.user, tokenTTL)
.exec();
} catch (err) {
// ignore
}
req.role = tokenData.role;
req.user = tokenData.user;
}
} else {
req.role = tokenData.role;
req.user = tokenData.user;

View file

@ -253,6 +253,12 @@
},
"auth": {
"authentication": {
"create:any": ["*", "!token"]
}
},
"tokenAuth": {
"authentication": {
"create:any": ["*"]
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1 +1 @@
define({ "name": "wildduck", "version": "1.0.0", "description": "WildDuck API docs", "title": "WildDuck API", "url": "https://api.wildduck.email", "sampleUrl": false, "defaultVersion": "0.0.0", "apidoc": "0.3.0", "generator": { "name": "apidoc", "time": "2019-02-26T12:32:31.900Z", "url": "http://apidocjs.com", "version": "0.17.7" } });
define({ "name": "wildduck", "version": "1.0.0", "description": "WildDuck API docs", "title": "WildDuck API", "url": "https://api.wildduck.email", "sampleUrl": false, "defaultVersion": "0.0.0", "apidoc": "0.3.0", "generator": { "name": "apidoc", "time": "2019-03-20T21:18:58.140Z", "url": "http://apidocjs.com", "version": "0.17.7" } });

View file

@ -1 +1 @@
{ "name": "wildduck", "version": "1.0.0", "description": "WildDuck API docs", "title": "WildDuck API", "url": "https://api.wildduck.email", "sampleUrl": false, "defaultVersion": "0.0.0", "apidoc": "0.3.0", "generator": { "name": "apidoc", "time": "2019-02-26T12:32:31.900Z", "url": "http://apidocjs.com", "version": "0.17.7" } }
{ "name": "wildduck", "version": "1.0.0", "description": "WildDuck API docs", "title": "WildDuck API", "url": "https://api.wildduck.email", "sampleUrl": false, "defaultVersion": "0.0.0", "apidoc": "0.3.0", "generator": { "name": "apidoc", "time": "2019-03-20T21:18:58.140Z", "url": "http://apidocjs.com", "version": "0.17.7" } }

View file

@ -32,6 +32,7 @@ module.exports = (db, server, userHandler) => {
* @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 {Boolean} [token=false] If true then generates a temporary access token that is valid for this user
* @apiParam {String} [sess] Session identifier for the logs
* @apiParam {String} [ip] IP address for the logs
*
@ -41,6 +42,7 @@ module.exports = (db, server, userHandler) => {
* @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
* @apiSuccess {String} [token] If access token was requested then this is the value to use as access token when making API requests on behalf of logged in user
*
* @apiError error Description of the error
* @apiError [code] Error code
@ -103,6 +105,11 @@ module.exports = (db, server, userHandler) => {
.empty('')
.uri(),
token: Joi.boolean()
.truthy(['Y', 'true', 'yes', 'on', '1', 1])
.falsy(['N', 'false', 'no', 'off', '0', 0, ''])
.default(false),
sess: Joi.string().max(255),
ip: Joi.string().ip({
version: ['ipv4', 'ipv6'],
@ -123,8 +130,13 @@ module.exports = (db, server, userHandler) => {
return next();
}
let permission = roles.can(req.role).createAny('authentication');
// permissions check
req.validate(roles.can(req.role).createAny('authentication'));
req.validate(permission);
// filter out unallowed fields
result.value = permission.filter(result.value);
let meta = {
protocol: result.value.protocol,
@ -175,11 +187,25 @@ module.exports = (db, server, userHandler) => {
requirePasswordChange: authData.requirePasswordChange
};
if (result.value.token) {
try {
authResponse.token = await userHandler.generateAuthToken(authData.user);
} catch (err) {
let response = {
error: err.message,
code: 'AuthFailed' || err.code,
id: user.toString()
};
res.json(response);
return next();
}
}
if (authData.u2fAuthRequest) {
authResponse.u2fAuthRequest = authData.u2fAuthRequest;
}
res.json(authResponse);
res.json(permission.filter(authResponse));
return next();
})
);

View file

@ -3323,6 +3323,44 @@ class UserHandler {
}
);
}
async generateAuthToken(user) {
let accessToken = crypto.randomBytes(20).toString('hex');
let tokenHash = crypto
.createHash('sha256')
.update(accessToken)
.digest('hex');
let key = 'tn:token:' + tokenHash;
let ttl = config.api.accessControl.tokenTTL || 3600;
let tokenData = {
user: user.toString(),
role: 'user',
created: Date.now(),
ttl,
// signature
s: crypto
.createHmac('sha256', config.api.accessControl.secret)
.update(
JSON.stringify({
token: accessToken,
user: user.toString(),
role: 'user'
})
)
.digest('hex')
};
await this.redis
.multi()
.hmset(key, tokenData)
.sadd('tn:user:' + user, tokenHash)
.expire(key, ttl)
.expire('tn:user:' + user, ttl)
.exec();
return accessToken;
}
}
function rateLimitResponse(res, callback) {