mirror of
https://github.com/NginxProxyManager/nginx-proxy-manager.git
synced 2025-06-18 18:16:26 +00:00
FEAT: Add Open ID Connect authentication method
* add `oidc-config` setting allowing an admin user to configure parameters * modify login page to show another button when oidc is configured * add dependency `openid-client` `v5.4.0` * add backend route to process "OAuth2 Authorization Code" flow initialisation * add backend route to process callback of above flow * sign in the authenticated user with internal jwt token if internal user with email matching the one retrieved from oauth claims exists Note: Only Open ID Connect Discovery is supported which most modern Identity Providers offer. Tested with Authentik 2023.2.2 and Keycloak 18.0.2
This commit is contained in:
committed by
Marcell Fülöp
parent
5920b0cf5e
commit
caeb2934f0
@ -59,6 +59,8 @@ function fetch(verb, path, data, options) {
|
||||
},
|
||||
|
||||
beforeSend: function (xhr) {
|
||||
// allow unauthenticated access to OIDC configuration
|
||||
if (path === "settings/oidc-config") return;
|
||||
xhr.setRequestHeader('Authorization', 'Bearer ' + (token ? token.t : null));
|
||||
},
|
||||
|
||||
|
@ -434,6 +434,11 @@ module.exports = {
|
||||
App.UI.showModalDialog(new View({model: model}));
|
||||
});
|
||||
}
|
||||
if (model.get('id') === 'oidc-config') {
|
||||
require(['./main', './settings/oidc-config/main'], function (App, View) {
|
||||
App.UI.showModalDialog(new View({model: model}));
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -9,6 +9,14 @@
|
||||
<% if (id === 'default-site') { %>
|
||||
<%- i18n('settings', 'default-site-' + value) %>
|
||||
<% } %>
|
||||
<% if (id === 'oidc-config' && meta && meta.name && meta.clientID && meta.clientSecret && meta.issuerURL && meta.redirectURL) { %>
|
||||
<%- meta.name %>
|
||||
<% if (!meta.enabled) { %>
|
||||
(Disabled)
|
||||
<% } %>
|
||||
<% } else if (id === 'oidc-config') { %>
|
||||
Not configured
|
||||
<% } %>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
|
47
frontend/js/app/settings/oidc-config/main.ejs
Normal file
47
frontend/js/app/settings/oidc-config/main.ejs
Normal file
@ -0,0 +1,47 @@
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title"><%- i18n('settings', id) %></h5>
|
||||
<button type="button" class="close cancel" aria-label="Close" data-dismiss="modal"> </button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form>
|
||||
<div class="row">
|
||||
<div class="col-sm-12 col-md-12">
|
||||
<div class="form-group">
|
||||
<div class="form-label"><%- description %></div>
|
||||
<div class="custom-controls-stacked">
|
||||
<div class="form-group">
|
||||
<div class="form-label">Name</div>
|
||||
<input class="form-control name-input" name="meta[name]" placeholder="" type="text" value="<%- meta && typeof meta.name !== 'undefined' ? meta.name : '' %>">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="form-label">Client ID</div>
|
||||
<input class="form-control id-input" name="meta[clientID]" placeholder="" type="text" value="<%- meta && typeof meta.clientID !== 'undefined' ? meta.clientID : '' %>">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="form-label">Client Secret</div>
|
||||
<input class="form-control secret-input" name="meta[clientSecret]" placeholder="" type="text" value="<%- meta && typeof meta.clientSecret !== 'undefined' ? meta.clientSecret : '' %>">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="form-label">Issuer URL</div>
|
||||
<input class="form-control issuer-input" name="meta[issuerURL]" placeholder="https://" type="url" value="<%- meta && typeof meta.issuerURL !== 'undefined' ? meta.issuerURL : '' %>">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="form-label">Redirect URL</div>
|
||||
<input class="form-control redirect-url-input" name="meta[redirectURL]" placeholder="https://" type="url" value="<%- meta && typeof meta.redirectURL !== 'undefined' ? meta.redirectURL : '' %>">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="form-label">Enabled</div>
|
||||
<input class="form-check enabled-input" name="meta[enabled]" placeholder="" type="checkbox" <%- meta && typeof meta.enabled !== 'undefined' && meta.enabled === true ? 'checked="checked"' : '' %> >
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary cancel" data-dismiss="modal"><%- i18n('str', 'cancel') %></button>
|
||||
<button type="button" class="btn btn-teal save"><%- i18n('str', 'save') %></button>
|
||||
</div>
|
||||
</div>
|
47
frontend/js/app/settings/oidc-config/main.js
Normal file
47
frontend/js/app/settings/oidc-config/main.js
Normal file
@ -0,0 +1,47 @@
|
||||
const Mn = require('backbone.marionette');
|
||||
const App = require('../../main');
|
||||
const template = require('./main.ejs');
|
||||
|
||||
require('jquery-serializejson');
|
||||
require('selectize');
|
||||
|
||||
module.exports = Mn.View.extend({
|
||||
template: template,
|
||||
className: 'modal-dialog',
|
||||
|
||||
ui: {
|
||||
form: 'form',
|
||||
buttons: '.modal-footer button',
|
||||
cancel: 'button.cancel',
|
||||
save: 'button.save',
|
||||
},
|
||||
|
||||
events: {
|
||||
'click @ui.save': function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (!this.ui.form[0].checkValidity()) {
|
||||
$('<input type="submit">').hide().appendTo(this.ui.form).click().remove();
|
||||
return;
|
||||
}
|
||||
|
||||
let view = this;
|
||||
let data = this.ui.form.serializeJSON();
|
||||
data.id = this.model.get('id');
|
||||
if (data.meta.enabled) {
|
||||
data.meta.enabled = data.meta.enabled === "on" || data.meta.enabled === "true";
|
||||
}
|
||||
|
||||
this.ui.buttons.prop('disabled', true).addClass('btn-disabled');
|
||||
App.Api.Settings.update(data)
|
||||
.then(result => {
|
||||
view.model.set(result);
|
||||
App.UI.closeModal();
|
||||
})
|
||||
.catch(err => {
|
||||
alert(err.message);
|
||||
this.ui.buttons.prop('disabled', false).removeClass('btn-disabled');
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
@ -5,6 +5,7 @@
|
||||
"username": "Username",
|
||||
"password": "Password",
|
||||
"sign-in": "Sign in",
|
||||
"sign-in-with": "Sign in with",
|
||||
"sign-out": "Sign out",
|
||||
"try-again": "Try again",
|
||||
"name": "Name",
|
||||
|
@ -5,7 +5,7 @@
|
||||
<div class="card-body p-6">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-sm-12 col-md-6">
|
||||
<div class="col-sm-12 col-md-6 margin-auto">
|
||||
<div class="text-center p-6">
|
||||
<img src="/images/logo-text-vertical-grey.png" alt="Logo" />
|
||||
<div class="text-center text-muted mt-5">
|
||||
@ -27,6 +27,13 @@
|
||||
<div class="form-footer">
|
||||
<button type="submit" class="btn btn-teal btn-block"><%- i18n('str', 'sign-in') %></button>
|
||||
</div>
|
||||
<div class="form-footer login-oidc">
|
||||
<div class="separator"><slot>OR</slot></div>
|
||||
<button type="button" id="login-oidc" class="btn btn-teal btn-block">
|
||||
<%- i18n('str', 'sign-in-with') %> <span class="oidc-provider"></span>
|
||||
</button>
|
||||
<div class="invalid-feedback oidc-error"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -3,17 +3,22 @@ const Mn = require('backbone.marionette');
|
||||
const template = require('./login.ejs');
|
||||
const Api = require('../../app/api');
|
||||
const i18n = require('../../app/i18n');
|
||||
const Tokens = require('../../app/tokens');
|
||||
|
||||
module.exports = Mn.View.extend({
|
||||
template: template,
|
||||
className: 'page-single',
|
||||
|
||||
ui: {
|
||||
form: 'form',
|
||||
identity: 'input[name="identity"]',
|
||||
secret: 'input[name="secret"]',
|
||||
error: '.secret-error',
|
||||
button: 'button'
|
||||
form: 'form',
|
||||
identity: 'input[name="identity"]',
|
||||
secret: 'input[name="secret"]',
|
||||
error: '.secret-error',
|
||||
button: 'button[type=submit]',
|
||||
oidcLogin: 'div.login-oidc',
|
||||
oidcButton: 'button#login-oidc',
|
||||
oidcError: '.oidc-error',
|
||||
oidcProvider: 'span.oidc-provider'
|
||||
},
|
||||
|
||||
events: {
|
||||
@ -30,6 +35,56 @@ module.exports = Mn.View.extend({
|
||||
this.ui.error.text(err.message).show();
|
||||
this.ui.button.removeClass('btn-loading').prop('disabled', false);
|
||||
});
|
||||
},
|
||||
'click @ui.oidcButton': function(e) {
|
||||
this.ui.identity.prop('disabled', true);
|
||||
this.ui.secret.prop('disabled', true);
|
||||
this.ui.button.prop('disabled', true);
|
||||
this.ui.oidcButton.addClass('btn-loading').prop('disabled', true);
|
||||
// redirect to initiate oauth flow
|
||||
document.location.replace('/api/oidc/');
|
||||
},
|
||||
},
|
||||
|
||||
async onRender() {
|
||||
// read oauth callback state cookies
|
||||
let cookies = document.cookie.split(';'),
|
||||
token, expiry, error;
|
||||
for (cookie of cookies) {
|
||||
let raw = cookie.split('='),
|
||||
name = raw[0].trim(),
|
||||
value = raw[1];
|
||||
if (name === 'npm_oidc') {
|
||||
let v = value.split('---');
|
||||
token = v[0];
|
||||
expiry = v[1];
|
||||
}
|
||||
if (name === 'npm_oidc_error') {
|
||||
console.log(' ERROR 000 > ', value);
|
||||
error = decodeURIComponent(value);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('login.js event > render', expiry, token);
|
||||
// register a newly acquired jwt token following successful oidc authentication
|
||||
if (token && expiry && (new Date(Date.parse(decodeURIComponent(expiry)))) > new Date() ) {
|
||||
console.log('login.js event > render >>>');
|
||||
Tokens.addToken(token);
|
||||
document.location.replace('/');
|
||||
}
|
||||
|
||||
// show error message following a failed oidc authentication
|
||||
if (error) {
|
||||
console.log(' ERROR > ', error);
|
||||
this.ui.oidcError.html(error);
|
||||
}
|
||||
|
||||
// fetch oidc configuration and show alternative action button if enabled
|
||||
let response = await Api.Settings.getById("oidc-config");
|
||||
if (response && response.meta && response.meta.enabled === true) {
|
||||
this.ui.oidcProvider.html(response.meta.name);
|
||||
this.ui.oidcLogin.show();
|
||||
this.ui.oidcError.show();
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -39,4 +39,34 @@ a:hover {
|
||||
|
||||
.col-login {
|
||||
max-width: 48rem;
|
||||
}
|
||||
|
||||
.margin-auto {
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.separator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.separator::before, .separator::after {
|
||||
content: "";
|
||||
flex: 1 1 0%;
|
||||
border-bottom: 1px solid #ccc;
|
||||
}
|
||||
|
||||
.separator:not(:empty)::before {
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
.separator:not(:empty)::after {
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
|
||||
.login-oidc {
|
||||
display: none;
|
||||
margin-top: 1em;
|
||||
}
|
Reference in New Issue
Block a user