add OIDC
This commit is contained in:
@@ -108,6 +108,7 @@ You can define which authentication backend to be used thanks to the `authentica
|
|||||||
* `internal` (default): _LMS_ uses an internal database to store users and their associated passwords (salted and hashed using [Bcrypt](https://en.wikipedia.org/wiki/Bcrypt)). Only the admin user can create, edit or remove other users.
|
* `internal` (default): _LMS_ uses an internal database to store users and their associated passwords (salted and hashed using [Bcrypt](https://en.wikipedia.org/wiki/Bcrypt)). Only the admin user can create, edit or remove other users.
|
||||||
* `PAM`: the user/password authentication request is forwarded to PAM. A default example PAM configuration file is provided at [`conf/pam/lms`](conf/pam/lms), which demonstrates how to authenticate using system passwords. **Note:** this file is just a base example; depending on your PAM setup, you may need to adjust the service file or PAM rules to ensure proper privileges.
|
* `PAM`: the user/password authentication request is forwarded to PAM. A default example PAM configuration file is provided at [`conf/pam/lms`](conf/pam/lms), which demonstrates how to authenticate using system passwords. **Note:** this file is just a base example; depending on your PAM setup, you may need to adjust the service file or PAM rules to ensure proper privileges.
|
||||||
* `http-headers`: _LMS_ uses a configurable HTTP header field, typically set by a reverse proxy to handle [SSO](https://en.wikipedia.org/wiki/Single_sign-on), to extract the login name. You can customize the field to be used using the `http-headers-login-field` option.
|
* `http-headers`: _LMS_ uses a configurable HTTP header field, typically set by a reverse proxy to handle [SSO](https://en.wikipedia.org/wiki/Single_sign-on), to extract the login name. You can customize the field to be used using the `http-headers-login-field` option.
|
||||||
|
* `oidc`: _LMS_ authenticates users through an OpenID Connect provider using the authorization code flow. Configure the client and provider endpoints with the `oidc-*` options in `lms.conf`, set `public-url`, and register `<public-url>/oidc/callback` as the client redirect URI. Users are created automatically on first login; the first user is an administrator. By default the stable `sub` claim is used as the LMS login name; `email` (verified addresses only) and `name` are also supported through `oidc-login-name-claim`.
|
||||||
__Note__: the first created user is the admin user.
|
__Note__: the first created user is the admin user.
|
||||||
### `internal` backend: reset admin password
|
### `internal` backend: reset admin password
|
||||||
Open the the database file located in `/var/lms/lms.db` using `sqlite3`:
|
Open the the database file located in `/var/lms/lms.db` using `sqlite3`:
|
||||||
|
|||||||
@@ -1,49 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" ?>
|
|
||||||
<messages>
|
|
||||||
|
|
||||||
<message id="Lms.Admin.InternetRadio.template">
|
|
||||||
<legend>${tr:Lms.Admin.InternetRadio.title}</legend>
|
|
||||||
<p class="text-body-secondary">${tr:Lms.Admin.InternetRadio.help}</p>
|
|
||||||
<div class="card mb-4"><div class="card-body">
|
|
||||||
<div class="row g-3 align-items-end">
|
|
||||||
<div class="col-12 col-lg-3"><label class="form-label">${tr:Lms.Admin.InternetRadio.name}</label>${name class="form-control"}</div>
|
|
||||||
<div class="col-12 col-lg-4"><label class="form-label">${tr:Lms.Admin.InternetRadio.stream-url}</label>${stream-url class="form-control"}</div>
|
|
||||||
<div class="col-12 col-lg-4"><label class="form-label">${tr:Lms.Admin.InternetRadio.homepage-url}</label>${homepage-url class="form-control"}</div>
|
|
||||||
<div class="col-12 col-lg-1 d-grid">${add-btn class="btn btn-primary"}</div>
|
|
||||||
</div>
|
|
||||||
</div></div>
|
|
||||||
${stations class="d-grid gap-1 mb-3 Lms-row-container"}
|
|
||||||
</message>
|
|
||||||
|
|
||||||
<message id="Lms.Admin.InternetRadio.template.entry">
|
|
||||||
<div class="d-flex align-items-center Lms-bg-dark-hover">
|
|
||||||
<div class="row align-items-center flex-fill overflow-hidden">
|
|
||||||
<div class="col-12 col-md-3"><div class="p-2 fw-semibold text-truncate">${name}</div></div>
|
|
||||||
<div class="col-12 col-md-5"><div class="p-2 text-truncate">${stream-url}</div></div>
|
|
||||||
<div class="col-12 col-md-4"><div class="p-2 text-body-secondary text-truncate">${homepage-url}</div></div>
|
|
||||||
</div>
|
|
||||||
<div class="p-2 d-flex gap-1">${edit-btn class="btn btn-sm btn-outline-secondary border-0"}${delete-btn class="btn btn-sm btn-outline-danger border-0"}</div>
|
|
||||||
</div>
|
|
||||||
</message>
|
|
||||||
|
|
||||||
<message id="Lms.Admin.InternetRadio.template.edit">
|
|
||||||
<div class="modal fade" tabindex="-1"><div class="modal-dialog"><div class="modal-content">
|
|
||||||
<div class="modal-header"><h5 class="modal-title">${tr:Lms.Admin.InternetRadio.edit}</h5></div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<div class="mb-3"><label class="form-label">${tr:Lms.Admin.InternetRadio.name}</label>${name class="form-control"}</div>
|
|
||||||
<div class="mb-3"><label class="form-label">${tr:Lms.Admin.InternetRadio.stream-url}</label>${stream-url class="form-control"}</div>
|
|
||||||
<div><label class="form-label">${tr:Lms.Admin.InternetRadio.homepage-url}</label>${homepage-url class="form-control"}</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">${save-btn class="btn btn-primary"}${cancel-btn class="btn btn-secondary"}</div>
|
|
||||||
</div></div></div>
|
|
||||||
</message>
|
|
||||||
|
|
||||||
<message id="Lms.Admin.InternetRadio.template.delete">
|
|
||||||
<div class="modal fade" tabindex="-1"><div class="modal-dialog"><div class="modal-content">
|
|
||||||
<div class="modal-header"><h5 class="modal-title">${tr:Lms.Admin.InternetRadio.delete-confirm}</h5></div>
|
|
||||||
<div class="modal-body">${tr:Lms.Admin.InternetRadio.delete-help}</div>
|
|
||||||
<div class="modal-footer">${delete-btn class="btn btn-danger"}${cancel-btn class="btn btn-secondary"}</div>
|
|
||||||
</div></div></div>
|
|
||||||
</message>
|
|
||||||
|
|
||||||
</messages>
|
|
||||||
@@ -35,7 +35,6 @@
|
|||||||
${podcasts class="nav-link"}
|
${podcasts class="nav-link"}
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
${radio class="nav-link"}
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
${filters class="d-flex align-items-center me-auto mb-2 mb-md-0"}
|
${filters class="d-flex align-items-center me-auto mb-2 mb-md-0"}
|
||||||
@@ -49,7 +48,6 @@
|
|||||||
<li>${media-libraries class="dropdown-item"}</li>
|
<li>${media-libraries class="dropdown-item"}</li>
|
||||||
<li>${admin-podcasts class="dropdown-item"}</li>
|
<li>${admin-podcasts class="dropdown-item"}</li>
|
||||||
<li>${admin-internet-radio class="dropdown-item"}</li>
|
<li>${admin-internet-radio class="dropdown-item"}</li>
|
||||||
<li>${scan-settings class="dropdown-item"}</li>
|
|
||||||
<li>${scanner class="dropdown-item"}</li>
|
<li>${scanner class="dropdown-item"}</li>
|
||||||
<li>${users class="dropdown-item"}</li>
|
<li>${users class="dropdown-item"}</li>
|
||||||
<li>${about class="dropdown-item"}</li>
|
<li>${about class="dropdown-item"}</li>
|
||||||
|
|||||||
@@ -213,6 +213,10 @@
|
|||||||
<message id="Lms.Auth.login">Log in</message>
|
<message id="Lms.Auth.login">Log in</message>
|
||||||
<message id="Lms.Auth.remember-me">Remember me</message>
|
<message id="Lms.Auth.remember-me">Remember me</message>
|
||||||
<message id="Lms.Auth.welcome">Welcome!</message>
|
<message id="Lms.Auth.welcome">Welcome!</message>
|
||||||
|
<message id="Lms.OIDCAuth.login">Log in with {1}</message>
|
||||||
|
<message id="Lms.OIDCAuth.failed">OpenID Connect authentication failed.</message>
|
||||||
|
<message id="Lms.OIDCAuth.unverified-email">The identity provider did not verify your email address.</message>
|
||||||
|
<message id="Lms.OIDCAuth.missing-claim">The identity provider did not return the configured login name.</message>
|
||||||
|
|
||||||
<!--Explore-->
|
<!--Explore-->
|
||||||
<message id="Lms.Explore.add-filter">Add filter</message>
|
<message id="Lms.Explore.add-filter">Add filter</message>
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<messages>
|
||||||
|
<message id="Lms.OIDCAuth.template">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row mt-3 justify-content-md-center">
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<h2>${tr:Lms.Auth.welcome}</h2>
|
||||||
|
<div class="mt-3">${login-btn class="btn btn-primary"}</div>
|
||||||
|
<div class="text-danger mt-3">${error}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</message>
|
||||||
|
</messages>
|
||||||
@@ -14,7 +14,6 @@
|
|||||||
<div class="btn-group me-2">
|
<div class="btn-group me-2">
|
||||||
${repeat-btn class="btn-check"}
|
${repeat-btn class="btn-check"}
|
||||||
<label class="btn btn-outline-info" title="${tr:Lms.PlayQueue.repeat}" for="${id:repeat-btn}">${tr:Lms.PlayQueue.template.repeat-btn}</label>
|
<label class="btn btn-outline-info" title="${tr:Lms.PlayQueue.repeat}" for="${id:repeat-btn}">${tr:Lms.PlayQueue.template.repeat-btn}</label>
|
||||||
${radio-btn class="btn-check"}
|
|
||||||
<label class="btn btn-outline-info" title="${tr:Lms.PlayQueue.radio-mode}" for="${id:radio-btn}">${tr:Lms.PlayQueue.template.radio-btn}</label>
|
<label class="btn btn-outline-info" title="${tr:Lms.PlayQueue.radio-mode}" for="${id:radio-btn}">${tr:Lms.PlayQueue.template.radio-btn}</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-inline-block me-2" title="${tr:Lms.save}">
|
<div class="d-inline-block me-2" title="${tr:Lms.save}">
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" ?>
|
|
||||||
<messages>
|
|
||||||
|
|
||||||
<message id="Lms.Radio.template">
|
|
||||||
<legend class="mb-3">${tr:Lms.Radio.radio}</legend>
|
|
||||||
<p class="text-body-secondary">${tr:Lms.Radio.description}</p>
|
|
||||||
${stations class="d-grid gap-2"}
|
|
||||||
</message>
|
|
||||||
|
|
||||||
<message id="Lms.Radio.template.station">
|
|
||||||
<div class="card w-100">
|
|
||||||
<div class="card-body d-flex align-items-center gap-3">
|
|
||||||
<div class="flex-fill min-width-0">
|
|
||||||
<h5 class="card-title text-truncate mb-1">${name}</h5>
|
|
||||||
${homepage class="small"}
|
|
||||||
</div>
|
|
||||||
${play-btn class="btn btn-primary flex-shrink-0"}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</message>
|
|
||||||
|
|
||||||
</messages>
|
|
||||||
+13
-1
@@ -74,12 +74,24 @@ lastfm-submit-period-hours = 1;
|
|||||||
acousticbrainz-api-base-url = "https://acousticbrainz.org";
|
acousticbrainz-api-base-url = "https://acousticbrainz.org";
|
||||||
|
|
||||||
# Authentication
|
# Authentication
|
||||||
# Available backends: "internal", "PAM", "http-headers"
|
# Available backends: "internal", "PAM", "http-headers", "oidc"
|
||||||
authentication-backend = "internal";
|
authentication-backend = "internal";
|
||||||
# The number of bcrypt rounds to be used when backend is set to "internal". The higher the more secure
|
# The number of bcrypt rounds to be used when backend is set to "internal". The higher the more secure
|
||||||
internal-password-bcrypt-round = 12;
|
internal-password-bcrypt-round = 12;
|
||||||
# The header to be used to read the authentication user when backend is set to "http-headers"
|
# The header to be used to read the authentication user when backend is set to "http-headers"
|
||||||
http-headers-login-field = "X-Forwarded-User";
|
http-headers-login-field = "X-Forwarded-User";
|
||||||
|
# OpenID Connect settings (used when authentication-backend is "oidc").
|
||||||
|
# public-url must also be set, and <public-url>/oidc/callback must be registered
|
||||||
|
# as an allowed redirect URI at the identity provider.
|
||||||
|
oidc-client-id = "";
|
||||||
|
oidc-client-secret = "";
|
||||||
|
oidc-authorization-endpoint = "";
|
||||||
|
oidc-token-endpoint = "";
|
||||||
|
oidc-userinfo-endpoint = "";
|
||||||
|
oidc-scope = "openid profile email";
|
||||||
|
oidc-provider-name = "OpenID Connect";
|
||||||
|
# Login name source: "sub" (stable provider subject), "email" (must be verified), or "name".
|
||||||
|
oidc-login-name-claim = "sub";
|
||||||
|
|
||||||
# Max entries in the login throttler (1 entry per IP address. For IPv6, the whole /64 block is used)
|
# Max entries in the login throttler (1 entry per IP address. For IPv6, the whole /64 block is used)
|
||||||
login-throttler-max-entries = 10000;
|
login-throttler-max-entries = 10000;
|
||||||
|
|||||||
+35
-2
@@ -21,6 +21,8 @@
|
|||||||
#include <thread>
|
#include <thread>
|
||||||
|
|
||||||
#include <Wt/WApplication.h>
|
#include <Wt/WApplication.h>
|
||||||
|
#include <Wt/Auth/AuthService.h>
|
||||||
|
#include <Wt/Auth/OidcService.h>
|
||||||
#include <Wt/WLogSink.h>
|
#include <Wt/WLogSink.h>
|
||||||
#include <Wt/WServer.h>
|
#include <Wt/WServer.h>
|
||||||
#include <boost/asio/io_context.hpp>
|
#include <boost/asio/io_context.hpp>
|
||||||
@@ -83,6 +85,8 @@ namespace lms
|
|||||||
return ui::AuthenticationBackend::PAM;
|
return ui::AuthenticationBackend::PAM;
|
||||||
if (backend == "http-headers")
|
if (backend == "http-headers")
|
||||||
return ui::AuthenticationBackend::Env;
|
return ui::AuthenticationBackend::Env;
|
||||||
|
if (backend == "oidc")
|
||||||
|
return ui::AuthenticationBackend::OIDC;
|
||||||
|
|
||||||
throw core::LmsException{ "Invalid config value for 'authentication-backend'" };
|
throw core::LmsException{ "Invalid config value for 'authentication-backend'" };
|
||||||
}
|
}
|
||||||
@@ -471,6 +475,9 @@ namespace lms
|
|||||||
core::Service<auth::IAuthTokenService> authTokenService{ auth::createAuthTokenService(*database, config->getULong("login-throttler-max-entriees", 10'000)) };
|
core::Service<auth::IAuthTokenService> authTokenService{ auth::createAuthTokenService(*database, config->getULong("login-throttler-max-entriees", 10'000)) };
|
||||||
core::Service<auth::IPasswordService> authPasswordService;
|
core::Service<auth::IPasswordService> authPasswordService;
|
||||||
core::Service<auth::IEnvService> authEnvService;
|
core::Service<auth::IEnvService> authEnvService;
|
||||||
|
Wt::Auth::AuthService oidcBaseAuthService;
|
||||||
|
std::unique_ptr<Wt::Auth::OidcService> oidcService;
|
||||||
|
std::string oidcLoginNameClaim{ config->getString("oidc-login-name-claim", "sub") };
|
||||||
|
|
||||||
authTokenService->registerDomain("ui", auth::IAuthTokenService::DomainParameters{
|
authTokenService->registerDomain("ui", auth::IAuthTokenService::DomainParameters{
|
||||||
.tokenMaxUseCount = 1,
|
.tokenMaxUseCount = 1,
|
||||||
@@ -493,6 +500,32 @@ namespace lms
|
|||||||
case ui::AuthenticationBackend::Env:
|
case ui::AuthenticationBackend::Env:
|
||||||
authEnvService.assign(auth::createEnvService("http-headers", *database));
|
authEnvService.assign(auth::createEnvService("http-headers", *database));
|
||||||
break;
|
break;
|
||||||
|
case ui::AuthenticationBackend::OIDC:
|
||||||
|
{
|
||||||
|
if (oidcLoginNameClaim != "sub" && oidcLoginNameClaim != "email" && oidcLoginNameClaim != "name")
|
||||||
|
throw core::LmsException{ "Invalid config value for 'oidc-login-name-claim'" };
|
||||||
|
|
||||||
|
std::string publicUrl{ config->getString("public-url", "") };
|
||||||
|
if (publicUrl.empty())
|
||||||
|
throw core::LmsException{ "'public-url' must be configured when using the OIDC authentication backend" };
|
||||||
|
while (publicUrl.size() > 1 && publicUrl.back() == '/')
|
||||||
|
publicUrl.pop_back();
|
||||||
|
|
||||||
|
ui::OIDCConfig oidcConfig{
|
||||||
|
.clientId = std::string{ config->getString("oidc-client-id", "") },
|
||||||
|
.clientSecret = std::string{ config->getString("oidc-client-secret", "") },
|
||||||
|
.authorizationEndpoint = std::string{ config->getString("oidc-authorization-endpoint", "") },
|
||||||
|
.tokenEndpoint = std::string{ config->getString("oidc-token-endpoint", "") },
|
||||||
|
.userInfoEndpoint = std::string{ config->getString("oidc-userinfo-endpoint", "") },
|
||||||
|
.redirectEndpoint = publicUrl + "/oidc/callback",
|
||||||
|
.scope = std::string{ config->getString("oidc-scope", "openid profile email") },
|
||||||
|
.name = std::string{ config->getString("oidc-provider-name", "OpenID Connect") },
|
||||||
|
};
|
||||||
|
if (oidcConfig.clientId.empty() || oidcConfig.clientSecret.empty() || oidcConfig.authorizationEndpoint.empty() || oidcConfig.tokenEndpoint.empty() || oidcConfig.userInfoEndpoint.empty())
|
||||||
|
throw core::LmsException{ "Incomplete OIDC configuration" };
|
||||||
|
oidcService = ui::createOIDCService(oidcBaseAuthService, oidcConfig);
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
image::init(argv[0]);
|
image::init(argv[0]);
|
||||||
@@ -534,8 +567,8 @@ namespace lms
|
|||||||
|
|
||||||
// bind UI entry point
|
// bind UI entry point
|
||||||
server.addEntryPoint(Wt::EntryPointType::Application,
|
server.addEntryPoint(Wt::EntryPointType::Application,
|
||||||
[&database, &appManager, uiAuthenticationBackend, serverInstanceId](const Wt::WEnvironment& env) {
|
[&database, &appManager, uiAuthenticationBackend, serverInstanceId, &oidcService, oidcLoginNameClaim](const Wt::WEnvironment& env) {
|
||||||
return ui::LmsApplication::create(env, *database, appManager, uiAuthenticationBackend, serverInstanceId);
|
return ui::LmsApplication::create(env, *database, appManager, uiAuthenticationBackend, serverInstanceId, oidcService.get(), oidcLoginNameClaim);
|
||||||
});
|
});
|
||||||
|
|
||||||
proxyScannerEventsToApplication(*scannerService, server);
|
proxyScannerEventsToApplication(*scannerService, server);
|
||||||
|
|||||||
@@ -20,6 +20,9 @@
|
|||||||
#include "Auth.hpp"
|
#include "Auth.hpp"
|
||||||
|
|
||||||
#include <Wt/Auth/HashFunction.h>
|
#include <Wt/Auth/HashFunction.h>
|
||||||
|
#include <Wt/Auth/AuthService.h>
|
||||||
|
#include <Wt/Auth/Identity.h>
|
||||||
|
#include <Wt/Auth/OidcService.h>
|
||||||
|
|
||||||
#include <Wt/WCheckBox.h>
|
#include <Wt/WCheckBox.h>
|
||||||
#include <Wt/WEnvironment.h>
|
#include <Wt/WEnvironment.h>
|
||||||
@@ -185,6 +188,22 @@ namespace lms::ui
|
|||||||
core::Service<auth::IAuthTokenService>::get()->clearAuthTokens(authTokenDomain, userId);
|
core::Service<auth::IAuthTokenService>::get()->clearAuthTokens(authTokenDomain, userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<Wt::Auth::OidcService> createOIDCService(const Wt::Auth::AuthService& authService, const OIDCConfig& config)
|
||||||
|
{
|
||||||
|
auto service{ std::make_unique<Wt::Auth::OidcService>(authService) };
|
||||||
|
service->setClientId(config.clientId);
|
||||||
|
service->setClientSecret(config.clientSecret);
|
||||||
|
service->setAuthEndpoint(config.authorizationEndpoint);
|
||||||
|
service->setTokenEndpoint(config.tokenEndpoint);
|
||||||
|
service->setUserInfoEndpoint(config.userInfoEndpoint);
|
||||||
|
service->setRedirectEndpoint(config.redirectEndpoint);
|
||||||
|
service->setAuthenticationScope(config.scope);
|
||||||
|
service->setName("oidc");
|
||||||
|
service->setDescription(config.name);
|
||||||
|
service->setPopupEnabled(false);
|
||||||
|
return service;
|
||||||
|
}
|
||||||
|
|
||||||
PasswordAuth::PasswordAuth(auth::IPasswordService& passwordService)
|
PasswordAuth::PasswordAuth(auth::IPasswordService& passwordService)
|
||||||
: Wt::WTemplateFormView{ Wt::WString::tr("Lms.Auth.template") }
|
: Wt::WTemplateFormView{ Wt::WString::tr("Lms.Auth.template") }
|
||||||
{
|
{
|
||||||
@@ -234,4 +253,61 @@ namespace lms::ui
|
|||||||
|
|
||||||
updateView(model.get());
|
updateView(model.get());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
OIDCAuth::OIDCAuth(const Wt::Auth::OidcService& service, std::string loginNameClaim)
|
||||||
|
: Wt::WTemplateFormView{ Wt::WString::tr("Lms.OIDCAuth.template") }
|
||||||
|
, _process{ service.createProcess(service.authenticationScope()) }
|
||||||
|
{
|
||||||
|
Wt::WPushButton* loginBtn{ bindNew<Wt::WPushButton>("login-btn", Wt::WString::tr("Lms.OIDCAuth.login").arg(service.description())) };
|
||||||
|
loginBtn->clicked().connect(_process.get(), &Wt::Auth::OAuthProcess::startAuthenticate);
|
||||||
|
|
||||||
|
_process->authenticated().connect(this, [this, loginNameClaim = std::move(loginNameClaim)](const Wt::Auth::Identity& identity) {
|
||||||
|
if (!identity.isValid())
|
||||||
|
{
|
||||||
|
bindString("error", Wt::WString::tr("Lms.OIDCAuth.failed"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string loginName;
|
||||||
|
if (loginNameClaim == "email")
|
||||||
|
{
|
||||||
|
if (!identity.emailVerified())
|
||||||
|
{
|
||||||
|
bindString("error", Wt::WString::tr("Lms.OIDCAuth.unverified-email"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
loginName = identity.email();
|
||||||
|
}
|
||||||
|
else if (loginNameClaim == "name")
|
||||||
|
loginName = identity.name().toUTF8();
|
||||||
|
else
|
||||||
|
loginName = identity.id();
|
||||||
|
|
||||||
|
if (loginName.empty())
|
||||||
|
{
|
||||||
|
bindString("error", Wt::WString::tr("Lms.OIDCAuth.missing-claim"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
db::User::pointer user;
|
||||||
|
{
|
||||||
|
auto transaction{ LmsApp->getDbSession().createWriteTransaction() };
|
||||||
|
user = db::User::find(LmsApp->getDbSession(), loginName);
|
||||||
|
if (!user)
|
||||||
|
{
|
||||||
|
const db::UserType type{ db::User::getCount(LmsApp->getDbSession()) == 0 ? db::UserType::ADMIN : db::UserType::REGULAR };
|
||||||
|
user = LmsApp->getDbSession().create<db::User>(loginName);
|
||||||
|
user.modify()->setType(type);
|
||||||
|
if (!identity.name().empty())
|
||||||
|
user.modify()->setDisplayName(identity.name().toUTF8());
|
||||||
|
}
|
||||||
|
user.modify()->setLastLogin(Wt::WDateTime::currentDateTime());
|
||||||
|
}
|
||||||
|
userLoggedIn.emit(user->getId());
|
||||||
|
});
|
||||||
|
|
||||||
|
bindEmpty("error");
|
||||||
|
}
|
||||||
|
|
||||||
|
OIDCAuth::~OIDCAuth() = default;
|
||||||
} // namespace lms::ui
|
} // namespace lms::ui
|
||||||
|
|||||||
@@ -21,6 +21,9 @@
|
|||||||
|
|
||||||
#include <Wt/WTemplateFormView.h>
|
#include <Wt/WTemplateFormView.h>
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
#include "database/objects/UserId.hpp"
|
#include "database/objects/UserId.hpp"
|
||||||
|
|
||||||
namespace lms::auth
|
namespace lms::auth
|
||||||
@@ -28,15 +31,37 @@ namespace lms::auth
|
|||||||
class IPasswordService;
|
class IPasswordService;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
namespace Wt::Auth
|
||||||
|
{
|
||||||
|
class AuthService;
|
||||||
|
class OAuthProcess;
|
||||||
|
class OidcService;
|
||||||
|
}
|
||||||
|
|
||||||
namespace lms::ui
|
namespace lms::ui
|
||||||
{
|
{
|
||||||
enum class AuthenticationBackend
|
enum class AuthenticationBackend
|
||||||
{
|
{
|
||||||
Internal,
|
Internal,
|
||||||
Env,
|
Env,
|
||||||
|
OIDC,
|
||||||
PAM,
|
PAM,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct OIDCConfig
|
||||||
|
{
|
||||||
|
std::string clientId;
|
||||||
|
std::string clientSecret;
|
||||||
|
std::string authorizationEndpoint;
|
||||||
|
std::string tokenEndpoint;
|
||||||
|
std::string userInfoEndpoint;
|
||||||
|
std::string redirectEndpoint;
|
||||||
|
std::string scope;
|
||||||
|
std::string name;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::unique_ptr<Wt::Auth::OidcService> createOIDCService(const Wt::Auth::AuthService& authService, const OIDCConfig& config);
|
||||||
|
|
||||||
db::UserId processAuthToken(const Wt::WEnvironment& env);
|
db::UserId processAuthToken(const Wt::WEnvironment& env);
|
||||||
|
|
||||||
class PasswordAuth : public Wt::WTemplateFormView
|
class PasswordAuth : public Wt::WTemplateFormView
|
||||||
@@ -46,4 +71,16 @@ namespace lms::ui
|
|||||||
|
|
||||||
Wt::Signal<db::UserId> userLoggedIn;
|
Wt::Signal<db::UserId> userLoggedIn;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
class OIDCAuth : public Wt::WTemplateFormView
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
OIDCAuth(const Wt::Auth::OidcService& service, std::string loginNameClaim);
|
||||||
|
~OIDCAuth() override;
|
||||||
|
|
||||||
|
Wt::Signal<db::UserId> userLoggedIn;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::unique_ptr<Wt::Auth::OAuthProcess> _process;
|
||||||
|
};
|
||||||
} // namespace lms::ui
|
} // namespace lms::ui
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ namespace lms::ui
|
|||||||
res->use(appRoot + "error");
|
res->use(appRoot + "error");
|
||||||
res->use(appRoot + "explore");
|
res->use(appRoot + "explore");
|
||||||
res->use(appRoot + "login");
|
res->use(appRoot + "login");
|
||||||
|
res->use(appRoot + "oidc-login");
|
||||||
res->use(appRoot + "main");
|
res->use(appRoot + "main");
|
||||||
res->use(appRoot + "mediaplayer");
|
res->use(appRoot + "mediaplayer");
|
||||||
res->use(appRoot + "messages");
|
res->use(appRoot + "messages");
|
||||||
@@ -151,9 +152,9 @@ namespace lms::ui
|
|||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
std::unique_ptr<Wt::WApplication> LmsApplication::create(const Wt::WEnvironment& env, db::IDb& db, LmsApplicationManager& appManager, AuthenticationBackend authBackend, const core::UUID& serverInstanceId)
|
std::unique_ptr<Wt::WApplication> LmsApplication::create(const Wt::WEnvironment& env, db::IDb& db, LmsApplicationManager& appManager, AuthenticationBackend authBackend, const core::UUID& serverInstanceId, const Wt::Auth::OidcService* oidcService, std::string oidcLoginNameClaim)
|
||||||
{
|
{
|
||||||
return std::make_unique<LmsApplication>(env, db, appManager, authBackend, serverInstanceId);
|
return std::make_unique<LmsApplication>(env, db, appManager, authBackend, serverInstanceId, oidcService, std::move(oidcLoginNameClaim));
|
||||||
}
|
}
|
||||||
|
|
||||||
LmsApplication* LmsApplication::instance()
|
LmsApplication* LmsApplication::instance()
|
||||||
@@ -161,11 +162,13 @@ namespace lms::ui
|
|||||||
return static_cast<LmsApplication*>(Wt::WApplication::instance());
|
return static_cast<LmsApplication*>(Wt::WApplication::instance());
|
||||||
}
|
}
|
||||||
|
|
||||||
LmsApplication::LmsApplication(const Wt::WEnvironment& env, db::IDb& db, LmsApplicationManager& appManager, AuthenticationBackend authBackend, const core::UUID& serverInstanceId)
|
LmsApplication::LmsApplication(const Wt::WEnvironment& env, db::IDb& db, LmsApplicationManager& appManager, AuthenticationBackend authBackend, const core::UUID& serverInstanceId, const Wt::Auth::OidcService* oidcService, std::string oidcLoginNameClaim)
|
||||||
: Wt::WApplication{ env }
|
: Wt::WApplication{ env }
|
||||||
, _db{ db }
|
, _db{ db }
|
||||||
, _appManager{ appManager }
|
, _appManager{ appManager }
|
||||||
, _authBackend{ authBackend }
|
, _authBackend{ authBackend }
|
||||||
|
, _oidcService{ oidcService }
|
||||||
|
, _oidcLoginNameClaim{ std::move(oidcLoginNameClaim) }
|
||||||
, _serverInstanceId{ serverInstanceId }
|
, _serverInstanceId{ serverInstanceId }
|
||||||
, _areDownloadsEnabled(core::Service<core::IConfig>::get()->getBool("ui-allow-downloads", true))
|
, _areDownloadsEnabled(core::Service<core::IConfig>::get()->getBool("ui-allow-downloads", true))
|
||||||
{
|
{
|
||||||
@@ -272,10 +275,20 @@ namespace lms::ui
|
|||||||
// Try to authenticate using auth token ("remember me" checkbox), may fail
|
// Try to authenticate using auth token ("remember me" checkbox), may fail
|
||||||
userId = processAuthToken(environment());
|
userId = processAuthToken(environment());
|
||||||
break;
|
break;
|
||||||
|
case AuthenticationBackend::OIDC:
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userId.isValid())
|
if (userId.isValid())
|
||||||
onUserLoggedIn(userId, false /* strongAuth */);
|
onUserLoggedIn(userId, false /* strongAuth */);
|
||||||
|
else if (_authBackend == AuthenticationBackend::OIDC)
|
||||||
|
{
|
||||||
|
assert(_oidcService);
|
||||||
|
OIDCAuth* auth{ root()->addNew<OIDCAuth>(*_oidcService, _oidcLoginNameClaim) };
|
||||||
|
auth->userLoggedIn.connect(this, [this](db::UserId authenticatedUserId) {
|
||||||
|
onUserLoggedIn(authenticatedUserId, true /* strongAuth */);
|
||||||
|
});
|
||||||
|
}
|
||||||
else
|
else
|
||||||
processPasswordAuth();
|
processPasswordAuth();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,10 +55,10 @@ namespace lms::ui
|
|||||||
class LmsApplication : public Wt::WApplication
|
class LmsApplication : public Wt::WApplication
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
LmsApplication(const Wt::WEnvironment& env, db::IDb& db, LmsApplicationManager& appManager, AuthenticationBackend authBackend, const core::UUID& serverInstanceId);
|
LmsApplication(const Wt::WEnvironment& env, db::IDb& db, LmsApplicationManager& appManager, AuthenticationBackend authBackend, const core::UUID& serverInstanceId, const Wt::Auth::OidcService* oidcService = nullptr, std::string oidcLoginNameClaim = "sub");
|
||||||
~LmsApplication() override;
|
~LmsApplication() override;
|
||||||
|
|
||||||
static std::unique_ptr<Wt::WApplication> create(const Wt::WEnvironment& env, db::IDb& db, LmsApplicationManager& appManager, AuthenticationBackend authBackend, const core::UUID& serverInstanceId);
|
static std::unique_ptr<Wt::WApplication> create(const Wt::WEnvironment& env, db::IDb& db, LmsApplicationManager& appManager, AuthenticationBackend authBackend, const core::UUID& serverInstanceId, const Wt::Auth::OidcService* oidcService = nullptr, std::string oidcLoginNameClaim = "sub");
|
||||||
static LmsApplication* instance();
|
static LmsApplication* instance();
|
||||||
|
|
||||||
// Session application data
|
// Session application data
|
||||||
@@ -115,6 +115,8 @@ namespace lms::ui
|
|||||||
Wt::Signal<> _preQuit;
|
Wt::Signal<> _preQuit;
|
||||||
LmsApplicationManager& _appManager;
|
LmsApplicationManager& _appManager;
|
||||||
const AuthenticationBackend _authBackend;
|
const AuthenticationBackend _authBackend;
|
||||||
|
const Wt::Auth::OidcService* const _oidcService;
|
||||||
|
const std::string _oidcLoginNameClaim;
|
||||||
const core::UUID _serverInstanceId;
|
const core::UUID _serverInstanceId;
|
||||||
const bool _areDownloadsEnabled;
|
const bool _areDownloadsEnabled;
|
||||||
scanner::Events _scannerEvents;
|
scanner::Events _scannerEvents;
|
||||||
|
|||||||
Reference in New Issue
Block a user