Compare commits

...

2 Commits

Author SHA1 Message Date
Julian Gassner
da22e0777e Fixed a bug that prevented the mfa to be enabled 2025-02-06 17:01:46 +00:00
Julian Gassner
0bfd2f901d Add possibility to remove mfa 2025-02-06 16:47:56 +00:00
9 changed files with 168 additions and 50 deletions

View File

@ -73,4 +73,25 @@ module.exports = {
.then(() => true);
});
},
disableMfaForUser: (data, userId) => {
return authModel
.query()
.where('user_id', userId)
.first()
.then((auth) => {
if (!auth) {
throw new error.AuthError('User not found.');
}
return auth.verifyPassword(data.secret)
.then((valid) => {
if (!valid) {
throw new error.AuthError('Invalid password.');
}
return authModel
.query()
.where('user_id', userId)
.update({ mfa_enabled: false, mfa_secret: null });
});
});
},
};

View File

@ -1,12 +1,11 @@
const express = require('express');
const jwtdecode = require('../lib/express/jwt-decode');
const apiValidator = require('../lib/validator/api');
const internalToken = require('../internal/token');
const schema = require('../schema');
const internalMfa = require('../internal/mfa');
const qrcode = require('qrcode');
const speakeasy = require('speakeasy');
const userModel = require('../models/user');
const express = require('express');
const jwtdecode = require('../lib/express/jwt-decode');
const apiValidator = require('../lib/validator/api');
const schema = require('../schema');
const internalMfa = require('../internal/mfa');
const qrcode = require('qrcode');
const speakeasy = require('speakeasy');
const userModel = require('../models/user');
let router = express.Router({
caseSensitive: true,
@ -14,24 +13,6 @@ let router = express.Router({
mergeParams: true
});
router
.route('/')
.options((_, res) => {
res.sendStatus(204);
})
.get(async (req, res, next) => {
internalToken.getFreshToken(res.locals.access, {
expiry: (typeof req.query.expiry !== 'undefined' ? req.query.expiry : null),
scope: (typeof req.query.scope !== 'undefined' ? req.query.scope : null)
})
.then((data) => {
res.status(200)
.send(data);
})
.catch(next);
});
router
.route('/create')
.post(jwtdecode(), (req, res, next) => {
@ -71,12 +52,13 @@ router
router
.route('/enable')
.post(jwtdecode(), (req, res, next) => {
apiValidator(schema.getValidationSchema('/mfa', 'post'), req.body).then((params) => {
apiValidator(schema.getValidationSchema('/mfa/enable', 'post'), req.body).then((params) => {
internalMfa.enableMfaForUser(res.locals.access.token.getUserId(), params.token)
.then(() => res.status(200).send({ success: true }))
.catch(next);
}
);});
).catch(next);
});
router
.route('/check')
@ -86,4 +68,14 @@ router
.catch(next);
});
router
.route('/delete')
.delete(jwtdecode(), (req, res, next) => {
apiValidator(schema.getValidationSchema('/mfa/delete', 'delete'), req.body).then((params) => {
internalMfa.disableMfaForUser(params, res.locals.access.token.getUserId())
.then(() => res.status(200).send({ success: true }))
.catch(next);
}).catch(next);
});
module.exports = router;

View File

@ -0,0 +1,44 @@
{
"operationId": "disableMfa",
"summary": "Disable multi-factor authentication for a user",
"tags": [
"MFA"
],
"requestBody": {
"description": "Payload to disable MFA",
"required": true,
"content": {
"application/json": {
"schema": {
"additionalProperties": false,
"properties": {
"secret": {
"type": "string",
"minLength": 1
}
},
"required": [
"secret"
]
}
}
}
},
"responses": {
"200": {
"description": "MFA disabled successfully",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"success": {
"type": "boolean"
}
}
}
}
}
}
}
}

View File

@ -1,14 +1,15 @@
{
"operationId": "enableMfa",
"summary": "Enable multi-factor authentication for a user",
"tags": ["MFA"],
"tags": [
"MFA"
],
"requestBody": {
"description": "MFA Token Payload",
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": false,
"properties": {
"token": {
@ -16,7 +17,9 @@
"minLength": 1
}
},
"required": ["token"]
"required": [
"token"
]
}
}
}

View File

