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
57 changes: 37 additions & 20 deletions Pages/LoginPage.php
Original file line number Diff line number Diff line change
Expand Up @@ -94,19 +94,24 @@ public function GetCaptcha();
public function SetAnnouncements($announcements);

/**
*
*
*/
public function SetGoogleUrl($URL);

/**
*
*
*/
public function SetMicrosoftUrl($URL);

/**
*
*
*/
public function SetFacebookUrl($URL);

/**
*
*/
public function SetKeycloakUrl($URL);
}

class LoginPage extends Page implements ILoginPage
Expand All @@ -119,7 +124,7 @@ public function __construct()

$this->presenter = new LoginPresenter($this); // $this pseudo variable of class object is Page object
$resumeUrl = $this->server->GetQuerystring(QueryStringKeys::REDIRECT);
if($resumeUrl !== NULL) $resumeUrl = str_replace('&&', '&', $resumeUrl);
if ($resumeUrl !== NULL) $resumeUrl = str_replace('&&', '&', $resumeUrl);
$this->Set('ResumeUrl', $resumeUrl);
$this->Set('ShowLoginError', false);
$this->Set('Languages', Resources::GetInstance()->AvailableLanguages);
Expand All @@ -128,11 +133,12 @@ public function __construct()
$this->Set('AllowFacebookLogin', Configuration::Instance()->GetSectionKey(ConfigSection::AUTHENTICATION, ConfigKeys::AUTHENTICATION_ALLOW_FACEBOOK, new BooleanConverter()));
$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()));
$scriptUrl = Configuration::Instance()->GetScriptUrl();
$parts = explode('://', $scriptUrl);
$this->Set('Protocol', $parts[0]);
if (isset($parts[1])) {
$this->Set('ScriptUrlNoProtocol', $parts[1]);
$this->Set('ScriptUrlNoProtocol', $parts[1]);
}
$this->Set('GoogleState', strtr(base64_encode("resume=$scriptUrl/external-auth.php%3Ftype%3Dgoogle%26redirect%3D$resumeUrl"), '+/=', '-_,'));
$this->Set('EnableCaptcha', Configuration::Instance()->GetSectionKey(ConfigSection::AUTHENTICATION, ConfigKeys::AUTHENTICATION_CAPTCHA_ON_LOGIN, new BooleanConverter()));
Expand Down Expand Up @@ -299,41 +305,52 @@ public function SetAnnouncements($announcements)
}

