diff --git a/www/aselect/handler.php b/www/aselect/handler.php new file mode 100644 index 0000000000000000000000000000000000000000..3c4d0262364b0855069a97729708d0234b396bfc --- /dev/null +++ b/www/aselect/handler.php @@ -0,0 +1,283 @@ +<?php + +// Work-in-Progress: +// +// This code can be used: +// a. to hook up an A-Select Agent or a "local" A-Select server +// (=SP) and bridge to a SAML 2.0 IdP, +// or +// b. as a bridge from simpleSAMLphp local/bridge "auth" to a +// "remote" A-Select server. +// +// Connect A-Select Agent: +// configure the agent.xml as follows (example): +// <aselect-server-id>default.aselect.org</aselect-server-id> +// <url>https://localhost/simplesaml/aselect/handler.php</url> +// +// Connect "local" A-Select Server: +// configure simpleSAMLphp as a "remote" aselect server as follows (example): +// <organization id="simplSAMLphp" +// server="localhost" +// friendly_name="simpleSAMLphp (TEST)" +// resourcegroup="remote_simplsamlphp_resources" /> +// +// <resourcegroup id="remote_simplsamlphp_resources" +// interval="30"> +// <resource id="simpleSAMLphp1"> +// <url>https://localhost/simplesaml/aselect/handler.php</url> +// </resource> +// </resourcegroup> +// +// Bridge to "remote" A-Select Server: +// configure simpleSAMLphp as a "local" aselect server as follows (example): +// <organization id="simplSAMLphp" server="localhost"> +// <level>1</level> +// <forced_authenticate>false</forced_authenticate> +// <attribute_policy>policyA</attribute_policy> +// </organization> +// +// set the "auth" handler in the idp-hosted metadata as follows: +// 'auth' => 'aselect/handler.php?request=bridge', +// +// TODO: +// - extend config possibilities and move it to config/config.php +// and metadata/aselect-sp-hosted.php +// - add robustness/error-handling/error-reporting +// - generic bridging +// - check the crypto related stuff (is encrypting rid enough?) +// - more checks on parameters (really needed?) + +require_once('../../www/_include.php'); +require_once('SimpleSAML/Logger.php'); +require_once('SimpleSAML/Configuration.php'); +require_once('SimpleSAML/Metadata/MetaDataStorageHandler.php'); + +$logger = new SimpleSAML_Logger(); +$config = SimpleSAML_Configuration::getInstance(); +$metadata = SimpleSAML_Metadata_MetaDataStorageHandler::getMetadataHandler(); + +$as_config = array( + 'server_id' => 'default.aselect.org', + 'organization_id' => 'simplSAMLphp', + 'authsp_level' => '10', + 'authsp' => 'simplSAMLphp', + 'app_level' => '10', + 'tgt_exp_time' => '1194590521000', + 'metadata' => $metadata->getMetaData('localhost', 'saml20-sp-hosted'), + 'saml20' => array( + 'sp_url_sso' => '/' . $config->getValue('baseurlpath') . '/saml2/sp/initSSO.php', + 'sp_url_slo' => '/' . $config->getValue('baseurlpath') . '/saml2/sp/initSLO.php', + ), + 'logout_url' => '/' . $config->getValue('baseurlpath') . 'logout.html', + 'remote_server_id' => 'default.aselect.org', + 'remote_organization_id' => 'testorg', + 'remote_server_url' => 'https://localhost/aselectserver/server' +); + +if ($_GET['local_rid']) session_id($_GET['local_rid']); else if ($_GET['rid']) session_id($_GET['rid']); + +session_start(); + +// handle authenticate request from an agent or a local server +function as_request_authenticate() { + global $as_config; + $_SESSION['return_url'] = array_key_exists('app_url', $_GET) ? $_GET['app_url'] : $_GET['local_as_url']; + $_SESSION['app_id'] = $_GET['app_id']; + print 'result_code=0000' . + '&a-select-server=' . $as_config['server_id'] . + '&rid=' . session_id() . + '&as_url=' . $_SERVER['PHP_SELF'] . '?request=login'; +} + +// handle browser login redirect from agent or local server +function as_request_login() { + global $as_config; + $return_url = $_SERVER['PHP_SELF'] . '?request=return'; + header('Location: ' . + $as_config['saml20']['sp_url_sso'] . + '?RelayState=' . $return_url); +} + +// handle browser return redirect from bridged IDP (SAML 2.0 for now) +function as_request_return() { + global $as_config, $config; + $rid = session_id(); + $publickey = $config->getBaseDir() . '/cert/' . $as_config['metadata']['certificate']; + if (!file_exists($publickey)) { + throw new Exception('Could not find public key file [' . $publickey . '] which is needed to encrypt the credentials.'); + } + $xmlseckey = new XMLSecurityKey(XMLSecurityKey::RSA_SHA1, array('type'=>'public')); + $xmlseckey->loadKey($publickey,TRUE); + //TODO; why does xmlseclibs not work!? + //$credentials = $xmlseckey->encryptData($rid); + if (openssl_public_encrypt($rid, $credentials, $xmlseckey->key) == FALSE) { + $logger->log(LOG_INFO, '1', 'aselect', 'handler', 'request', 'access', 'Could not encrypt aselect_credentials: ' . $rid . ' : ' . openssl_error_string()); + throw new Exception("Could not encrypt credentials!"); + } + $redirect = $_SESSION['return_url']; + if (!strrchr($redirect, '?')) { + $redirect .= '?'; + } else { + $redirect .= '&'; + } + $redirect .= + 'rid=' . $rid . + '&a-select-server=' . $as_config['server_id'] . + '&aselect_credentials=' . urlencode(base64_encode($credentials)); + header('Location: ' . $redirect); +} + +// handle verify credentials request from agent or local server +function as_request_verify_credentials() { + global $as_config, $config, $logger; + // NB: accomodate for weird a-select behaviour: agent and local a-select server pass in credentials in different ways... + $credentials = array_key_exists('local_organization', $_GET) ? base64_decode(urldecode($_GET['aselect_credentials'])) : base64_decode($_GET['aselect_credentials']); + $privatekey = $config->getBaseDir() . '/cert/' . $as_config['metadata']['privatekey']; + if (!file_exists($privatekey)) { + throw new Exception('Could not find private key file [' . $privatekey . '] which is needed to verify the credentials.'); + } + $xmlseckey = new XMLSecurityKey(XMLSecurityKey::RSA_SHA1, array('type'=>'private')); + $xmlseckey->loadKey($privatekey,TRUE); + //TODO; why does xmlseclibs not work!? + //$decrypted = $xmlseckey->decryptData($credentials); + if (openssl_private_decrypt($credentials, $decrypted, $xmlseckey->key) == FALSE) { + $logger->log(LOG_INFO, '1', 'aselect', 'handler', 'request', 'access', 'Could not decrypt aselect_credentials: ' . $credentials . ' : ' . openssl_error_string()); + throw new Exception("Could not decrypt credentials!"); + } + if ($decrypted != session_id()) { + $logger->log(LOG_INFO, '1', 'aselect', 'handler', 'request', 'access', 'Credentials incorrect or tampered with: ' . $decrypted . ' != ' . session_id()); + throw new Exception("Incorrect credentials!"); + } + $session = SimpleSAML_Session::getInstance(); + $serialized = ''; + foreach ($session->getAttributes() as $name => $values) { + foreach ($values as $value) { + if ($serialized != '') $serialized .= '&'; + $serialized .= urlencode($name) . '=' . urlencode($value); + } + } + print 'result_code=0000' . + '&app_id=' . $_SESSION['app_id'] . + '&uid=' . $session->getNameID() . + '&organization=' . $as_config['organization_id'] . + '&authsp_level=' . $as_config['authsp_level'] . + '&authsp=' . $as_config['authsp'] . + '&app_level=' . $as_config['app_level'] . + '&a-select-server=' . $as_config['server_id'] . + '&tgt_exp_time=' . $as_config['tgt_exp_time']; + if ($serialized != '') { + print '&attributes=' . base64_encode($serialized); + } +} + +// handle browser logout redirect from agent or local server +function as_request_logout() { + global $as_config; + header('Location: ' . + $as_config['saml20']['sp_url_slo'] . + '?RelayState=' . $as_config['logout_url']); +} + +// helper function for sending a non-browser request to a remote server +function as_call($url) { + global $logger; + $ch = curl_init(); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($ch, CURLOPT_URL, $url); + $result = curl_exec($ch); + $error = curl_error($ch); + curl_close($ch); + if ($result == FALSE) { + $logger->log(LOG_INFO, '1', 'aselect', 'handler', 'request', 'access', 'Request on remote server failed [' . $url . '] : ' . $error); + throw new Exception('Request on remote server failed: ' . $error); + } + $parms = array(); + foreach (explode('&', $result) as $p) { + $a = explode('=', $p); + $parms[$a[0]] = urldecode($a[1]); + } + if ($parms['result_code'] != '0000') { + $logger->log(LOG_INFO, '1', 'aselect', 'handler', 'request', 'access', 'Request on remote server returned error: ' . $result); + throw new Exception('Request on remote server returned error: ' . $result); + } + return $parms; +} + +// handle bridged authentication request from simpleSAMLphp to remote server +function as_request_bridge() { + global $as_config, $logger; + // perform authenticate + $_SESSION['relaystate'] = $_GET['RelayState']; + $url = $as_config['remote_server_url'] . + '?request=authenticate' . + '&required_level=' . $as_config['app_level'] . + '&local_organization=' . $as_config['organization_id'] . + '&a-select-server=' . $as_config['remote_server_id'] . + '&local_as_url=' . urlencode($_SERVER['PHP_SELF'] . + '?request=bridge_return' . + '&local_rid=' . session_id()); + $parms = as_call($url); + header('Location: ' . + $parms['as_url'] . + '&rid=' . $parms['rid'] . + '&a-select-server=' . $as_config['remote_server_id']); +} + +// handle browser return redirect from (bridged) remote server +function as_request_bridge_return() { + global $as_config, $logger; + if ((array_key_exists('aselect_credentials', $_GET) == FALSE) + || + (array_key_exists('rid', $_GET) == FALSE)) + { + $logger->log(LOG_INFO, '1', 'aselect', 'handler', 'request', 'access', 'Error on return from login at remote server!'); + throw new Exception('Error on return from login at remote server!'); + } + $url = $as_config['remote_server_url'] . + '?request=verify_credentials' . + '&rid=' . $_GET['rid'] . + '&local_organization=' . $as_config['organization_id'] . + '&a-select-server=' . $as_config['remote_server_id'] . + '&aselect_credentials=' . $_GET['aselect_credentials']; + $parms = as_call($url); + + SimpleSAML_Session::init('aselect', $_GET['rid'], true); + $session = SimpleSAML_Session::getInstance(); + + if (array_key_exists('attributes', $parms)) { + $parm = base64_decode($parms['attributes']); + $attributes = array(); + foreach (explode('&', $parm) as $p) { + $a = explode('=', $p); + if (array_key_exists($a[0], $attributes)) { + $attributes[$a[0]] = array(); + } + $attributes[$a[0]][] = urldecode($a[1]); + } + $session->setAttributes($attributes); + } + #$session->setNameID('test'); + header('Location: ' . $_SESSION['relaystate']); +} + +// demultiplex incoming request +try { + $logger->log(LOG_INFO, '1', 'aselect', 'handler', 'request', 'access', $_SERVER['REQUEST_URI']); + if ($_GET['request']) { + $handler = 'as_request_' . $_GET['request']; + $handler(); + } else { + // no request: + // a. present status page with logout button posting to "request=logout"" + // (mimic a-select behaviour), + print '<form method="post" action="' . $_SERVER['PHP_SELF'] . '?request=logout' . '"><input type="submit" value="Logout"></form>'; + // or + // b. assume that an empty request will always mean "logout" + // (potential problem when users accidentally browse here?) + } +} catch (Exception $e) { + print 'result_code=0001&error=' . urlencode($e); +} + +?>