app/libraries/Keycloak/Keycloak.php (293 lines of code) (raw):

<?php namespace Keycloak; use Keycloak\API\RoleMapper; use Keycloak\API\Roles; use Keycloak\API\Users; use Keycloak\KeycloakUtil; use CommonUtilities; use Exception; use Log; use Illuminate\Routing\UrlGenerator; use Illuminate\Support\Facades\Config; class Keycloak { private $realm; private $openid_connect_discovery_url; private $client_id; private $client_secret; private $callback_url; private $cafile_path; private $verify_peer; private $base_endpoint_url; private $admin_username; private $admin_password; private $gateway_id; // API clients private $role_mapper; private $roles; private $users; /** * Constructor * */ public function __construct($realm, $openid_connect_discovery_url, $client_id, $client_secret, $callback_url, $cafile_path, $verify_peer, $base_endpoint_url, $admin_username, $admin_password, $gateway_id) { $this->realm = $realm; $this->openid_connect_discovery_url = $openid_connect_discovery_url; $this->client_id = $client_id; $this->client_secret = $client_secret; $this->callback_url = $callback_url; $this->cafile_path = $cafile_path; $this->verify_peer = $verify_peer; $this->base_endpoint_url = $base_endpoint_url; $this->admin_username = $admin_username; $this->admin_password = $admin_password; $this->gateway_id = $gateway_id; $this->role_mapper = new RoleMapper($base_endpoint_url, $admin_username, $admin_password, $verify_peer, $this->cafile_path); $this->roles = new Roles($base_endpoint_url, $admin_username, $admin_password, $verify_peer, $this->cafile_path); $this->users = new Users($base_endpoint_url, $admin_username, $admin_password, $verify_peer, $this->cafile_path); } /** * Function to authenticate user * * @param string $username * @param string $password * @return boolean * @throws Exception */ public function authenticate($username, $password){ $config = $this->getOpenIDConnectDiscoveryConfiguration(); $token_endpoint = $config->token_endpoint; // Init cUrl. $r = curl_init($token_endpoint); curl_setopt($r, CURLOPT_RETURNTRANSFER, 1); // Decode compressed responses. curl_setopt($r, CURLOPT_ENCODING, 1); curl_setopt($r, CURLOPT_SSL_VERIFYPEER, $this->verify_peer); if($this->verify_peer && $this->cafile_path){ curl_setopt($r, CURLOPT_CAINFO, $this->cafile_path); } // Add client ID and client secret to the headers. curl_setopt($r, CURLOPT_HTTPHEADER, array( "Authorization: Basic " . base64_encode($this->client_id . ":" . $this->client_secret), )); // Assemble POST parameters for the request. $post_fields = "client_id=" . urlencode($this->client_id) . "&client_secret=" . urlencode($this->client_secret) . "&grant_type=password"; $post_fields .= "&username=" . urlencode($username) . "&password=" . urlencode($password); // Obtain and return the access token from the response. curl_setopt($r, CURLOPT_POST, true); curl_setopt($r, CURLOPT_POSTFIELDS, $post_fields); $response = curl_exec($r); if ($response == false) { die("curl_exec() failed. Error: " . curl_error($r)); } //Parse JSON return object. $result = json_decode($response); // Log::debug("password grant type authenciation response", array($result)); return $result; } public function getOAuthRequestCodeUrl($extra_params=null){ $config = $this->getOpenIDConnectDiscoveryConfiguration(); $authorization_endpoint = $config->authorization_endpoint; // TODO: add state variable to request and put into session $url = $authorization_endpoint . '?response_type=code&client_id=' . urlencode($this->client_id) . '&redirect_uri=' . urlencode($this->callback_url) . '&scope=openid'; if ($extra_params != null) { $url = $url . '&' . $extra_params; } return $url; } public function getOAuthToken($code){ $config = $this->getOpenIDConnectDiscoveryConfiguration(); $token_endpoint = $config->token_endpoint; // Init cUrl. $r = curl_init($token_endpoint); curl_setopt($r, CURLOPT_RETURNTRANSFER, 1); // Decode compressed responses. curl_setopt($r, CURLOPT_ENCODING, 1); curl_setopt($r, CURLOPT_SSL_VERIFYPEER, $this->verify_peer); if($this->verify_peer && $this->cafile_path){ curl_setopt($r, CURLOPT_CAINFO, $this->cafile_path); } // Add client ID and client secret to the headers. curl_setopt($r, CURLOPT_HTTPHEADER, array( "Authorization: Basic " . base64_encode($this->client_id . ":" . $this->client_secret), )); // Assemble POST parameters for the request. $post_fields = "code=" . urlencode($code) . "&grant_type=authorization_code&redirect_uri=" . urlencode($this->callback_url); // Obtain and return the access token from the response. curl_setopt($r, CURLOPT_POST, true); curl_setopt($r, CURLOPT_POSTFIELDS, $post_fields); $response = curl_exec($r); if ($response == false) { die("curl_exec() failed. Error: " . curl_error($r)); } //Parse JSON return object. $result = json_decode($response); Log::debug("getOAuthToken response", array($result)); return $result; } public function getUserProfileFromOAuthToken($token){ $config = $this->getOpenIDConnectDiscoveryConfiguration(); $userinfo_endpoint = $config->userinfo_endpoint; $r = curl_init($userinfo_endpoint); curl_setopt($r, CURLOPT_RETURNTRANSFER, 1); // Decode compressed responses. curl_setopt($r, CURLOPT_ENCODING, 1); curl_setopt($r, CURLOPT_SSL_VERIFYPEER, $this->verify_peer); if($this->verify_peer && $this->cafile_path){ curl_setopt($r, CURLOPT_CAINFO, $this->cafile_path); } curl_setopt($r, CURLOPT_HTTPHEADER, array( "Authorization: Bearer " . $token )); $response = curl_exec($r); if ($response == false) { die("curl_exec() failed. Error: " . curl_error($r)); } //Parse JSON return object. $userinfo = json_decode($response); Log::debug("Keycloak userinfo", array($userinfo)); $username = $userinfo->preferred_username; $firstname = $userinfo->given_name; $lastname = $userinfo->family_name; $email = $userinfo->email; // get roles from Keycloak API $role_mappings = $this->role_mapper->getRealmRoleMappingsForUser($this->realm, $userinfo->sub); $roles = []; foreach ($role_mappings as $role_mapping) { $roles[] = $role_mapping->name; } $roles = CommonUtilities::filterAiravataRoles($roles); return array('username'=>$username, 'firstname'=>$firstname, 'lastname'=>$lastname, 'email'=>$email, 'roles'=>$roles); } /** * Method to get refreshed access token * @param $refreshToken * @return mixed */ public function getRefreshedOAuthToken($refresh_token){ $config = $this->getOpenIDConnectDiscoveryConfiguration(); $token_endpoint = $config->token_endpoint; // Init cUrl. $r = curl_init($token_endpoint); curl_setopt($r, CURLOPT_RETURNTRANSFER, 1); // Decode compressed responses. curl_setopt($r, CURLOPT_ENCODING, 1); curl_setopt($r, CURLOPT_SSL_VERIFYPEER, $this->verify_peer); if($this->verify_peer && $this->cafile_path){ curl_setopt($r, CURLOPT_CAINFO, $this->cafile_path); } // Add client ID and client secret to the headers. curl_setopt($r, CURLOPT_HTTPHEADER, array( "Authorization: Basic " . base64_encode($this->client_id . ":" . $this->client_secret), )); // Assemble POST parameters for the request. $post_fields = "refresh_token=" . urlencode($refresh_token) . "&grant_type=refresh_token"; // Obtain and return the access token from the response. curl_setopt($r, CURLOPT_POST, true); curl_setopt($r, CURLOPT_POSTFIELDS, $post_fields); $response = curl_exec($r); if ($response == false) { die("curl_exec() failed. Error: " . curl_error($r)); } //Parse JSON return object. $result = json_decode($response); // Log::debug("getRefreshedOAuthToken response", array($result)); return $result; } /** * Function to get the OAuth logout url */ public function getOAuthLogoutUrl($redirect_uri) { $config = $this->getOpenIDConnectDiscoveryConfiguration(); $logout_endpoint = $config->end_session_endpoint; return $logout_endpoint . '?redirect_uri=' . rawurlencode($redirect_uri); } /** * Function to list users * * @return Array of usernames */ public function listUsers(){ $users = $this->users->getUsers($this->realm); $usernames = []; foreach ($users as $user) { Log::debug("user", array($user)); array_push($usernames, (object)["firstName"=>$user->firstName,"lastName"=>$user->lastName,"email"=>$user->email,"userEnabled"=>$user->enabled,"userName"=>$user->username]); } return $usernames; } /** * Function to search users * NOTE: Keycloak uses the keyword to search in the username, first and last * name and email address * @param $keyword * @return Array of usernames */ public function searchUsers($phrase){ $users = $this->users->searchUsers($this->realm, $phrase); $usernames = []; foreach ($users as $user) { array_push($usernames, (object)["firstName"=>$user->firstName,"lastName"=>$user->lastName,"email"=>$user->email,"userEnabled"=>$user->enabled,"userName"=>$user->username]); } return $usernames; } /** * Function to get the list of all existing roles * For Keycloak this is a list of "Realm roles" * * @return roles list */ public function getAllRoles(){ try { $roles = $this->roles->getRoles($this->realm); $role_names = []; foreach ($roles as $role) { $role_names[] = $role->name; } return CommonUtilities::filterAiravataRoles($role_names); } catch (Exception $ex) { throw new Exception("Unable to get all roles", 0, $ex); } } /** * Function to get roles of a user * For Keycloak this is a list of "Realm roles" * * @return array of role names */ public function getUserRoles( $username ){ try { // get userid from username $user_id = $this->getUserId($username); // Get the user's realm roles, then convert to an array of just names $roles = $this->role_mapper->getRealmRoleMappingsForUser($this->realm, $user_id); $role_names = []; foreach ($roles as $role) { $role_names[] = $role->name; } return CommonUtilities::filterAiravataRoles($role_names); } catch (Exception $ex) { throw new Exception("Unable to get User roles.", 0, $ex); } } /** * Function to update role list of user * * @param $username * @param $roles, an Array with two entries, "deleted" and "new", each of * which has a value of roles to be removed or added respectively * @return void */ public function updateUserRoles( $username, $roles){ // Log::debug("updateUserRoles", array($user_id, $roles)); try { // get userid from username $user_id = $this->getUserId($username); // Get all of the roles into an array keyed by role name $all_roles = $this->roles->getRoles($this->realm); $roles_by_name = []; foreach ($all_roles as $role) { $roles_by_name[$role->name] = $role; } // Process the role deletions if(isset($roles["deleted"])){ if(!is_array($roles["deleted"])) $roles["deleted"] = array($roles["deleted"]); foreach ($roles["deleted"] as $role) { $this->role_mapper->deleteRealmRoleMappingsToUser($this->realm, $user_id, array($roles_by_name[$role])); } } // Process the role additions if(isset($roles["new"])){ if(!is_array($roles["new"])) $roles["new"] = array($roles["new"]); foreach ($roles["new"] as $role) { $this->role_mapper->addRealmRoleMappingsToUser($this->realm, $user_id, array($roles_by_name[$role])); } } } catch (Exception $ex) { throw new Exception("Unable to update role of the user.", 0, $ex); } } /** * Function to get the user profile of a user * @param $username */ public function getUserProfile($username){ $user = $this->users->getUserByUsername($this->realm, $username); if($user != null){ $result = []; $result["email"] = $user->email; $result["firstname"] = $user->firstName; $result["lastname"] = $user->lastName; $result["userEnabled"] = $user->enabled; return $result; }else{ return []; } } /** * Function to check whether a user exists with the given userId * @param $username * @return bool */ public function usernameExists($username){ try{ $user = $this->users->getUserByUsername($this->realm, $username); return $user != null; }catch (Exception $ex){ // Username does not exists return false; } } // TODO: move this to IamAdminServices public function isUpdatePasswordRequired($username) { try{ $user = $this->users->getUserByUsername($this->realm, $username); if ($user != null) { return in_array("UPDATE_PASSWORD", $user->requiredActions); } else { return false; } }catch (Exception $ex){ // Username does not exists return false; } } public function getAdminAuthzToken() { $access_token = KeycloakUtil::getAPIAccessToken($this->base_endpoint_url, $this->realm, $this->admin_username, $this->admin_password, $this->verify_peer, $this->cafile_path); $authzToken = new \Airavata\Model\Security\AuthzToken(); $authzToken->accessToken = $access_token; $authzToken->claimsMap['gatewayID'] = $this->gateway_id; $authzToken->claimsMap['userName'] = $this->admin_username; return $authzToken; } /** * Get the user's Keycloak user_id from their username */ private function getUserId($username) { $user = $this->users->getUserByUsername($this->realm, $username); if ($user != null) { return $user->id; } else { throw new Exception("No user found with username $username"); } } private function getOpenIDConnectDiscoveryConfiguration() { // TODO: cache the result of the request $r = curl_init($this->openid_connect_discovery_url); curl_setopt($r, CURLOPT_RETURNTRANSFER, 1); // Decode compressed responses. curl_setopt($r, CURLOPT_ENCODING, 1); curl_setopt($r, CURLOPT_SSL_VERIFYPEER, $this->verify_peer); if($this->verify_peer && $this->cafile_path){ curl_setopt($r, CURLOPT_CAINFO, $this->cafile_path); } $result = curl_exec($r); if ($result == false) { die("curl_exec() failed. Error: " . curl_error($r)); } $json = json_decode($result); // Log::debug("openid connect discovery configuration", array($json)); return $json; } }