mirror of
https://github.com/nodemailer/wildduck.git
synced 2025-11-10 00:11:06 +08:00
experimental user token generation on auth
This commit is contained in:
parent
bc59ac1451
commit
5fffe6fb79
8 changed files with 95 additions and 6 deletions
19
api.js
19
api.js
|
|
@ -247,6 +247,25 @@ server.use(
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// ignore
|
// 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 {
|
} else {
|
||||||
req.role = tokenData.role;
|
req.role = tokenData.role;
|
||||||
req.user = tokenData.user;
|
req.user = tokenData.user;
|
||||||
|
|
|
||||||
|
|
@ -253,6 +253,12 @@
|
||||||
},
|
},
|
||||||
|
|
||||||
"auth": {
|
"auth": {
|
||||||
|
"authentication": {
|
||||||
|
"create:any": ["*", "!token"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"tokenAuth": {
|
||||||
"authentication": {
|
"authentication": {
|
||||||
"create:any": ["*"]
|
"create:any": ["*"]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -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"
}
});
|
||||||
|
|
|
||||||
|
|
@ -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"
}
}
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ module.exports = (db, server, userHandler) => {
|
||||||
* @apiParam {String} password Password
|
* @apiParam {String} password Password
|
||||||
* @apiParam {String} [protocol] Application identifier for security logs
|
* @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} [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} [sess] Session identifier for the logs
|
||||||
* @apiParam {String} [ip] IP address 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} scope The scope this authentication is valid for
|
||||||
* @apiSuccess {String[]} require2fa List of enabled 2FA mechanisms
|
* @apiSuccess {String[]} require2fa List of enabled 2FA mechanisms
|
||||||
* @apiSuccess {Boolean} requirePasswordChange Indicates if account hassword has been reset and should be replaced
|
* @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 error Description of the error
|
||||||
* @apiError [code] Error code
|
* @apiError [code] Error code
|
||||||
|
|
@ -103,6 +105,11 @@ module.exports = (db, server, userHandler) => {
|
||||||
.empty('')
|
.empty('')
|
||||||
.uri(),
|
.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),
|
sess: Joi.string().max(255),
|
||||||
ip: Joi.string().ip({
|
ip: Joi.string().ip({
|
||||||
version: ['ipv4', 'ipv6'],
|
version: ['ipv4', 'ipv6'],
|
||||||
|
|
@ -123,8 +130,13 @@ module.exports = (db, server, userHandler) => {
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let permission = roles.can(req.role).createAny('authentication');
|
||||||
|
|
||||||
// permissions check
|
// 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 = {
|
let meta = {
|
||||||
protocol: result.value.protocol,
|
protocol: result.value.protocol,
|
||||||
|
|
@ -175,11 +187,25 @@ module.exports = (db, server, userHandler) => {
|
||||||
requirePasswordChange: authData.requirePasswordChange
|
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) {
|
if (authData.u2fAuthRequest) {
|
||||||
authResponse.u2fAuthRequest = authData.u2fAuthRequest;
|
authResponse.u2fAuthRequest = authData.u2fAuthRequest;
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json(authResponse);
|
res.json(permission.filter(authResponse));
|
||||||
return next();
|
return next();
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
function rateLimitResponse(res, callback) {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue