From 7918c9d6b45781bc3cfb2d51bed379e5dfa54d19 Mon Sep 17 00:00:00 2001 From: Lars Lehmann Date: Tue, 29 Apr 2025 22:20:45 +0200 Subject: [PATCH 1/5] Add Oauth2 --- Pages/LoginPage.php | 21 ++++++ .../ExternalAuthLoginPresenter.php | 69 +++++++++++++++++++ Presenters/LoginPresenter.php | 31 +++++++++ Web/oauth2-auth.php | 14 ++++ config/config.dist.php | 11 +++ lib/Config/ConfigKeys.php | 10 +++ tests/Presenters/LoginPresenterTest.php | 2 + tpl/login.tpl | 4 ++ 8 files changed, 162 insertions(+) create mode 100644 Web/oauth2-auth.php diff --git a/Pages/LoginPage.php b/Pages/LoginPage.php index e3df15d58..4211ad5f4 100644 --- a/Pages/LoginPage.php +++ b/Pages/LoginPage.php @@ -112,6 +112,12 @@ public function SetFacebookUrl($URL); * */ public function SetKeycloakUrl($URL); + + /** + * + */ + public function SetOauth2Url($URL); + public function SetOauth2Name($Name); } class LoginPage extends Page implements ILoginPage @@ -134,6 +140,7 @@ public function __construct() $this->Set('AllowGoogleLogin', Configuration::Instance()->GetSectionKey(ConfigSection::AUTHENTICATION, ConfigKeys::AUTHENTICATION_ALLOW_GOOGLE, new BooleanConverter())); $this->Set('AllowMicrosoftLogin', Configuration::Instance()->GetSectionKey(ConfigSection::AUTHENTICATION, ConfigKeys::AUTHENTICATION_ALLOW_MICROSOFT, new BooleanConverter())); $this->Set('AllowKeycloakLogin', Configuration::Instance()->GetSectionKey(ConfigSection::AUTHENTICATION, ConfigKeys::AUTHENTICATION_ALLOW_KEYCLOAK, new BooleanConverter())); + $this->Set('AllowOauth2Login', Configuration::Instance()->GetSectionKey(ConfigSection::AUTHENTICATION, ConfigKeys::AUTHENTICATION_ALLOW_OAUTH2, new BooleanConverter())); $scriptUrl = Configuration::Instance()->GetScriptUrl(); $parts = explode('://', $scriptUrl); $this->Set('Protocol', $parts[0]); @@ -353,4 +360,18 @@ public function SetKeycloakUrl($KeycloakUrl) $this->Set('KeycloakUrl', $KeycloakUrl); } } + + public function SetOauth2Url($Oauth2Url) + { + if (Configuration::Instance()->GetSectionKey(ConfigSection::AUTHENTICATION, ConfigKeys::AUTHENTICATION_ALLOW_OAUTH2, new BooleanConverter())) { + $this->Set('Oauth2Url', $Oauth2Url); + } + } + + public function SetOauth2Name($Oauth2Name) + { + if (Configuration::Instance()->GetSectionKey(ConfigSection::AUTHENTICATION, ConfigKeys::AUTHENTICATION_ALLOW_OAUTH2, new BooleanConverter())) { + $this->Set('Oauth2Name', $Oauth2Name); + } + } } diff --git a/Presenters/Authentication/ExternalAuthLoginPresenter.php b/Presenters/Authentication/ExternalAuthLoginPresenter.php index abaa24394..54282c363 100644 --- a/Presenters/Authentication/ExternalAuthLoginPresenter.php +++ b/Presenters/Authentication/ExternalAuthLoginPresenter.php @@ -38,6 +38,9 @@ public function PageLoad() if ($this->page->GetType() == 'keycloak') { $this->ProcessKeycloakSingleSignOn(); } + if ($this->page->GetType() == 'oauth2') { + $this->ProcessOauth2SingleSignOn(); + } } /** @@ -228,6 +231,72 @@ private function ProcessKeycloakSingleSignOn() $this->processUserData($username, $email, $firstName, $lastName, $phone, $organization, $title); } + private function ProcessOauth2SingleSignOn() + { + $code = $_GET['code']; + + $oauth2UrlToken = Configuration::Instance()->GetSectionKey(ConfigSection::AUTHENTICATION, ConfigKeys::OAUTH2_URL_TOKEN); + $oauth2UrlUserinfo = Configuration::Instance()->GetSectionKey(ConfigSection::AUTHENTICATION, ConfigKeys::OAUTH2_URL_USERINFO); + $clientId = Configuration::Instance()->GetSectionKey(ConfigSection::AUTHENTICATION, ConfigKeys::OAUTH2_CLIENT_ID); + $clientSecret = Configuration::Instance()->GetSectionKey(ConfigSection::AUTHENTICATION, ConfigKeys::OAUTH2_CLIENT_SECRET); + $redirectUri = rtrim(Configuration::Instance()->GetScriptUrl(), 'Web/') . Configuration::Instance()->GetSectionKey(ConfigSection::AUTHENTICATION, ConfigKeys::OAUTH2_REDIRECT_URI); + + // Prepare the POST data for the token request. + $postData = [ + 'grant_type' => 'authorization_code', + 'code' => $code, + 'redirect_uri' => $redirectUri, + 'client_id' => $clientId, + 'client_secret' => $clientSecret, + ]; + + $client = new \GuzzleHttp\Client(); + + try { + $response = $client->post($oauth2UrlToken, [ + 'form_params' => $postData, + ]); + } catch (\Exception $e) { + $this->page->ShowError(['Error retrieving Oauth2 token: ' . $e->getMessage()]); + return; + } + + $tokenData = json_decode($response->getBody(), true); + if (!isset($tokenData['access_token'])) { + $this->page->ShowError(['Access token not found in Oauth2 response']); + return; + } + $accessToken = $tokenData['access_token']; + + try { + $userResponse = $client->get($oauth2UrlUserinfo, [ + 'headers' => [ + 'Authorization' => 'Bearer ' . $accessToken, + ], + ]); + } catch (\Exception $e) { + $this->page->ShowError(['Error retrieving Oauth2 user info: ' . $e->getMessage()]); + return; + } + + $userData = json_decode($userResponse->getBody(), true); + + $email = isset($userData['email']) ? $userData['email'] : ''; + $firstName = isset($userData['given_name']) ? $userData['given_name'] : 'not set'; + $lastName = isset($userData['family_name']) ? $userData['family_name'] : 'not set'; + $username = isset($userData['preferred_username']) ? $userData['preferred_username'] : $email; + $phone = isset($userData['phone_number']) ? $userData['phone_number'] : ''; + $organization = isset($userData['organization']) ? $userData['organization'] : ''; + $title = isset($userData['title']) ? $userData['title'] : ''; + + if (empty($email)) { + $this->page->ShowError(["Email is not set in your Oauth2 profile. Please update your profile and try again."]); + return; + } + + $this->processUserData($username, $email, $firstName, $lastName, $phone, $organization, $title); + } + /** * Processes user given data, creates a user in database if it doesn't exist and logs it in diff --git a/Presenters/LoginPresenter.php b/Presenters/LoginPresenter.php index ee60a80b6..1522b19fc 100644 --- a/Presenters/LoginPresenter.php +++ b/Presenters/LoginPresenter.php @@ -127,6 +127,8 @@ public function PageLoad() $this->_page->SetMicrosoftUrl($this->GetMicrosoftUrl()); $this->_page->SetFacebookUrl($this->GetFacebookUrl()); $this->_page->SetKeycloakUrl($this->GetKeycloakUrl()); + $this->_page->SetOauth2Url($this->GetOauth2Url()); + $this->_page->SetOauth2Name($this->GetOauth2Name()); } public function Login() @@ -318,4 +320,33 @@ public function GetKeycloakUrl() return $keycloakUrl; } } + + public function GetOauth2Url() + { + if (Configuration::Instance()->GetSectionKey(ConfigSection::AUTHENTICATION, ConfigKeys::AUTHENTICATION_ALLOW_OAUTH2, new BooleanConverter())) { + // Retrieve Oauth2 configuration values + $baseUrl = Configuration::Instance()->GetSectionKey(ConfigSection::AUTHENTICATION, ConfigKeys::OAUTH2_URL_AUTHORIZE); + $clientId = Configuration::Instance()->GetSectionKey(ConfigSection::AUTHENTICATION, ConfigKeys::OAUTH2_CLIENT_ID); + $redirectUri = rtrim(Configuration::Instance()->GetScriptUrl(), 'Web/') . Configuration::Instance()->GetSectionKey(ConfigSection::AUTHENTICATION, ConfigKeys::OAUTH2_REDIRECT_URI); + + // Construct the Oauth2 authentication URL + $Oauth2Url = $baseUrl + . '?client_id=' . urlencode($clientId) + . '&redirect_uri=' . urlencode($redirectUri) + . '&response_type=code' + . '&scope=' . urlencode('openid email profile'); + + return $Oauth2Url; + } + } + + public function GetOauth2Name() + { + if (Configuration::Instance()->GetSectionKey(ConfigSection::AUTHENTICATION, ConfigKeys::AUTHENTICATION_ALLOW_OAUTH2, new BooleanConverter())) { + // Retrieve Oauth2 configuration values + $Oauth2Name = Configuration::Instance()->GetSectionKey(ConfigSection::AUTHENTICATION, ConfigKeys::OAUTH2_NAME); + + return $Oauth2Name; + } + } } diff --git a/Web/oauth2-auth.php b/Web/oauth2-auth.php new file mode 100644 index 000000000..2a1f13afd --- /dev/null +++ b/Web/oauth2-auth.php @@ -0,0 +1,14 @@ +_PageLoadWasCalled = true; diff --git a/tpl/login.tpl b/tpl/login.tpl index 0c629b956..9b95630ee 100644 --- a/tpl/login.tpl +++ b/tpl/login.tpl @@ -97,6 +97,10 @@ {translate key='SignInWith'} Keycloak {/if} + {if $AllowOauth2Login} + {translate key='SignInWith'} + {$Oauth2Name} + {/if} {if $facebookError}

From a3c4bb6a4bb05a9234c58f5c9773c14a9b57c008 Mon Sep 17 00:00:00 2001 From: Lars Lehmann Date: Wed, 30 Apr 2025 21:21:23 +0200 Subject: [PATCH 2/5] Add basic Oauth2 docs --- doc/Oauth2-Configuration.md | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 doc/Oauth2-Configuration.md diff --git a/doc/Oauth2-Configuration.md b/doc/Oauth2-Configuration.md new file mode 100644 index 000000000..c761e06f3 --- /dev/null +++ b/doc/Oauth2-Configuration.md @@ -0,0 +1,32 @@ +# Oauth2 Configuration +You can use any IDP which supports Oauth2 like [authentik](https://goauthentik.io) for authentification with LibreBooking + +## IDP Configuration +First you need to create a Client in your IDP in Confidential mode (Client ID and Client Secret). +The Client need to allow redirects to `/Web/oauth2-auth.php` ex. `https://librebooking.com/Web/oauth2-auth.php` and need the scoopes `openid`, `email` and `profile`. + +The mapping of Oauth2 attributes to LibreBooking attributes is: +- `email` -> `email` +- `given_name` -> `firstName` +- `family_name` -> `lastName` +- `preferred_username` -> `username` +- `phone` -> `phone_number` +- `organization` -> `organization` +- `title` -> `title` + +## LibreBooking Config +To connect LibreBooking with your Oauth2 IDP you need to add the following vars to your `config.php`, in this example with authentik as IDP with the url `authentik.io`. +```php +$conf['settings']['authentication']['allow.oauth2.login'] = 'true'; // Enable Oauth2 +$conf['settings']['authentication']['oauth2.name'] = 'authentik'; // Display name of Oauth2 IDP +$conf['settings']['authentication']['oauth2.url.authorize'] = 'https://authentik.io/application/o/authorize/'; // Oauth2 Authorization Endpoint +$conf['settings']['authentication']['oauth2.url.token'] = 'https://authentik.io/application/o/token/'; // Oauth2 Token Endpoint +$conf['settings']['authentication']['oauth2.url.userinfo'] = 'https://authentik.io/application/o/userinfo/'; // Oauth2 Userinfo Endpoint +$conf['settings']['authentication']['oauth2.client.id'] = 'c3zzBXq9Qw3K9KErd9ta6tQgvVhr6wT3rkQaInz8'; +$conf['settings']['authentication']['oauth2.client.secret'] = '13246zgtfd4t456zhg8rdgf98g789df7gFG56z5zhb'; +``` + +To hide the internal LibreBooking Login you can additional add the following variable. +```php +$conf['settings']['authentication']['hide.booked.login.prompt'] = 'true'; +``` From 356b79c84e8e01fbd8498fc21be8a2e5aa1438a5 Mon Sep 17 00:00:00 2001 From: Lars Lehmann Date: Sat, 10 May 2025 21:36:54 +0200 Subject: [PATCH 3/5] Fix unit test --- tests/Presenters/LoginPresenterTest.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/Presenters/LoginPresenterTest.php b/tests/Presenters/LoginPresenterTest.php index b636e7466..a053eedeb 100644 --- a/tests/Presenters/LoginPresenterTest.php +++ b/tests/Presenters/LoginPresenterTest.php @@ -233,6 +233,8 @@ public function SetKeycloakUrl($URL) {} public function SetOauth2Url($URL) {} + public function SetOauth2Name($Name) {} + public function PageLoad() { $this->_PageLoadWasCalled = true; From ba0c095143cdddbadfe8d0f7201537adbd887c7d Mon Sep 17 00:00:00 2001 From: Lars Lehmann <33843261+larsl-net@users.noreply.github.com> Date: Mon, 12 May 2025 12:54:29 +0200 Subject: [PATCH 4/5] Small changes --- doc/Oauth2-Configuration.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/Oauth2-Configuration.md b/doc/Oauth2-Configuration.md index c761e06f3..df75e5af7 100644 --- a/doc/Oauth2-Configuration.md +++ b/doc/Oauth2-Configuration.md @@ -1,7 +1,7 @@ # Oauth2 Configuration -You can use any IDP which supports Oauth2 like [authentik](https://goauthentik.io) for authentification with LibreBooking +You can use any IdP (Identity Provider) which supports Oauth2 like [authentik](https://goauthentik.io) or [Keycloak](https://www.keycloak.org/) for authentification with LibreBooking -## IDP Configuration +## IdP Configuration First you need to create a Client in your IDP in Confidential mode (Client ID and Client Secret). The Client need to allow redirects to `/Web/oauth2-auth.php` ex. `https://librebooking.com/Web/oauth2-auth.php` and need the scoopes `openid`, `email` and `profile`. From c97b0abb55b2eed730e54a9260c1389914ca4956 Mon Sep 17 00:00:00 2001 From: Lars Lehmann <33843261+larsl-net@users.noreply.github.com> Date: Mon, 12 May 2025 12:56:37 +0200 Subject: [PATCH 5/5] Small changes --- doc/Oauth2-Configuration.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/Oauth2-Configuration.md b/doc/Oauth2-Configuration.md index df75e5af7..e58db9392 100644 --- a/doc/Oauth2-Configuration.md +++ b/doc/Oauth2-Configuration.md @@ -2,7 +2,7 @@ You can use any IdP (Identity Provider) which supports Oauth2 like [authentik](https://goauthentik.io) or [Keycloak](https://www.keycloak.org/) for authentification with LibreBooking ## IdP Configuration -First you need to create a Client in your IDP in Confidential mode (Client ID and Client Secret). +First you need to create a Client in your IdP in Confidential mode (Client ID and Client Secret). The Client need to allow redirects to `/Web/oauth2-auth.php` ex. `https://librebooking.com/Web/oauth2-auth.php` and need the scoopes `openid`, `email` and `profile`. The mapping of Oauth2 attributes to LibreBooking attributes is: @@ -15,10 +15,10 @@ The mapping of Oauth2 attributes to LibreBooking attributes is: - `title` -> `title` ## LibreBooking Config -To connect LibreBooking with your Oauth2 IDP you need to add the following vars to your `config.php`, in this example with authentik as IDP with the url `authentik.io`. +To connect LibreBooking with your Oauth2 IdP you need to add the following vars to your `config.php`, in this example with authentik as IdP with the url `authentik.io`. ```php $conf['settings']['authentication']['allow.oauth2.login'] = 'true'; // Enable Oauth2 -$conf['settings']['authentication']['oauth2.name'] = 'authentik'; // Display name of Oauth2 IDP +$conf['settings']['authentication']['oauth2.name'] = 'authentik'; // Display name of Oauth2 IdP $conf['settings']['authentication']['oauth2.url.authorize'] = 'https://authentik.io/application/o/authorize/'; // Oauth2 Authorization Endpoint $conf['settings']['authentication']['oauth2.url.token'] = 'https://authentik.io/application/o/token/'; // Oauth2 Token Endpoint $conf['settings']['authentication']['oauth2.url.userinfo'] = 'https://authentik.io/application/o/userinfo/'; // Oauth2 Userinfo Endpoint