diff --git a/docs/api.md b/docs/api.md index 8bf60c1f..226a2e0f 100644 --- a/docs/api.md +++ b/docs/api.md @@ -321,6 +321,33 @@ Response for a successful operation: } ``` +### Reset user password + +#### POST /users/{user}/password/reset + +Generates a new temporary password and resets 2FA if set. Once user password is reset, then authentication results +will include `requirePasswordChange: true` parameter. This means that the user should not be able to perform regular +actions before the password has been changed. + +**Parameters** + +- **user** (required) is the ID of the user + +**Example** + +``` +curl -XPOST "http://localhost:8080/users/59467f27535f8f0f067ba8e6/password/reset" -H 'content-type: application/json' -d '{}' +``` + +Response for a successful operation: + +```json +{ + "success":true, + "password": "somesecretvalue" +} +``` + ## Authentication ### Authenticate an user @@ -343,7 +370,8 @@ Authenticates an user - **id** is the id of the authenticated user - **username** is the user name of the logged in user (useful if you logged in used) - **scope** is the scope this authentication is valid for -- **require2fa** if `true` the the user should also [provide a 2FA token](#verify-2fa) before the user is allowed to proceed +- **require2fa** if `true` then the user should also [provide a 2FA token](#verify-2fa) before the user is allowed to proceed +- **requirePasswordChange** if `true` then the user should be forced to change their password **Example** @@ -364,7 +392,8 @@ Response for a successful operation: "id": "5971da1754cfdc7f0983b2ec", "username": "testuser", "scope": "pop3", - "require2fa": false + "require2fa": false, + "requirePasswordChange": false } ``` @@ -417,10 +446,11 @@ Wild Duck supports TOTP based 2FA. If 2FA is enabled then users are requested to 2FA checks do not happen magically, your application must be 2FA aware: 1. Authenticate user with the [/authenticate](#authenticate-an-user) call -2. If authentication ends with `require2fa:false` then do nothing, otherwise continue with Step 3. -3. Request TOTP token from the user before allowing to perform other actions -4. Check the token with [/user/{user}/2fa?token=123456](#check-2fa) -5. If token verification succeeds then user is authenticated +2. If authentication result includes `requirePasswordChange:true` then force user to change their password +3. If authentication result includes `require2fa:false` then do nothing, the user is now authenticated. Otherwise continue with Step 4. +4. Request TOTP token from the user before allowing to perform other actions +5. Check the token with [/user/{user}/2fa?token=123456](#check-2fa) +6. If token verification succeeds then user is authenticated ### Setup 2FA diff --git a/lib/api/auth.js b/lib/api/auth.js index 75a7b54d..94666057 100644 --- a/lib/api/auth.js +++ b/lib/api/auth.js @@ -58,7 +58,8 @@ module.exports = (db, server, userHandler) => { id: authData.user, username: authData.username, scope: authData.scope, - require2fa: authData.require2fa + require2fa: authData.require2fa, + requirePasswordChange: authData.requirePasswordChange }); return next(); diff --git a/lib/api/users.js b/lib/api/users.js index 7da92c31..0f70c348 100644 --- a/lib/api/users.js +++ b/lib/api/users.js @@ -413,7 +413,7 @@ module.exports = (db, server, userHandler) => { return next(); } - let user = new ObjectID(result.value.iuserd); + let user = new ObjectID(result.value.user); db.users.collection('users').findOne({ _id: user @@ -506,6 +506,42 @@ module.exports = (db, server, userHandler) => { }); }); }); + + server.post('/users/:user/password/reset', (req, res, next) => { + res.charSet('utf-8'); + + const schema = Joi.object().keys({ + user: Joi.string().hex().lowercase().length(24).required() + }); + + const result = Joi.validate(req.params, schema, { + abortEarly: false, + convert: true + }); + + if (result.error) { + res.json({ + error: result.error.message + }); + return next(); + } + + let user = new ObjectID(result.value.user); + + userHandler.reset(user, (err, password) => { + if (err) { + res.json({ + error: err.message + }); + return next(); + } + res.json({ + success: true, + password + }); + return next(); + }); + }); }; function checkPubKey(pubKey, done) { diff --git a/lib/user-handler.js b/lib/user-handler.js index c0ab21d9..738a1627 100644 --- a/lib/user-handler.js +++ b/lib/user-handler.js @@ -389,6 +389,8 @@ class UserHandler { created: new Date(), + requirePasswordChange: false, + // until setup value is not true, this account is not usable activated: false, disabled: true, @@ -564,7 +566,7 @@ class UserHandler { }); } - reset(username, callback) { + reset(user, callback) { let password = generatePassword.generate({ length: 12, uppercase: true, @@ -573,7 +575,7 @@ class UserHandler { }); return this.users.collection('users').findOneAndUpdate({ - username + _id: user }, { $set: { enabled2fa: false, @@ -583,12 +585,12 @@ class UserHandler { } }, {}, (err, result) => { if (err) { - log.error('DB', 'UPDATEFAIL username=%s error=%s', username, err.message); + log.error('DB', 'UPDATEFAIL id=%s error=%s', user, err.message); return callback(new Error('Database Error, failed to reset user credentials')); } if (!result || !result.value) { - return callback(new Error('Could not update user')); + return callback(new Error('Could not update user ' + user)); } return callback(null, password); @@ -847,6 +849,7 @@ class UserHandler { } if (key === 'password') { $set.password = bcrypt.hashSync(data[key], consts.BCRYPT_ROUNDS); + $set.requirePasswordChange = false; $set.passwordChange = new Date(); passwordChanged = true; return;