blob: 2aeff0184e4db08d6446e1c0681fbd190ab222a3 [file] [log] [blame]
<?php
namespace Keycloak;
use CommonUtilities;
use Exception;
use Illuminate\Routing\UrlGenerator;
use Illuminate\Support\Facades\Config;
use Keycloak\API\RoleMapper;
use Keycloak\API\Roles;
use Keycloak\API\Users;
use Log;
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;
private $custos_credentials_uri;
// 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, $custos_credentials_uri)
{
$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->custos_credentials_uri = $custos_credentials_uri;
$this->role_mapper = new RoleMapper($openid_connect_discovery_url, $base_endpoint_url, $admin_username, $admin_password, $verify_peer, $this->cafile_path, $this->client_id, $this->client_secret, $this->custos_credentials_uri);
$this->roles = new Roles($openid_connect_discovery_url, $base_endpoint_url, $admin_username, $admin_password, $verify_peer, $this->cafile_path, $this->client_id, $this->client_secret, $this->custos_credentials_uri);
$this->users = new Users($openid_connect_discovery_url, $base_endpoint_url, $admin_username, $admin_password, $verify_peer, $this->cafile_path, $this->client_id, $this->client_secret, $this->custos_credentials_uri);
}
/**
* Function to authenticate user
*
* @param string $username
* @param string $password
* @return boolean
* @throws Exception
*/
public function authenticate($username, $password)
{
Log::info("Calling authenticate ", array($username));
$config = KeycloakUtil::getOpenIDConnectDiscoveryConfiguration($this->openid_connect_discovery_url, $this->client_id, $this->client_secret);
$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);
}
$auth_credentials = KeycloakUtil::getAuthCredentials($this->custos_credentials_uri, $this->client_id, $this->client_secret);
$iam_secret = $auth_credentials->iam_client_secret;
// Add client ID and client secret to the headers.
curl_setopt($r, CURLOPT_HTTPHEADER, array(
"Authorization: Basic " . base64_encode($this->client_id . ":" . $iam_secret),
));
// Assemble POST parameters for the request.
$post_fields = "client_id=" . urlencode($this->client_id) . "&client_secret=" . urlencode($iam_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)
{
Log::info("Calling getOAuthRequestCodeUrl ", array($extra_params));
$config = KeycloakUtil::getOpenIDConnectDiscoveryConfiguration($this->openid_connect_discovery_url, $this->client_id, $this->client_secret);
$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 = KeycloakUtil::getOpenIDConnectDiscoveryConfiguration($this->openid_connect_discovery_url, $this->client_id, $this->client_secret);
$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);
}
$auth_credentials = KeycloakUtil::getAuthCredentials($this->custos_credentials_uri, $this->client_id, $this->client_secret);
$iam_secret = $auth_credentials->iam_client_secret;
// Add client ID and client secret to the headers.
curl_setopt($r, CURLOPT_HTTPHEADER, array(
"Authorization: Basic " . base64_encode($this->client_id . ":" . $iam_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 = KeycloakUtil::getOpenIDConnectDiscoveryConfiguration($this->openid_connect_discovery_url, $this->client_id, $this->client_secret);
$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);
$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($username);
$roles = [];
foreach ($role_mappings as $role_mapping) {
$roles[] = $role_mapping;
}
$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 = KeycloakUtil::getOpenIDConnectDiscoveryConfiguration($this->openid_connect_discovery_url, $this->client_id, $this->client_secret);
$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);
}
$auth_credentials = KeycloakUtil::getAuthCredentials($this->custos_credentials_uri, $this->client_id, $this->client_secret);
$iam_secret = $auth_credentials->iam_client_secret;
// Add client ID and client secret to the headers.
curl_setopt($r, CURLOPT_HTTPHEADER, array(
"Authorization: Basic " . base64_encode($this->client_id . ":" . $iam_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 = KeycloakUtil::getOpenIDConnectDiscoveryConfiguration($this->openid_connect_discovery_url, $this->client_id, $this->client_secret);
$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->first_name, "lastName" => $user->last_name, "email" => $user->email, "userEnabled" => ($user->state === "ACTIVE"), "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->first_name, "lastName" => $user->last_name, "email" => $user->email, "userEnabled" => ($user->state === "ACTIVE"), "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
// Get the user's realm roles, then convert to an array of just names
$roles = $this->role_mapper->getRealmRoleMappingsForUser($username);
$role_names = [];
foreach ($roles as $role) {
$role_names[] = $role;
}
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
// 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, $username, 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, $username, 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->first_name;
$result["lastname"] = $user->last_name;
$result["userEnabled"] = ($user->state === "ACTIVE");
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->openid_connect_discovery_url, $this->custos_credentials_uri,
$this->admin_username, $this->admin_password, $this->verify_peer, $this->cafile_path,
$this->client_id, $this->client_secret);
$authzToken = new \Airavata\Model\Security\AuthzToken();
$authzToken->accessToken = $access_token;
$authzToken->claimsMap['gatewayID'] = $this->gateway_id;
$authzToken->claimsMap['userName'] = $this->admin_username;
$authzToken->claimsMap['custosId'] = $this->client_id;
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");
}
}
}