mirror of
https://github.com/NginxProxyManager/nginx-proxy-manager.git
synced 2025-10-05 04:10:10 +00:00
Finish MFA implementation
This commit is contained in:
@@ -202,7 +202,46 @@ module.exports = {
|
||||
return fetch('get', '');
|
||||
},
|
||||
|
||||
Mfa: {
|
||||
create: function () {
|
||||
return fetch('post', 'mfa/create');
|
||||
},
|
||||
enable: function (token) {
|
||||
return fetch('post', 'mfa/enable', {token: token});
|
||||
},
|
||||
check: function () {
|
||||
return fetch('get', 'mfa/check');
|
||||
}
|
||||
},
|
||||
|
||||
Tokens: {
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {String} identity
|
||||
* @param {String} secret
|
||||
* @param {String} token
|
||||
* @param {Boolean} wipe
|
||||
* @returns {Promise}
|
||||
*/
|
||||
|
||||
loginWithMFA: function (identity, secret, mfaToken, wipe) {
|
||||
return fetch('post', 'tokens', {identity: identity, secret: secret, mfa_token: mfaToken})
|
||||
.then(response => {
|
||||
if (response.token) {
|
||||
if (wipe) {
|
||||
Tokens.clearTokens();
|
||||
}
|
||||
|
||||
// Set storage token
|
||||
Tokens.addToken(response.token);
|
||||
return response.token;
|
||||
} else {
|
||||
Tokens.clearTokens();
|
||||
throw(new Error('No token returned'));
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {String} identity
|
||||
|
@@ -25,6 +25,16 @@
|
||||
<div class="invalid-feedback secret-error"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-12 col-md-12">
|
||||
<button type="button" class="btn btn-info add-mfa">Add MFA</button>
|
||||
<p class="qr-instructions" style="display: none;">Scan this QR code in your authenticator app to set up MFA and then enter the current MFA code in the input field.</p>
|
||||
<div class="mfa-validation-container" style="display: none;">
|
||||
<label class="form-label"><%- i18n('str', 'mfa') %> <span class="form-required">*</span></label>
|
||||
<input name="mfa_validation" type="text" class="form-control" placeholder="000000" value="" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% if (isAdmin() && !isSelf()) { %>
|
||||
<div class="col-sm-12 col-md-12">
|
||||
<div class="form-label"><%- i18n('roles', 'title') %></div>
|
||||
|
@@ -14,7 +14,10 @@ module.exports = Mn.View.extend({
|
||||
buttons: '.modal-footer button',
|
||||
cancel: 'button.cancel',
|
||||
save: 'button.save',
|
||||
error: '.secret-error'
|
||||
error: '.secret-error',
|
||||
addMfa: '.add-mfa',
|
||||
mfaValidation: '.mfa-validation-container', // added binding
|
||||
qrInstructions: '.qr-instructions' // added binding for instructions
|
||||
},
|
||||
|
||||
events: {
|
||||
@@ -25,6 +28,10 @@ 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;
|
||||
|
||||
let show_password = this.model.get('email') === 'admin@example.com';
|
||||
|
||||
// admin@example.com is not allowed
|
||||
@@ -62,6 +69,15 @@ module.exports = Mn.View.extend({
|
||||
}
|
||||
|
||||
view.model.set(result);
|
||||
|
||||
if (mfaToken) {
|
||||
return App.Api.Mfa.enable(mfaToken)
|
||||
.then(() => result);
|
||||
}
|
||||
console.log(result);
|
||||
return result;
|
||||
})
|
||||
.then(result => {
|
||||
App.UI.closeModal(function () {
|
||||
if (method === App.Api.Users.create) {
|
||||
// Show permissions dialog immediately
|
||||
@@ -75,6 +91,18 @@ module.exports = Mn.View.extend({
|
||||
this.ui.error.text(err.message).show();
|
||||
this.ui.buttons.prop('disabled', false).removeClass('btn-disabled');
|
||||
});
|
||||
},
|
||||
'click @ui.addMfa': function (e) {
|
||||
let view = this;
|
||||
App.Api.Mfa.create()
|
||||
.then(response => {
|
||||
view.ui.addMfa.replaceWith(`<img class="qr-code" src="${response.qrCode}" alt="QR Code">`);
|
||||
view.ui.qrInstructions.show();
|
||||
view.ui.mfaValidation.show();
|
||||
})
|
||||
.catch(err => {
|
||||
view.ui.error.text(err.message).show();
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@@ -104,5 +132,24 @@ module.exports = Mn.View.extend({
|
||||
if (typeof options.model === 'undefined' || !options.model) {
|
||||
this.model = new UserModel.Model();
|
||||
}
|
||||
},
|
||||
|
||||
onRender: function () {
|
||||
let view = this;
|
||||
App.Api.Mfa.check()
|
||||
.then(response => {
|
||||
if (response.active) {
|
||||
view.ui.addMfa.hide();
|
||||
view.ui.qrInstructions.hide();
|
||||
view.ui.mfaValidation.hide();
|
||||
} else {
|
||||
view.ui.addMfa.show();
|
||||
view.ui.qrInstructions.hide();
|
||||
view.ui.mfaValidation.hide();
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
view.ui.error.text(err.message).show();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@@ -2,6 +2,7 @@
|
||||
"en": {
|
||||
"str": {
|
||||
"email-address": "Email address",
|
||||
"mfa": "Multi factor authentication token",
|
||||
"username": "Username",
|
||||
"password": "Password",
|
||||
"sign-in": "Sign in",
|
||||
@@ -38,7 +39,8 @@
|
||||
"any": "Any"
|
||||
},
|
||||
"login": {
|
||||
"title": "Login to your account"
|
||||
"title": "Login to your account",
|
||||
"mfa_required_text": "Please enter your MFA token to continue"
|
||||
},
|
||||
"main": {
|
||||
"app": "Nginx Proxy Manager",
|
||||
|
@@ -1,3 +1,4 @@
|
||||
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col col-login mx-auto">
|
||||
@@ -24,6 +25,12 @@
|
||||
<input name="secret" type="password" class="form-control" placeholder="<%- i18n('str', 'password') %>" required>
|
||||
<div class="invalid-feedback secret-error"></div>
|
||||
</div>
|
||||
<div class="form-group mfa-group" style="display: none;">
|
||||
<p class="mfa-info"><%- i18n('login', 'mfa_required_text') %>:</p>
|
||||
<label class="form-label"><%- i18n('str', 'mfa') %></label>
|
||||
<input name="mfa_token" type="text" class="form-control" placeholder="<%- i18n('str', 'mfa') %>">
|
||||
<div class="invalid-feedback mfa-error"></div>
|
||||
</div>
|
||||
<div class="form-footer">
|
||||
<button type="submit" class="btn btn-teal btn-block"><%- i18n('str', 'sign-in') %></button>
|
||||
</div>
|
||||
@@ -34,4 +41,4 @@
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@@ -13,7 +13,11 @@ module.exports = Mn.View.extend({
|
||||
identity: 'input[name="identity"]',
|
||||
secret: 'input[name="secret"]',
|
||||
error: '.secret-error',
|
||||
button: 'button'
|
||||
error_mfa:'.mfa-error',
|
||||
button: 'button',
|
||||
mfaGroup: '.mfa-group', // added MFA group selector
|
||||
mfaToken: 'input[name="mfa_token"]', // added MFA token input
|
||||
mfaInfo: '.mfa-info' // added MFA info element
|
||||
},
|
||||
|
||||
events: {
|
||||
@@ -22,14 +26,36 @@ module.exports = Mn.View.extend({
|
||||
this.ui.button.addClass('btn-loading').prop('disabled', true);
|
||||
this.ui.error.hide();
|
||||
|
||||
Api.Tokens.login(this.ui.identity.val(), this.ui.secret.val(), true)
|
||||
if(this.ui.mfaToken.val()) {
|
||||
Api.Tokens.loginWithMFA(this.ui.identity.val(), this.ui.secret.val(), this.ui.mfaToken.val(), true)
|
||||
.then(() => {
|
||||
window.location = '/';
|
||||
})
|
||||
.catch(err => {
|
||||
this.ui.error.text(err.message).show();
|
||||
if (err.message === 'Invalid MFA token.') {
|
||||
this.ui.error_mfa.text(err.message).show();
|
||||
} else {
|
||||
this.ui.error.text(err.message).show();
|
||||
}
|
||||
this.ui.button.removeClass('btn-loading').prop('disabled', false);
|
||||
});
|
||||
} else {
|
||||
Api.Tokens.login(this.ui.identity.val(), this.ui.secret.val(), true)
|
||||
.then(() => {
|
||||
window.location = '/';
|
||||
})
|
||||
.catch(err => {
|
||||
if (err.message === 'MFA token required') {
|
||||
this.ui.mfaGroup.show();
|
||||
this.ui.mfaInfo.show();
|
||||
} else {
|
||||
this.ui.error.text(err.message).show();
|
||||
}
|
||||
this.ui.button.removeClass('btn-loading').prop('disabled', false);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
},
|
||||
|
||||
@@ -40,3 +66,5 @@ module.exports = Mn.View.extend({
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
Reference in New Issue
Block a user