Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions Pages/LoginPage.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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]);
Expand Down Expand Up @@ -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);
}
}
}
69 changes: 69 additions & 0 deletions Presenters/Authentication/ExternalAuthLoginPresenter.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ public function PageLoad()
if ($this->page->GetType() == 'keycloak') {
$this->ProcessKeycloakSingleSignOn();
}
if ($this->page->GetType() == 'oauth2') {
$this->ProcessOauth2SingleSignOn();
}
}

/**
Expand Down Expand Up @@ -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
Expand Down
31 changes: 31 additions & 0 deletions Presenters/LoginPresenter.php
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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;
}
}
}
14 changes: 14 additions & 0 deletions Web/oauth2-auth.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php
define('ROOT_DIR', '../');

require_once(ROOT_DIR . 'lib/Common/namespace.php');

//Checks if the user was authenticated by oauth2 and redirects to external authentication page
if (isset($_GET['code'])) {
$code = filter_input(INPUT_GET, 'code');
header("Location: " . ROOT_DIR . "Web/external-auth.php?type=oauth2&code=" . $code);
exit;
} else {
header("Location:" . ROOT_DIR . "Web");
exit();
}
11 changes: 11 additions & 0 deletions config/config.dist.php
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@
$conf['settings']['authentication']['allow.facebook.login'] = 'false';
$conf['settings']['authentication']['allow.google.login'] = 'false';
$conf['settings']['authentication']['allow.microsoft.login'] = 'false';
$conf['settings']['authentication']['allow.oauth2.login'] = 'false';
$conf['settings']['authentication']['required.email.domains'] = '';
$conf['settings']['authentication']['hide.booked.login.prompt'] = 'false';
$conf['settings']['authentication']['captcha.on.login'] = 'false';
Expand Down Expand Up @@ -250,6 +251,16 @@
$conf['settings']['authentication']['keycloak.client.id'] = '';
$conf['settings']['authentication']['keycloak.client.secret'] = '';
$conf['settings']['authentication']['keycloak.client.uri'] = '/Web/keycloak-auth.php';
/**
* OAuth2 login configuration
*/
$conf['settings']['authentication']['oauth2.name'] = 'OAuth2';
$conf['settings']['authentication']['oauth2.url.authorize'] = '';
$conf['settings']['authentication']['oauth2.url.token'] = '';
$conf['settings']['authentication']['oauth2.url.userinfo'] = '';
$conf['settings']['authentication']['oauth2.client.id'] = '';
$conf['settings']['authentication']['oauth2.client.secret'] = '';
$conf['settings']['authentication']['oauth2.client.uri'] = '/Web/oauth2-auth.php';
/**
* Delete old data job configuration
* Activate the deleteolddata.php as a background job to use this feature
Expand Down
32 changes: 32 additions & 0 deletions doc/Oauth2-Configuration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Oauth2 Configuration
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).
The Client need to allow redirects to `<LibreBooking URL>/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';
```
10 changes: 10 additions & 0 deletions lib/Config/ConfigKeys.php
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ class ConfigKeys
public const AUTHENTICATION_ALLOW_GOOGLE = 'allow.google.login';
public const AUTHENTICATION_ALLOW_MICROSOFT = 'allow.microsoft.login';
public const AUTHENTICATION_ALLOW_KEYCLOAK = 'allow.keycloak.login';
public const AUTHENTICATION_ALLOW_OAUTH2 = 'allow.oauth2.login';
public const AUTHENTICATION_REQUIRED_EMAIL_DOMAINS = 'required.email.domains';
public const AUTHENTICATION_HIDE_BOOKED_LOGIN_PROMPT = 'hide.booked.login.prompt';
public const AUTHENTICATION_CAPTCHA_ON_LOGIN = 'captcha.on.login';
Expand Down Expand Up @@ -187,6 +188,15 @@ class ConfigKeys
public const KEYCLOAK_CLIENT_ID = 'keycloak.client.id';
public const KEYCLOAK_CLIENT_SECRET = 'keycloak.client.secret';
public const KEYCLOAK_REDIRECT_URI = 'keycloak.client.uri';

public const OAUTH2_NAME = 'oauth2.name';
public const OAUTH2_URL_AUTHORIZE = 'oauth2.url.authorize';
public const OAUTH2_URL_TOKEN = 'oauth2.url.token';
public const OAUTH2_URL_USERINFO = 'oauth2.url.userinfo';
public const OAUTH2_CLIENT_ID = 'oauth2.client.id';
public const OAUTH2_CLIENT_SECRET = 'oauth2.client.secret';
public const OAUTH2_REDIRECT_URI = 'oauth2.client.uri';

public const YEARS_OLD_DATA = 'years.old.data';
public const DELETE_OLD_ANNOUNCEMENTS = 'delete.old.announcements';
public const DELETE_OLD_BLACKOUTS = 'delete.old.blackouts';
Expand Down
4 changes: 4 additions & 0 deletions tests/Presenters/LoginPresenterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,10 @@ public function SetFacebookUrl($URL) {}

public function SetKeycloakUrl($URL) {}

public function SetOauth2Url($URL) {}

public function SetOauth2Name($Name) {}

public function PageLoad()
{
$this->_PageLoadWasCalled = true;
Expand Down
4 changes: 4 additions & 0 deletions tpl/login.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,10 @@
<a type="button" href="{$KeycloakUrl}" class="btn btn-outline-primary">{translate key='SignInWith'}<span class="fw-medium">
Keycloak</span></a>
{/if}
{if $AllowOauth2Login}
<a type="button" href="{$Oauth2Url}" class="btn btn-outline-primary">{translate key='SignInWith'}<span class="fw-medium">
{$Oauth2Name}</span></a>
{/if}
</section>
{if $facebookError}
<p class="text-center my-3">
Expand Down