/**
* Sends the created google url in the presenter to the smarty page
* Sends the created google url in the presenter to the smarty page
*/
public function SetGoogleUrl($googleUrl){
if(Configuration::Instance()->GetSectionKey(ConfigSection::AUTHENTICATION, ConfigKeys::AUTHENTICATION_ALLOW_GOOGLE, new BooleanConverter())){
$this->Set('GoogleUrl',$googleUrl);
public function SetGoogleUrl($googleUrl)
{
if (Configuration::Instance()->GetSectionKey(ConfigSection::AUTHENTICATION, ConfigKeys::AUTHENTICATION_ALLOW_GOOGLE, new BooleanConverter())) {
$this->Set('GoogleUrl', $googleUrl);
}
}

/**
* Sends the created microsoft url in the presenter to the smarty page
* Sends the created microsoft url in the presenter to the smarty page
*/
public function SetMicrosoftUrl($microsoftUrl){
if(Configuration::Instance()->GetSectionKey(ConfigSection::AUTHENTICATION, ConfigKeys::AUTHENTICATION_ALLOW_MICROSOFT, new BooleanConverter())){
$this->Set('MicrosoftUrl',$microsoftUrl);
public function SetMicrosoftUrl($microsoftUrl)
{
if (Configuration::Instance()->GetSectionKey(ConfigSection::AUTHENTICATION, ConfigKeys::AUTHENTICATION_ALLOW_MICROSOFT, new BooleanConverter())) {
$this->Set('MicrosoftUrl', $microsoftUrl);
}
}

/**
* Sends the created facebook url in the presenter to the smarty page
* Sends the created facebook url in the presenter to the smarty page
*/
public function SetFacebookUrl($FacebookUrl){
if(Configuration::Instance()->GetSectionKey(ConfigSection::AUTHENTICATION, ConfigKeys::AUTHENTICATION_ALLOW_FACEBOOK, new BooleanConverter())){
$this->Set('FacebookUrl',$FacebookUrl);
public function SetFacebookUrl($FacebookUrl)
{
if (Configuration::Instance()->GetSectionKey(ConfigSection::AUTHENTICATION, ConfigKeys::AUTHENTICATION_ALLOW_FACEBOOK, new BooleanConverter())) {
$this->Set('FacebookUrl', $FacebookUrl);
}
}

/**
* Temporary solution for facebook auth SDK error
* Temporary solution for facebook auth SDK error
* After facebook failed authentication user is redirected to login page (this one) and is shown a message to try again
* Error occurs rarely (FacebookSDKException)
*/
private function SetFacebookErrorMessage(){
private function SetFacebookErrorMessage()
{
if (isset($_SESSION['facebook_error']) && $_SESSION['facebook_error'] == true) {
$this->Set('facebookError',$_SESSION['facebook_error']);
$this->Set('facebookError', $_SESSION['facebook_error']);
unset($_SESSION['facebook_error']);
}
}

public function SetKeycloakUrl($KeycloakUrl)
{
if (Configuration::Instance()->GetSectionKey(ConfigSection::AUTHENTICATION, ConfigKeys::AUTHENTICATION_ALLOW_KEYCLOAK, new BooleanConverter())) {
$this->Set('KeycloakUrl', $KeycloakUrl);
}
}
}
155 changes: 116 additions & 39 deletions Presenters/Authentication/ExternalAuthLoginPresenter.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ public function PageLoad()
if ($this->page->GetType() == 'microsoft') {
$this->ProcessMicrosoftSingleSignOn();
}
if ($this->page->GetType() == 'keycloak') {
$this->ProcessKeycloakSingleSignOn();
}
}

/**
Expand All @@ -54,32 +57,32 @@ private function ProcessGoogleSingleSignOn()
$token = $client->fetchAccessTokenWithAuthCode($_GET['code']);
//set the access token that it received
$client->setAccessToken($token['access_token']);
//Using the Google API to get the user information

//Using the Google API to get the user information
$google_oauth = new Google\Service\Oauth2($client);
$google_account_info = $google_oauth->userinfo->get();

//Save the informations needed to authenticate the login
$email = $google_account_info->email;
$firstName = $google_account_info->given_name;
$lastName = $google_account_info->family_name;

//Process $userData as needed (e.g., create a user, log in, etc.)
$this->processUserData($email,$email,$firstName,$lastName);
$this->processUserData($email, $email, $firstName, $lastName);
}
}

/**
* Exchanges the code given by microsoft in _GET for a token and with said token retrieves client data
*/
private function ProcessMicrosoftSingleSignOn()
{
{
if (isset($_GET['code'])) {
$code = filter_input(INPUT_GET, 'code');

$tokenEndpoint = 'https://login.microsoftonline.com/'
.urlencode(Configuration::Instance()->GetSectionKey(ConfigSection::AUTHENTICATION, ConfigKeys::MICROSOFT_TENANT_ID))
.'/oauth2/v2.0/token';
. urlencode(Configuration::Instance()->GetSectionKey(ConfigSection::AUTHENTICATION, ConfigKeys::MICROSOFT_TENANT_ID))
. '/oauth2/v2.0/token';

$postData = [
'client_id' => Configuration::Instance()->GetSectionKey(ConfigSection::AUTHENTICATION, ConfigKeys::MICROSOFT_CLIENT_ID),
Expand All @@ -95,13 +98,13 @@ private function ProcessMicrosoftSingleSignOn()
$response = $client->post($tokenEndpoint, [
'form_params' => $postData,
]);

// Decode the JSON response
$tokenData = json_decode($response->getBody(), true);

// Extract the access token from the response
$accessToken = $tokenData['access_token'];

//Get user information
$graphApiEndpoint = 'https://graph.microsoft.com/v1.0/me';

Expand All @@ -111,80 +114,154 @@ private function ProcessMicrosoftSingleSignOn()
'Authorization' => 'Bearer ' . $accessToken,
],
]);

// Decode the JSON response
$userData = json_decode($response->getBody(), true);

// Handle the user data as needed
$email = $userData['mail'];
$firstName = $userData['givenName'];;
$lastName = $userData['surname'];

//Process $userData as needed (e.g., create a user, log in, etc.)
$this->processUserData($email,$email,$firstName,$lastName);
$this->processUserData($email, $email, $firstName, $lastName);
}

}

/**
* Gets token created in facebook-auth.php and exchanges it for the client data
* Unlike the other two (microsoft and google) the token must be obtained directly in the redirect uri, therefore can't be sent here for exchange(?)
*/
private function ProcessFacebookSingleSignOn(){

private function ProcessFacebookSingleSignOn()
{

$facebook_Client = new Facebook\Facebook([
'app_id' => Configuration::Instance()->GetSectionKey(ConfigSection::AUTHENTICATION, ConfigKeys::FACEBOOK_CLIENT_ID),
'app_secret' => Configuration::Instance()->GetSectionKey(ConfigSection::AUTHENTICATION, ConfigKeys::FACEBOOK_CLIENT_SECRET),
'default_graph_version' => 'v2.5'
]);

if (isset($_SESSION['facebook_access_token'])) {
$facebook_Client->setDefaultAccessToken(unserialize($_SESSION['facebook_access_token']));
}
unset($_SESSION['facebook_access_token']);

$profile_request = $facebook_Client ->get('/me?fields=name,first_name,last_name,email');
$profile = $profile_request ->getGraphUser();
$profile_request = $facebook_Client->get('/me?fields=name,first_name,last_name,email');
$profile = $profile_request->getGraphUser();

$email = $profile->getField('email');
$firstName = $profile->getField('first_name');
$lastName = $profile->getField('last_name');

//Process $userData as needed (e.g., create a user, log in, etc.)
$this->processUserData($email,$email,$firstName,$lastName);

$this->processUserData($email, $email, $firstName, $lastName);
}

private function ProcessKeycloakSingleSignOn()
{
$code = $_GET['code'];

$keycloakUrl = Configuration::Instance()->GetSectionKey(ConfigSection::AUTHENTICATION, ConfigKeys::KEYCLOAK_URL);
$realm = Configuration::Instance()->GetSectionKey(ConfigSection::AUTHENTICATION, ConfigKeys::KEYCLOAK_REALM);
$clientId = Configuration::Instance()->GetSectionKey(ConfigSection::AUTHENTICATION, ConfigKeys::KEYCLOAK_CLIENT_ID);
$clientSecret = Configuration::Instance()->GetSectionKey(ConfigSection::AUTHENTICATION, ConfigKeys::KEYCLOAK_CLIENT_SECRET);
$redirectUri = rtrim(Configuration::Instance()->GetScriptUrl(), 'Web/') . Configuration::Instance()->GetSectionKey(ConfigSection::AUTHENTICATION, ConfigKeys::KEYCLOAK_REDIRECT_URI);

$tokenEndpoint = rtrim($keycloakUrl, '/') . '/realms/' . urlencode($realm) . '/protocol/openid-connect/token';

// 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($tokenEndpoint, [
'form_params' => $postData,
]);
} catch (\Exception $e) {
$this->page->ShowError(['Error retrieving Keycloak token: ' . $e->getMessage()]);
return;
}

$tokenData = json_decode($response->getBody(), true);
if (!isset($tokenData['access_token'])) {
$this->page->ShowError(['Access token not found in Keycloak response']);
return;
}
$accessToken = $tokenData['access_token'];

// Build the userinfo endpoint URL (again, without '/auth').
$userInfoEndpoint = rtrim($keycloakUrl, '/') . '/realms/' . urlencode($realm) . '/protocol/openid-connect/userinfo';

try {
$userResponse = $client->get($userInfoEndpoint, [
'headers' => [
'Authorization' => 'Bearer ' . $accessToken,
],
]);
} catch (\Exception $e) {
$this->page->ShowError(['Error retrieving Keycloak 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 Keycloak 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
*/
private function processUserData($username,$email,$firstName,$lastName){
private function processUserData($username, $email, $firstName, $lastName, $phone = null, $organization = null, $title = null)
{
$requiredDomainValidator = new RequiredEmailDomainValidator($email);
$requiredDomainValidator->Validate();
if (!$requiredDomainValidator->IsValid()) {
$this->page->ShowError(array(Resources::GetInstance()->GetString('InvalidEmailDomain')));
return;
}
if($this->registration->UserExists($username,$email)){
if ($this->registration->UserExists($username, $email)) {
$this->authentication->Login($email, new WebLoginContext(new LoginData()));
LoginRedirector::Redirect($this->page);
}
else{
$this->registration->Synchronize(new AuthenticatedUser(
$username,
$email,
$firstName,
$lastName,
Password::GenerateRandom(),
Resources::GetInstance()->CurrentLanguage,
Configuration::Instance()->GetDefaultTimezone(),
null,
null,
null),
} else {
$this->registration->Synchronize(
new AuthenticatedUser(
$username,
$email,
$firstName,
$lastName,
Password::GenerateRandom(),
Resources::GetInstance()->CurrentLanguage,
Configuration::Instance()->GetDefaultTimezone(),
$phone,
$organization,
$title
),
false,
false);
false
);
$this->authentication->Login($email, new WebLoginContext(new LoginData()));
LoginRedirector::Redirect($this->page);
}
}
}
}
Loading