mirror of
https://github.com/NginxProxyManager/nginx-proxy-manager.git
synced 2025-06-14 08:24:27 +00:00
Add possibility to remove mfa
This commit is contained in:
parent
6228a54ecf
commit
0bfd2f901d
@ -1,5 +1,5 @@
|
|||||||
const authModel = require('../models/auth');
|
const authModel = require('../models/auth');
|
||||||
const error = require('../lib/error');
|
const error = require('../lib/error');
|
||||||
const speakeasy = require('speakeasy');
|
const speakeasy = require('speakeasy');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
@ -13,10 +13,10 @@ module.exports = {
|
|||||||
throw new error.AuthError('MFA is not enabled for this user.');
|
throw new error.AuthError('MFA is not enabled for this user.');
|
||||||
}
|
}
|
||||||
const verified = speakeasy.totp.verify({
|
const verified = speakeasy.totp.verify({
|
||||||
secret: auth.mfa_secret,
|
secret: auth.mfa_secret,
|
||||||
encoding: 'base32',
|
encoding: 'base32',
|
||||||
token: token,
|
token: token,
|
||||||
window: 2
|
window: 2
|
||||||
});
|
});
|
||||||
if (!verified) {
|
if (!verified) {
|
||||||
throw new error.AuthError('Invalid MFA token.');
|
throw new error.AuthError('Invalid MFA token.');
|
||||||
@ -58,10 +58,10 @@ module.exports = {
|
|||||||
throw new error.AuthError('MFA is not set up for this user.');
|
throw new error.AuthError('MFA is not set up for this user.');
|
||||||
}
|
}
|
||||||
const verified = speakeasy.totp.verify({
|
const verified = speakeasy.totp.verify({
|
||||||
secret: auth.mfa_secret,
|
secret: auth.mfa_secret,
|
||||||
encoding: 'base32',
|
encoding: 'base32',
|
||||||
token: token,
|
token: token,
|
||||||
window: 2
|
window: 2
|
||||||
});
|
});
|
||||||
if (!verified) {
|
if (!verified) {
|
||||||
throw new error.AuthError('Invalid MFA token.');
|
throw new error.AuthError('Invalid MFA token.');
|
||||||
@ -73,4 +73,25 @@ module.exports = {
|
|||||||
.then(() => true);
|
.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 });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
@ -1,37 +1,18 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const jwtdecode = require('../lib/express/jwt-decode');
|
const jwtdecode = require('../lib/express/jwt-decode');
|
||||||
const apiValidator = require('../lib/validator/api');
|
const apiValidator = require('../lib/validator/api');
|
||||||
const internalToken = require('../internal/token');
|
const schema = require('../schema');
|
||||||
const schema = require('../schema');
|
const internalMfa = require('../internal/mfa');
|
||||||
const internalMfa = require('../internal/mfa');
|
const qrcode = require('qrcode');
|
||||||
const qrcode = require('qrcode');
|
const speakeasy = require('speakeasy');
|
||||||
const speakeasy = require('speakeasy');
|
const userModel = require('../models/user');
|
||||||
const userModel = require('../models/user');
|
|
||||||
|
|
||||||
let router = express.Router({
|
let router = express.Router({
|
||||||
caseSensitive: true,
|
caseSensitive: true,
|
||||||
strict: true,
|
strict: true,
|
||||||
mergeParams: true
|
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
|
router
|
||||||
.route('/create')
|
.route('/create')
|
||||||
.post(jwtdecode(), (req, res, next) => {
|
.post(jwtdecode(), (req, res, next) => {
|
||||||
@ -54,7 +35,7 @@ router
|
|||||||
.then(({ secret, user }) => {
|
.then(({ secret, user }) => {
|
||||||
const otpAuthUrl = speakeasy.otpauthURL({
|
const otpAuthUrl = speakeasy.otpauthURL({
|
||||||
secret: secret.ascii,
|
secret: secret.ascii,
|
||||||
label: user.email,
|
label: user.email,
|
||||||
issuer: 'Nginx Proxy Manager'
|
issuer: 'Nginx Proxy Manager'
|
||||||
});
|
});
|
||||||
qrcode.toDataURL(otpAuthUrl, (err, dataUrl) => {
|
qrcode.toDataURL(otpAuthUrl, (err, dataUrl) => {
|
||||||
@ -71,12 +52,13 @@ router
|
|||||||
router
|
router
|
||||||
.route('/enable')
|
.route('/enable')
|
||||||
.post(jwtdecode(), (req, res, next) => {
|
.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)
|
internalMfa.enableMfaForUser(res.locals.access.token.getUserId(), params.token)
|
||||||
.then(() => res.status(200).send({ success: true }))
|
.then(() => res.status(200).send({ success: true }))
|
||||||
.catch(next);
|
.catch(next);
|
||||||
}
|
}
|
||||||
);});
|
).catch(next);
|
||||||
|
});
|
||||||
|
|
||||||
router
|
router
|
||||||
.route('/check')
|
.route('/check')
|
||||||
@ -86,4 +68,14 @@ router
|
|||||||
.catch(next);
|
.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;
|
module.exports = router;
|
||||||
|
44
backend/schema/paths/mfa/delete/delete.json
Normal file
44
backend/schema/paths/mfa/delete/delete.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,14 +1,15 @@
|
|||||||
{
|
{
|
||||||
"operationId": "enableMfa",
|
"operationId": "enableMfa",
|
||||||
"summary": "Enable multi-factor authentication for a user",
|
"summary": "Enable multi-factor authentication for a user",
|
||||||
"tags": ["MFA"],
|
"tags": [
|
||||||
|
"MFA"
|
||||||
|
],
|
||||||
"requestBody": {
|
"requestBody": {
|
||||||
"description": "MFA Token Payload",
|
"description": "MFA Token Payload",
|
||||||
"required": true,
|
"required": true,
|
||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "object",
|
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"properties": {
|
"properties": {
|
||||||
"token": {
|
"token": {
|
||||||
@ -16,7 +17,9 @@
|
|||||||
"minLength": 1
|
"minLength": 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["token"]
|
"required": [
|
||||||
|
"token"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -38,4 +41,4 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -15,9 +15,14 @@
|
|||||||
"$ref": "./paths/get.json"
|
"$ref": "./paths/get.json"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/mfa": {
|
"/mfa/enable": {
|
||||||
"post": {
|
"post": {
|
||||||
"$ref": "./paths/mfa/post.json"
|
"$ref": "./paths/mfa/enable/post.json"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/mfa/delete": {
|
||||||
|
"delete": {
|
||||||
|
"$ref": "./paths/mfa/delete/delete.json"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/audit-log": {
|
"/audit-log": {
|
||||||
|
@ -211,6 +211,9 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
check: function () {
|
check: function () {
|
||||||
return fetch('get', 'mfa/check');
|
return fetch('get', 'mfa/check');
|
||||||
|
},
|
||||||
|
delete: function (secret) {
|
||||||
|
return fetch('delete', 'mfa/delete', {secret: secret});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -27,12 +27,22 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-sm-12 col-md-12">
|
<div class="col-sm-12 col-md-12">
|
||||||
<label class="form-label mfa-label" style="display: none;"><%- i18n('mfa', 'mfa') %></label>
|
<label class="form-label mfa-label"><%- i18n('mfa', 'mfa') %></label>
|
||||||
<button type="button" class="btn btn-info add-mfa"><%- i18n('mfa', 'add-mfa') %></button>
|
<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>
|
<p class="qr-instructions" style="display: none;"><%- i18n('mfa', 'mfa-setup-instruction') %></p>
|
||||||
<div class="mfa-validation-container" style="display: none;">
|
<div class="mfa-validation-container" style="display: none;">
|
||||||
<label class="form-label"><%- i18n('mfa', 'mfa-token') %> <span class="form-required">*</span></label>
|
<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="">
|
<input name="mfa_validation" type="text" class="form-control" placeholder="000000" value="">
|
||||||
|
<div class="invalid-feedback mfa-error"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -15,10 +15,14 @@ module.exports = Mn.View.extend({
|
|||||||
cancel: 'button.cancel',
|
cancel: 'button.cancel',
|
||||||
save: 'button.save',
|
save: 'button.save',
|
||||||
error: '.secret-error',
|
error: '.secret-error',
|
||||||
addMfa: '.add-mfa',
|
mfaError: '.mfa-error',
|
||||||
mfaLabel: '.mfa-label', // added binding
|
addMfa: '.mfa-add',
|
||||||
mfaValidation: '.mfa-validation-container', // added binding
|
mfaValidation: '.mfa-validation-container',
|
||||||
qrInstructions: '.qr-instructions' // added binding for instructions
|
qrInstructions: '.qr-instructions',
|
||||||
|
removeMfa: '.mfa-remove',
|
||||||
|
removeMfaConfirmContainer: '.mfa-remove-confirm-container',
|
||||||
|
removeMfaConfirm: '.mfa-remove-confirm',
|
||||||
|
removeMfaPassword: '.mfa-remove-password-field'
|
||||||
},
|
},
|
||||||
|
|
||||||
events: {
|
events: {
|
||||||
@ -75,7 +79,6 @@ module.exports = Mn.View.extend({
|
|||||||
return App.Api.Mfa.enable(mfaToken)
|
return App.Api.Mfa.enable(mfaToken)
|
||||||
.then(() => result);
|
.then(() => result);
|
||||||
}
|
}
|
||||||
console.log(result);
|
|
||||||
return result;
|
return result;
|
||||||
})
|
})
|
||||||
.then(result => {
|
.then(result => {
|
||||||
@ -106,6 +109,31 @@ module.exports = Mn.View.extend({
|
|||||||
.catch(err => {
|
.catch(err => {
|
||||||
view.ui.error.text(err.message).show();
|
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 +171,17 @@ module.exports = Mn.View.extend({
|
|||||||
.then(response => {
|
.then(response => {
|
||||||
if (response.active) {
|
if (response.active) {
|
||||||
view.ui.addMfa.hide();
|
view.ui.addMfa.hide();
|
||||||
view.ui.mfaLabel.hide();
|
|
||||||
view.ui.qrInstructions.hide();
|
view.ui.qrInstructions.hide();
|
||||||
view.ui.mfaValidation.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');
|
view.ui.mfaValidation.find('input[name="mfa_validation"]').removeAttr('required');
|
||||||
} else {
|
} else {
|
||||||
view.ui.addMfa.show();
|
view.ui.addMfa.show();
|
||||||
view.ui.mfaLabel.show();
|
|
||||||
view.ui.qrInstructions.hide();
|
view.ui.qrInstructions.hide();
|
||||||
view.ui.mfaValidation.hide();
|
view.ui.mfaValidation.hide();
|
||||||
|
view.ui.removeMfa.hide();
|
||||||
|
view.ui.removeMfaConfirmContainer.hide();
|
||||||
view.ui.mfaValidation.find('input[name="mfa_validation"]').removeAttr('required');
|
view.ui.mfaValidation.find('input[name="mfa_validation"]').removeAttr('required');
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -39,9 +39,13 @@
|
|||||||
},
|
},
|
||||||
"mfa": {
|
"mfa": {
|
||||||
"mfa": "Multi Factor Authentication",
|
"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-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": {
|
"login": {
|
||||||
"title": "Login to your account",
|
"title": "Login to your account",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user