@ -15,9 +15,14 @@
"$ref": "./paths/get.json"
}
},
"/mfa": {
"/mfa/enable": {
"post": {
"$ref": "./paths/mfa/post.json"
"$ref": "./paths/mfa/enable/post.json"
}
},
"/mfa/delete": {
"delete": {
"$ref": "./paths/mfa/delete/delete.json"
}
},
"/audit-log": {

View File

@ -211,6 +211,9 @@ module.exports = {
},
check: function () {
return fetch('get', 'mfa/check');
},
delete: function (secret) {
return fetch('delete', 'mfa/delete', {secret: secret});
}
},

View File

@ -27,12 +27,22 @@
</div>
<div class="col-sm-12 col-md-12">
<label class="form-label mfa-label" style="display: none;"><%- i18n('mfa', 'mfa') %></label>
<button type="button" class="btn btn-info add-mfa"><%- i18n('mfa', 'add-mfa') %></button>
<label class="form-label mfa-label"><%- i18n('mfa', 'mfa') %></label>
<button type="button" class="btn btn-info mfa-add"><%- i18n('mfa', 'mfa-add') %></button>
<button type="button" class="btn btn-danger mfa-remove" style="display: none;"><%- i18n('mfa', 'mfa-remove') %></button>
<div class="mfa-remove-confirm-container" style="display: none;">
<div class="form-group">
<label class="form-label"><%- i18n('mfa', 'confirm-password') %></label>
<input name="mfa_password" type="password" class="form-control mfa-remove-password-field" placeholder="<%- i18n('mfa', 'enter-password') %>">
<div class="invalid-feedback mfa-error"></div>
</div>
<button type="button" class="btn btn-danger mfa-remove-confirm"><%- i18n('mfa', 'confirm-remove-mfa') %></button>
</div>
<p class="qr-instructions" style="display: none;"><%- i18n('mfa', 'mfa-setup-instruction') %></p>
<div class="mfa-validation-container" style="display: none;">
<label class="form-label"><%- i18n('mfa', 'mfa-token') %> <span class="form-required">*</span></label>
<input name="mfa_validation" type="text" class="form-control" placeholder="000000" value="">
<div class="invalid-feedback mfa-error"></div>
</div>
</div>

View File

@ -15,10 +15,14 @@ module.exports = Mn.View.extend({
cancel: 'button.cancel',
save: 'button.save',
error: '.secret-error',
addMfa: '.add-mfa',
mfaLabel: '.mfa-label', // added binding
mfaValidation: '.mfa-validation-container', // added binding
qrInstructions: '.qr-instructions' // added binding for instructions
mfaError: '.mfa-error',
addMfa: '.mfa-add',
mfaValidation: '.mfa-validation-container',
qrInstructions: '.qr-instructions',
removeMfa: '.mfa-remove',
removeMfaConfirmContainer: '.mfa-remove-confirm-container',
removeMfaConfirm: '.mfa-remove-confirm',
removeMfaPassword: '.mfa-remove-password-field'
},
events: {
@ -29,9 +33,9 @@ module.exports = Mn.View.extend({
let view = this;
let data = this.ui.form.serializeJSON();
// Save "mfa_validation" value and remove it from data
let mfaToken = data.mfa_validation;
delete data.mfa_validation;
delete data.mfa_password;
let show_password = this.model.get('email') === 'admin@example.com';
@ -73,9 +77,13 @@ module.exports = Mn.View.extend({
if (mfaToken) {
return App.Api.Mfa.enable(mfaToken)
.then(() => result);
.then(() => result)
.catch(err => {
view.ui.mfaError.text(err.message).show();
err.mfaHandled = true;
return Promise.reject(err);
});
}
console.log(result);
return result;
})
.then(result => {
@ -89,7 +97,9 @@ module.exports = Mn.View.extend({
});
})
.catch(err => {
this.ui.error.text(err.message).show();
if (!err.mfaHandled) {
this.ui.error.text(err.message).show();
}
this.ui.buttons.prop('disabled', false).removeClass('btn-disabled');
});
},
@ -106,6 +116,31 @@ module.exports = Mn.View.extend({
.catch(err => {
view.ui.error.text(err.message).show();
});
},
'click @ui.removeMfa': function (e) {
// Show confirmation section with a password field and confirm button
this.ui.removeMfa.hide();
this.ui.removeMfaConfirmContainer.show();
},
'click @ui.removeMfaConfirm': function (e) {
let view = this;
let password = view.ui.removeMfaPassword.val();
if (!password) {
view.ui.error.text('Password required to remove MFA').show();
return;
}
App.Api.Mfa.delete(password)
.then(() => {
view.ui.addMfa.show();
view.ui.qrInstructions.hide();
view.ui.mfaValidation.hide();
view.ui.removeMfaConfirmContainer.hide();
view.ui.removeMfa.hide();
view.ui.mfaValidation.find('input[name="mfa_validation"]').removeAttr('required');
})
.catch(err => {
view.ui.mfaError.text(err.message).show();
});
}
},
@ -143,16 +178,17 @@ module.exports = Mn.View.extend({
.then(response => {
if (response.active) {
view.ui.addMfa.hide();
view.ui.mfaLabel.hide();
view.ui.qrInstructions.hide();
view.ui.mfaValidation.hide();
// Remove required attribute if MFA is active & field is hidden
view.ui.removeMfa.show();
view.ui.removeMfaConfirmContainer.hide();
view.ui.mfaValidation.find('input[name="mfa_validation"]').removeAttr('required');
} else {
view.ui.addMfa.show();
view.ui.mfaLabel.show();
view.ui.qrInstructions.hide();
view.ui.mfaValidation.hide();
view.ui.removeMfa.hide();
view.ui.removeMfaConfirmContainer.hide();
view.ui.mfaValidation.find('input[name="mfa_validation"]').removeAttr('required');
}
})

View File

@ -39,9 +39,13 @@
},
"mfa": {
"mfa": "Multi Factor Authentication",
"add-mfa": "Generate secret",
"mfa-add": "Add Multi Factor Authentication",
"mfa-remove": "Remove Multi Factor Authentication",
"mfa-setup-instruction": "Scan this QR code in your authenticator app to set up MFA and then enter the current MFA code in the input field.",
"mfa-token": "Multi factor authentication token"
"mfa-token": "Multi factor authentication token",
"confirm-password": "Please enter your password to confirm",
"enter-password": "Enter Password",
"confirm-remove-mfa": "Confirm Multi Factor Authentication removal"
},
"login": {
"title": "Login to your account",