mirror of
https://github.com/NginxProxyManager/nginx-proxy-manager.git
synced 2025-06-14 00:14:27 +00:00
Compare commits
2 Commits
6228a54ecf
...
da22e0777e
Author | SHA1 | Date | |
---|---|---|---|
|
da22e0777e | ||
|
0bfd2f901d |
@ -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 });
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
|
@ -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;
|
||||
|
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",
|
||||
"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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -38,4 +41,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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": {
|
||||
|
@ -211,6 +211,9 @@ module.exports = {
|
||||
},
|
||||
check: function () {
|
||||
return fetch('get', 'mfa/check');
|
||||
},
|
||||
delete: function (secret) {
|
||||
return fetch('delete', 'mfa/delete', {secret: secret});
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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');
|
||||
}
|
||||
})
|
||||
|
@ -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",
|
||||
|
Loading…
x
Reference in New Issue
Block a user