From 7787f08768ad18990b55c10d5864d61434cf03c0 Mon Sep 17 00:00:00 2001 From: Lorenzo Cattaneo Date: Fri, 20 Jul 2018 11:10:54 +0200 Subject: [PATCH 01/69] added base class wrapper files --- src/PhpSaml.php | 18 ++++++++++++++ src/PhpSamlInterface.php | 0 src/config/saml_config.php | 48 ++++++++++++++++++++++++++++++++++++ src/saml/PhpSamlOneLogin.php | 5 ++++ www3/index.php | 0 5 files changed, 71 insertions(+) create mode 100644 src/PhpSaml.php create mode 100644 src/PhpSamlInterface.php create mode 100644 src/config/saml_config.php create mode 100644 src/saml/PhpSamlOneLogin.php create mode 100644 www3/index.php diff --git a/src/PhpSaml.php b/src/PhpSaml.php new file mode 100644 index 0000000..5ee4fe9 --- /dev/null +++ b/src/PhpSaml.php @@ -0,0 +1,18 @@ +php_saml = new PhpSamlOneLogin($settings); + break; + default: + $this->php_saml = new PhpSamlOneLogin($settings); + break; + } + } + +} \ No newline at end of file diff --git a/src/PhpSamlInterface.php b/src/PhpSamlInterface.php new file mode 100644 index 0000000..e69de29 diff --git a/src/config/saml_config.php b/src/config/saml_config.php new file mode 100644 index 0000000..a91e5fe --- /dev/null +++ b/src/config/saml_config.php @@ -0,0 +1,48 @@ + false, + + // Enable debug mode (to print errors). + 'debug' => true, + + 'sp' => array ( + 'entityId' => $spBaseUrl . '/metadata.php', + 'assertionConsumerService' => array ( + 'url' => $spBaseUrl . '/index.php?acs', + ), + 'singleLogoutService' => array ( + 'url' => $spBaseUrl . '/index.php?sls', + ), + 'NameIDFormat' => 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient', + 'x509cert' => '{{sp_cert}}', + 'privateKey' => '{{sp_key}}', + ), + 'idp' => array ( + 'entityId' => $idpEntityId, + 'singleSignOnService' => array ( + 'url' => $idpSSO, + ), + 'singleLogoutService' => array ( + 'url' => $idpSLO, + ), + 'x509cert' => '{{idp_cert}}', + ), + 'security' => array ( + 'authnRequestsSigned' => true, + 'logoutRequestSigned' => true, + 'logoutResponseSigned' => true, + 'signMetadata' => true, + 'signatureAlgorithm' => 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256', + 'requestedAuthnContext' => array('https://www.spid.gov.it/SpidL1'), + ), + ); diff --git a/src/saml/PhpSamlOneLogin.php b/src/saml/PhpSamlOneLogin.php new file mode 100644 index 0000000..d22b697 --- /dev/null +++ b/src/saml/PhpSamlOneLogin.php @@ -0,0 +1,5 @@ + Date: Fri, 20 Jul 2018 12:07:49 +0200 Subject: [PATCH 02/69] added default config and strategy pattern --- src/PhpSaml.php | 4 +-- src/PhpSamlInterface.php | 0 ...ml_config.php => onelogin_saml_config.php} | 14 ++++++--- src/saml/PhpSamlOneLogin.php | 5 --- src/saml_strategy/PhpSamlInterface.php | 7 +++++ src/saml_strategy/PhpSamlOneLogin.php | 31 +++++++++++++++++++ www3/index.php | 11 +++++++ 7 files changed, 60 insertions(+), 12 deletions(-) delete mode 100644 src/PhpSamlInterface.php rename src/config/{saml_config.php => onelogin_saml_config.php} (85%) delete mode 100644 src/saml/PhpSamlOneLogin.php create mode 100644 src/saml_strategy/PhpSamlInterface.php create mode 100644 src/saml_strategy/PhpSamlOneLogin.php diff --git a/src/PhpSaml.php b/src/PhpSaml.php index 5ee4fe9..6d5835b 100644 --- a/src/PhpSaml.php +++ b/src/PhpSaml.php @@ -4,8 +4,8 @@ class PhpSaml implements PhpSamlInterface { private $php_saml = null; - public function __construct($settings, $mode = 'onelogin') { - switch ($variable) { + public function __construct($settings = null, $mode = 'onelogin') { + switch ($mode) { case 'onelogin': $this->php_saml = new PhpSamlOneLogin($settings); break; diff --git a/src/PhpSamlInterface.php b/src/PhpSamlInterface.php deleted file mode 100644 index e69de29..0000000 diff --git a/src/config/saml_config.php b/src/config/onelogin_saml_config.php similarity index 85% rename from src/config/saml_config.php rename to src/config/onelogin_saml_config.php index a91e5fe..bbd1bbd 100644 --- a/src/config/saml_config.php +++ b/src/config/onelogin_saml_config.php @@ -1,11 +1,15 @@ $spBaseUrl . '/index.php?sls', ), 'NameIDFormat' => 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient', - 'x509cert' => '{{sp_cert}}', - 'privateKey' => '{{sp_key}}', + 'x509cert' => $spCrtFile, + 'privateKey' => $spKeyFile, ), 'idp' => array ( 'entityId' => $idpEntityId, @@ -35,7 +39,7 @@ 'singleLogoutService' => array ( 'url' => $idpSLO, ), - 'x509cert' => '{{idp_cert}}', + 'x509cert' => $idpCertFile, ), 'security' => array ( 'authnRequestsSigned' => true, diff --git a/src/saml/PhpSamlOneLogin.php b/src/saml/PhpSamlOneLogin.php deleted file mode 100644 index d22b697..0000000 --- a/src/saml/PhpSamlOneLogin.php +++ /dev/null @@ -1,5 +0,0 @@ -init($settings); + print_r($this->settings); + } + + function init($settings) + { + require_once(__DIR__ . '/../config/onelogin_saml_config.php'); + $this->settings = is_null($settings) ? $defaultSettings : array_merge($defaultSettings, $settings); + } + + function login() + { + + } + + function logout() + { + + } + +} \ No newline at end of file diff --git a/www3/index.php b/www3/index.php index e69de29..963f930 100644 --- a/www3/index.php +++ b/www3/index.php @@ -0,0 +1,11 @@ + Date: Fri, 20 Jul 2018 12:40:54 +0200 Subject: [PATCH 03/69] implemented login --- src/PhpSaml.php | 7 +- src/saml_strategy/PhpSamlOneLogin.php | 116 +++++++++++++++++++++++++- 2 files changed, 120 insertions(+), 3 deletions(-) diff --git a/src/PhpSaml.php b/src/PhpSaml.php index 6d5835b..7b719f8 100644 --- a/src/PhpSaml.php +++ b/src/PhpSaml.php @@ -1,10 +1,15 @@ php_saml = new PhpSamlOneLogin($settings); diff --git a/src/saml_strategy/PhpSamlOneLogin.php b/src/saml_strategy/PhpSamlOneLogin.php index c2164e2..0ce618b 100644 --- a/src/saml_strategy/PhpSamlOneLogin.php +++ b/src/saml_strategy/PhpSamlOneLogin.php @@ -1,10 +1,13 @@ settings); } - function init($settings) + private function init($settings) { require_once(__DIR__ . '/../config/onelogin_saml_config.php'); $this->settings = is_null($settings) ? $defaultSettings : array_merge($defaultSettings, $settings); + $auth = new OneLogin_Saml2_Auth($this->settings); } function login() { + if ( $auth->isAuthenticated ) { + return; + } + $auth->login(); + $requestID = null; + if (isset($_SESSION['AuthNRequestID'])) { + $requestID = $_SESSION['AuthNRequestID']; + } + + $auth->processResponse($requestID); + unset($_SESSION['AuthNRequestID']); + + $errors = $auth->getErrors(); + if (!empty($errors)) { + return $errors; + } + + if (!$auth->isAuthenticated()) { + return false; + } + + $_SESSION['samlUserdata'] = $auth->getAttributes(); + $_SESSION['samlNameId'] = $auth->getNameId(); + $_SESSION['samlNameIdFormat'] = $auth->getNameIdFormat(); + $_SESSION['samlSessionIndex'] = $auth->getSessionIndex(); + + if (isset($_POST['RelayState']) && OneLogin_Saml2_Utils::getSelfURL() != $_POST['RelayState']) { + $auth->redirectTo($_POST['RelayState']); + } + + $attributes = $_SESSION['samlUserdata']; + $nameId = $_SESSION['samlNameId']; + + echo '

Identified user: '. htmlentities($nameId) .'

'; + + if (!empty($attributes)) { + echo '

'._('User attributes:').'

'; + echo ''; + foreach ($attributes as $attributeName => $attributeValues) { + echo ''; + } + echo '
'._('Name').''._('Values').'
' . htmlentities($attributeName) . '
    '; + foreach ($attributeValues as $attributeValue) { + echo '
  • ' . htmlentities($attributeValue) . '
  • '; + } + echo '
'; + } else { + echo _('No attributes found.'); + } } function logout() - { + { + if (isset($_GET['sso'])) { // SSO action. Will send an AuthNRequest to the IdP + $auth->login(); + } else if (isset($_GET['sso2'])) { // Another SSO action + $returnTo = $spBaseUrl.'/demo1/attrs.php'; // but set a custom RelayState URL + $auth->login($returnTo); + } else if (isset($_GET['slo'])) { // SLO action. Will sent a Logout Request to IdP + $auth->logout(); + } else if (isset($_GET['acs'])) { // Assertion Consumer Service + $auth->processResponse(); // Process the Response of the IdP, get the + // attributes and put then at + // $_SESSION['samlUserdata'] + + $errors = $auth->getErrors(); // This method receives an array with the errors + // that could took place during the process + + if (!empty($errors)) { + echo '

', implode(', ', $errors), '

'; + } + // This check if the response was + if (!$auth->isAuthenticated()) { // sucessfully validated and the user + echo "

Not authenticated

"; // data retrieved or not + exit(); + } + + $_SESSION['samlUserdata'] = $auth->getAttributes(); // Retrieves user data + if (isset($_POST['RelayState']) && OneLogin_Saml2_Utils::getSelfURL() != $_POST['RelayState']) { + $auth->redirectTo($_POST['RelayState']); // Redirect if there is a + } // relayState set + } else if (isset($_GET['sls'])) { // Single Logout Service + $auth->processSLO(); // Process the Logout Request & Logout Response + $errors = $auth->getErrors(); // Retrieves possible validation errors + if (empty($errors)) { + echo '

Sucessfully logged out

'; + } else { + echo '

', implode(', ', $errors), '

'; + } + } + + if (isset($_SESSION['samlUserdata'])) { // If there is user data we print it. + if (!empty($_SESSION['samlUserdata'])) { + $attributes = $_SESSION['samlUserdata']; + echo 'You have the following attributes:
'; + echo ''; + foreach ($attributes as $attributeName => $attributeValues) { + echo ''; + } + echo '
NameValues
' . htmlentities($attributeName) . '
    '; + foreach ($attributeValues as $attributeValue) { + echo '
  • ' . htmlentities($attributeValue) . '
  • '; + } + echo '
'; + } else { // If there is not user data, we notify + echo "

You don't have any attribute

"; + } + echo '

Logout

'; // Print some links with possible + } else { // actions + echo '

Login

'; + echo '

Login and access to attrs.php page

'; + } } } \ No newline at end of file From a5fe8ce397e6e653fc8a25b5c6756172241cdbb3 Mon Sep 17 00:00:00 2001 From: Lorenzo Cattaneo Date: Fri, 20 Jul 2018 12:45:21 +0200 Subject: [PATCH 04/69] implemented logout --- src/saml_strategy/PhpSamlOneLogin.php | 90 ++++----------------------- 1 file changed, 12 insertions(+), 78 deletions(-) diff --git a/src/saml_strategy/PhpSamlOneLogin.php b/src/saml_strategy/PhpSamlOneLogin.php index 0ce618b..b032b63 100644 --- a/src/saml_strategy/PhpSamlOneLogin.php +++ b/src/saml_strategy/PhpSamlOneLogin.php @@ -24,8 +24,8 @@ private function init($settings) function login() { - if ( $auth->isAuthenticated ) { - return; + if ($auth->isAuthenticated) { + return false; } $auth->login(); @@ -51,93 +51,27 @@ function login() $_SESSION['samlNameIdFormat'] = $auth->getNameIdFormat(); $_SESSION['samlSessionIndex'] = $auth->getSessionIndex(); - if (isset($_POST['RelayState']) && OneLogin_Saml2_Utils::getSelfURL() != $_POST['RelayState']) { - $auth->redirectTo($_POST['RelayState']); + if (!empty($_SESSION['samlUserdata'])) { + return true; } - $attributes = $_SESSION['samlUserdata']; - $nameId = $_SESSION['samlNameId']; - - echo '

Identified user: '. htmlentities($nameId) .'

'; - - if (!empty($attributes)) { - echo '

'._('User attributes:').'

'; - echo ''; - foreach ($attributes as $attributeName => $attributeValues) { - echo ''; - } - echo '
'._('Name').''._('Values').'
' . htmlentities($attributeName) . '
    '; - foreach ($attributeValues as $attributeValue) { - echo '
  • ' . htmlentities($attributeValue) . '
  • '; - } - echo '
'; - } else { - echo _('No attributes found.'); - } + return false; } function logout() { - if (isset($_GET['sso'])) { // SSO action. Will send an AuthNRequest to the IdP - $auth->login(); - } else if (isset($_GET['sso2'])) { // Another SSO action - $returnTo = $spBaseUrl.'/demo1/attrs.php'; // but set a custom RelayState URL - $auth->login($returnTo); - } else if (isset($_GET['slo'])) { // SLO action. Will sent a Logout Request to IdP + if (!$auth->isAuthenticated()) { + return false; + } $auth->logout(); - } else if (isset($_GET['acs'])) { // Assertion Consumer Service - $auth->processResponse(); // Process the Response of the IdP, get the - // attributes and put then at - // $_SESSION['samlUserdata'] - - $errors = $auth->getErrors(); // This method receives an array with the errors - // that could took place during the process + $auth->processSLO(); + $errors = $auth->getErrors(); if (!empty($errors)) { - echo '

', implode(', ', $errors), '

'; - } - // This check if the response was - if (!$auth->isAuthenticated()) { // sucessfully validated and the user - echo "

Not authenticated

"; // data retrieved or not - exit(); - } - - $_SESSION['samlUserdata'] = $auth->getAttributes(); // Retrieves user data - if (isset($_POST['RelayState']) && OneLogin_Saml2_Utils::getSelfURL() != $_POST['RelayState']) { - $auth->redirectTo($_POST['RelayState']); // Redirect if there is a - } // relayState set - } else if (isset($_GET['sls'])) { // Single Logout Service - $auth->processSLO(); // Process the Logout Request & Logout Response - $errors = $auth->getErrors(); // Retrieves possible validation errors - if (empty($errors)) { - echo '

Sucessfully logged out

'; - } else { - echo '

', implode(', ', $errors), '

'; - } - } - - if (isset($_SESSION['samlUserdata'])) { // If there is user data we print it. - if (!empty($_SESSION['samlUserdata'])) { - $attributes = $_SESSION['samlUserdata']; - echo 'You have the following attributes:
'; - echo ''; - foreach ($attributes as $attributeName => $attributeValues) { - echo ''; - } - echo '
NameValues
' . htmlentities($attributeName) . '
    '; - foreach ($attributeValues as $attributeValue) { - echo '
  • ' . htmlentities($attributeValue) . '
  • '; - } - echo '
'; - } else { // If there is not user data, we notify - echo "

You don't have any attribute

"; + return $errors; } - echo '

Logout

'; // Print some links with possible - } else { // actions - echo '

Login

'; - echo '

Login and access to attrs.php page

'; - } + return true; } } \ No newline at end of file From 4095b4374fa050f83bec546fad113bae398d6274 Mon Sep 17 00:00:00 2001 From: Lorenzo Cattaneo Date: Fri, 20 Jul 2018 14:41:34 +0200 Subject: [PATCH 05/69] added settings validation --- ...saml_config.php => OneloginSamlConfig.php} | 0 src/helper/ArrayHelper.php | 20 +++++++++++++++++++ src/saml_strategy/PhpSamlOneLogin.php | 20 +++++++++++++++---- 3 files changed, 36 insertions(+), 4 deletions(-) rename src/config/{onelogin_saml_config.php => OneloginSamlConfig.php} (100%) create mode 100644 src/helper/ArrayHelper.php diff --git a/src/config/onelogin_saml_config.php b/src/config/OneloginSamlConfig.php similarity index 100% rename from src/config/onelogin_saml_config.php rename to src/config/OneloginSamlConfig.php diff --git a/src/helper/ArrayHelper.php b/src/helper/ArrayHelper.php new file mode 100644 index 0000000..17812f1 --- /dev/null +++ b/src/helper/ArrayHelper.php @@ -0,0 +1,20 @@ + $v) { + if (is_array($arr1[$k]) && is_array($arr2[$k])) { + $d = array_diff_key_recursive($arr1[$k], $arr2[$k]); + + if ($d) { + $diff[$k] = $d; + } + } + } + + return $diff; +} + +?> \ No newline at end of file diff --git a/src/saml_strategy/PhpSamlOneLogin.php b/src/saml_strategy/PhpSamlOneLogin.php index b032b63..229c67a 100644 --- a/src/saml_strategy/PhpSamlOneLogin.php +++ b/src/saml_strategy/PhpSamlOneLogin.php @@ -1,5 +1,6 @@ settings = is_null($settings) ? $defaultSettings : array_merge($defaultSettings, $settings); + require_once(__DIR__ . '/../config/OneloginSamlConfig.php'); + if (!is_null($settings)) { + $diff = array_diff_key_recursive($settings, $defaultSettings); + if (!empty($diff)) { + $message = "The following keys are invalid for settings array: "; + array_walk_recursive($diff, function($v, $k) { + $message .= $k . ", "; + }); + throw new Exception($message, 1); + } + } + $this->settings = is_null($settings) ? $defaultSettings : array_merge_recursive($defaultSettings, $settings); + $auth = new OneLogin_Saml2_Auth($this->settings); } - function login() + public function login() { if ($auth->isAuthenticated) { return false; @@ -58,7 +70,7 @@ function login() return false; } - function logout() + public function logout() { if (!$auth->isAuthenticated()) { return false; From 61cdee19baa21ce305e44150844fb360f486276d Mon Sep 17 00:00:00 2001 From: Lorenzo Cattaneo Date: Fri, 20 Jul 2018 16:20:11 +0200 Subject: [PATCH 06/69] code cleanup and added settings management class --- bin/Configuration.php | 8 ++ composer.json | 12 +++ src/PhpSaml.php | 10 ++- src/config/OneloginSamlConfig.php | 106 +++++++++++++---------- src/helper/ArrayHelper.php | 30 ++++--- src/helper/IdpHelper.php | 36 ++++++++ src/saml_strategy/PhpSamlOneLogin.php | 39 +++++---- templates/settings.tpl | 45 ---------- www/attrs.php | 25 ------ www/index.php | 116 +------------------------ www/metadata.php | 32 ------- www2/attrs.php | 25 ------ www2/index.php | 119 -------------------------- www2/metadata.php | 29 ------- www3/index.php | 11 --- 15 files changed, 167 insertions(+), 476 deletions(-) create mode 100644 bin/Configuration.php create mode 100644 src/helper/IdpHelper.php delete mode 100644 templates/settings.tpl delete mode 100644 www/attrs.php delete mode 100644 www/metadata.php delete mode 100644 www2/attrs.php delete mode 100644 www2/index.php delete mode 100644 www2/metadata.php delete mode 100644 www3/index.php diff --git a/bin/Configuration.php b/bin/Configuration.php new file mode 100644 index 0000000..58e43de --- /dev/null +++ b/bin/Configuration.php @@ -0,0 +1,8 @@ +php_saml = new PhpSamlOneLogin($settings); + $this->php_saml = new PhpSamlOneLogin($idpUrl, $settings); break; default: - $this->php_saml = new PhpSamlOneLogin($settings); + $this->php_saml = new PhpSamlOneLogin($idpUrl, $settings); break; } } diff --git a/src/config/OneloginSamlConfig.php b/src/config/OneloginSamlConfig.php index bbd1bbd..1f76a2a 100644 --- a/src/config/OneloginSamlConfig.php +++ b/src/config/OneloginSamlConfig.php @@ -1,52 +1,72 @@ false, + function __construct() + { + $this->spAcsUrl = $this->spBaseUrl . '/index.php?acs'; + $this->spSloUrl = $this->spBaseUrl . '/index.php?sls'; + $this->idpSSO = $this->idpEntityId . '/sso'; + $this->idpSLO = $this->idpEntityId . '/slo'; + } - // Enable debug mode (to print errors). - 'debug' => true, + public function getSettings() + { + return $defaultSettings = array( + // If 'strict' is True, then the PHP Toolkit will reject unsigned + // or unencrypted messages if it expects them to be signed or encrypted. + // Also it will reject the messages if the SAML standard is not strictly + // followed: Destination, NameId, Conditions ... are validated too. + 'strict' => false, + + // Enable debug mode (to print errors). + 'debug' => true, - 'sp' => array ( - 'entityId' => $spBaseUrl . '/metadata.php', - 'assertionConsumerService' => array ( - 'url' => $spBaseUrl . '/index.php?acs', + 'sp' => array( + 'entityId' => $spBaseUrl . '/metadata.php', + 'assertionConsumerService' => array( + 'url' => $spAcsUrl, + ), + 'singleLogoutService' => array( + 'url' => $spSloUrl, + ), + 'NameIDFormat' => 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient', + 'x509cert' => $spCrtFile, + 'privateKey' => $spKeyFile, ), - 'singleLogoutService' => array ( - 'url' => $spBaseUrl . '/index.php?sls', + 'idp' => array( + 'entityId' => $idpEntityId, + 'singleSignOnService' => array( + 'url' => $idpSSO, + ), + 'singleLogoutService' => array( + 'url' => $idpSLO, + ), + 'x509cert' => $idpCertFile, ), - 'NameIDFormat' => 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient', - 'x509cert' => $spCrtFile, - 'privateKey' => $spKeyFile, - ), - 'idp' => array ( - 'entityId' => $idpEntityId, - 'singleSignOnService' => array ( - 'url' => $idpSSO, + 'security' => array( + 'authnRequestsSigned' => true, + 'logoutRequestSigned' => true, + 'logoutResponseSigned' => true, + 'signMetadata' => true, + 'signatureAlgorithm' => 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256', + 'requestedAuthnContext' => array('https://www.spid.gov.it/SpidL1'), ), - 'singleLogoutService' => array ( - 'url' => $idpSLO, - ), - 'x509cert' => $idpCertFile, - ), - 'security' => array ( - 'authnRequestsSigned' => true, - 'logoutRequestSigned' => true, - 'logoutResponseSigned' => true, - 'signMetadata' => true, - 'signatureAlgorithm' => 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256', - 'requestedAuthnContext' => array('https://www.spid.gov.it/SpidL1'), - ), - ); + ); + } + + public function updateSettings($settings) { + + } +} \ No newline at end of file diff --git a/src/helper/ArrayHelper.php b/src/helper/ArrayHelper.php index 17812f1..7737567 100644 --- a/src/helper/ArrayHelper.php +++ b/src/helper/ArrayHelper.php @@ -1,20 +1,22 @@ $v) { - if (is_array($arr1[$k]) && is_array($arr2[$k])) { - $d = array_diff_key_recursive($arr1[$k], $arr2[$k]); - - if ($d) { - $diff[$k] = $d; +class ArrayHelper +{ + public static function array_diff_key_recursive(array $arr1, array $arr2) + { + $diff = array_diff_key($arr1, $arr2); + $intersect = array_intersect_key($arr1, $arr2); + + foreach ($intersect as $k => $v) { + if (is_array($arr1[$k]) && is_array($arr2[$k])) { + $d = array_diff_key_recursive($arr1[$k], $arr2[$k]); + + if ($d) { + $diff[$k] = $d; + } } } + + return $diff; } - - return $diff; } - -?> \ No newline at end of file diff --git a/src/helper/IdpHelper.php b/src/helper/IdpHelper.php new file mode 100644 index 0000000..e238a4b --- /dev/null +++ b/src/helper/IdpHelper.php @@ -0,0 +1,36 @@ +xpath('//ns0:EntityDescriptor/@entityID')[0]; + $metadata['idp_sso'] = $xml->xpath('//ns0:SingleSignOnService/@Location')[0]; + $metadata['idp_slo'] = $xml->xpath('//ns0:SingleLogoutService/@Location')[0]; + $metadata['idp_cert'] = $xml->xpath('//ns1:X509Certificate')[0]; + + file_put_contents("../config/idp/" . $idpName, print_r($metadata, true)); + + return $metadata; + } +} \ No newline at end of file diff --git a/src/saml_strategy/PhpSamlOneLogin.php b/src/saml_strategy/PhpSamlOneLogin.php index 229c67a..9de9f66 100644 --- a/src/saml_strategy/PhpSamlOneLogin.php +++ b/src/saml_strategy/PhpSamlOneLogin.php @@ -1,36 +1,45 @@ init($settings); + if (filter_var($idpUrl, FILTER_VALIDATE_URL)) { + throw new Exception("The provided idp URL is not a valid URL", 1); + } + $this->init($idpUrl, $settings); print_r($this->settings); } - private function init($settings) + private function init($idpUrl, $settings) { - require_once(__DIR__ . '/../config/OneloginSamlConfig.php'); + $settingsHelper = new OneloginSamlConfig(); + $defaultSettings = $settingsHelper->getSettings(); if (!is_null($settings)) { - $diff = array_diff_key_recursive($settings, $defaultSettings); + $diff = ArrayHelper::array_diff_key_recursive($settings, $defaultSettings); if (!empty($diff)) { - $message = "The following keys are invalid for settings array: "; - array_walk_recursive($diff, function($v, $k) { + $message = "The following keys are invalid for the provided settings array: "; + array_walk_recursive($diff, function ($v, $k) { $message .= $k . ", "; }); throw new Exception($message, 1); } } $this->settings = is_null($settings) ? $defaultSettings : array_merge_recursive($defaultSettings, $settings); - + $metadata = IdpHelper::getMetadata($idpUrl); + + $auth = new OneLogin_Saml2_Auth($this->settings); } @@ -45,15 +54,15 @@ public function login() if (isset($_SESSION['AuthNRequestID'])) { $requestID = $_SESSION['AuthNRequestID']; } - + $auth->processResponse($requestID); unset($_SESSION['AuthNRequestID']); - + $errors = $auth->getErrors(); if (!empty($errors)) { return $errors; } - + if (!$auth->isAuthenticated()) { return false; } @@ -71,7 +80,7 @@ public function login() } public function logout() - { + { if (!$auth->isAuthenticated()) { return false; } diff --git a/templates/settings.tpl b/templates/settings.tpl deleted file mode 100644 index acef17c..0000000 --- a/templates/settings.tpl +++ /dev/null @@ -1,45 +0,0 @@ - false, - - // Enable debug mode (to print errors). - 'debug' => true, - - 'sp' => array ( - 'entityId' => '{{sp_base}}/metadata.php', - 'assertionConsumerService' => array ( - 'url' => '{{sp_base}}/index.php?acs', - ), - 'singleLogoutService' => array ( - 'url' => '{{sp_base}}/index.php?sls', - ), - 'NameIDFormat' => 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient', - 'x509cert' => '{{sp_cert}}', - 'privateKey' => '{{sp_key}}', - ), - 'idp' => array ( - 'entityId' => '{{idp_entityid}}', - 'singleSignOnService' => array ( - 'url' => '{{idp_sso}}', - ), - 'singleLogoutService' => array ( - 'url' => '{{idp_slo}}', - ), - 'x509cert' => '{{idp_cert}}', - ), - 'security' => array ( - 'authnRequestsSigned' => true, - 'logoutRequestSigned' => true, - 'logoutResponseSigned' => true, - 'signMetadata' => true, - 'signatureAlgorithm' => 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256', - 'requestedAuthnContext' => array('https://www.spid.gov.it/SpidL1'), - ), - ); diff --git a/www/attrs.php b/www/attrs.php deleted file mode 100644 index 9905e61..0000000 --- a/www/attrs.php +++ /dev/null @@ -1,25 +0,0 @@ -'; - echo ''; - foreach ($attributes as $attributeName => $attributeValues) { - echo ''; - } - echo '
NameValues
' . htmlentities($attributeName) . '
    '; - foreach ($attributeValues as $attributeValue) { - echo '
  • ' . htmlentities($attributeValue) . '
  • '; - } - echo '
'; - } else { - echo "

You don't have any attribute

"; - } - - echo '

Logout

'; -} else { - echo '

Login and access later to this page

'; -} diff --git a/www/index.php b/www/index.php index 4b1d796..963f930 100644 --- a/www/index.php +++ b/www/index.php @@ -3,121 +3,9 @@ /** * SAML Handler */ - session_start(); require_once("../vendor/autoload.php"); +require_once("../src/saml_strategy/PhpSamlOneLogin.php"); -use OneLogin\Saml2\Auth; -use OneLogin\Saml2\Utils; - -require_once 'settings.php'; - -$auth = new Auth($settingsInfo); - -if (isset($_GET['sso'])) { - $auth->login(); - - # If AuthNRequest ID need to be saved in order to later validate it, do instead - # $ssoBuiltUrl = $auth->login(null, array(), false, false, true); - # $_SESSION['AuthNRequestID'] = $auth->getLastRequestID(); - # header('Pragma: no-cache'); - # header('Cache-Control: no-cache, must-revalidate'); - # header('Location: ' . $ssoBuiltUrl); - # exit(); -} elseif (isset($_GET['sso2'])) { - $returnTo = $spBaseUrl . '/attrs.php'; - $auth->login($returnTo); -} elseif (isset($_GET['slo'])) { - $returnTo = null; - $parameters = array(); - $nameId = null; - $sessionIndex = null; - $nameIdFormat = null; - - if (isset($_SESSION['samlNameId'])) { - $nameId = $_SESSION['samlNameId']; - } - if (isset($_SESSION['samlSessionIndex'])) { - $sessionIndex = $_SESSION['samlSessionIndex']; - } - if (isset($_SESSION['samlNameIdFormat'])) { - $nameIdFormat = $_SESSION['samlNameIdFormat']; - } - - $auth->logout($returnTo, $parameters, $nameId, $sessionIndex, false, $nameIdFormat, 'aaa'); - - # If LogoutRequest ID need to be saved in order to later validate it, do instead - # $sloBuiltUrl = $auth->logout(null, $parameters, $nameId, $sessionIndex, true); - # $_SESSION['LogoutRequestID'] = $auth->getLastRequestID(); - # header('Pragma: no-cache'); - # header('Cache-Control: no-cache, must-revalidate'); - # header('Location: ' . $sloBuiltUrl); - # exit(); -} elseif (isset($_GET['acs'])) { - if (isset($_SESSION) && isset($_SESSION['AuthNRequestID'])) { - $requestID = $_SESSION['AuthNRequestID']; - } else { - $requestID = null; - } - - $auth->processResponse($requestID); - - $errors = $auth->getErrors(); - - if (!empty($errors)) { - echo '

' . implode(', ', $errors) . '

'; - } - - if (!$auth->isAuthenticated()) { - echo '

Not authenticated

'; - exit(); - } - - $_SESSION['samlUserdata'] = $auth->getAttributes(); - $_SESSION['samlNameId'] = $auth->getNameId(); - $_SESSION['samlNameIdFormat'] = $auth->getNameIdFormat(); - $_SESSION['samlSessionIndex'] = $auth->getSessionIndex(); - unset($_SESSION['AuthNRequestID']); - if (isset($_POST['RelayState']) && Utils::getSelfURL() != $_POST['RelayState']) { - $auth->redirectTo($_POST['RelayState']); - } -} elseif (isset($_GET['sls'])) { - if (isset($_SESSION) && isset($_SESSION['LogoutRequestID'])) { - $requestID = $_SESSION['LogoutRequestID']; - } else { - $requestID = null; - } - - $auth->processSLO(false, $requestID); - $errors = $auth->getErrors(); - if (empty($errors)) { - echo '

Sucessfully logged out

'; - } else { - echo '

' . implode(', ', $errors) . '

'; - } -} - -if (isset($_SESSION['samlUserdata'])) { - if (!empty($_SESSION['samlUserdata'])) { - $attributes = $_SESSION['samlUserdata']; - echo 'You have the following attributes:
'; - echo ''; - foreach ($attributes as $attributeName => $attributeValues) { - echo ''; - } - echo '
NameValues
' . htmlentities($attributeName) . '
    '; - foreach ($attributeValues as $attributeValue) { - echo '
  • ' . htmlentities($attributeValue) . '
  • '; - } - echo '
'; - } else { - echo "

You don't have any attribute

"; - } - - echo '

Logout

'; -} else { - echo '

Login

'; - echo '

Login and access to attrs.php page

'; - echo '

Show the SP metadata

'; -} +$onelogin = new PhpSamlOneLogin; diff --git a/www/metadata.php b/www/metadata.php deleted file mode 100644 index 0500dee..0000000 --- a/www/metadata.php +++ /dev/null @@ -1,32 +0,0 @@ -getSettings(); - // Now we only validate SP settings - $settings = new Settings($settingsInfo, true); - $metadata = $settings->getSPMetadata(); - $errors = $settings->validateMetadata($metadata); - if (empty($errors)) { - header('Content-Type: text/xml'); - echo $metadata; - } else { - throw new Error( - 'Invalid SP metadata: '.implode(', ', $errors), - Error::METADATA_SP_INVALID - ); - } -} catch (Exception $e) { - echo $e->getMessage(); -} diff --git a/www2/attrs.php b/www2/attrs.php deleted file mode 100644 index 9905e61..0000000 --- a/www2/attrs.php +++ /dev/null @@ -1,25 +0,0 @@ -'; - echo ''; - foreach ($attributes as $attributeName => $attributeValues) { - echo ''; - } - echo '
NameValues
' . htmlentities($attributeName) . '
    '; - foreach ($attributeValues as $attributeValue) { - echo '
  • ' . htmlentities($attributeValue) . '
  • '; - } - echo '
'; - } else { - echo "

You don't have any attribute

"; - } - - echo '

Logout

'; -} else { - echo '

Login and access later to this page

'; -} diff --git a/www2/index.php b/www2/index.php deleted file mode 100644 index b87ad3a..0000000 --- a/www2/index.php +++ /dev/null @@ -1,119 +0,0 @@ -login(); - - # If AuthNRequest ID need to be saved in order to later validate it, do instead - # $ssoBuiltUrl = $auth->login(null, array(), false, false, true); - # $_SESSION['AuthNRequestID'] = $auth->getLastRequestID(); - # header('Pragma: no-cache'); - # header('Cache-Control: no-cache, must-revalidate'); - # header('Location: ' . $ssoBuiltUrl); - # exit(); -} elseif (isset($_GET['sso2'])) { - $returnTo = $spBaseUrl . '/attrs.php'; - $auth->login($returnTo); -} elseif (isset($_GET['slo'])) { - $returnTo = null; - $parameters = array(); - $nameId = null; - $sessionIndex = null; - $nameIdFormat = null; - - if (isset($_SESSION['samlNameId'])) { - $nameId = $_SESSION['samlNameId']; - } - if (isset($_SESSION['samlSessionIndex'])) { - $sessionIndex = $_SESSION['samlSessionIndex']; - } - if (isset($_SESSION['samlNameIdFormat'])) { - $nameIdFormat = $_SESSION['samlNameIdFormat']; - } - - $auth->logout($returnTo, $parameters, $nameId, $sessionIndex, false, $nameIdFormat); - - # If LogoutRequest ID need to be saved in order to later validate it, do instead - # $sloBuiltUrl = $auth->logout(null, $parameters, $nameId, $sessionIndex, true); - # $_SESSION['LogoutRequestID'] = $auth->getLastRequestID(); - # header('Pragma: no-cache'); - # header('Cache-Control: no-cache, must-revalidate'); - # header('Location: ' . $sloBuiltUrl); - # exit(); -} elseif (isset($_GET['acs'])) { - if (isset($_SESSION) && isset($_SESSION['AuthNRequestID'])) { - $requestID = $_SESSION['AuthNRequestID']; - } else { - $requestID = null; - } - - $auth->processResponse($requestID); - - $errors = $auth->getErrors(); - - if (!empty($errors)) { - echo '

' . implode(', ', $errors) . '

'; - } - - if (!$auth->isAuthenticated()) { - echo '

Not authenticated

'; - exit(); - } - - $_SESSION['samlUserdata'] = $auth->getAttributes(); - $_SESSION['samlNameId'] = $auth->getNameId(); - $_SESSION['samlNameIdFormat'] = $auth->getNameIdFormat(); - $_SESSION['samlSessionIndex'] = $auth->getSessionIndex(); - unset($_SESSION['AuthNRequestID']); - if (isset($_POST['RelayState']) && OneLogin_Saml2_Utils::getSelfURL() != $_POST['RelayState']) { - $auth->redirectTo($_POST['RelayState']); - } -} elseif (isset($_GET['sls'])) { - if (isset($_SESSION) && isset($_SESSION['LogoutRequestID'])) { - $requestID = $_SESSION['LogoutRequestID']; - } else { - $requestID = null; - } - - $auth->processSLO(false, $requestID); - $errors = $auth->getErrors(); - if (empty($errors)) { - echo '

Sucessfully logged out

'; - } else { - echo '

' . implode(', ', $errors) . '

'; - } -} - -if (isset($_SESSION['samlUserdata'])) { - if (!empty($_SESSION['samlUserdata'])) { - $attributes = $_SESSION['samlUserdata']; - echo 'You have the following attributes:
'; - echo ''; - foreach ($attributes as $attributeName => $attributeValues) { - echo ''; - } - echo '
NameValues
' . htmlentities($attributeName) . '
    '; - foreach ($attributeValues as $attributeValue) { - echo '
  • ' . htmlentities($attributeValue) . '
  • '; - } - echo '
'; - } else { - echo "

You don't have any attribute

"; - } - - echo '

Logout

'; -} else { - echo '

Login

'; - echo '

Login and access to attrs.php page

'; - echo '

Show the SP metadata

'; -} diff --git a/www2/metadata.php b/www2/metadata.php deleted file mode 100644 index 7f00b44..0000000 --- a/www2/metadata.php +++ /dev/null @@ -1,29 +0,0 @@ -getSettings(); - // Now we only validate SP settings - $settings = new OneLogin_Saml2_Settings($settingsInfo, true); - $metadata = $settings->getSPMetadata(); - $errors = $settings->validateMetadata($metadata); - if (empty($errors)) { - header('Content-Type: text/xml'); - echo $metadata; - } else { - throw new OneLogin_Saml2_Error( - 'Invalid SP metadata: '.implode(', ', $errors), - OneLogin_Saml2_Error::METADATA_SP_INVALID - ); - } -} catch (Exception $e) { - echo $e->getMessage(); -} diff --git a/www3/index.php b/www3/index.php deleted file mode 100644 index 963f930..0000000 --- a/www3/index.php +++ /dev/null @@ -1,11 +0,0 @@ - Date: Fri, 20 Jul 2018 17:48:35 +0200 Subject: [PATCH 07/69] added automatic idp metadata configuration --- src/config/OneloginSamlConfig.php | 40 +++++++++++++++++--------- src/saml_strategy/PhpSamlInterface.php | 1 + src/saml_strategy/PhpSamlOneLogin.php | 15 +++++++--- 3 files changed, 39 insertions(+), 17 deletions(-) diff --git a/src/config/OneloginSamlConfig.php b/src/config/OneloginSamlConfig.php index 1f76a2a..971119a 100644 --- a/src/config/OneloginSamlConfig.php +++ b/src/config/OneloginSamlConfig.php @@ -2,23 +2,23 @@ class OneloginSamlConfig { - // Default values - var $spBaseUrl = ''; - var $idpEntityId = ''; - var $spAcsUrl = null; - var $spSloUrl = null; - var $idpSSO = null; - var $idpSLO = null; - var $spKeyFile = 'sp.key'; - var $spCrtFile = 'sp.crt'; - var $idpCertFile = ''; + // Default values SP + private $spBaseUrl = null; + private $spKeyFile = 'sp.key'; + private $spCrtFile = 'sp.crt'; + private $spAcsUrl = null; + private $spSloUrl = null; + // Default values IDP + private $idpEntityId = null; + private $idpSSO = null; + private $idpSLO = null; + private $idpCertFile = null; function __construct() { + // Default values $this->spAcsUrl = $this->spBaseUrl . '/index.php?acs'; $this->spSloUrl = $this->spBaseUrl . '/index.php?sls'; - $this->idpSSO = $this->idpEntityId . '/sso'; - $this->idpSLO = $this->idpEntityId . '/slo'; } public function getSettings() @@ -67,6 +67,20 @@ public function getSettings() } public function updateSettings($settings) { - + foreach ($settings as $key => $value) { + if (property_exists($key) && strpos("idp", $key) === false) { + $this->{$key} = $value; + } + } + return $this->getSettings(); + } + + public function updateIdpMetadata($metadata) { + foreach ($metadata as $key => $value) { + if (property_exists($key) && strpos("idp", $key) !== false) { + $this->{$key} = $value; + } + } + return $this->getSettings(); } } \ No newline at end of file diff --git a/src/saml_strategy/PhpSamlInterface.php b/src/saml_strategy/PhpSamlInterface.php index 9454590..cdf38b0 100644 --- a/src/saml_strategy/PhpSamlInterface.php +++ b/src/saml_strategy/PhpSamlInterface.php @@ -2,6 +2,7 @@ interface PhpSamlInterface { public function init($settings); + public function isAuthenticated(); public function login(); public function logout(); } \ No newline at end of file diff --git a/src/saml_strategy/PhpSamlOneLogin.php b/src/saml_strategy/PhpSamlOneLogin.php index 9de9f66..afa9f86 100644 --- a/src/saml_strategy/PhpSamlOneLogin.php +++ b/src/saml_strategy/PhpSamlOneLogin.php @@ -25,9 +25,8 @@ function __construct($idpUrl, $settings = null) private function init($idpUrl, $settings) { $settingsHelper = new OneloginSamlConfig(); - $defaultSettings = $settingsHelper->getSettings(); if (!is_null($settings)) { - $diff = ArrayHelper::array_diff_key_recursive($settings, $defaultSettings); + $diff = ArrayHelper::array_diff_key_recursive($settings, get_object_vars($settingsHelper)); if (!empty($diff)) { $message = "The following keys are invalid for the provided settings array: "; array_walk_recursive($diff, function ($v, $k) { @@ -35,12 +34,20 @@ private function init($idpUrl, $settings) }); throw new Exception($message, 1); } + $settingsHelper->updateSpSettings($settings); } - $this->settings = is_null($settings) ? $defaultSettings : array_merge_recursive($defaultSettings, $settings); $metadata = IdpHelper::getMetadata($idpUrl); + $settingsHelper->updateIdpMetadata($metadata); + $auth = new OneLogin_Saml2_Auth($settingsHelper->getSettings()); + } - $auth = new OneLogin_Saml2_Auth($this->settings); + public function isAuthenticated() + { + if ($auth->isAuthenticated) { + return false; + } + return true; } public function login() From 7a20325c773ab4d56b3059f2004fa164dc7ada4d Mon Sep 17 00:00:00 2001 From: Lorenzo Cattaneo Date: Fri, 20 Jul 2018 18:04:57 +0200 Subject: [PATCH 08/69] changed idp metadata storage to xml --- src/helper/IdpHelper.php | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/helper/IdpHelper.php b/src/helper/IdpHelper.php index e238a4b..c2bdb3b 100644 --- a/src/helper/IdpHelper.php +++ b/src/helper/IdpHelper.php @@ -7,7 +7,7 @@ public static function getMetadata($idpUrl) // Check if a file containing this idp metadata already exists $idpName = str_replace(".", "_", parse_url($idpUrl, PHP_URL_HOST)); if (file_exists("../config/idp/" . $idpName)) { - $metadata = file_get_contents("../config/idp/" . $idpName); + $metadata = simplexml_load_file("../config/idp/" . $idpName . '.xml'); return $metadata; } @@ -22,15 +22,18 @@ public static function getMetadata($idpUrl) curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0); $data = curl_exec($ch); curl_close($ch); + + $dom = new DOMDocument(); + $dom->loadXML($data); + $dom->save("../config/idp/" . $idpName . ".xml"); + $xml = new SimpleXMLElement($data); $metadata = array(); $metadata['idp_entityid'] = $xml->xpath('//ns0:EntityDescriptor/@entityID')[0]; $metadata['idp_sso'] = $xml->xpath('//ns0:SingleSignOnService/@Location')[0]; $metadata['idp_slo'] = $xml->xpath('//ns0:SingleLogoutService/@Location')[0]; $metadata['idp_cert'] = $xml->xpath('//ns1:X509Certificate')[0]; - - file_put_contents("../config/idp/" . $idpName, print_r($metadata, true)); - + return $metadata; } } \ No newline at end of file From 439104ffb437f7c2fc77fdef150d66f706dd5421 Mon Sep 17 00:00:00 2001 From: Lorenzo Cattaneo Date: Fri, 20 Jul 2018 18:10:50 +0200 Subject: [PATCH 09/69] added interface methods to generic PhpSaml class --- src/PhpSaml.php | 23 +++++++++++++++++++---- src/saml_strategy/PhpSamlInterface.php | 1 - 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/src/PhpSaml.php b/src/PhpSaml.php index accfdb5..fee9de1 100644 --- a/src/PhpSaml.php +++ b/src/PhpSaml.php @@ -1,9 +1,9 @@ php_saml = new PhpSamlOneLogin($idpUrl, $settings); + $this->phpSaml = new PhpSamlOneLogin($idpUrl, $settings); break; default: - $this->php_saml = new PhpSamlOneLogin($idpUrl, $settings); + $this->phpSaml = new PhpSamlOneLogin($idpUrl, $settings); break; } } + public function isAuthenticated() + { + $this->phpSaml->isAuthenticated(); + } + + public function login() + { + $this->phpSaml->login(); + } + + public function logout() + { + $this->phpSaml->logout(); + } + } \ No newline at end of file diff --git a/src/saml_strategy/PhpSamlInterface.php b/src/saml_strategy/PhpSamlInterface.php index cdf38b0..bdee638 100644 --- a/src/saml_strategy/PhpSamlInterface.php +++ b/src/saml_strategy/PhpSamlInterface.php @@ -1,7 +1,6 @@ Date: Sat, 21 Jul 2018 00:41:55 +0200 Subject: [PATCH 10/69] WIP readme --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 1296dc5..17895df 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # spid-php2 +Work in progress. Do not merge this branch. + Software Development Kit (SDK) for easy SPID SSO integration based on [php-saml](https://github.com/onelogin/php-saml). This component acts as a SPID SP (Service Provider) and logs you in via an external IDP (IDentity Provider). From 5362c0e86572ce459eaaa263d3aa8d256d6a0418 Mon Sep 17 00:00:00 2001 From: Lorenzo Cattaneo Date: Thu, 26 Jul 2018 08:34:58 +0200 Subject: [PATCH 11/69] modifica getmetadata idp --- .DS_Store | Bin 0 -> 8196 bytes {www => example}/index.php | 0 src/PhpSaml.php | 8 +++--- src/helper/IdpHelper.php | 37 ++++++-------------------- src/saml_strategy/PhpSamlOneLogin.php | 10 +++---- 5 files changed, 17 insertions(+), 38 deletions(-) create mode 100644 .DS_Store rename {www => example}/index.php (100%) diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..f82ed19c8c40230dfce7d7f02e7b2ba3225d6d8d GIT binary patch literal 8196 zcmeHM&2G~`5S~p+>l9Tje`+sCz94aEP}5RzK$Sw894Y~-2!aEk)_-kH9Xr@g(-MMm z;T?cC;88enY@pmI5P8O@l& z=XpI*N^0Z=B!fOtNKeS4H`GI`i)KJGpc&8%Xa+O`|04re$B&B$Cpb;qm;%=J1+Sv#q z#||PJXxw1z2juOaq6m{^vVjZpD7s%j&5%P8b*PIr!54xU_FtnH6*WuN88idvIkcbm zS^8Gcye52x*n>~I@OMNORuG`)(V^&fapmQ$l-CMYrsyGU&^oo~F}10cM~i)a%&>74 z=m_2-!RntcB7b3&zQAMFhCz?8x_zvYul&Rc*?a+C!Izg7;@E^<3%l!(n_ngSP|&yu z<|P+Z^c+OrgO){A)C4~spA^0k9gHo*<`U{5W{Q^iX~?eK1ZKSD4y>k_b5Xghg3V1Z z+f===_Qh$)+Mk5|F4p4k73Oy-;y(_{nLNs4MU=DHGVI3WMB?%AsUG>U@bzGXl2z5dqR z_4O!BEkE>Pq3pXJATOW$p&RuZQ7?28VW%x845MOHw&oTFgOz)W=H1%LaM2vpYAef& z=IUx~III}+w`yw}+nuAggLlLCpAaY%rrx16E3c2+&*&)<4%%@TMBy%nqfXKR2n-Rp zh~hj!;wa_`>Jxqi6hD4h)GUq?x9A}B+2c3%2=H5s8JSyb3fl5E9&;dh$a3g;g?8bI zKL)Dep%BCW8GQ5o<*Z>Nx@ZQ@KLcgCg$17fw@&~5fBrYBcc2;244ldUn`pM14NU+3 zyP$~VxweVAjmithjieMRXhb>=DbjJs<9`^UZvx77>Pn8J#0bj2{}7phpSaml = new PhpSamlOneLogin($idpUrl, $settings); + $this->phpSaml = new PhpSamlOneLogin($idpName, $settings); break; default: - $this->phpSaml = new PhpSamlOneLogin($idpUrl, $settings); + $this->phpSaml = new PhpSamlOneLogin($idpName, $settings); break; } } diff --git a/src/helper/IdpHelper.php b/src/helper/IdpHelper.php index c2bdb3b..236632f 100644 --- a/src/helper/IdpHelper.php +++ b/src/helper/IdpHelper.php @@ -2,38 +2,17 @@ class IdpHelper { - public static function getMetadata($idpUrl) + public static function getMetadata($idpName) { - // Check if a file containing this idp metadata already exists - $idpName = str_replace(".", "_", parse_url($idpUrl, PHP_URL_HOST)); if (file_exists("../config/idp/" . $idpName)) { - $metadata = simplexml_load_file("../config/idp/" . $idpName . '.xml'); + $xml = simplexml_load_file("../config/idp/" . $idpName . '.xml'); + $metadata = array(); + $metadata['idp_entityid'] = $xml->xpath('//ns0:EntityDescriptor/@entityID')[0]; + $metadata['idp_sso'] = $xml->xpath('//ns0:SingleSignOnService/@Location')[0]; + $metadata['idp_slo'] = $xml->xpath('//ns0:SingleLogoutService/@Location')[0]; + $metadata['idp_cert'] = $xml->xpath('//ns1:X509Certificate')[0]; + return $metadata; } - - // File doesn't exist yet, get metadata from idp url and save it to file - $ch = curl_init(); - curl_setopt($ch, CURLOPT_URL, $idpUrl); - curl_setopt($ch, CURLOPT_FAILONERROR, 1); - curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); - curl_setopt($ch, CURLOPT_TIMEOUT, 15); - curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0); - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0); - $data = curl_exec($ch); - curl_close($ch); - - $dom = new DOMDocument(); - $dom->loadXML($data); - $dom->save("../config/idp/" . $idpName . ".xml"); - - $xml = new SimpleXMLElement($data); - $metadata = array(); - $metadata['idp_entityid'] = $xml->xpath('//ns0:EntityDescriptor/@entityID')[0]; - $metadata['idp_sso'] = $xml->xpath('//ns0:SingleSignOnService/@Location')[0]; - $metadata['idp_slo'] = $xml->xpath('//ns0:SingleLogoutService/@Location')[0]; - $metadata['idp_cert'] = $xml->xpath('//ns1:X509Certificate')[0]; - - return $metadata; } } \ No newline at end of file diff --git a/src/saml_strategy/PhpSamlOneLogin.php b/src/saml_strategy/PhpSamlOneLogin.php index afa9f86..a2d34c8 100644 --- a/src/saml_strategy/PhpSamlOneLogin.php +++ b/src/saml_strategy/PhpSamlOneLogin.php @@ -13,16 +13,16 @@ class PhpSamlOneLogin implements PhpSamlInterface var $settings; var $auth; - function __construct($idpUrl, $settings = null) + function __construct($idpName, $settings = null) { - if (filter_var($idpUrl, FILTER_VALIDATE_URL)) { + if (filter_var($idpName, FILTER_VALIDATE_URL)) { throw new Exception("The provided idp URL is not a valid URL", 1); } - $this->init($idpUrl, $settings); + $this->init($idpName, $settings); print_r($this->settings); } - private function init($idpUrl, $settings) + private function init($idpName, $settings) { $settingsHelper = new OneloginSamlConfig(); if (!is_null($settings)) { @@ -36,7 +36,7 @@ private function init($idpUrl, $settings) } $settingsHelper->updateSpSettings($settings); } - $metadata = IdpHelper::getMetadata($idpUrl); + $metadata = IdpHelper::getMetadata($idpName); $settingsHelper->updateIdpMetadata($metadata); $auth = new OneLogin_Saml2_Auth($settingsHelper->getSettings()); From 30262b844545177de620074d90432956cc42149e Mon Sep 17 00:00:00 2001 From: Lorenzo Cattaneo Date: Thu, 26 Jul 2018 09:02:15 +0200 Subject: [PATCH 12/69] added sp cert settings --- src/config/OneloginSamlConfig.php | 32 ++++++++++++++++++--------- src/helper/SpHelper.php | 27 ++++++++++++++++++++++ src/saml_strategy/PhpSamlOneLogin.php | 13 ++++++----- 3 files changed, 57 insertions(+), 15 deletions(-) create mode 100644 src/helper/SpHelper.php diff --git a/src/config/OneloginSamlConfig.php b/src/config/OneloginSamlConfig.php index 971119a..d35c5da 100644 --- a/src/config/OneloginSamlConfig.php +++ b/src/config/OneloginSamlConfig.php @@ -3,7 +3,8 @@ class OneloginSamlConfig { // Default values SP - private $spBaseUrl = null; + private $spBaseUrl = ''; + private $spEntityId = null; private $spKeyFile = 'sp.key'; private $spCrtFile = 'sp.crt'; private $spAcsUrl = null; @@ -19,6 +20,7 @@ function __construct() // Default values $this->spAcsUrl = $this->spBaseUrl . '/index.php?acs'; $this->spSloUrl = $this->spBaseUrl . '/index.php?sls'; + $this->spEntityId = $this->spBaseUrl . '/metadata.php'; } public function getSettings() @@ -34,26 +36,26 @@ public function getSettings() 'debug' => true, 'sp' => array( - 'entityId' => $spBaseUrl . '/metadata.php', + 'entityId' => $this->spEntityId, 'assertionConsumerService' => array( - 'url' => $spAcsUrl, + 'url' => $this->spAcsUrl, ), 'singleLogoutService' => array( - 'url' => $spSloUrl, + 'url' => $this->spSloUrl, ), 'NameIDFormat' => 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient', - 'x509cert' => $spCrtFile, - 'privateKey' => $spKeyFile, + 'x509cert' => $this->spCrtFile, + 'privateKey' => $this->spKeyFile, ), 'idp' => array( - 'entityId' => $idpEntityId, + 'entityId' => $this->idpEntityId, 'singleSignOnService' => array( - 'url' => $idpSSO, + 'url' => $this->idpSSO, ), 'singleLogoutService' => array( - 'url' => $idpSLO, + 'url' => $this->idpSLO, ), - 'x509cert' => $idpCertFile, + 'x509cert' => $this->idpCertFile, ), 'security' => array( 'authnRequestsSigned' => true, @@ -83,4 +85,14 @@ public function updateIdpMetadata($metadata) { } return $this->getSettings(); } + + public function updateSpData($sp) { + if (!is_array($sp)) + throw new Exception("Invalid SP certificate data provided", 1); + + $this->spKeyFile = $sp['key']; + $this->spCrtFile = $sp['cert']; + + return $this->getSettings(); + } } \ No newline at end of file diff --git a/src/helper/SpHelper.php b/src/helper/SpHelper.php new file mode 100644 index 0000000..1476e9c --- /dev/null +++ b/src/helper/SpHelper.php @@ -0,0 +1,27 @@ +init($idpName, $settings); + $this->init($idpMetadataFile, $spCertFile, $spKeyFile, $settings); print_r($this->settings); } - private function init($idpName, $settings) + private function init($idpMetadataFile, $spCertFile, $spKeyFile, $settings) { $settingsHelper = new OneloginSamlConfig(); if (!is_null($settings)) { @@ -36,9 +36,12 @@ private function init($idpName, $settings) } $settingsHelper->updateSpSettings($settings); } - $metadata = IdpHelper::getMetadata($idpName); + $metadata = IdpHelper::getMetadata($idpMetadataFile); $settingsHelper->updateIdpMetadata($metadata); + $sp = SpHelper::getSpCert($spCertFile, $spKeyFile); + $settingsHelper->updateSpData($sp); + $auth = new OneLogin_Saml2_Auth($settingsHelper->getSettings()); } From d00cbc54d651416a885d5287a6b01021ee5fbf61 Mon Sep 17 00:00:00 2001 From: Lorenzo Cattaneo Date: Thu, 26 Jul 2018 09:46:08 +0200 Subject: [PATCH 13/69] added namespaces --- .DS_Store | Bin 8196 -> 8196 bytes bin/Configuration.php | 8 ---- bin/configure.php | 63 ------------------------- composer.json | 8 ++-- src/PhpSaml.php | 5 ++ src/config/OneloginSamlConfig.php | 7 ++- src/helper/ArrayHelper.php | 2 + src/helper/IdpHelper.php | 2 + src/helper/SpHelper.php | 2 + src/saml_strategy/PhpSamlInterface.php | 2 + src/saml_strategy/PhpSamlOneLogin.php | 12 +++-- 11 files changed, 31 insertions(+), 80 deletions(-) delete mode 100644 bin/Configuration.php delete mode 100755 bin/configure.php diff --git a/.DS_Store b/.DS_Store index f82ed19c8c40230dfce7d7f02e7b2ba3225d6d8d..2137870ec5135ccb2df51ce4b88d941bb650d631 100644 GIT binary patch delta 131 zcmZp1XmOa}&nUhzU^hRb_+}mfbw=J)h6;v6hFpdMh8%{}$*Cgpn+*kZnR(e5${11^ y@)%MW@)?RIy9nQqL=h './tmp', -)); -$template = $twig->load('settings.tpl'); - -// read configuration -$yaml = \Symfony\Component\Yaml\Yaml::parseFile('config.yaml'); -foreach ($yaml as $k => $v) { - $$k = $v; -} - -# read SP key and cert from the files generated by openssl -$sp_key_raw = file_get_contents($sp_key_file); -$sp_cert_raw = file_get_contents($sp_cert_file); - -# get rid of '-----' lines -function clean_openssl($k) -{ - $ck = ''; - foreach (preg_split("/((\r?\n)|(\r\n?))/", $k) as $l) { - if (strpos($l, '-----') === false) { - $ck .= $l; - } - } - return $ck; -} - -$sp_key = clean_openssl($sp_key_raw); -$sp_cert = clean_openssl($sp_cert_raw); - -# retrieve the IDP metadata and extract information -$ch = curl_init(); -curl_setopt($ch, CURLOPT_URL, $idp_metadata_url); -curl_setopt($ch, CURLOPT_FAILONERROR, 1); -curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1); -curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); -curl_setopt($ch, CURLOPT_TIMEOUT, 15); -curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0); -curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0); -$data = curl_exec($ch); -curl_close($ch); -$xml = new SimpleXMLElement($data); -$idp_entityid = $xml->xpath('//ns0:EntityDescriptor/@entityID')[0]; -$idp_sso = $xml->xpath('//ns0:SingleSignOnService/@Location')[0]; -$idp_slo = $xml->xpath('//ns0:SingleLogoutService/@Location')[0]; -$idp_cert = $xml->xpath('//ns1:X509Certificate')[0]; - -echo $template->render(array( - 'sp_base' => $sp_base, - 'sp_cert' => $sp_cert, - 'sp_key' => $sp_key, - 'idp_entityid' => $idp_entityid, - 'idp_sso' => $idp_sso, - 'idp_slo' => $idp_slo, - 'idp_cert' => $idp_cert, -)); diff --git a/composer.json b/composer.json index b4cc511..4274d76 100644 --- a/composer.json +++ b/composer.json @@ -12,9 +12,11 @@ ], "post-update-cmd": [ "setup\\Configuration::setup" - ], + ], "autoload": { - "classmap": ["bin/"] - } + "psr-4": { + "SpidPHP\\": "src/" + } + } } } diff --git a/src/PhpSaml.php b/src/PhpSaml.php index 8b336f7..0a8cbb7 100644 --- a/src/PhpSaml.php +++ b/src/PhpSaml.php @@ -1,5 +1,10 @@ false, + 'strict' => true, // Enable debug mode (to print errors). 'debug' => true, @@ -70,7 +72,8 @@ public function getSettings() public function updateSettings($settings) { foreach ($settings as $key => $value) { - if (property_exists($key) && strpos("idp", $key) === false) { + // do not update idp os sp cert file values, they are updated in their own method + if (property_exists($key) && strpos("idp", $key) === false && strpos("file", $key) === false) { $this->{$key} = $value; } } diff --git a/src/helper/ArrayHelper.php b/src/helper/ArrayHelper.php index 7737567..29154a0 100644 --- a/src/helper/ArrayHelper.php +++ b/src/helper/ArrayHelper.php @@ -1,5 +1,7 @@ Date: Thu, 26 Jul 2018 10:01:35 +0200 Subject: [PATCH 14/69] WIP: basic example --- example/index.php | 22 ++++++++++++++++++++-- src/PhpSaml.php | 6 +++--- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/example/index.php b/example/index.php index 963f930..825f182 100644 --- a/example/index.php +++ b/example/index.php @@ -6,6 +6,24 @@ session_start(); require_once("../vendor/autoload.php"); -require_once("../src/saml_strategy/PhpSamlOneLogin.php"); -$onelogin = new PhpSamlOneLogin; +use SpidPHP\PhpSaml; + +$settings = [ + 'sp' => array( + 'entityId' => $this->spEntityId, + 'assertionConsumerService' => array( + 'url' => $this->spAcsUrl, + ), + 'singleLogoutService' => array( + 'url' => $this->spSloUrl, + ), + 'NameIDFormat' => 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient', + ), + ]; + +$onelogin = new PhpSaml("http://idp.simevo.com", $settings); + +if (!$onelogin->isAuthenticated()) $onelogin->login(); + +if ($onelogin->login()) $onelogin->logout(); diff --git a/src/PhpSaml.php b/src/PhpSaml.php index 0a8cbb7..9b14ac3 100644 --- a/src/PhpSaml.php +++ b/src/PhpSaml.php @@ -10,7 +10,7 @@ class PhpSaml implements PhpSamlInterface private $phpSaml = null; - public function __construct($idpName, $settings = null, $mode = 'onelogin') + public function __construct($idpMetadataFile, $spCertFile, $spKeyFile, $settings = null, $mode = 'onelogin') { if (session_status() == PHP_SESSION_NONE) { @@ -19,10 +19,10 @@ public function __construct($idpName, $settings = null, $mode = 'onelogin') switch ($mode) { case 'onelogin': - $this->phpSaml = new PhpSamlOneLogin($idpName, $settings); + $this->phpSaml = new PhpSamlOneLogin($idpMetadataFile, $spCertFile, $spKeyFile, $settings); break; default: - $this->phpSaml = new PhpSamlOneLogin($idpName, $settings); + $this->phpSaml = new PhpSamlOneLogin($idpMetadataFile, $spCertFile, $spKeyFile, $settings); break; } } From bec70c06bc464d140ea526d366f5b14ab09dab75 Mon Sep 17 00:00:00 2001 From: Lorenzo Cattaneo Date: Thu, 26 Jul 2018 12:51:37 +0200 Subject: [PATCH 15/69] moving all settings to array format --- example/index.php | 2 +- src/PhpSaml.php | 28 +++++++++++++++------ src/config/OneloginSamlConfig.php | 28 ++++++++++++++++++--- src/helper/IdpHelper.php | 19 +++++++------- src/saml_strategy/PhpSamlInterface.php | 3 ++- src/saml_strategy/PhpSamlOneLogin.php | 34 ++++++++++++-------------- 6 files changed, 73 insertions(+), 41 deletions(-) diff --git a/example/index.php b/example/index.php index 825f182..48604af 100644 --- a/example/index.php +++ b/example/index.php @@ -22,7 +22,7 @@ ), ]; -$onelogin = new PhpSaml("http://idp.simevo.com", $settings); +$onelogin = new PhpSaml("http://idp.simevo.com", "/sp.key", "/sp.crt", $settings); if (!$onelogin->isAuthenticated()) $onelogin->login(); diff --git a/src/PhpSaml.php b/src/PhpSaml.php index 9b14ac3..693cc8b 100644 --- a/src/PhpSaml.php +++ b/src/PhpSaml.php @@ -9,31 +9,45 @@ class PhpSaml implements PhpSamlInterface { private $phpSaml = null; + private $mode = null; + private $settings = null; - public function __construct($idpMetadataFile, $spCertFile, $spKeyFile, $settings = null, $mode = 'onelogin') + public function __construct($settings = null, $mode = 'onelogin') { - + /* if (session_status() == PHP_SESSION_NONE) { session_start(); } + */ + + $this->mode = $mode; + $this->settings = $settings; + } - switch ($mode) { + private function initStrategy() + { + switch ($this->mode) { case 'onelogin': - $this->phpSaml = new PhpSamlOneLogin($idpMetadataFile, $spCertFile, $spKeyFile, $settings); + $this->phpSaml = new PhpSamlOneLogin($this->settings); break; default: - $this->phpSaml = new PhpSamlOneLogin($idpMetadataFile, $spCertFile, $spKeyFile, $settings); + $this->phpSaml = new PhpSamlOneLogin($this->settings); break; } } + public function getSupportedIdps() + { + return array(); + } + public function isAuthenticated() { $this->phpSaml->isAuthenticated(); } - public function login() - { + public function login( $idpName, $redirectTo = null, $level = 1 ) + { $this->phpSaml->login(); } diff --git a/src/config/OneloginSamlConfig.php b/src/config/OneloginSamlConfig.php index 4a87ab7..22137dc 100644 --- a/src/config/OneloginSamlConfig.php +++ b/src/config/OneloginSamlConfig.php @@ -2,6 +2,10 @@ namespace SpidPHP\Config; +use SpidPHP\Helpers\SpHelper; +use SpidPHP\Helpers\IdpHelper; + + class OneloginSamlConfig { // Default values SP @@ -9,6 +13,8 @@ class OneloginSamlConfig private $spEntityId = null; private $spKeyFile = 'sp.key'; private $spCrtFile = 'sp.crt'; + private $spKeyFileValue = null; + private $spCrtFileValue = null; private $spAcsUrl = null; private $spSloUrl = null; // Default values IDP @@ -17,6 +23,8 @@ class OneloginSamlConfig private $idpSLO = null; private $idpCertFile = null; + private $is_not_updatable = ['spKeyFileValue', 'spCrtFileValue', 'idpEntityId', 'idpSSO', 'idpSLO', 'idpCertFile']; + function __construct() { // Default values @@ -27,7 +35,7 @@ function __construct() public function getSettings() { - return $defaultSettings = array( + return array( // If 'strict' is True, then the PHP Toolkit will reject unsigned // or unencrypted messages if it expects them to be signed or encrypted. // Also it will reject the messages if the SAML standard is not strictly @@ -73,14 +81,26 @@ public function getSettings() public function updateSettings($settings) { foreach ($settings as $key => $value) { // do not update idp os sp cert file values, they are updated in their own method - if (property_exists($key) && strpos("idp", $key) === false && strpos("file", $key) === false) { - $this->{$key} = $value; + if (!property_exists($key)) { + continue; } + if (in_array($key, $this->is_not_updatable)) { + continue; + } + + $this->{$key} = $value; + } + // Get .key and .cert files content and add it to configuration + if (!file_exists($this->spKeyFile) || !file_exists($this->spCrtFile)) { + throw new Exception("The path for .key and .cert files is invalid", 1); } + $sp = SpHelper::getSpCert($this->spKeyFile, $this->spCrtFile); + $this->updateSpData($sp); return $this->getSettings(); } - public function updateIdpMetadata($metadata) { + public function updateIdpMetadata($idpName) { + $metadata = IdpHelper::getMetadata($idpName); foreach ($metadata as $key => $value) { if (property_exists($key) && strpos("idp", $key) !== false) { $this->{$key} = $value; diff --git a/src/helper/IdpHelper.php b/src/helper/IdpHelper.php index c4c6b3e..272b606 100644 --- a/src/helper/IdpHelper.php +++ b/src/helper/IdpHelper.php @@ -6,15 +6,16 @@ class IdpHelper { public static function getMetadata($idpName) { - if (file_exists("../config/idp/" . $idpName)) { - $xml = simplexml_load_file("../config/idp/" . $idpName . '.xml'); - $metadata = array(); - $metadata['idp_entityid'] = $xml->xpath('//ns0:EntityDescriptor/@entityID')[0]; - $metadata['idp_sso'] = $xml->xpath('//ns0:SingleSignOnService/@Location')[0]; - $metadata['idp_slo'] = $xml->xpath('//ns0:SingleLogoutService/@Location')[0]; - $metadata['idp_cert'] = $xml->xpath('//ns1:X509Certificate')[0]; - - return $metadata; + if (!file_exists("../config/idp/" . $idpName)) { + throw new Exception("Invalid IDP Requested", 1); } + $xml = simplexml_load_file("../config/idp/" . $idpName . '.xml'); + $metadata = array(); + $metadata['idp_entityid'] = $xml->xpath('//ns0:EntityDescriptor/@entityID')[0]; + $metadata['idp_sso'] = $xml->xpath('//ns0:SingleSignOnService/@Location')[0]; + $metadata['idp_slo'] = $xml->xpath('//ns0:SingleLogoutService/@Location')[0]; + $metadata['idp_cert'] = $xml->xpath('//ns1:X509Certificate')[0]; + + return $metadata; } } \ No newline at end of file diff --git a/src/saml_strategy/PhpSamlInterface.php b/src/saml_strategy/PhpSamlInterface.php index 31b8488..e12b173 100644 --- a/src/saml_strategy/PhpSamlInterface.php +++ b/src/saml_strategy/PhpSamlInterface.php @@ -3,7 +3,8 @@ namespace SpidPHP\Strategy\Interfaces; interface PhpSamlInterface { + public function getSupportedIdps(); public function isAuthenticated(); - public function login(); + public function login( $idpName, $redirectTo = null, $level = 1 ); public function logout(); } \ No newline at end of file diff --git a/src/saml_strategy/PhpSamlOneLogin.php b/src/saml_strategy/PhpSamlOneLogin.php index 8b989f2..60993cf 100644 --- a/src/saml_strategy/PhpSamlOneLogin.php +++ b/src/saml_strategy/PhpSamlOneLogin.php @@ -14,23 +14,21 @@ class PhpSamlOneLogin implements PhpSamlInterface { - var $settings; - var $auth; + private $settings; + private $auth; - function __construct($idpMetadataFile, $spCertFile, $spKeyFile, $settings = null) + function __construct($settings) { - if (filter_var($idpMetadataFile, FILTER_VALIDATE_URL)) { - throw new Exception("The provided idp URL is not a valid URL", 1); - } - $this->init($idpMetadataFile, $spCertFile, $spKeyFile, $settings); + $this->settings = $settings; + $this->init($settings); print_r($this->settings); } - private function init($idpMetadataFile, $spCertFile, $spKeyFile, $settings) + private function init($idpName) { $settingsHelper = new OneloginSamlConfig(); - if (!is_null($settings)) { - $diff = ArrayHelper::array_diff_key_recursive($settings, get_object_vars($settingsHelper)); + if (!is_null($this->settings)) { + $diff = ArrayHelper::array_diff_key_recursive($this->settings, get_object_vars($settingsHelper)); if (!empty($diff)) { $message = "The following keys are invalid for the provided settings array: "; array_walk_recursive($diff, function ($v, $k) { @@ -38,27 +36,24 @@ private function init($idpMetadataFile, $spCertFile, $spKeyFile, $settings) }); throw new Exception($message, 1); } - $settingsHelper->updateSpSettings($settings); + $settingsHelper->updateSettings($this->settings); } - $metadata = IdpHelper::getMetadata($idpMetadataFile); - $settingsHelper->updateIdpMetadata($metadata); - - $sp = SpHelper::getSpCert($spCertFile, $spKeyFile); - $settingsHelper->updateSpData($sp); - - $auth = new OneLogin_Saml2_Auth($settingsHelper->getSettings()); + $settingsHelper->updateIdpMetadata($idpName); + $this->auth = new OneLogin_Saml2_Auth($settingsHelper->getSettings()); } public function isAuthenticated() { + if (is_null($this->auth)) return false; if ($auth->isAuthenticated) { return false; } return true; } - public function login() + public function login( $idpName, $redirectTo = null, $level = 1 ) { + if (is_null($this->auth)) $this->init($idpName); if ($auth->isAuthenticated) { return false; } @@ -95,6 +90,7 @@ public function login() public function logout() { + if (is_null($this->auth)) return false; if (!$auth->isAuthenticated()) { return false; } From 88c69c1ba592a543c3c52a9204ef1f482f939ed6 Mon Sep 17 00:00:00 2001 From: Lorenzo Cattaneo Date: Thu, 26 Jul 2018 14:52:58 +0200 Subject: [PATCH 16/69] Fix namespace and test settings generation --- .vscode/launch.json | 22 ++++++++++++ composer.json | 10 +++--- composer.lock | 29 ++++++++-------- example/index.php | 25 ++++++-------- src/{helper => Helpers}/ArrayHelper.php | 0 src/{helper => Helpers}/IdpHelper.php | 2 +- src/{helper => Helpers}/SpHelper.php | 0 src/PhpSaml.php | 21 +++++------- .../Interfaces}/PhpSamlInterface.php | 0 .../PhpSamlOneLogin.php | 34 ++++++++++--------- src/config/OneloginSamlConfig.php | 28 +++++++-------- 11 files changed, 94 insertions(+), 77 deletions(-) create mode 100644 .vscode/launch.json rename src/{helper => Helpers}/ArrayHelper.php (100%) rename src/{helper => Helpers}/IdpHelper.php (91%) rename src/{helper => Helpers}/SpHelper.php (100%) rename src/{saml_strategy => Strategy/Interfaces}/PhpSamlInterface.php (100%) rename src/{saml_strategy => Strategy}/PhpSamlOneLogin.php (75%) diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..9d9ed65 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,22 @@ +{ + // Usare IntelliSense per informazioni sui possibili attributi. + // Al passaggio del mouse vengono visualizzate le descrizioni degli attributi esistenti. + // Per ulteriori informazioni, visitare: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Listen for XDebug", + "type": "php", + "request": "launch", + "port": 9000 + }, + { + "name": "Launch currently open script", + "type": "php", + "request": "launch", + "program": "${file}", + "cwd": "${fileDirname}", + "port": 9000 + } + ] +} \ No newline at end of file diff --git a/composer.json b/composer.json index 4274d76..6d293f2 100644 --- a/composer.json +++ b/composer.json @@ -12,11 +12,11 @@ ], "post-update-cmd": [ "setup\\Configuration::setup" - ], - "autoload": { - "psr-4": { - "SpidPHP\\": "src/" - } + ] + }, + "autoload": { + "psr-4": { + "SpidPHP\\": "src/" } } } diff --git a/composer.lock b/composer.lock index 4a6f9a3..bedc328 100644 --- a/composer.lock +++ b/composer.lock @@ -1,7 +1,7 @@ { "_readme": [ "This file locks the dependencies of your project to a known state", - "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], "content-hash": "d0f2b5c5b2a0656dd94e55792b743bdf", @@ -12,12 +12,12 @@ "source": { "type": "git", "url": "https://github.com/onelogin/php-saml.git", - "reference": "c98647228e5260004fe6bc31158f322ea94c152c" + "reference": "03efada0d2485268576a810834f2d61f9ff659aa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/onelogin/php-saml/zipball/c98647228e5260004fe6bc31158f322ea94c152c", - "reference": "c98647228e5260004fe6bc31158f322ea94c152c", + "url": "https://api.github.com/repos/onelogin/php-saml/zipball/03efada0d2485268576a810834f2d61f9ff659aa", + "reference": "03efada0d2485268576a810834f2d61f9ff659aa", "shasum": "" }, "require": { @@ -54,7 +54,7 @@ "onelogin", "saml" ], - "time": "2018-06-19T00:33:13+00:00" + "time": "2018-07-08T16:01:32+00:00" }, { "name": "robrichards/xmlseclibs", @@ -263,7 +263,7 @@ }, { "name": "symfony/yaml", - "version": "v4.1.1", + "version": "v4.1.2", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", @@ -322,20 +322,21 @@ }, { "name": "twig/twig", - "version": "v2.4.8", + "version": "v2.5.0", "source": { "type": "git", "url": "https://github.com/twigphp/Twig.git", - "reference": "7b604c89da162034bdf4bb66310f358d313dd16d" + "reference": "6a5f676b77a90823c2d4eaf76137b771adf31323" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/Twig/zipball/7b604c89da162034bdf4bb66310f358d313dd16d", - "reference": "7b604c89da162034bdf4bb66310f358d313dd16d", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/6a5f676b77a90823c2d4eaf76137b771adf31323", + "reference": "6a5f676b77a90823c2d4eaf76137b771adf31323", "shasum": "" }, "require": { "php": "^7.0", + "symfony/polyfill-ctype": "^1.8", "symfony/polyfill-mbstring": "~1.0" }, "require-dev": { @@ -346,7 +347,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.4-dev" + "dev-master": "2.5-dev" } }, "autoload": { @@ -375,16 +376,16 @@ }, { "name": "Twig Team", - "homepage": "http://twig.sensiolabs.org/contributors", + "homepage": "https://twig.symfony.com/contributors", "role": "Contributors" } ], "description": "Twig, the flexible, fast, and secure template language for PHP", - "homepage": "http://twig.sensiolabs.org", + "homepage": "https://twig.symfony.com", "keywords": [ "templating" ], - "time": "2018-04-02T09:24:19+00:00" + "time": "2018-07-13T07:18:09+00:00" } ], "packages-dev": [], diff --git a/example/index.php b/example/index.php index 48604af..a5abf56 100644 --- a/example/index.php +++ b/example/index.php @@ -3,27 +3,22 @@ /** * SAML Handler */ -session_start(); -require_once("../vendor/autoload.php"); +require_once(__DIR__ . "/../vendor/autoload.php"); use SpidPHP\PhpSaml; $settings = [ - 'sp' => array( - 'entityId' => $this->spEntityId, - 'assertionConsumerService' => array( - 'url' => $this->spAcsUrl, - ), - 'singleLogoutService' => array( - 'url' => $this->spSloUrl, - ), - 'NameIDFormat' => 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient', - ), + 'spBaseUrl' => "BASE URL", + 'spEntityId' => "ENTITY ID", + 'spKeyFile' => __DIR__ . "/../sp.key", + 'spCrtFile' => __DIR__ . "/../sp.crt", ]; -$onelogin = new PhpSaml("http://idp.simevo.com", "/sp.key", "/sp.crt", $settings); + print_r($settings); -if (!$onelogin->isAuthenticated()) $onelogin->login(); +$onelogin = new PhpSaml($settings); +$onelogin->login("nome"); +//if (!$onelogin->isAuthenticated()) $onelogin->login(); -if ($onelogin->login()) $onelogin->logout(); +//if ($onelogin->login()) $onelogin->logout(); diff --git a/src/helper/ArrayHelper.php b/src/Helpers/ArrayHelper.php similarity index 100% rename from src/helper/ArrayHelper.php rename to src/Helpers/ArrayHelper.php diff --git a/src/helper/IdpHelper.php b/src/Helpers/IdpHelper.php similarity index 91% rename from src/helper/IdpHelper.php rename to src/Helpers/IdpHelper.php index 272b606..7fcea81 100644 --- a/src/helper/IdpHelper.php +++ b/src/Helpers/IdpHelper.php @@ -7,7 +7,7 @@ class IdpHelper public static function getMetadata($idpName) { if (!file_exists("../config/idp/" . $idpName)) { - throw new Exception("Invalid IDP Requested", 1); + throw new \Exception("Invalid IDP Requested", 1); } $xml = simplexml_load_file("../config/idp/" . $idpName . '.xml'); $metadata = array(); diff --git a/src/helper/SpHelper.php b/src/Helpers/SpHelper.php similarity index 100% rename from src/helper/SpHelper.php rename to src/Helpers/SpHelper.php diff --git a/src/PhpSaml.php b/src/PhpSaml.php index 693cc8b..22267a4 100644 --- a/src/PhpSaml.php +++ b/src/PhpSaml.php @@ -1,5 +1,4 @@ mode = $mode; $this->settings = $settings; } - private function initStrategy() + private function initStrategy($idpName) { switch ($this->mode) { case 'onelogin': - $this->phpSaml = new PhpSamlOneLogin($this->settings); + $this->phpSaml = new PhpSamlOneLogin($idpName, $this->settings); break; default: - $this->phpSaml = new PhpSamlOneLogin($this->settings); + $this->phpSaml = new PhpSamlOneLogin($idpName, $this->settings); break; } } @@ -42,17 +35,21 @@ public function getSupportedIdps() } public function isAuthenticated() - { + { + if (is_null($this->phpSaml)) return false; $this->phpSaml->isAuthenticated(); } public function login( $idpName, $redirectTo = null, $level = 1 ) { - $this->phpSaml->login(); + if (is_null($this->phpSaml)) $this->initStrategy($idpName); + die(); + $this->phpSaml->login($redirectTo); } public function logout() { + if (is_null($this->phpSaml)) return false; $this->phpSaml->logout(); } diff --git a/src/saml_strategy/PhpSamlInterface.php b/src/Strategy/Interfaces/PhpSamlInterface.php similarity index 100% rename from src/saml_strategy/PhpSamlInterface.php rename to src/Strategy/Interfaces/PhpSamlInterface.php diff --git a/src/saml_strategy/PhpSamlOneLogin.php b/src/Strategy/PhpSamlOneLogin.php similarity index 75% rename from src/saml_strategy/PhpSamlOneLogin.php rename to src/Strategy/PhpSamlOneLogin.php index 60993cf..6df67fe 100644 --- a/src/saml_strategy/PhpSamlOneLogin.php +++ b/src/Strategy/PhpSamlOneLogin.php @@ -13,38 +13,42 @@ class PhpSamlOneLogin implements PhpSamlInterface { - - private $settings; private $auth; - function __construct($settings) + function __construct($idpName, $settings) { - $this->settings = $settings; - $this->init($settings); + $this->init($idpName, $settings); print_r($this->settings); } - private function init($idpName) + private function init($idpName, $settings) { $settingsHelper = new OneloginSamlConfig(); - if (!is_null($this->settings)) { - $diff = ArrayHelper::array_diff_key_recursive($this->settings, get_object_vars($settingsHelper)); + if (!is_null($settings)) { + $diff = ArrayHelper::array_diff_key_recursive($settings, get_object_vars($settingsHelper)); if (!empty($diff)) { $message = "The following keys are invalid for the provided settings array: "; - array_walk_recursive($diff, function ($v, $k) { - $message .= $k . ", "; - }); - throw new Exception($message, 1); + $first = true; + foreach ($diff as $key => $value) { + if ($first) $message .= $key; + $first = false; + $message .= ", " . $key; + } + throw new \Exception($message, 1); } - $settingsHelper->updateSettings($this->settings); + $settingsHelper->updateSettings($settings); } $settingsHelper->updateIdpMetadata($idpName); $this->auth = new OneLogin_Saml2_Auth($settingsHelper->getSettings()); } + public function getSupportedIdps() + { + return array(); + } + public function isAuthenticated() { - if (is_null($this->auth)) return false; if ($auth->isAuthenticated) { return false; } @@ -53,7 +57,6 @@ public function isAuthenticated() public function login( $idpName, $redirectTo = null, $level = 1 ) { - if (is_null($this->auth)) $this->init($idpName); if ($auth->isAuthenticated) { return false; } @@ -90,7 +93,6 @@ public function login( $idpName, $redirectTo = null, $level = 1 ) public function logout() { - if (is_null($this->auth)) return false; if (!$auth->isAuthenticated()) { return false; } diff --git a/src/config/OneloginSamlConfig.php b/src/config/OneloginSamlConfig.php index 22137dc..4239951 100644 --- a/src/config/OneloginSamlConfig.php +++ b/src/config/OneloginSamlConfig.php @@ -9,19 +9,19 @@ class OneloginSamlConfig { // Default values SP - private $spBaseUrl = ''; - private $spEntityId = null; - private $spKeyFile = 'sp.key'; - private $spCrtFile = 'sp.crt'; + var $spBaseUrl = ''; + var $spEntityId = null; + var $spKeyFile = 'sp.key'; + var $spCrtFile = 'sp.crt'; private $spKeyFileValue = null; private $spCrtFileValue = null; - private $spAcsUrl = null; - private $spSloUrl = null; + var $spAcsUrl = null; + var $spSloUrl = null; // Default values IDP - private $idpEntityId = null; - private $idpSSO = null; - private $idpSLO = null; - private $idpCertFile = null; + var $idpEntityId = null; + var $idpSSO = null; + var $idpSLO = null; + var $idpCertFile = null; private $is_not_updatable = ['spKeyFileValue', 'spCrtFileValue', 'idpEntityId', 'idpSSO', 'idpSLO', 'idpCertFile']; @@ -81,7 +81,7 @@ public function getSettings() public function updateSettings($settings) { foreach ($settings as $key => $value) { // do not update idp os sp cert file values, they are updated in their own method - if (!property_exists($key)) { + if (!property_exists(OneloginSamlConfig::class, $key)) { continue; } if (in_array($key, $this->is_not_updatable)) { @@ -92,7 +92,7 @@ public function updateSettings($settings) { } // Get .key and .cert files content and add it to configuration if (!file_exists($this->spKeyFile) || !file_exists($this->spCrtFile)) { - throw new Exception("The path for .key and .cert files is invalid", 1); + throw new \Exception("The path for .key and .cert files is invalid", 1); } $sp = SpHelper::getSpCert($this->spKeyFile, $this->spCrtFile); $this->updateSpData($sp); @@ -102,7 +102,7 @@ public function updateSettings($settings) { public function updateIdpMetadata($idpName) { $metadata = IdpHelper::getMetadata($idpName); foreach ($metadata as $key => $value) { - if (property_exists($key) && strpos("idp", $key) !== false) { + if (property_exists(OneloginSamlConfig::class, $key) && strpos("idp", $key) !== false) { $this->{$key} = $value; } } @@ -111,7 +111,7 @@ public function updateIdpMetadata($idpName) { public function updateSpData($sp) { if (!is_array($sp)) - throw new Exception("Invalid SP certificate data provided", 1); + throw new \Exception("Invalid SP certificate data provided", 1); $this->spKeyFile = $sp['key']; $this->spCrtFile = $sp['cert']; From cf34f1a85d2be27058ed257810c2f8584ee0bb1b Mon Sep 17 00:00:00 2001 From: Lorenzo Cattaneo Date: Thu, 26 Jul 2018 15:52:03 +0200 Subject: [PATCH 17/69] improved settings generation --- example/index.php | 8 +++--- src/.DS_Store | Bin 0 -> 6148 bytes src/Config/.DS_Store | Bin 0 -> 6148 bytes src/Config/idp/.DS_Store | Bin 0 -> 6148 bytes src/Config/idp/testenv2.xml | 39 ++++++++++++++++++++++++++++++ src/Helpers/IdpHelper.php | 13 +++++----- src/Strategy/PhpSamlOneLogin.php | 3 ++- src/config/OneloginSamlConfig.php | 8 +++--- 8 files changed, 57 insertions(+), 14 deletions(-) create mode 100644 src/.DS_Store create mode 100644 src/Config/.DS_Store create mode 100644 src/Config/idp/.DS_Store create mode 100644 src/Config/idp/testenv2.xml diff --git a/example/index.php b/example/index.php index a5abf56..ef870d5 100644 --- a/example/index.php +++ b/example/index.php @@ -9,16 +9,18 @@ use SpidPHP\PhpSaml; $settings = [ - 'spBaseUrl' => "BASE URL", - 'spEntityId' => "ENTITY ID", + 'spBaseUrl' => "sp.simevo.com:8000", + 'spEntityId' => "sp.simevo.com:8000/metadata.php", 'spKeyFile' => __DIR__ . "/../sp.key", 'spCrtFile' => __DIR__ . "/../sp.crt", + 'spAcsUrl' => "sp.simevo.com:8000/index.php?acs", + 'spSloUrl' => "sp.simevo.com:8000/index.php?slo" ]; print_r($settings); $onelogin = new PhpSaml($settings); -$onelogin->login("nome"); +$onelogin->login("testenv2"); //if (!$onelogin->isAuthenticated()) $onelogin->login(); //if ($onelogin->login()) $onelogin->logout(); diff --git a/src/.DS_Store b/src/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..a500411ee3c77ad6f80303e03aa780d1f91ce863 GIT binary patch literal 6148 zcmeHK!EVz)5S>j!x=Dq|0jWLug2W+0HKF2wDp_eO(OV@WH~?yGViHTPH;Nsi5>olX zN5CKODSQE+18;Vhs!rh03qr6X&A!?3ytVhO<@FMg7|ez~QJ07$oUzeH^9$qa>}%Gr znHEs!F;W^GXX;Iysd%m0CH_VQc$gvLCLEZX+Bi*ftuyRLXQ)g3sSaa`?R~WSo9zFyhnb2+4B~@-+$Qi_V)bc zvMsmo`TNIb=NIo6AC{M&^fXA|h1GV);!pUDpf<^O;WW=wK0=;3oAi_t%4mXiO2xI; zo#pxhzb@AnozNJyAw_f!)&s=o*IDA@3QMBC#OM*)ghq6+Qop@YkFGPPfK$LJa1#aW zCs1zPgyd?S0#1SdLIM6hcyPwRVr5W&I?(7N0I-R)HpKiN#2n9JV6ifYD==ZGKtmPw zh#?FedC%4b7Au2>PQo5OguSz{ClsOYj`2NRP9iYqTBm?hpsv7%>2`VlKl=UozfN*J zr+`!7pHe_H55vO&reyclwaM{b>%q_9Y+P3vyrLk`TQO>RE8c-?L*C;67+9J%rYLs$C0eK|*&5UQf>*tldLqwuC%|fC!5lJ-0S`*zJ!sFalq~<&g zpx`<5=d{o%ES}EGzrX?cX*=Ui>y<7UH7P*<|E)`DZ#R@=3Y5`AgL5 zjeGYWNZFFDN3BosP|xBrDW{WOGCpK$KPiW)(?4ik+~`SqkoRZD?I$~0l}WDpW0Ra` zeS{o+%vGjmJv~*~#LN@h0V$iZdEDNZ&%19r!RxTQ=mhgH?7r#*TU+5`(Ui}ghi~^z zPS3y2zb(H1usb2}ks5jI@ER_lxaFP2V^wH1fQO@>(tA?Wr-C#MK$D`F5;}#w1COFF z@D1oCjcB++;3e{J6A06bXTUSy8CVYn+;J!$uE*>6x9|*j2G)oHJ|6-!#>io1(HtG< zObGzw-$*0S&CfqDUItlY+%;TOcT!kWB z=^+9WP9n1Cd(VJpV3~osHQRjuzx#9jzntVP&wyv(zhXc%cH`Y1mgMf%mBsO08=#$_ qv2a{v@g@a|xr!0XSMdg#5%>dEfRV$>B0LcPBVcIoooC>$GH?Z3B5R)j literal 0 HcmV?d00001 diff --git a/src/Config/idp/.DS_Store b/src/Config/idp/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..505157fb3008733e968f85caab1c60219c19f558 GIT binary patch literal 6148 zcmeHKyH3ME5S)b+k)TLPc_kGRe8DLS1vNi_0(6EH1VrhIf8j6qGnjn{6d4*M8Z;~I z&fR*)XHVhv0#lzJ6y8FcTDq}=C?{SNJJmCx{c%D>$pK$IWwq!m; z{NmqXxEMCq>tVBIjX&Tk{rEkME149K0#ZNX}$M^Th@&DXr%3&+HuGaq!Keg<3@nH2bI1wH^OHXL>U literal 0 HcmV?d00001 diff --git a/src/Config/idp/testenv2.xml b/src/Config/idp/testenv2.xml new file mode 100644 index 0000000..1030cb0 --- /dev/null +++ b/src/Config/idp/testenv2.xml @@ -0,0 +1,39 @@ + + + + + + + + MIIDiDCCAnCgAwIBAgIJAJnqANY7GvtNMA0GCSqGSIb3DQEBCwUAMFkxCzAJBgNV + BAYTAklUMQ4wDAYDVQQIDAVJdGFseTENMAsGA1UEBwwEUm9tZTERMA8GA1UECgwI + dGVzdGVudjIxGDAWBgNVBAMMDyR7YW5zaWJsZV9mcWRufTAeFw0xODA3MTkxMzEw + MDVaFw0xOTA3MTkxMzEwMDVaMFkxCzAJBgNVBAYTAklUMQ4wDAYDVQQIDAVJdGFs + eTENMAsGA1UEBwwEUm9tZTERMA8GA1UECgwIdGVzdGVudjIxGDAWBgNVBAMMDyR7 + YW5zaWJsZV9mcWRufTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALTP + ycItk7xvubL0WgegvbtCvhzeCq13WorzjYQdpWvUwY8YmL51h6ssKfWMfnSj96Ix + lJw2HTm+TgHK2/S7Iw7gh6xoaY+LDmkZGJKzUSFR/Hn7WbFNb3jytowiNDQdcBZd + hNhEnDvd2fLMGxD81qdlMx3y5XHP+p9Gc8bjp8aShGw+DQpWBXcfoDnCXz5ywmnR + oD66CnMFMQXmD0wZf79/0fY+Muwn83r7P0h6bJCFDjPdGiSeo7q5AJKkERoNoedc + HThCuT0uN36bDaVBIcSxVtPjvVhfcNIVN5JHaCLZT89aU8DAJlTUiO3u5Nj4aDjD + XAhxMWkwGbHXCznhjVMCAwEAAaNTMFEwHQYDVR0OBBYEFH1/ReU+04oYtVpMgD/z + VCPuagu6MB8GA1UdIwQYMBaAFH1/ReU+04oYtVpMgD/zVCPuagu6MA8GA1UdEwEB + /wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAAWT9+r+ojVmxUUZvq8/TumEX9Y0 + FxxQebTcPMeumA58mp9kDCeNK73PZ6cbQSGXzwbNmHw48N2kZMl3rS8ddYPG3nFx + EZO5Xi0De2SLGwLZX43lfD4BHhhqhTlnBK8cL+LvySQ32X1vL8aKly/UTez3/DCr + dTqFjp1V0PdY2q0Ni3UAiO90MpFGbP+aAQ1LGtI0EWpDCVAqqgAA2EA+s0AlwbC5 + cuDbQUjHA5dhkWT/4kvHB/E5zPZUtRV4d3mYBs33lfIyKXWjjgR4T8j01wMk2LWC + nBU8i4mCb1w3v94KcRbnK23bJLz+zNp6I3belhLknSahyjKPl+6FBSyW/GE= + + + + + + + + urn:oasis:names:tc:SAML:2.0:nameid-format:transient + + + + + diff --git a/src/Helpers/IdpHelper.php b/src/Helpers/IdpHelper.php index 7fcea81..ed5fd6f 100644 --- a/src/Helpers/IdpHelper.php +++ b/src/Helpers/IdpHelper.php @@ -6,15 +6,16 @@ class IdpHelper { public static function getMetadata($idpName) { - if (!file_exists("../config/idp/" . $idpName)) { + if (!file_exists(__DIR__ . "/../config/idp/" . $idpName . ".xml")) { throw new \Exception("Invalid IDP Requested", 1); } - $xml = simplexml_load_file("../config/idp/" . $idpName . '.xml'); + $xml = simplexml_load_file(__DIR__ . "/../config/idp/" . $idpName . '.xml'); + $metadata = array(); - $metadata['idp_entityid'] = $xml->xpath('//ns0:EntityDescriptor/@entityID')[0]; - $metadata['idp_sso'] = $xml->xpath('//ns0:SingleSignOnService/@Location')[0]; - $metadata['idp_slo'] = $xml->xpath('//ns0:SingleLogoutService/@Location')[0]; - $metadata['idp_cert'] = $xml->xpath('//ns1:X509Certificate')[0]; + $metadata['idpEntityId'] = $xml->attributes()->entityID; + $metadata['idpSSO'] = $xml->xpath('//SingleSignOnService')[0]->attributes()->Location; + $metadata['idpSLO'] = $xml->xpath('//SingleLogoutService')[0]->attributes()->Location; + $metadata['idpCertValue'] = $xml->xpath('//X509Certificate')[0]; return $metadata; } diff --git a/src/Strategy/PhpSamlOneLogin.php b/src/Strategy/PhpSamlOneLogin.php index 6df67fe..5f38208 100644 --- a/src/Strategy/PhpSamlOneLogin.php +++ b/src/Strategy/PhpSamlOneLogin.php @@ -39,7 +39,8 @@ private function init($idpName, $settings) $settingsHelper->updateSettings($settings); } $settingsHelper->updateIdpMetadata($idpName); - $this->auth = new OneLogin_Saml2_Auth($settingsHelper->getSettings()); + print_r($settingsHelper->getSettings()); die(); + $this->auth = new Auth($settingsHelper->getSettings()); } public function getSupportedIdps() diff --git a/src/config/OneloginSamlConfig.php b/src/config/OneloginSamlConfig.php index 4239951..dfc452f 100644 --- a/src/config/OneloginSamlConfig.php +++ b/src/config/OneloginSamlConfig.php @@ -21,9 +21,9 @@ class OneloginSamlConfig var $idpEntityId = null; var $idpSSO = null; var $idpSLO = null; - var $idpCertFile = null; + var $idpCertValue = null; - private $is_not_updatable = ['spKeyFileValue', 'spCrtFileValue', 'idpEntityId', 'idpSSO', 'idpSLO', 'idpCertFile']; + private $is_not_updatable = ['spKeyFileValue', 'spCrtFileValue', 'idpEntityId', 'idpSSO', 'idpSLO', 'idpCertValue']; function __construct() { @@ -65,7 +65,7 @@ public function getSettings() 'singleLogoutService' => array( 'url' => $this->idpSLO, ), - 'x509cert' => $this->idpCertFile, + 'x509cert' => $this->idpCertValue, ), 'security' => array( 'authnRequestsSigned' => true, @@ -102,7 +102,7 @@ public function updateSettings($settings) { public function updateIdpMetadata($idpName) { $metadata = IdpHelper::getMetadata($idpName); foreach ($metadata as $key => $value) { - if (property_exists(OneloginSamlConfig::class, $key) && strpos("idp", $key) !== false) { + if (property_exists(OneloginSamlConfig::class, $key) && strpos($key, "idp") !== false) { $this->{$key} = $value; } } From af5b721203fb96e8674d88777b09aed00a166075 Mon Sep 17 00:00:00 2001 From: Lorenzo Cattaneo Date: Thu, 26 Jul 2018 16:00:07 +0200 Subject: [PATCH 18/69] fix variable references --- example/index.php | 10 +++++----- src/PhpSaml.php | 1 - src/Strategy/PhpSamlOneLogin.php | 28 +++++++++++++--------------- 3 files changed, 18 insertions(+), 21 deletions(-) diff --git a/example/index.php b/example/index.php index ef870d5..c6d46a8 100644 --- a/example/index.php +++ b/example/index.php @@ -7,14 +7,14 @@ require_once(__DIR__ . "/../vendor/autoload.php"); use SpidPHP\PhpSaml; - +$base = "http://sp.simevo.com:8000"; $settings = [ - 'spBaseUrl' => "sp.simevo.com:8000", - 'spEntityId' => "sp.simevo.com:8000/metadata.php", + 'spBaseUrl' => $base, + 'spEntityId' => $base."/metadata.php", 'spKeyFile' => __DIR__ . "/../sp.key", 'spCrtFile' => __DIR__ . "/../sp.crt", - 'spAcsUrl' => "sp.simevo.com:8000/index.php?acs", - 'spSloUrl' => "sp.simevo.com:8000/index.php?slo" + 'spAcsUrl' => $base."/index.php?acs", + 'spSloUrl' => $base."/index.php?slo" ]; print_r($settings); diff --git a/src/PhpSaml.php b/src/PhpSaml.php index 22267a4..db35c4c 100644 --- a/src/PhpSaml.php +++ b/src/PhpSaml.php @@ -43,7 +43,6 @@ public function isAuthenticated() public function login( $idpName, $redirectTo = null, $level = 1 ) { if (is_null($this->phpSaml)) $this->initStrategy($idpName); - die(); $this->phpSaml->login($redirectTo); } diff --git a/src/Strategy/PhpSamlOneLogin.php b/src/Strategy/PhpSamlOneLogin.php index 5f38208..7a12c38 100644 --- a/src/Strategy/PhpSamlOneLogin.php +++ b/src/Strategy/PhpSamlOneLogin.php @@ -18,7 +18,6 @@ class PhpSamlOneLogin implements PhpSamlInterface function __construct($idpName, $settings) { $this->init($idpName, $settings); - print_r($this->settings); } private function init($idpName, $settings) @@ -39,7 +38,6 @@ private function init($idpName, $settings) $settingsHelper->updateSettings($settings); } $settingsHelper->updateIdpMetadata($idpName); - print_r($settingsHelper->getSettings()); die(); $this->auth = new Auth($settingsHelper->getSettings()); } @@ -58,32 +56,32 @@ public function isAuthenticated() public function login( $idpName, $redirectTo = null, $level = 1 ) { - if ($auth->isAuthenticated) { + if ($this->auth->isAuthenticated) { return false; } - $auth->login(); + $this->auth->login(); $requestID = null; if (isset($_SESSION['AuthNRequestID'])) { $requestID = $_SESSION['AuthNRequestID']; } - $auth->processResponse($requestID); + $this->auth->processResponse($requestID); unset($_SESSION['AuthNRequestID']); - $errors = $auth->getErrors(); + $errors = $this->auth->getErrors(); if (!empty($errors)) { return $errors; } - if (!$auth->isAuthenticated()) { + if (!$this->auth->isAuthenticated()) { return false; } - $_SESSION['samlUserdata'] = $auth->getAttributes(); - $_SESSION['samlNameId'] = $auth->getNameId(); - $_SESSION['samlNameIdFormat'] = $auth->getNameIdFormat(); - $_SESSION['samlSessionIndex'] = $auth->getSessionIndex(); + $_SESSION['samlUserdata'] = $this->auth->getAttributes(); + $_SESSION['samlNameId'] = $this->auth->getNameId(); + $_SESSION['samlNameIdFormat'] = $this->auth->getNameIdFormat(); + $_SESSION['samlSessionIndex'] = $this->auth->getSessionIndex(); if (!empty($_SESSION['samlUserdata'])) { return true; @@ -94,13 +92,13 @@ public function login( $idpName, $redirectTo = null, $level = 1 ) public function logout() { - if (!$auth->isAuthenticated()) { + if (!$this->auth->isAuthenticated()) { return false; } - $auth->logout(); - $auth->processSLO(); + $this->auth->logout(); + $this->auth->processSLO(); - $errors = $auth->getErrors(); + $errors = $this->auth->getErrors(); if (!empty($errors)) { return $errors; } From 7edd48371509269bdcea5bee245a41a71ab1bfc8 Mon Sep 17 00:00:00 2001 From: Lorenzo Cattaneo Date: Thu, 26 Jul 2018 16:16:13 +0200 Subject: [PATCH 19/69] fixed xml parsing --- example/index.php | 6 +++--- src/Helpers/IdpHelper.php | 8 ++++---- src/PhpSaml.php | 2 +- src/Strategy/Interfaces/PhpSamlInterface.php | 2 +- src/Strategy/PhpSamlOneLogin.php | 9 +++++---- 5 files changed, 14 insertions(+), 13 deletions(-) diff --git a/example/index.php b/example/index.php index c6d46a8..2f02790 100644 --- a/example/index.php +++ b/example/index.php @@ -17,10 +17,10 @@ 'spSloUrl' => $base."/index.php?slo" ]; - print_r($settings); - $onelogin = new PhpSaml($settings); -$onelogin->login("testenv2"); +$result = $onelogin->login("testenv2"); + +var_dump($result); //if (!$onelogin->isAuthenticated()) $onelogin->login(); //if ($onelogin->login()) $onelogin->logout(); diff --git a/src/Helpers/IdpHelper.php b/src/Helpers/IdpHelper.php index ed5fd6f..44bf76c 100644 --- a/src/Helpers/IdpHelper.php +++ b/src/Helpers/IdpHelper.php @@ -12,10 +12,10 @@ public static function getMetadata($idpName) $xml = simplexml_load_file(__DIR__ . "/../config/idp/" . $idpName . '.xml'); $metadata = array(); - $metadata['idpEntityId'] = $xml->attributes()->entityID; - $metadata['idpSSO'] = $xml->xpath('//SingleSignOnService')[0]->attributes()->Location; - $metadata['idpSLO'] = $xml->xpath('//SingleLogoutService')[0]->attributes()->Location; - $metadata['idpCertValue'] = $xml->xpath('//X509Certificate')[0]; + $metadata['idpEntityId'] = $xml->attributes()->entityID->__toString(); + $metadata['idpSSO'] = $xml->xpath('//SingleSignOnService')[0]->attributes()->Location->__toString(); + $metadata['idpSLO'] = $xml->xpath('//SingleLogoutService')[0]->attributes()->Location->__toString(); + $metadata['idpCertValue'] = $xml->xpath('//X509Certificate')[0]->__toString(); return $metadata; } diff --git a/src/PhpSaml.php b/src/PhpSaml.php index db35c4c..2f7a977 100644 --- a/src/PhpSaml.php +++ b/src/PhpSaml.php @@ -40,7 +40,7 @@ public function isAuthenticated() $this->phpSaml->isAuthenticated(); } - public function login( $idpName, $redirectTo = null, $level = 1 ) + public function login( $idpName, $redirectTo = '', $level = 1 ) { if (is_null($this->phpSaml)) $this->initStrategy($idpName); $this->phpSaml->login($redirectTo); diff --git a/src/Strategy/Interfaces/PhpSamlInterface.php b/src/Strategy/Interfaces/PhpSamlInterface.php index e12b173..1bb6b7b 100644 --- a/src/Strategy/Interfaces/PhpSamlInterface.php +++ b/src/Strategy/Interfaces/PhpSamlInterface.php @@ -5,6 +5,6 @@ interface PhpSamlInterface { public function getSupportedIdps(); public function isAuthenticated(); - public function login( $idpName, $redirectTo = null, $level = 1 ); + public function login( $idpName, $redirectTo = '', $level = 1 ); public function logout(); } \ No newline at end of file diff --git a/src/Strategy/PhpSamlOneLogin.php b/src/Strategy/PhpSamlOneLogin.php index 7a12c38..2dd669b 100644 --- a/src/Strategy/PhpSamlOneLogin.php +++ b/src/Strategy/PhpSamlOneLogin.php @@ -54,13 +54,14 @@ public function isAuthenticated() return true; } - public function login( $idpName, $redirectTo = null, $level = 1 ) + public function login( $idpName, $redirectTo = '', $level = 1 ) { - if ($this->auth->isAuthenticated) { + if ($this->auth->isAuthenticated()) { return false; } - $this->auth->login(); - + + $this->auth->login($redirectTo); + $requestID = null; if (isset($_SESSION['AuthNRequestID'])) { $requestID = $_SESSION['AuthNRequestID']; From 991d6e1171735b4695ba413367d78f42e29eb247 Mon Sep 17 00:00:00 2001 From: Lorenzo Cattaneo Date: Thu, 26 Jul 2018 18:18:01 +0200 Subject: [PATCH 20/69] WIP: sp metadata generation --- .DS_Store | Bin 8196 -> 8196 bytes example/index.php | 9 +-- example/metadata.php | 25 +++++++++ src/{PhpSaml.php => SpidPHP.php} | 11 +++- src/Strategy/Interfaces/PhpSamlInterface.php | 1 + src/Strategy/PhpSamlOneLogin.php | 55 ++++++++++++++++--- 6 files changed, 86 insertions(+), 15 deletions(-) create mode 100644 example/metadata.php rename src/{PhpSaml.php => SpidPHP.php} (82%) diff --git a/.DS_Store b/.DS_Store index 2137870ec5135ccb2df51ce4b88d941bb650d631..207e9eb093e9c48a6ed10101cd775d66007a764f 100644 GIT binary patch delta 43 zcmZp1XmOa}&nUJrU^hRb*km37*UjYu6WAv= delta 65 zcmZp1XmOa}&nUhzU^hRb_+%adS8-;BB!*0eJkOl` $base, @@ -17,10 +17,11 @@ 'spSloUrl' => $base."/index.php?slo" ]; -$onelogin = new PhpSaml($settings); -$result = $onelogin->login("testenv2"); +$onelogin = new SpidPHP($settings); +//$result = $onelogin->login("testenv2"); + +$metadata = $onelogin->getSPMetadata(); -var_dump($result); //if (!$onelogin->isAuthenticated()) $onelogin->login(); //if ($onelogin->login()) $onelogin->logout(); diff --git a/example/metadata.php b/example/metadata.php new file mode 100644 index 0000000..45199b5 --- /dev/null +++ b/example/metadata.php @@ -0,0 +1,25 @@ +getSettings(); + // Now we only validate SP settings + $settings = new OneLogin_Saml2_Settings($settingsInfo, true); + $metadata = $settings->getSPMetadata(); + $errors = $settings->validateMetadata($metadata); + if (empty($errors)) { + header('Content-Type: text/xml'); + echo $metadata; + } else { + throw new OneLogin_Saml2_Error( + 'Invalid SP metadata: '.implode(', ', $errors), + OneLogin_Saml2_Error::METADATA_SP_INVALID + ); + } +} catch (Exception $e) { + echo $e->getMessage(); +} \ No newline at end of file diff --git a/src/PhpSaml.php b/src/SpidPHP.php similarity index 82% rename from src/PhpSaml.php rename to src/SpidPHP.php index 2f7a977..96ecf93 100644 --- a/src/PhpSaml.php +++ b/src/SpidPHP.php @@ -4,7 +4,7 @@ use SpidPHP\Strategy\Interfaces\PhpSamlInterface; use SpidPHP\Strategy\PhpSamlOneLogin; -class PhpSaml implements PhpSamlInterface +class SpidPHP implements PhpSamlInterface { private $phpSaml = null; @@ -17,7 +17,7 @@ public function __construct($settings = null, $mode = 'onelogin') $this->settings = $settings; } - private function initStrategy($idpName) + private function initStrategy($idpName = null) { switch ($this->mode) { case 'onelogin': @@ -29,6 +29,12 @@ private function initStrategy($idpName) } } + public function getSPMetadata() + { + if (is_null($this->phpSaml)) $this->initStrategy(); + $this->phpSaml->getSPMetadata(); + } + public function getSupportedIdps() { return array(); @@ -43,6 +49,7 @@ public function isAuthenticated() public function login( $idpName, $redirectTo = '', $level = 1 ) { if (is_null($this->phpSaml)) $this->initStrategy($idpName); + return; $this->phpSaml->login($redirectTo); } diff --git a/src/Strategy/Interfaces/PhpSamlInterface.php b/src/Strategy/Interfaces/PhpSamlInterface.php index 1bb6b7b..aaa496d 100644 --- a/src/Strategy/Interfaces/PhpSamlInterface.php +++ b/src/Strategy/Interfaces/PhpSamlInterface.php @@ -3,6 +3,7 @@ namespace SpidPHP\Strategy\Interfaces; interface PhpSamlInterface { + public function getSPMetadata(); public function getSupportedIdps(); public function isAuthenticated(); public function login( $idpName, $redirectTo = '', $level = 1 ); diff --git a/src/Strategy/PhpSamlOneLogin.php b/src/Strategy/PhpSamlOneLogin.php index 2dd669b..e8c660f 100644 --- a/src/Strategy/PhpSamlOneLogin.php +++ b/src/Strategy/PhpSamlOneLogin.php @@ -10,21 +10,30 @@ use OneLogin\Saml2\Auth; use OneLogin\Saml2\Utils; +use OneLogin\Saml2\Settings; class PhpSamlOneLogin implements PhpSamlInterface { + private $idpName = null; + private $settings = null; + private $auth; + private $settingsHelper; + private $oneloginSettings; - function __construct($idpName, $settings) + function __construct($idpName = null, $settings) { - $this->init($idpName, $settings); + $this->idpName = $idpName ?? "testenv2"; + $this->settings = $settings; + $this->init(); } - private function init($idpName, $settings) + private function init() { $settingsHelper = new OneloginSamlConfig(); - if (!is_null($settings)) { - $diff = ArrayHelper::array_diff_key_recursive($settings, get_object_vars($settingsHelper)); + $this->settingsHelper = $settingsHelper; + if (!is_null($this->settings)) { + $diff = ArrayHelper::array_diff_key_recursive($this->settings, get_object_vars($settingsHelper)); if (!empty($diff)) { $message = "The following keys are invalid for the provided settings array: "; $first = true; @@ -35,10 +44,36 @@ private function init($idpName, $settings) } throw new \Exception($message, 1); } - $settingsHelper->updateSettings($settings); + $settingsHelper->updateSettings($this->settings); + } + + $this->settingsHelper->updateIdpMetadata($this->idpName); + $this->settings = new Settings($this->settingsHelper->getSettings()); + $this->auth = new Auth($this->settingsHelper->getSettings()); + } + + private function rebuildPhpSamlOnelogin($idpName) + { + if ($this->idpName != $idpName) { + $this->idpName = $idpName; + $this->init(); } - $settingsHelper->updateIdpMetadata($idpName); - $this->auth = new Auth($settingsHelper->getSettings()); + } + + public function getSPMetadata() + { + $oneloginSettings = new Settings($this->settings->getSettings()); + $metadata = $oneloginSettings->getSPMetadata(); + var_dump($oneloginSettings); + die(); + $errors = $oneloginSettings->validateMetadata($metadata); + if (!empty($errors)) { + throw new OneLogin_Saml2_Error( + 'Invalid SP metadata: '.implode(', ', $errors), + OneLogin_Saml2_Error::METADATA_SP_INVALID + ); + } + return $metadata; } public function getSupportedIdps() @@ -48,7 +83,7 @@ public function getSupportedIdps() public function isAuthenticated() { - if ($auth->isAuthenticated) { + if ($this->auth->isAuthenticated) { return false; } return true; @@ -56,6 +91,8 @@ public function isAuthenticated() public function login( $idpName, $redirectTo = '', $level = 1 ) { + $this->rebuildPhpSamlOnelogin($idpName); + if ($this->auth->isAuthenticated()) { return false; } From adecf84d42226c165060d1660fd192d99d91a447 Mon Sep 17 00:00:00 2001 From: Lorenzo Cattaneo Date: Fri, 27 Jul 2018 09:53:25 +0200 Subject: [PATCH 21/69] fixed SP metadata generation --- src/SpidPHP.php | 1 - src/Strategy/PhpSamlOneLogin.php | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/SpidPHP.php b/src/SpidPHP.php index 96ecf93..fbc8ea9 100644 --- a/src/SpidPHP.php +++ b/src/SpidPHP.php @@ -49,7 +49,6 @@ public function isAuthenticated() public function login( $idpName, $redirectTo = '', $level = 1 ) { if (is_null($this->phpSaml)) $this->initStrategy($idpName); - return; $this->phpSaml->login($redirectTo); } diff --git a/src/Strategy/PhpSamlOneLogin.php b/src/Strategy/PhpSamlOneLogin.php index e8c660f..263232a 100644 --- a/src/Strategy/PhpSamlOneLogin.php +++ b/src/Strategy/PhpSamlOneLogin.php @@ -62,9 +62,9 @@ private function rebuildPhpSamlOnelogin($idpName) public function getSPMetadata() { - $oneloginSettings = new Settings($this->settings->getSettings()); + $oneloginSettings = new Settings($this->settingsHelper->getSettings()); $metadata = $oneloginSettings->getSPMetadata(); - var_dump($oneloginSettings); + var_dump($metadata); die(); $errors = $oneloginSettings->validateMetadata($metadata); if (!empty($errors)) { From 1a806c6dd10d84e4861e9986c7207fd21fed15dc Mon Sep 17 00:00:00 2001 From: Lorenzo Cattaneo Date: Fri, 27 Jul 2018 12:02:47 +0200 Subject: [PATCH 22/69] fixed methods return data --- example/index.php | 4 +++- src/SpidPHP.php | 8 ++++---- src/Strategy/PhpSamlOneLogin.php | 3 +-- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/example/index.php b/example/index.php index c777a34..efe645d 100644 --- a/example/index.php +++ b/example/index.php @@ -7,7 +7,7 @@ require_once(__DIR__ . "/../vendor/autoload.php"); use SpidPHP\SpidPHP; -$base = "http://sp.simevo.com:8000"; +$base = "http://spid.test.com"; $settings = [ 'spBaseUrl' => $base, 'spEntityId' => $base."/metadata.php", @@ -22,6 +22,8 @@ $metadata = $onelogin->getSPMetadata(); +header('Content-Type: text/xml'); +echo $metadata; //if (!$onelogin->isAuthenticated()) $onelogin->login(); //if ($onelogin->login()) $onelogin->logout(); diff --git a/src/SpidPHP.php b/src/SpidPHP.php index fbc8ea9..194fae0 100644 --- a/src/SpidPHP.php +++ b/src/SpidPHP.php @@ -32,7 +32,7 @@ private function initStrategy($idpName = null) public function getSPMetadata() { if (is_null($this->phpSaml)) $this->initStrategy(); - $this->phpSaml->getSPMetadata(); + return $this->phpSaml->getSPMetadata(); } public function getSupportedIdps() @@ -43,19 +43,19 @@ public function getSupportedIdps() public function isAuthenticated() { if (is_null($this->phpSaml)) return false; - $this->phpSaml->isAuthenticated(); + return $this->phpSaml->isAuthenticated(); } public function login( $idpName, $redirectTo = '', $level = 1 ) { if (is_null($this->phpSaml)) $this->initStrategy($idpName); - $this->phpSaml->login($redirectTo); + return $this->phpSaml->login($redirectTo); } public function logout() { if (is_null($this->phpSaml)) return false; - $this->phpSaml->logout(); + return $this->phpSaml->logout(); } } \ No newline at end of file diff --git a/src/Strategy/PhpSamlOneLogin.php b/src/Strategy/PhpSamlOneLogin.php index 263232a..5d55062 100644 --- a/src/Strategy/PhpSamlOneLogin.php +++ b/src/Strategy/PhpSamlOneLogin.php @@ -64,8 +64,7 @@ public function getSPMetadata() { $oneloginSettings = new Settings($this->settingsHelper->getSettings()); $metadata = $oneloginSettings->getSPMetadata(); - var_dump($metadata); - die(); + $errors = $oneloginSettings->validateMetadata($metadata); if (!empty($errors)) { throw new OneLogin_Saml2_Error( From 98f3a20d2ba4dbac37d200e40dbb9a4886c7f650 Mon Sep 17 00:00:00 2001 From: Lorenzo Cattaneo Date: Fri, 27 Jul 2018 12:20:00 +0200 Subject: [PATCH 23/69] removed useless dependencies and fix case sensitive names --- composer.json | 2 -- src/Helpers/IdpHelper.php | 4 ++-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/composer.json b/composer.json index 6d293f2..d0d694a 100644 --- a/composer.json +++ b/composer.json @@ -1,8 +1,6 @@ { "require": { "onelogin/php-saml": "3.0.0.x-dev", - "twig/twig": "^2.4", - "symfony/yaml": "^4.1", "squizlabs/php_codesniffer": "*" }, diff --git a/src/Helpers/IdpHelper.php b/src/Helpers/IdpHelper.php index 44bf76c..443d093 100644 --- a/src/Helpers/IdpHelper.php +++ b/src/Helpers/IdpHelper.php @@ -6,10 +6,10 @@ class IdpHelper { public static function getMetadata($idpName) { - if (!file_exists(__DIR__ . "/../config/idp/" . $idpName . ".xml")) { + if (!file_exists(__DIR__ . "/../Config/idp/" . $idpName . ".xml")) { throw new \Exception("Invalid IDP Requested", 1); } - $xml = simplexml_load_file(__DIR__ . "/../config/idp/" . $idpName . '.xml'); + $xml = simplexml_load_file(__DIR__ . "/../Config/idp/" . $idpName . '.xml'); $metadata = array(); $metadata['idpEntityId'] = $xml->attributes()->entityID->__toString(); From c005a207691d3410391252d75a06b2cd398831ea Mon Sep 17 00:00:00 2001 From: Lorenzo Cattaneo Date: Fri, 27 Jul 2018 12:28:11 +0200 Subject: [PATCH 24/69] removed session in login --- example/index.php | 9 +++----- example/metadata.php | 37 ++++++++++++++------------------ example/settings.php | 0 src/Strategy/PhpSamlOneLogin.php | 16 +++++++------- 4 files changed, 27 insertions(+), 35 deletions(-) create mode 100644 example/settings.php diff --git a/example/index.php b/example/index.php index efe645d..fae6339 100644 --- a/example/index.php +++ b/example/index.php @@ -5,9 +5,10 @@ */ require_once(__DIR__ . "/../vendor/autoload.php"); +require_once(__DIR__ . "/settings.php"); use SpidPHP\SpidPHP; -$base = "http://spid.test.com"; +$base = "http://sp.simevo.com:8000"; $settings = [ 'spBaseUrl' => $base, 'spEntityId' => $base."/metadata.php", @@ -18,12 +19,8 @@ ]; $onelogin = new SpidPHP($settings); -//$result = $onelogin->login("testenv2"); +$result = $onelogin->login("testenv2"); -$metadata = $onelogin->getSPMetadata(); - -header('Content-Type: text/xml'); -echo $metadata; //if (!$onelogin->isAuthenticated()) $onelogin->login(); //if ($onelogin->login()) $onelogin->logout(); diff --git a/example/metadata.php b/example/metadata.php index 45199b5..374fc15 100644 --- a/example/metadata.php +++ b/example/metadata.php @@ -1,25 +1,20 @@ getSettings(); - // Now we only validate SP settings - $settings = new OneLogin_Saml2_Settings($settingsInfo, true); - $metadata = $settings->getSPMetadata(); - $errors = $settings->validateMetadata($metadata); - if (empty($errors)) { - header('Content-Type: text/xml'); - echo $metadata; - } else { - throw new OneLogin_Saml2_Error( - 'Invalid SP metadata: '.implode(', ', $errors), - OneLogin_Saml2_Error::METADATA_SP_INVALID - ); - } -} catch (Exception $e) { - echo $e->getMessage(); -} \ No newline at end of file +require_once(__DIR__ . "/../vendor/autoload.php"); +require_once(__DIR__ . "/settings.php"); + +use SpidPHP\SpidPHP; + +$onelogin = new SpidPHP($settings); + +$metadata = $onelogin->getSPMetadata(); + +header('Content-Type: text/xml'); +echo $metadata; +//if (!$onelogin->isAuthenticated()) $onelogin->login(); + +//if ($onelogin->login()) $onelogin->logout(); diff --git a/example/settings.php b/example/settings.php new file mode 100644 index 0000000..e69de29 diff --git a/src/Strategy/PhpSamlOneLogin.php b/src/Strategy/PhpSamlOneLogin.php index 5d55062..b5119bb 100644 --- a/src/Strategy/PhpSamlOneLogin.php +++ b/src/Strategy/PhpSamlOneLogin.php @@ -114,14 +114,14 @@ public function login( $idpName, $redirectTo = '', $level = 1 ) if (!$this->auth->isAuthenticated()) { return false; } - - $_SESSION['samlUserdata'] = $this->auth->getAttributes(); - $_SESSION['samlNameId'] = $this->auth->getNameId(); - $_SESSION['samlNameIdFormat'] = $this->auth->getNameIdFormat(); - $_SESSION['samlSessionIndex'] = $this->auth->getSessionIndex(); - - if (!empty($_SESSION['samlUserdata'])) { - return true; + $data = array(); + $data['samlUserdata'] = $this->auth->getAttributes(); + $data['samlNameId'] = $this->auth->getNameId(); + $data['samlNameIdFormat'] = $this->auth->getNameIdFormat(); + $data['samlSessionIndex'] = $this->auth->getSessionIndex(); + + if (!empty($data['samlUserdata'])) { + return $data; } return false; From 43f8f67bbce65e9e405d4b25a83f880211b9434e Mon Sep 17 00:00:00 2001 From: Lorenzo Cattaneo Date: Fri, 27 Jul 2018 13:59:00 +0200 Subject: [PATCH 25/69] saving userdata in class instance --- example/index.php | 88 ++++++++++++++++++++++++++++++++ example/settings.php | 11 ++++ src/Strategy/PhpSamlOneLogin.php | 11 ++-- 3 files changed, 105 insertions(+), 5 deletions(-) diff --git a/example/index.php b/example/index.php index fae6339..5395740 100644 --- a/example/index.php +++ b/example/index.php @@ -24,3 +24,91 @@ //if (!$onelogin->isAuthenticated()) $onelogin->login(); //if ($onelogin->login()) $onelogin->logout(); + + + +if (isset($_GET['sso'])) { + $result = $onelogin->login("testenv2"); + print_r($result); +} elseif (isset($_GET['slo'])) { + $returnTo = null; + $parameters = array(); + $nameId = null; + $sessionIndex = null; + $nameIdFormat = null; + if (isset($_SESSION['samlNameId'])) { + $nameId = $_SESSION['samlNameId']; + } + if (isset($_SESSION['samlSessionIndex'])) { + $sessionIndex = $_SESSION['samlSessionIndex']; + } + if (isset($_SESSION['samlNameIdFormat'])) { + $nameIdFormat = $_SESSION['samlNameIdFormat']; + } + $auth->logout($returnTo, $parameters, $nameId, $sessionIndex, false, $nameIdFormat); + # If LogoutRequest ID need to be saved in order to later validate it, do instead + # $sloBuiltUrl = $auth->logout(null, $parameters, $nameId, $sessionIndex, true); + # $_SESSION['LogoutRequestID'] = $auth->getLastRequestID(); + # header('Pragma: no-cache'); + # header('Cache-Control: no-cache, must-revalidate'); + # header('Location: ' . $sloBuiltUrl); + # exit(); +} elseif (isset($_GET['acs'])) { + if (isset($_SESSION) && isset($_SESSION['AuthNRequestID'])) { + $requestID = $_SESSION['AuthNRequestID']; + } else { + $requestID = null; + } + $auth->processResponse($requestID); + $errors = $auth->getErrors(); + if (!empty($errors)) { + echo '

' . implode(', ', $errors) . '

'; + } + if (!$auth->isAuthenticated()) { + echo '

Not authenticated

'; + exit(); + } + $_SESSION['samlUserdata'] = $auth->getAttributes(); + $_SESSION['samlNameId'] = $auth->getNameId(); + $_SESSION['samlNameIdFormat'] = $auth->getNameIdFormat(); + $_SESSION['samlSessionIndex'] = $auth->getSessionIndex(); + unset($_SESSION['AuthNRequestID']); + if (isset($_POST['RelayState']) && OneLogin_Saml2_Utils::getSelfURL() != $_POST['RelayState']) { + $auth->redirectTo($_POST['RelayState']); + } +} elseif (isset($_GET['sls'])) { + if (isset($_SESSION) && isset($_SESSION['LogoutRequestID'])) { + $requestID = $_SESSION['LogoutRequestID']; + } else { + $requestID = null; + } + $auth->processSLO(false, $requestID); + $errors = $auth->getErrors(); + if (empty($errors)) { + echo '

Sucessfully logged out

'; + } else { + echo '

' . implode(', ', $errors) . '

'; + } +} +if (isset($_SESSION['samlUserdata'])) { + if (!empty($_SESSION['samlUserdata'])) { + $attributes = $_SESSION['samlUserdata']; + echo 'You have the following attributes:
'; + echo ''; + foreach ($attributes as $attributeName => $attributeValues) { + echo ''; + } + echo '
NameValues
' . htmlentities($attributeName) . '
    '; + foreach ($attributeValues as $attributeValue) { + echo '
  • ' . htmlentities($attributeValue) . '
  • '; + } + echo '
'; + } else { + echo "

You don't have any attribute

"; + } + echo '

Logout

'; +} else { + echo '

Login

'; + echo '

Login and access to attrs.php page

'; + echo '

Show the SP metadata

'; +} diff --git a/example/settings.php b/example/settings.php index e69de29..4a0cd20 100644 --- a/example/settings.php +++ b/example/settings.php @@ -0,0 +1,11 @@ + $base, + 'spEntityId' => $base."/metadata.php", + 'spKeyFile' => __DIR__ . "/../sp.key", + 'spCrtFile' => __DIR__ . "/../sp.crt", + 'spAcsUrl' => $base."/index.php?acs", + 'spSloUrl' => $base."/index.php?slo" + ]; diff --git a/src/Strategy/PhpSamlOneLogin.php b/src/Strategy/PhpSamlOneLogin.php index b5119bb..8b0ed6f 100644 --- a/src/Strategy/PhpSamlOneLogin.php +++ b/src/Strategy/PhpSamlOneLogin.php @@ -20,6 +20,7 @@ class PhpSamlOneLogin implements PhpSamlInterface private $auth; private $settingsHelper; private $oneloginSettings; + private $userdata; function __construct($idpName = null, $settings) { @@ -114,11 +115,11 @@ public function login( $idpName, $redirectTo = '', $level = 1 ) if (!$this->auth->isAuthenticated()) { return false; } - $data = array(); - $data['samlUserdata'] = $this->auth->getAttributes(); - $data['samlNameId'] = $this->auth->getNameId(); - $data['samlNameIdFormat'] = $this->auth->getNameIdFormat(); - $data['samlSessionIndex'] = $this->auth->getSessionIndex(); + $this->userdata = array(); + $this->userdata['samlUserdata'] = $this->auth->getAttributes(); + $this->userdata['samlNameId'] = $this->auth->getNameId(); + $this->userdata['samlNameIdFormat'] = $this->auth->getNameIdFormat(); + $this->userdata['samlSessionIndex'] = $this->auth->getSessionIndex(); if (!empty($data['samlUserdata'])) { return $data; From 85a15024183dceb2dfc7de4ef9dfef65c2209943 Mon Sep 17 00:00:00 2001 From: Lorenzo Cattaneo Date: Fri, 27 Jul 2018 14:53:09 +0200 Subject: [PATCH 26/69] fix login idp --- example/index.php | 47 ++++++++++---------------------- src/Helpers/IdpHelper.php | 3 +- src/SpidPHP.php | 2 +- src/Strategy/PhpSamlOneLogin.php | 4 +-- 4 files changed, 19 insertions(+), 37 deletions(-) diff --git a/example/index.php b/example/index.php index 5395740..a37ff2c 100644 --- a/example/index.php +++ b/example/index.php @@ -8,51 +8,31 @@ require_once(__DIR__ . "/settings.php"); use SpidPHP\SpidPHP; -$base = "http://sp.simevo.com:8000"; -$settings = [ - 'spBaseUrl' => $base, - 'spEntityId' => $base."/metadata.php", - 'spKeyFile' => __DIR__ . "/../sp.key", - 'spCrtFile' => __DIR__ . "/../sp.crt", - 'spAcsUrl' => $base."/index.php?acs", - 'spSloUrl' => $base."/index.php?slo" - ]; + + +echo '

Login

'; +echo '

Login and access to attrs.php page

'; +echo '

Show the SP metadata

'; $onelogin = new SpidPHP($settings); -$result = $onelogin->login("testenv2"); + +if (isset($_GET['sso'])) { + $result = $onelogin->login("testenv2"); + print_r($result); + die(); +} //if (!$onelogin->isAuthenticated()) $onelogin->login(); //if ($onelogin->login()) $onelogin->logout(); - +/* if (isset($_GET['sso'])) { $result = $onelogin->login("testenv2"); print_r($result); } elseif (isset($_GET['slo'])) { - $returnTo = null; - $parameters = array(); - $nameId = null; - $sessionIndex = null; - $nameIdFormat = null; - if (isset($_SESSION['samlNameId'])) { - $nameId = $_SESSION['samlNameId']; - } - if (isset($_SESSION['samlSessionIndex'])) { - $sessionIndex = $_SESSION['samlSessionIndex']; - } - if (isset($_SESSION['samlNameIdFormat'])) { - $nameIdFormat = $_SESSION['samlNameIdFormat']; - } - $auth->logout($returnTo, $parameters, $nameId, $sessionIndex, false, $nameIdFormat); - # If LogoutRequest ID need to be saved in order to later validate it, do instead - # $sloBuiltUrl = $auth->logout(null, $parameters, $nameId, $sessionIndex, true); - # $_SESSION['LogoutRequestID'] = $auth->getLastRequestID(); - # header('Pragma: no-cache'); - # header('Cache-Control: no-cache, must-revalidate'); - # header('Location: ' . $sloBuiltUrl); - # exit(); + $onelogin->logout(); } elseif (isset($_GET['acs'])) { if (isset($_SESSION) && isset($_SESSION['AuthNRequestID'])) { $requestID = $_SESSION['AuthNRequestID']; @@ -112,3 +92,4 @@ echo '

Login and access to attrs.php page

'; echo '

Show the SP metadata

'; } +*/ \ No newline at end of file diff --git a/src/Helpers/IdpHelper.php b/src/Helpers/IdpHelper.php index 443d093..23176f5 100644 --- a/src/Helpers/IdpHelper.php +++ b/src/Helpers/IdpHelper.php @@ -9,6 +9,7 @@ public static function getMetadata($idpName) if (!file_exists(__DIR__ . "/../Config/idp/" . $idpName . ".xml")) { throw new \Exception("Invalid IDP Requested", 1); } + $xml = simplexml_load_file(__DIR__ . "/../Config/idp/" . $idpName . '.xml'); $metadata = array(); @@ -16,7 +17,7 @@ public static function getMetadata($idpName) $metadata['idpSSO'] = $xml->xpath('//SingleSignOnService')[0]->attributes()->Location->__toString(); $metadata['idpSLO'] = $xml->xpath('//SingleLogoutService')[0]->attributes()->Location->__toString(); $metadata['idpCertValue'] = $xml->xpath('//X509Certificate')[0]->__toString(); - + return $metadata; } } \ No newline at end of file diff --git a/src/SpidPHP.php b/src/SpidPHP.php index 194fae0..ae088ed 100644 --- a/src/SpidPHP.php +++ b/src/SpidPHP.php @@ -49,7 +49,7 @@ public function isAuthenticated() public function login( $idpName, $redirectTo = '', $level = 1 ) { if (is_null($this->phpSaml)) $this->initStrategy($idpName); - return $this->phpSaml->login($redirectTo); + return $this->phpSaml->login($idpName, $redirectTo); } public function logout() diff --git a/src/Strategy/PhpSamlOneLogin.php b/src/Strategy/PhpSamlOneLogin.php index 8b0ed6f..8d72acc 100644 --- a/src/Strategy/PhpSamlOneLogin.php +++ b/src/Strategy/PhpSamlOneLogin.php @@ -24,7 +24,7 @@ class PhpSamlOneLogin implements PhpSamlInterface function __construct($idpName = null, $settings) { - $this->idpName = $idpName ?? "testenv2"; + $this->idpName = $idpName ?? "testenv2"; $this->settings = $settings; $this->init(); } @@ -49,7 +49,7 @@ private function init() } $this->settingsHelper->updateIdpMetadata($this->idpName); - $this->settings = new Settings($this->settingsHelper->getSettings()); + $this->oneloginSettings = new Settings($this->settingsHelper->getSettings()); $this->auth = new Auth($this->settingsHelper->getSettings()); } From 0b0545da9d9c9ac38c2e201c832c30d6497430ec Mon Sep 17 00:00:00 2001 From: Lorenzo Cattaneo Date: Fri, 27 Jul 2018 15:14:59 +0200 Subject: [PATCH 27/69] added getAttributes method --- src/SpidPHP.php | 5 +++++ src/Strategy/Interfaces/PhpSamlInterface.php | 1 + src/Strategy/PhpSamlOneLogin.php | 5 +++++ 3 files changed, 11 insertions(+) diff --git a/src/SpidPHP.php b/src/SpidPHP.php index ae088ed..0453db7 100644 --- a/src/SpidPHP.php +++ b/src/SpidPHP.php @@ -58,4 +58,9 @@ public function logout() return $this->phpSaml->logout(); } + public function getAttributes() + { + return $this->phpSaml->getAttributes(); + } + } \ No newline at end of file diff --git a/src/Strategy/Interfaces/PhpSamlInterface.php b/src/Strategy/Interfaces/PhpSamlInterface.php index aaa496d..d21acf3 100644 --- a/src/Strategy/Interfaces/PhpSamlInterface.php +++ b/src/Strategy/Interfaces/PhpSamlInterface.php @@ -8,4 +8,5 @@ public function getSupportedIdps(); public function isAuthenticated(); public function login( $idpName, $redirectTo = '', $level = 1 ); public function logout(); + public function getAttributes(); } \ No newline at end of file diff --git a/src/Strategy/PhpSamlOneLogin.php b/src/Strategy/PhpSamlOneLogin.php index 8d72acc..3f2d06a 100644 --- a/src/Strategy/PhpSamlOneLogin.php +++ b/src/Strategy/PhpSamlOneLogin.php @@ -144,4 +144,9 @@ public function logout() return true; } + public function getAttributes() + { + return $this->userdata; + } + } \ No newline at end of file From b0f124984004ea15a19d6fb96423c4593890decb Mon Sep 17 00:00:00 2001 From: Lorenzo Cattaneo Date: Fri, 27 Jul 2018 15:24:48 +0200 Subject: [PATCH 28/69] WIP: login example --- example/index.php | 14 +------------- example/login.php | 28 ++++++++++++++++++++++++++++ example/logout.php | 0 3 files changed, 29 insertions(+), 13 deletions(-) create mode 100644 example/login.php create mode 100644 example/logout.php diff --git a/example/index.php b/example/index.php index a37ff2c..3f6eb94 100644 --- a/example/index.php +++ b/example/index.php @@ -10,21 +10,9 @@ use SpidPHP\SpidPHP; -echo '

Login

'; -echo '

Login and access to attrs.php page

'; +echo '

Login

'; echo '

Show the SP metadata

'; -$onelogin = new SpidPHP($settings); - -if (isset($_GET['sso'])) { - $result = $onelogin->login("testenv2"); - print_r($result); - die(); -} - -//if (!$onelogin->isAuthenticated()) $onelogin->login(); - -//if ($onelogin->login()) $onelogin->logout(); /* diff --git a/example/login.php b/example/login.php new file mode 100644 index 0000000..3984e2d --- /dev/null +++ b/example/login.php @@ -0,0 +1,28 @@ +isAuthenticated() === false) { + $result = $onelogin->login("testenv2"); + print_r($result); + exit(); +} + +$attributes = $onelogin->getAttributes(); + +foreach ($attributes as $key => $attribute) { + echo $attribute . "\n"; +} + + diff --git a/example/logout.php b/example/logout.php new file mode 100644 index 0000000..e69de29 From 5633b9e9874094f096bea903224bb9af8162e1e1 Mon Sep 17 00:00:00 2001 From: Lorenzo Cattaneo Date: Fri, 27 Jul 2018 16:19:36 +0200 Subject: [PATCH 29/69] cleanup + added login response handler --- config.yaml.example | 12 -- example/login.php | 2 - saml-schema-assertion-2.0.xsd | 283 --------------------------- saml-schema-metadata-SPID-SP.xsd | 139 -------------- src/Strategy/PhpSamlOneLogin.php | 48 ++--- xenc-schema.xsd | 145 -------------- xml.xsd | 287 ---------------------------- xmldsig-core-schema.xsd | 318 ------------------------------- 8 files changed, 18 insertions(+), 1216 deletions(-) delete mode 100644 config.yaml.example delete mode 100644 saml-schema-assertion-2.0.xsd delete mode 100644 saml-schema-metadata-SPID-SP.xsd delete mode 100644 xenc-schema.xsd delete mode 100644 xml.xsd delete mode 100644 xmldsig-core-schema.xsd diff --git a/config.yaml.example b/config.yaml.example deleted file mode 100644 index 2b9ce93..0000000 --- a/config.yaml.example +++ /dev/null @@ -1,12 +0,0 @@ ---- -# SERVICE PROVIDER CONFIGURATION - -# SP base URL -sp_base: "http://sp2.simevo.com" - -# SP key and certificate location -sp_key_file: "sp.key" -sp_cert_file: "sp.crt" - -# URL for IDP metadata -idp_metadata_url: "https://idp.simevo.com/metadata" diff --git a/example/login.php b/example/login.php index 3984e2d..13f75c6 100644 --- a/example/login.php +++ b/example/login.php @@ -11,8 +11,6 @@ $onelogin = new SpidPHP($settings); -print_r($_POST); - if ($onelogin->isAuthenticated() === false) { $result = $onelogin->login("testenv2"); print_r($result); diff --git a/saml-schema-assertion-2.0.xsd b/saml-schema-assertion-2.0.xsd deleted file mode 100644 index bbad992..0000000 --- a/saml-schema-assertion-2.0.xsd +++ /dev/null @@ -1,283 +0,0 @@ - - - - - - - Document identifier: saml-schema-assertion-2.0 - Location: http://docs.oasis-open.org/security/saml/v2.0/ - Revision history: - V1.0 (November, 2002): - Initial Standard Schema. - V1.1 (September, 2003): - Updates within the same V1.0 namespace. - V2.0 (March, 2005): - New assertion schema for SAML V2.0 namespace. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/saml-schema-metadata-SPID-SP.xsd b/saml-schema-metadata-SPID-SP.xsd deleted file mode 100644 index c0ca8ad..0000000 --- a/saml-schema-metadata-SPID-SP.xsd +++ /dev/null @@ -1,139 +0,0 @@ - - - - - - - - Document identifier: Location: Revision history: - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Strategy/PhpSamlOneLogin.php b/src/Strategy/PhpSamlOneLogin.php index 3f2d06a..1a1e309 100644 --- a/src/Strategy/PhpSamlOneLogin.php +++ b/src/Strategy/PhpSamlOneLogin.php @@ -83,7 +83,23 @@ public function getSupportedIdps() public function isAuthenticated() { - if ($this->auth->isAuthenticated) { + if (isset($_POST) && isset($_POST['SAMLResponse'])) { + $this->auth->processResponse(); + + $errors = $this->auth->getErrors(); + + if (!empty($errors)) { + return $errors; + } + + $this->userdata = array(); + $this->userdata['samlUserdata'] = $this->auth->getAttributes(); + $this->userdata['samlNameId'] = $this->auth->getNameId(); + $this->userdata['samlNameIdFormat'] = $this->auth->getNameIdFormat(); + $this->userdata['samlSessionIndex'] = $this->auth->getSessionIndex(); + + } + if ($this->auth->isAuthenticated() === false) { return false; } return true; @@ -98,39 +114,11 @@ public function login( $idpName, $redirectTo = '', $level = 1 ) } $this->auth->login($redirectTo); - - $requestID = null; - if (isset($_SESSION['AuthNRequestID'])) { - $requestID = $_SESSION['AuthNRequestID']; - } - - $this->auth->processResponse($requestID); - unset($_SESSION['AuthNRequestID']); - - $errors = $this->auth->getErrors(); - if (!empty($errors)) { - return $errors; - } - - if (!$this->auth->isAuthenticated()) { - return false; - } - $this->userdata = array(); - $this->userdata['samlUserdata'] = $this->auth->getAttributes(); - $this->userdata['samlNameId'] = $this->auth->getNameId(); - $this->userdata['samlNameIdFormat'] = $this->auth->getNameIdFormat(); - $this->userdata['samlSessionIndex'] = $this->auth->getSessionIndex(); - - if (!empty($data['samlUserdata'])) { - return $data; - } - - return false; } public function logout() { - if (!$this->auth->isAuthenticated()) { + if ($this->auth->isAuthenticated() === false) { return false; } $this->auth->logout(); diff --git a/xenc-schema.xsd b/xenc-schema.xsd deleted file mode 100644 index dd85887..0000000 --- a/xenc-schema.xsd +++ /dev/null @@ -1,145 +0,0 @@ - - - - - - ]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/xml.xsd b/xml.xsd deleted file mode 100644 index aea7d0d..0000000 --- a/xml.xsd +++ /dev/null @@ -1,287 +0,0 @@ - - - - - - -
-

About the XML namespace

- -
-

- This schema document describes the XML namespace, in a form - suitable for import by other schema documents. -

-

- See - http://www.w3.org/XML/1998/namespace.html and - - http://www.w3.org/TR/REC-xml for information - about this namespace. -

-

- Note that local names in this namespace are intended to be - defined only by the World Wide Web Consortium or its subgroups. - The names currently defined in this namespace are listed below. - They should not be used with conflicting semantics by any Working - Group, specification, or document instance. -

-

- See further below in this document for more information about how to refer to this schema document from your own - XSD schema documents and about the - namespace-versioning policy governing this schema document. -

-
-
-
-
- - - - -
- -

lang (as an attribute name)

-

- denotes an attribute whose value - is a language code for the natural language of the content of - any element; its value is inherited. This name is reserved - by virtue of its definition in the XML specification.

- -
-
-

Notes

-

- Attempting to install the relevant ISO 2- and 3-letter - codes as the enumerated possible values is probably never - going to be a realistic possibility. -

-

- See BCP 47 at - http://www.rfc-editor.org/rfc/bcp/bcp47.txt - and the IANA language subtag registry at - - http://www.iana.org/assignments/language-subtag-registry - for further information. -

-

- The union allows for the 'un-declaration' of xml:lang with - the empty string. -

-
-
-
- - - - - - - - - -
- - - - -
- -

space (as an attribute name)

-

- denotes an attribute whose - value is a keyword indicating what whitespace processing - discipline is intended for the content of the element; its - value is inherited. This name is reserved by virtue of its - definition in the XML specification.

- -
-
-
- - - - - - -
- - - -
- -

base (as an attribute name)

-

- denotes an attribute whose value - provides a URI to be used as the base for interpreting any - relative URIs in the scope of the element on which it - appears; its value is inherited. This name is reserved - by virtue of its definition in the XML Base specification.

- -

- See http://www.w3.org/TR/xmlbase/ - for information about this attribute. -

-
-
-
-
- - - - -
- -

id (as an attribute name)

-

- denotes an attribute whose value - should be interpreted as if declared to be of type ID. - This name is reserved by virtue of its definition in the - xml:id specification.

- -

- See http://www.w3.org/TR/xml-id/ - for information about this attribute. -

-
-
-
-
- - - - - - - - - - -
- -

Father (in any context at all)

- -
-

- denotes Jon Bosak, the chair of - the original XML Working Group. This name is reserved by - the following decision of the W3C XML Plenary and - XML Coordination groups: -

-
-

- In appreciation for his vision, leadership and - dedication the W3C XML Plenary on this 10th day of - February, 2000, reserves for Jon Bosak in perpetuity - the XML name "xml:Father". -

-
-
-
-
-
- - - -
-

About this schema document

- -
-

- This schema defines attributes and an attribute group suitable - for use by schemas wishing to allow xml:base, - xml:lang, xml:space or - xml:id attributes on elements they define. -

-

- To enable this, such a schema must import this schema for - the XML namespace, e.g. as follows: -

-
-          <schema . . .>
-           . . .
-           <import namespace="http://www.w3.org/XML/1998/namespace"
-                      schemaLocation="http://www.w3.org/2001/xml.xsd"/>
-     
-

- or -

-
-           <import namespace="http://www.w3.org/XML/1998/namespace"
-                      schemaLocation="http://www.w3.org/2009/01/xml.xsd"/>
-     
-

- Subsequently, qualified reference to any of the attributes or the - group defined below will have the desired effect, e.g. -

-
-          <type . . .>
-           . . .
-           <attributeGroup ref="xml:specialAttrs"/>
-     
-

- will define a type which will schema-validate an instance element - with any of those attributes. -

-
-
-
-
- - - -
-

Versioning policy for this schema document

-
-

- In keeping with the XML Schema WG's standard versioning - policy, this schema document will persist at - - http://www.w3.org/2009/01/xml.xsd. -

-

- At the date of issue it can also be found at - - http://www.w3.org/2001/xml.xsd. -

-

- The schema document at that URI may however change in the future, - in order to remain compatible with the latest version of XML - Schema itself, or with the XML namespace itself. In other words, - if the XML Schema or XML namespaces change, the version of this - document at - http://www.w3.org/2001/xml.xsd - - will change accordingly; the version at - - http://www.w3.org/2009/01/xml.xsd - - will not change. -

-

- Previous dated (and unchanging) versions of this schema - document are at: -

- -
-
-
-
- -
- diff --git a/xmldsig-core-schema.xsd b/xmldsig-core-schema.xsd deleted file mode 100644 index df126b3..0000000 --- a/xmldsig-core-schema.xsd +++ /dev/null @@ -1,318 +0,0 @@ - - - - - - ]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - From 770f58093ca6a4a3a129810abb6972269181b369 Mon Sep 17 00:00:00 2001 From: Lorenzo Cattaneo Date: Fri, 27 Jul 2018 16:35:07 +0200 Subject: [PATCH 30/69] added requestid for login validation --- src/Strategy/PhpSamlOneLogin.php | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/Strategy/PhpSamlOneLogin.php b/src/Strategy/PhpSamlOneLogin.php index 1a1e309..0d62399 100644 --- a/src/Strategy/PhpSamlOneLogin.php +++ b/src/Strategy/PhpSamlOneLogin.php @@ -18,6 +18,7 @@ class PhpSamlOneLogin implements PhpSamlInterface private $settings = null; private $auth; + private $authRequestID; private $settingsHelper; private $oneloginSettings; private $userdata; @@ -83,9 +84,9 @@ public function getSupportedIdps() public function isAuthenticated() { - if (isset($_POST) && isset($_POST['SAMLResponse'])) { - $this->auth->processResponse(); - + if (!is_null($this->authRequestID)) { + $this->auth->processResponse($this->authRequestID); + $this->authRequestID = null; $errors = $this->auth->getErrors(); if (!empty($errors)) { @@ -113,7 +114,13 @@ public function login( $idpName, $redirectTo = '', $level = 1 ) return false; } - $this->auth->login($redirectTo); + $ssoBuiltUrl = $this->auth->login($redirectTo, array(), false, false, true); + $this->authRequestID = $this->auth->getLastRequestID(); + + header('Pragma: no-cache'); + header('Cache-Control: no-cache, must-revalidate'); + header('Location: ' . $ssoBuiltUrl); + exit(); } public function logout() From 26de813c17c9f0761d3add9f8d9f8a06a981e3e9 Mon Sep 17 00:00:00 2001 From: Paolo Greppi Date: Mon, 30 Jul 2018 08:33:28 +0200 Subject: [PATCH 31/69] exclude and remove .vscode --- .gitignore | 1 + .vscode/launch.json | 22 ---------------------- 2 files changed, 1 insertion(+), 22 deletions(-) delete mode 100644 .vscode/launch.json diff --git a/.gitignore b/.gitignore index 1dbeb38..5c59cd6 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ config.yaml .directory AuthnRequest.patched LogoutRequest.patched +.vscode diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index 9d9ed65..0000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - // Usare IntelliSense per informazioni sui possibili attributi. - // Al passaggio del mouse vengono visualizzate le descrizioni degli attributi esistenti. - // Per ulteriori informazioni, visitare: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "name": "Listen for XDebug", - "type": "php", - "request": "launch", - "port": 9000 - }, - { - "name": "Launch currently open script", - "type": "php", - "request": "launch", - "program": "${file}", - "cwd": "${fileDirname}", - "port": 9000 - } - ] -} \ No newline at end of file From 73ae19b49e99df030901c08dcd248034a5ce420b Mon Sep 17 00:00:00 2001 From: Paolo Greppi Date: Mon, 30 Jul 2018 08:43:52 +0200 Subject: [PATCH 32/69] clean up Makefile --- Makefile | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/Makefile b/Makefile index d7728ca..99b49c1 100644 --- a/Makefile +++ b/Makefile @@ -1,9 +1,4 @@ all: sp.key AuthnRequest.patched LogoutRequest.patched - # clean up twig cache - rm -rf tmp - mkdir -p tmp - ./bin/configure.php > www/settings.php - cp www/settings.php www2/settings.php AuthnRequest.patched: AuthnRequest.diff if [ -e $@ ]; then patch -R vendor/onelogin/php-saml/lib/Saml2/AuthnRequest.php $@; fi @@ -19,4 +14,4 @@ sp.key: openssl req -x509 -nodes -sha256 -days 365 -newkey rsa:2048 -subj "/C=IT/ST=Italy/L=Rome/O=testenv2/CN=localhost" -keyout sp.key -out sp.crt clean: - rm -rf tmp vendor www/settings.php + rm -rf vendor From 30ce2b9c594b1e3eb1c50fad7ecc0dd730d376d2 Mon Sep 17 00:00:00 2001 From: Paolo Greppi Date: Mon, 30 Jul 2018 08:46:24 +0200 Subject: [PATCH 33/69] exclude and remove .DS_store files --- .DS_Store | Bin 8196 -> 0 bytes .gitignore | 2 ++ src/.DS_Store | Bin 6148 -> 0 bytes src/Config/.DS_Store | Bin 6148 -> 0 bytes src/Config/idp/.DS_Store | Bin 6148 -> 0 bytes 5 files changed, 2 insertions(+) delete mode 100644 .DS_Store delete mode 100644 src/.DS_Store delete mode 100644 src/Config/.DS_Store delete mode 100644 src/Config/idp/.DS_Store diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index 207e9eb093e9c48a6ed10101cd775d66007a764f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8196 zcmeHM&2G~`5S~p+>l9UO0nuKNd_m&Spr)nbfGUMFIaDgBA_xwES|@f}Q^yXr)3k)3 zTzCiI4R{nz9C;8P0KWYp*iKsYfDn}3YIerEGqbzj&aN{q5wS|gE)gvfkpbd-?lP(| zh4*tBDLK`09h||Q$frl-&@1Yo)RPI>RkUR`ShGy?x6 z0{nh3L7bNrA4o2)4pbrq0G~j!D3Fl{2pd!Jvf=~Dg(-Z>?13p$rcw+h&Czdib?~y{ z1IeX1F=_vgEqf2xtUG5#YJ|Jl#je9@9Z8e~-{|n{0AvkM=gb zpxN+(2DEX&*f;Q7-$fBFXQ)X5wWy6cLhc{J@+G8z{Iq4xI)|kPyCxlGEWczduZl5! z%+95KjCVp7X5c|`=vc^YJUMAE`SpyKsk5}%yAkaz+N2Gt(L<_JE$y4`LHm6Il`FJ` zmF$B!y1%}RoPjKT%J?wo0oHPeS@Ly`tcNXR^a~iLhW)IiI z8tY%gPSXD*{I@ZeCZAz?l{p?Q<8(QaCuv;~6G`1X{D#y_Yl)pn#=O#MbfdO98J`<% z&yC{c<*zK4pSUzRWf;@O^vv{&`a#gG$5z~lDpvT6KW$s_K|{%pgTNcgR^y55ba&>i zZ3KR7xxNz$W7l?|^8Bgm+d;PybbLD!e(JKrFbYOtXKu0ATfMtv-YKp2m&{(Nw7Rln zuC0~&{erP@v$Vds+d6sOd((gW0pOr8^)97Z`8=#YLQ=~+s)xQ8`1@QPG)YGQ(8tCF z6s7?Zp_l|HrF&6fN-Z)2)+|m52djywPM)l>(amoUW+ZNpDOk(Rl}KgZa7>r&o^k)VYOj6hzlZIS2ywbQ@gr!Z F;0G5C;=BL= diff --git a/.gitignore b/.gitignore index 5c59cd6..50b1cd9 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,5 @@ config.yaml AuthnRequest.patched LogoutRequest.patched .vscode +.DS_Store + diff --git a/src/.DS_Store b/src/.DS_Store deleted file mode 100644 index a500411ee3c77ad6f80303e03aa780d1f91ce863..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHK!EVz)5S>j!x=Dq|0jWLug2W+0HKF2wDp_eO(OV@WH~?yGViHTPH;Nsi5>olX zN5CKODSQE+18;Vhs!rh03qr6X&A!?3ytVhO<@FMg7|ez~QJ07$oUzeH^9$qa>}%Gr znHEs!F;W^GXX;Iysd%m0CH_VQc$gvLCLEZX+Bi*ftuyRLXQ)g3sSaa`?R~WSo9zFyhnb2+4B~@-+$Qi_V)bc zvMsmo`TNIb=NIo6AC{M&^fXA|h1GV);!pUDpf<^O;WW=wK0=;3oAi_t%4mXiO2xI; zo#pxhzb@AnozNJyAw_f!)&s=o*IDA@3QMBC#OM*)ghq6+Qop@YkFGPPfK$LJa1#aW zCs1zPgyd?S0#1SdLIM6hcyPwRVr5W&I?(7N0I-R)HpKiN#2n9JV6ifYD==ZGKtmPw zh#?FedC%4b7Au2>PQo5OguSz{ClsOYj`2NRP9iYqTBm?hpsv7%>2`VlKl=UozfN*J zr+`!7pHe_H55vO&reyclwaM{b>%q_9Y+P3vyrLk`TQO>RE8c-?L*C;67+9J%rYLs$C0eK|*&5UQf>*tldLqwuC%|fC!5lJ-0S`*zJ!sFalq~<&g zpx`<5=d{o%ES}EGzrX?cX*=Ui>y<7UH7P*<|E)`DZ#R@=3Y5`AgL5 zjeGYWNZFFDN3BosP|xBrDW{WOGCpK$KPiW)(?4ik+~`SqkoRZD?I$~0l}WDpW0Ra` zeS{o+%vGjmJv~*~#LN@h0V$iZdEDNZ&%19r!RxTQ=mhgH?7r#*TU+5`(Ui}ghi~^z zPS3y2zb(H1usb2}ks5jI@ER_lxaFP2V^wH1fQO@>(tA?Wr-C#MK$D`F5;}#w1COFF z@D1oCjcB++;3e{J6A06bXTUSy8CVYn+;J!$uE*>6x9|*j2G)oHJ|6-!#>io1(HtG< zObGzw-$*0S&CfqDUItlY+%;TOcT!kWB z=^+9WP9n1Cd(VJpV3~osHQRjuzx#9jzntVP&wyv(zhXc%cH`Y1mgMf%mBsO08=#$_ qv2a{v@g@a|xr!0XSMdg#5%>dEfRV$>B0LcPBVcIoooC>$GH?Z3B5R)j diff --git a/src/Config/idp/.DS_Store b/src/Config/idp/.DS_Store deleted file mode 100644 index 505157fb3008733e968f85caab1c60219c19f558..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHKyH3ME5S)b+k)TLPc_kGRe8DLS1vNi_0(6EH1VrhIf8j6qGnjn{6d4*M8Z;~I z&fR*)XHVhv0#lzJ6y8FcTDq}=C?{SNJJmCx{c%D>$pK$IWwq!m; z{NmqXxEMCq>tVBIjX&Tk{rEkME149K0#ZNX}$M^Th@&DXr%3&+HuGaq!Keg<3@nH2bI1wH^OHXL>U From 3a5be353965a1c6601b78be4b1e7cd99d8448b2b Mon Sep 17 00:00:00 2001 From: Paolo Greppi Date: Mon, 30 Jul 2018 09:14:52 +0200 Subject: [PATCH 34/69] unify src/config to src/Config --- src/{config => Config}/OneloginSamlConfig.php | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/{config => Config}/OneloginSamlConfig.php (100%) diff --git a/src/config/OneloginSamlConfig.php b/src/Config/OneloginSamlConfig.php similarity index 100% rename from src/config/OneloginSamlConfig.php rename to src/Config/OneloginSamlConfig.php From f66963a1a204edb72de7b99feeb797c3676607bd Mon Sep 17 00:00:00 2001 From: Paolo Greppi Date: Mon, 30 Jul 2018 09:15:17 +0200 Subject: [PATCH 35/69] use paths for php-saml v 3.x --- Makefile | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index 99b49c1..2ae1a17 100644 --- a/Makefile +++ b/Makefile @@ -1,13 +1,13 @@ all: sp.key AuthnRequest.patched LogoutRequest.patched AuthnRequest.patched: AuthnRequest.diff - if [ -e $@ ]; then patch -R vendor/onelogin/php-saml/lib/Saml2/AuthnRequest.php $@; fi - patch -N vendor/onelogin/php-saml/lib/Saml2/AuthnRequest.php $< + if [ -e $@ ]; then patch -R vendor/onelogin/php-saml/src/Saml2/AuthnRequest.php $@; fi + patch -N vendor/onelogin/php-saml/src/Saml2/AuthnRequest.php $< cp AuthnRequest.diff $@ LogoutRequest.patched: LogoutRequest.diff - if [ -e $@ ]; then patch -R vendor/onelogin/php-saml/lib/Saml2/LogoutRequest.php $@; fi - patch -N vendor/onelogin/php-saml/lib/Saml2/LogoutRequest.php $< + if [ -e $@ ]; then patch -R vendor/onelogin/php-saml/src/Saml2/LogoutRequest.php $@; fi + patch -N vendor/onelogin/php-saml/src/Saml2/LogoutRequest.php $< cp LogoutRequest.diff $@ sp.key: From cdf607af93bfe4ef301775f9dcd1b493676cc590 Mon Sep 17 00:00:00 2001 From: Paolo Greppi Date: Mon, 30 Jul 2018 10:11:50 +0200 Subject: [PATCH 36/69] add links to index, and assertion consuming service page --- example/acs.php | 25 +++++++++++++++ example/index.php | 81 ++--------------------------------------------- 2 files changed, 27 insertions(+), 79 deletions(-) create mode 100644 example/acs.php diff --git a/example/acs.php b/example/acs.php new file mode 100644 index 0000000..93d8e8c --- /dev/null +++ b/example/acs.php @@ -0,0 +1,25 @@ +isAuthenticated()) { + $attributes = $onelogin->getAttributes(); + echo "logged in !" . PHP_EOL; + var_dump($attributes); + foreach ($attributes as $key => $attribute) { + echo $attribute . "\n"; + } +} else { + echo "not logged in !" . PHP_EOL; +} + +echo '

Login

'; diff --git a/example/index.php b/example/index.php index 3f6eb94..f9a3c71 100644 --- a/example/index.php +++ b/example/index.php @@ -1,83 +1,6 @@ Login

'; echo '

Show the SP metadata

'; - - - -/* -if (isset($_GET['sso'])) { - $result = $onelogin->login("testenv2"); - print_r($result); -} elseif (isset($_GET['slo'])) { - $onelogin->logout(); -} elseif (isset($_GET['acs'])) { - if (isset($_SESSION) && isset($_SESSION['AuthNRequestID'])) { - $requestID = $_SESSION['AuthNRequestID']; - } else { - $requestID = null; - } - $auth->processResponse($requestID); - $errors = $auth->getErrors(); - if (!empty($errors)) { - echo '

' . implode(', ', $errors) . '

'; - } - if (!$auth->isAuthenticated()) { - echo '

Not authenticated

'; - exit(); - } - $_SESSION['samlUserdata'] = $auth->getAttributes(); - $_SESSION['samlNameId'] = $auth->getNameId(); - $_SESSION['samlNameIdFormat'] = $auth->getNameIdFormat(); - $_SESSION['samlSessionIndex'] = $auth->getSessionIndex(); - unset($_SESSION['AuthNRequestID']); - if (isset($_POST['RelayState']) && OneLogin_Saml2_Utils::getSelfURL() != $_POST['RelayState']) { - $auth->redirectTo($_POST['RelayState']); - } -} elseif (isset($_GET['sls'])) { - if (isset($_SESSION) && isset($_SESSION['LogoutRequestID'])) { - $requestID = $_SESSION['LogoutRequestID']; - } else { - $requestID = null; - } - $auth->processSLO(false, $requestID); - $errors = $auth->getErrors(); - if (empty($errors)) { - echo '

Sucessfully logged out

'; - } else { - echo '

' . implode(', ', $errors) . '

'; - } -} -if (isset($_SESSION['samlUserdata'])) { - if (!empty($_SESSION['samlUserdata'])) { - $attributes = $_SESSION['samlUserdata']; - echo 'You have the following attributes:
'; - echo ''; - foreach ($attributes as $attributeName => $attributeValues) { - echo ''; - } - echo '
NameValues
' . htmlentities($attributeName) . '
    '; - foreach ($attributeValues as $attributeValue) { - echo '
  • ' . htmlentities($attributeValue) . '
  • '; - } - echo '
'; - } else { - echo "

You don't have any attribute

"; - } - echo '

Logout

'; -} else { - echo '

Login

'; - echo '

Login and access to attrs.php page

'; - echo '

Show the SP metadata

'; -} -*/ \ No newline at end of file +echo '

Logout

'; +echo '

Assertion Consuming Service

'; From 7d86a9d75b97eb3023e8ec1fa7392d47f7affcbb Mon Sep 17 00:00:00 2001 From: Paolo Greppi Date: Mon, 30 Jul 2018 10:17:36 +0200 Subject: [PATCH 37/69] implement logout --- example/logout.php | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/example/logout.php b/example/logout.php index e69de29..1d474cd 100644 --- a/example/logout.php +++ b/example/logout.php @@ -0,0 +1,18 @@ +isAuthenticated()) { + $result = $onelogin->logout(); + print_r($result); + exit(); +} From a4849c31aabbfc83bf31b03115233962c2713537 Mon Sep 17 00:00:00 2001 From: Paolo Greppi Date: Mon, 30 Jul 2018 10:18:03 +0200 Subject: [PATCH 38/69] configure acs and slo --- example/settings.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/example/settings.php b/example/settings.php index 4a0cd20..378dbd0 100644 --- a/example/settings.php +++ b/example/settings.php @@ -6,6 +6,6 @@ 'spEntityId' => $base."/metadata.php", 'spKeyFile' => __DIR__ . "/../sp.key", 'spCrtFile' => __DIR__ . "/../sp.crt", - 'spAcsUrl' => $base."/index.php?acs", - 'spSloUrl' => $base."/index.php?slo" + 'spAcsUrl' => $base."/acs.php", + 'spSloUrl' => $base."/logout.php" ]; From 8af2459a2ae0a3ffbbc7cb1413afc0bfce7627ae Mon Sep 17 00:00:00 2001 From: Lorenzo Cattaneo Date: Mon, 30 Jul 2018 22:33:42 +0200 Subject: [PATCH 39/69] fix login status --- src/Strategy/PhpSamlOneLogin.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Strategy/PhpSamlOneLogin.php b/src/Strategy/PhpSamlOneLogin.php index 0d62399..cb4111a 100644 --- a/src/Strategy/PhpSamlOneLogin.php +++ b/src/Strategy/PhpSamlOneLogin.php @@ -84,6 +84,7 @@ public function getSupportedIdps() public function isAuthenticated() { + if (isset($_SESSION) && isset($_SESSION['authReqID'])) $this->authRequestID =$_SESSION['authReqID']; if (!is_null($this->authRequestID)) { $this->auth->processResponse($this->authRequestID); $this->authRequestID = null; @@ -115,7 +116,11 @@ public function login( $idpName, $redirectTo = '', $level = 1 ) } $ssoBuiltUrl = $this->auth->login($redirectTo, array(), false, false, true); + if (session_status() == PHP_SESSION_NONE) { + session_start(); + } $this->authRequestID = $this->auth->getLastRequestID(); + $_SESSION['authReqID'] = $this->auth->getLastRequestID(); header('Pragma: no-cache'); header('Cache-Control: no-cache, must-revalidate'); From 138be5bb8ea918e40d3f438152096e0349983a71 Mon Sep 17 00:00:00 2001 From: Lorenzo Cattaneo Date: Mon, 30 Jul 2018 23:27:39 +0200 Subject: [PATCH 40/69] testing session test saving request id to session --- src/Strategy/PhpSamlOneLogin.php | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Strategy/PhpSamlOneLogin.php b/src/Strategy/PhpSamlOneLogin.php index cb4111a..29775b7 100644 --- a/src/Strategy/PhpSamlOneLogin.php +++ b/src/Strategy/PhpSamlOneLogin.php @@ -84,7 +84,10 @@ public function getSupportedIdps() public function isAuthenticated() { - if (isset($_SESSION) && isset($_SESSION['authReqID'])) $this->authRequestID =$_SESSION['authReqID']; + if (isset($_SESSION) && isset($_SESSION['authReqID'])) { + $this->rebuildPhpSamlOnelogin($_SESSION['idpName']); + $this->authRequestID =$_SESSION['authReqID']; + } if (!is_null($this->authRequestID)) { $this->auth->processResponse($this->authRequestID); $this->authRequestID = null; @@ -116,11 +119,10 @@ public function login( $idpName, $redirectTo = '', $level = 1 ) } $ssoBuiltUrl = $this->auth->login($redirectTo, array(), false, false, true); - if (session_status() == PHP_SESSION_NONE) { - session_start(); - } + $this->authRequestID = $this->auth->getLastRequestID(); $_SESSION['authReqID'] = $this->auth->getLastRequestID(); + $_SESSION['idpName'] = $idpName; header('Pragma: no-cache'); header('Cache-Control: no-cache, must-revalidate'); From 1426c795fac3ba4a2391bcf8ebfbd1e58c4ac432 Mon Sep 17 00:00:00 2001 From: Lorenzo Cattaneo Date: Mon, 30 Jul 2018 23:50:17 +0200 Subject: [PATCH 41/69] WIP saving request id to session --- src/SpidPHP.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/SpidPHP.php b/src/SpidPHP.php index 0453db7..0c7c2e9 100644 --- a/src/SpidPHP.php +++ b/src/SpidPHP.php @@ -12,7 +12,8 @@ class SpidPHP implements PhpSamlInterface private $settings = null; public function __construct($settings = null, $mode = 'onelogin') - { + { + session_start(); $this->mode = $mode; $this->settings = $settings; } @@ -42,6 +43,7 @@ public function getSupportedIdps() public function isAuthenticated() { + if (isset($_SESSION['idpName']) && is_null($this->phpSaml)) $this->initStrategy($_SESSION['idpName']); if (is_null($this->phpSaml)) return false; return $this->phpSaml->isAuthenticated(); } From 3ded4aa26ddd332485573777c2ce28e0afea4898 Mon Sep 17 00:00:00 2001 From: Lorenzo Cattaneo Date: Tue, 31 Jul 2018 22:28:40 +0200 Subject: [PATCH 42/69] fix esempio login funzionante --- example/acs.php | 7 +++++-- example/login.php | 11 +---------- example/settings.php | 2 +- src/Strategy/PhpSamlOneLogin.php | 3 ++- 4 files changed, 9 insertions(+), 14 deletions(-) diff --git a/example/acs.php b/example/acs.php index 93d8e8c..c8bf76a 100644 --- a/example/acs.php +++ b/example/acs.php @@ -16,10 +16,13 @@ echo "logged in !" . PHP_EOL; var_dump($attributes); foreach ($attributes as $key => $attribute) { - echo $attribute . "\n"; + echo $key .": " . $attribute . "\n"; } + + echo '

Login

'; } else { echo "not logged in !" . PHP_EOL; + echo '

Login

'; } -echo '

Login

'; + diff --git a/example/login.php b/example/login.php index 13f75c6..a9607f2 100644 --- a/example/login.php +++ b/example/login.php @@ -12,15 +12,6 @@ $onelogin = new SpidPHP($settings); if ($onelogin->isAuthenticated() === false) { - $result = $onelogin->login("testenv2"); - print_r($result); - exit(); + $onelogin->login("testenv2"); } - -$attributes = $onelogin->getAttributes(); - -foreach ($attributes as $key => $attribute) { - echo $attribute . "\n"; -} - diff --git a/example/settings.php b/example/settings.php index 378dbd0..054c3a3 100644 --- a/example/settings.php +++ b/example/settings.php @@ -1,6 +1,6 @@ $base, 'spEntityId' => $base."/metadata.php", diff --git a/src/Strategy/PhpSamlOneLogin.php b/src/Strategy/PhpSamlOneLogin.php index 29775b7..38419c8 100644 --- a/src/Strategy/PhpSamlOneLogin.php +++ b/src/Strategy/PhpSamlOneLogin.php @@ -88,7 +88,7 @@ public function isAuthenticated() $this->rebuildPhpSamlOnelogin($_SESSION['idpName']); $this->authRequestID =$_SESSION['authReqID']; } - if (!is_null($this->authRequestID)) { + if (!is_null($this->authRequestID) && isset($_POST['SAMLResponse'])) { $this->auth->processResponse($this->authRequestID); $this->authRequestID = null; $errors = $this->auth->getErrors(); @@ -102,6 +102,7 @@ public function isAuthenticated() $this->userdata['samlNameId'] = $this->auth->getNameId(); $this->userdata['samlNameIdFormat'] = $this->auth->getNameIdFormat(); $this->userdata['samlSessionIndex'] = $this->auth->getSessionIndex(); + $_SESSION['userdata'] = $this->userdata; } if ($this->auth->isAuthenticated() === false) { From e8ef3357d3e1ffaf704da4d39b24a9f271498076 Mon Sep 17 00:00:00 2001 From: Lorenzo Cattaneo Date: Tue, 31 Jul 2018 22:55:02 +0200 Subject: [PATCH 43/69] moved logout function to check auth --- example/logout.php | 4 +--- src/SpidPHP.php | 4 +++- src/Strategy/PhpSamlOneLogin.php | 25 ++++++++++++++++++------- 3 files changed, 22 insertions(+), 11 deletions(-) diff --git a/example/logout.php b/example/logout.php index 1d474cd..e618fe2 100644 --- a/example/logout.php +++ b/example/logout.php @@ -12,7 +12,5 @@ $onelogin = new SpidPHP($settings); if ($onelogin->isAuthenticated()) { - $result = $onelogin->logout(); - print_r($result); - exit(); + $onelogin->logout(); } diff --git a/src/SpidPHP.php b/src/SpidPHP.php index 0c7c2e9..7f99141 100644 --- a/src/SpidPHP.php +++ b/src/SpidPHP.php @@ -56,12 +56,14 @@ public function login( $idpName, $redirectTo = '', $level = 1 ) public function logout() { + if (isset($_SESSION['idpName']) && is_null($this->phpSaml)) $this->initStrategy($_SESSION['idpName']); if (is_null($this->phpSaml)) return false; return $this->phpSaml->logout(); } public function getAttributes() - { + { + if (is_null($this->phpSaml)) return false; return $this->phpSaml->getAttributes(); } diff --git a/src/Strategy/PhpSamlOneLogin.php b/src/Strategy/PhpSamlOneLogin.php index 38419c8..1a7599e 100644 --- a/src/Strategy/PhpSamlOneLogin.php +++ b/src/Strategy/PhpSamlOneLogin.php @@ -88,6 +88,17 @@ public function isAuthenticated() $this->rebuildPhpSamlOnelogin($_SESSION['idpName']); $this->authRequestID =$_SESSION['authReqID']; } + + if (isset($_SESSION) && isset($_SESSION['LogoutRequestID'])) { + $this->auth->processSLO(false, $_SESSION['LogoutRequestID']); + + $errors = $this->auth->getErrors(); + if (!empty($errors)) { + return $errors; + } + return false; + } + if (!is_null($this->authRequestID) && isset($_POST['SAMLResponse'])) { $this->auth->processResponse($this->authRequestID); $this->authRequestID = null; @@ -137,14 +148,14 @@ public function logout() return false; } $this->auth->logout(); - $this->auth->processSLO(); - $errors = $this->auth->getErrors(); - if (!empty($errors)) { - return $errors; - } - - return true; + $sloBuiltUrl = $this->auth->logout(null, array(), null, null, true); + $_SESSION['LogoutRequestID'] = $this->auth->getLastRequestID(); + + header('Pragma: no-cache'); + header('Cache-Control: no-cache, must-revalidate'); + header('Location: ' . $sloBuiltUrl); + exit(); } public function getAttributes() From ee7a556985e1fa33a9ec4aaac1f62b6b5b62173a Mon Sep 17 00:00:00 2001 From: Lorenzo Cattaneo Date: Thu, 2 Aug 2018 14:18:00 +0200 Subject: [PATCH 44/69] better example and check authentication status --- example/acs.php | 3 +-- example/login.php | 2 ++ example/logout.php | 2 ++ example/metadata.php | 3 --- src/Strategy/PhpSamlOneLogin.php | 15 ++++++++------- 5 files changed, 13 insertions(+), 12 deletions(-) diff --git a/example/acs.php b/example/acs.php index c8bf76a..25cf118 100644 --- a/example/acs.php +++ b/example/acs.php @@ -14,12 +14,11 @@ if ($onelogin->isAuthenticated()) { $attributes = $onelogin->getAttributes(); echo "logged in !" . PHP_EOL; - var_dump($attributes); foreach ($attributes as $key => $attribute) { echo $key .": " . $attribute . "\n"; } - echo '

Login

'; + echo '

Logout

'; } else { echo "not logged in !" . PHP_EOL; echo '

Login

'; diff --git a/example/login.php b/example/login.php index a9607f2..3f24a21 100644 --- a/example/login.php +++ b/example/login.php @@ -13,5 +13,7 @@ if ($onelogin->isAuthenticated() === false) { $onelogin->login("testenv2"); +} else { + echo "Already logged in!"; } diff --git a/example/logout.php b/example/logout.php index e618fe2..6f56ac5 100644 --- a/example/logout.php +++ b/example/logout.php @@ -13,4 +13,6 @@ if ($onelogin->isAuthenticated()) { $onelogin->logout(); +} else { + echo "Logged out!"; } diff --git a/example/metadata.php b/example/metadata.php index 374fc15..3358573 100644 --- a/example/metadata.php +++ b/example/metadata.php @@ -15,6 +15,3 @@ header('Content-Type: text/xml'); echo $metadata; -//if (!$onelogin->isAuthenticated()) $onelogin->login(); - -//if ($onelogin->login()) $onelogin->logout(); diff --git a/src/Strategy/PhpSamlOneLogin.php b/src/Strategy/PhpSamlOneLogin.php index 1a7599e..8d16f56 100644 --- a/src/Strategy/PhpSamlOneLogin.php +++ b/src/Strategy/PhpSamlOneLogin.php @@ -54,7 +54,7 @@ private function init() $this->auth = new Auth($this->settingsHelper->getSettings()); } - private function rebuildPhpSamlOnelogin($idpName) + private function changeIdp($idpName) { if ($this->idpName != $idpName) { $this->idpName = $idpName; @@ -84,13 +84,14 @@ public function getSupportedIdps() public function isAuthenticated() { - if (isset($_SESSION) && isset($_SESSION['authReqID'])) { - $this->rebuildPhpSamlOnelogin($_SESSION['idpName']); - $this->authRequestID =$_SESSION['authReqID']; + if (isset($_SESSION) && isset($_SESSION['idpName'])) { + $this->changeIdp($_SESSION['idpName']); + $this->authRequestID = $_SESSION['authReqID']; } if (isset($_SESSION) && isset($_SESSION['LogoutRequestID'])) { $this->auth->processSLO(false, $_SESSION['LogoutRequestID']); + unset($_SESSION['LogoutRequestID']); $errors = $this->auth->getErrors(); if (!empty($errors)) { @@ -99,9 +100,9 @@ public function isAuthenticated() return false; } - if (!is_null($this->authRequestID) && isset($_POST['SAMLResponse'])) { - $this->auth->processResponse($this->authRequestID); - $this->authRequestID = null; + if (isset($_SESSION['authReqID']) && isset($_POST['SAMLResponse'])) { + $this->auth->processResponse($_SESSION['authReqID']); + unset($_SESSION['authReqID']); $errors = $this->auth->getErrors(); if (!empty($errors)) { From 9aa48ba338332f0fa179ea4138a1827f01841872 Mon Sep 17 00:00:00 2001 From: Lorenzo Cattaneo Date: Thu, 2 Aug 2018 14:20:39 +0200 Subject: [PATCH 45/69] bugfix method name --- src/Strategy/PhpSamlOneLogin.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Strategy/PhpSamlOneLogin.php b/src/Strategy/PhpSamlOneLogin.php index 8d16f56..63f7e3f 100644 --- a/src/Strategy/PhpSamlOneLogin.php +++ b/src/Strategy/PhpSamlOneLogin.php @@ -125,7 +125,7 @@ public function isAuthenticated() public function login( $idpName, $redirectTo = '', $level = 1 ) { - $this->rebuildPhpSamlOnelogin($idpName); + $this->changeIdp()($idpName); if ($this->auth->isAuthenticated()) { return false; From 095c9e99e1dd6d16500a1e4f255f1e576b4e7251 Mon Sep 17 00:00:00 2001 From: Lorenzo Cattaneo Date: Thu, 2 Aug 2018 14:22:38 +0200 Subject: [PATCH 46/69] bugfix --- src/Strategy/PhpSamlOneLogin.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Strategy/PhpSamlOneLogin.php b/src/Strategy/PhpSamlOneLogin.php index 63f7e3f..48cac81 100644 --- a/src/Strategy/PhpSamlOneLogin.php +++ b/src/Strategy/PhpSamlOneLogin.php @@ -125,7 +125,7 @@ public function isAuthenticated() public function login( $idpName, $redirectTo = '', $level = 1 ) { - $this->changeIdp()($idpName); + $this->changeIdp($idpName); if ($this->auth->isAuthenticated()) { return false; From 2aded590b445da0052e54363aed41f3e42c485c2 Mon Sep 17 00:00:00 2001 From: Lorenzo Cattaneo Date: Thu, 2 Aug 2018 14:43:23 +0200 Subject: [PATCH 47/69] test requesting attributes --- src/Config/OneloginSamlConfig.php | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/Config/OneloginSamlConfig.php b/src/Config/OneloginSamlConfig.php index dfc452f..4018209 100644 --- a/src/Config/OneloginSamlConfig.php +++ b/src/Config/OneloginSamlConfig.php @@ -67,6 +67,24 @@ public function getSettings() ), 'x509cert' => $this->idpCertValue, ), + "attributeConsumingService"=> array( + "serviceName" => "SP test", + "serviceDescription" => "Test Service", + "requestedAttributes" => array( + array( + "name" => "name", + "isRequired" => false, + ), + array( + "familyName" => "name", + "isRequired" => false, + ), + array( + "name" => "fiscalNumber", + "isRequired" => false, + ), + ) + ), 'security' => array( 'authnRequestsSigned' => true, 'logoutRequestSigned' => true, From fb6aaca51be07db77bf78e5e774d8a72a013b975 Mon Sep 17 00:00:00 2001 From: Lorenzo Cattaneo Date: Thu, 2 Aug 2018 15:12:21 +0200 Subject: [PATCH 48/69] testing settings - request attributes --- src/Config/OneloginSamlConfig.php | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/src/Config/OneloginSamlConfig.php b/src/Config/OneloginSamlConfig.php index 4018209..adebd22 100644 --- a/src/Config/OneloginSamlConfig.php +++ b/src/Config/OneloginSamlConfig.php @@ -71,17 +71,23 @@ public function getSettings() "serviceName" => "SP test", "serviceDescription" => "Test Service", "requestedAttributes" => array( - array( - "name" => "name", - "isRequired" => false, + array ( + 'nameFormat' => OneLogin\Saml2\Constants::ATTRNAME_FORMAT_URI, + 'isRequired' => true, + 'name' => 'name', + 'friendlyName' => 'Nome' ), - array( - "familyName" => "name", - "isRequired" => false, + array ( + 'nameFormat' => OneLogin\Saml2\Constants::ATTRNAME_FORMAT_URI, + 'isRequired' => true, + 'name' => 'familyName', + 'friendlyName' => 'Cognome' ), - array( - "name" => "fiscalNumber", - "isRequired" => false, + array ( + 'nameFormat' => OneLogin\Saml2\Constants::ATTRNAME_FORMAT_URI, + 'isRequired' => true, + 'name' => 'fiscalNumber', + 'friendlyName' => 'Codice Fiscale' ), ) ), From 155e2bef39d0d1ef8da6183835ab490beef98eb0 Mon Sep 17 00:00:00 2001 From: Lorenzo Cattaneo Date: Thu, 2 Aug 2018 15:13:21 +0200 Subject: [PATCH 49/69] fixed namespace --- src/Config/OneloginSamlConfig.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Config/OneloginSamlConfig.php b/src/Config/OneloginSamlConfig.php index adebd22..61c76e8 100644 --- a/src/Config/OneloginSamlConfig.php +++ b/src/Config/OneloginSamlConfig.php @@ -72,19 +72,19 @@ public function getSettings() "serviceDescription" => "Test Service", "requestedAttributes" => array( array ( - 'nameFormat' => OneLogin\Saml2\Constants::ATTRNAME_FORMAT_URI, + 'nameFormat' => \OneLogin\Saml2\Constants::ATTRNAME_FORMAT_URI, 'isRequired' => true, 'name' => 'name', 'friendlyName' => 'Nome' ), array ( - 'nameFormat' => OneLogin\Saml2\Constants::ATTRNAME_FORMAT_URI, + 'nameFormat' => \OneLogin\Saml2\Constants::ATTRNAME_FORMAT_URI, 'isRequired' => true, 'name' => 'familyName', 'friendlyName' => 'Cognome' ), array ( - 'nameFormat' => OneLogin\Saml2\Constants::ATTRNAME_FORMAT_URI, + 'nameFormat' => \OneLogin\Saml2\Constants::ATTRNAME_FORMAT_URI, 'isRequired' => true, 'name' => 'fiscalNumber', 'friendlyName' => 'Codice Fiscale' From 6750bc7dcc613b76a3ba21e32f1b62cf1b114871 Mon Sep 17 00:00:00 2001 From: Paolo Greppi Date: Thu, 2 Aug 2018 15:38:23 +0200 Subject: [PATCH 50/69] move the attrCS key under sp --- src/Config/OneloginSamlConfig.php | 50 +++++++++++++++---------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/src/Config/OneloginSamlConfig.php b/src/Config/OneloginSamlConfig.php index 61c76e8..bca06c8 100644 --- a/src/Config/OneloginSamlConfig.php +++ b/src/Config/OneloginSamlConfig.php @@ -56,6 +56,30 @@ public function getSettings() 'NameIDFormat' => 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient', 'x509cert' => $this->spCrtFile, 'privateKey' => $this->spKeyFile, + "attributeConsumingService"=> array( + "serviceName" => "SP test", + "serviceDescription" => "Test Service", + "requestedAttributes" => array( + array ( + 'nameFormat' => \OneLogin\Saml2\Constants::ATTRNAME_FORMAT_URI, + 'isRequired' => true, + 'name' => 'name', + 'friendlyName' => 'Nome' + ), + array ( + 'nameFormat' => \OneLogin\Saml2\Constants::ATTRNAME_FORMAT_URI, + 'isRequired' => true, + 'name' => 'familyName', + 'friendlyName' => 'Cognome' + ), + array ( + 'nameFormat' => \OneLogin\Saml2\Constants::ATTRNAME_FORMAT_URI, + 'isRequired' => true, + 'name' => 'fiscalNumber', + 'friendlyName' => 'Codice Fiscale' + ), + ) + ), ), 'idp' => array( 'entityId' => $this->idpEntityId, @@ -67,30 +91,6 @@ public function getSettings() ), 'x509cert' => $this->idpCertValue, ), - "attributeConsumingService"=> array( - "serviceName" => "SP test", - "serviceDescription" => "Test Service", - "requestedAttributes" => array( - array ( - 'nameFormat' => \OneLogin\Saml2\Constants::ATTRNAME_FORMAT_URI, - 'isRequired' => true, - 'name' => 'name', - 'friendlyName' => 'Nome' - ), - array ( - 'nameFormat' => \OneLogin\Saml2\Constants::ATTRNAME_FORMAT_URI, - 'isRequired' => true, - 'name' => 'familyName', - 'friendlyName' => 'Cognome' - ), - array ( - 'nameFormat' => \OneLogin\Saml2\Constants::ATTRNAME_FORMAT_URI, - 'isRequired' => true, - 'name' => 'fiscalNumber', - 'friendlyName' => 'Codice Fiscale' - ), - ) - ), 'security' => array( 'authnRequestsSigned' => true, 'logoutRequestSigned' => true, @@ -142,4 +142,4 @@ public function updateSpData($sp) { return $this->getSettings(); } -} \ No newline at end of file +} From f8120da447d9a55ce2ac30d1448df73ecf7ecc4b Mon Sep 17 00:00:00 2001 From: Paolo Greppi Date: Fri, 3 Aug 2018 10:51:49 +0200 Subject: [PATCH 51/69] cleanup patched files --- Makefile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Makefile b/Makefile index cca4cc9..ee5440e 100644 --- a/Makefile +++ b/Makefile @@ -17,3 +17,5 @@ sp.key: clean: rm -rf vendor + rm -f AuthnRequest.patched + rm -f LogoutRequest.patched From 9ef1595495573475648e01e2c9128ae8270e0202 Mon Sep 17 00:00:00 2001 From: Paolo Greppi Date: Fri, 3 Aug 2018 10:52:11 +0200 Subject: [PATCH 52/69] composer update --- composer.lock | 262 +++----------------------------------------------- 1 file changed, 11 insertions(+), 251 deletions(-) diff --git a/composer.lock b/composer.lock index bedc328..b2aabb3 100644 --- a/composer.lock +++ b/composer.lock @@ -1,10 +1,10 @@ { "_readme": [ "This file locks the dependencies of your project to a known state", - "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "d0f2b5c5b2a0656dd94e55792b743bdf", + "content-hash": "9c614b6245789a0e2cf885b1e6fc7014", "packages": [ { "name": "onelogin/php-saml", @@ -12,12 +12,12 @@ "source": { "type": "git", "url": "https://github.com/onelogin/php-saml.git", - "reference": "03efada0d2485268576a810834f2d61f9ff659aa" + "reference": "e0c5827d7ccff72b6cf19f55420cad0e4eea5faf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/onelogin/php-saml/zipball/03efada0d2485268576a810834f2d61f9ff659aa", - "reference": "03efada0d2485268576a810834f2d61f9ff659aa", + "url": "https://api.github.com/repos/onelogin/php-saml/zipball/e0c5827d7ccff72b6cf19f55420cad0e4eea5faf", + "reference": "e0c5827d7ccff72b6cf19f55420cad0e4eea5faf", "shasum": "" }, "require": { @@ -54,7 +54,7 @@ "onelogin", "saml" ], - "time": "2018-07-08T16:01:32+00:00" + "time": "2018-08-02T15:43:25+00:00" }, { "name": "robrichards/xmlseclibs", @@ -98,16 +98,16 @@ }, { "name": "squizlabs/php_codesniffer", - "version": "3.3.0", + "version": "3.3.1", "source": { "type": "git", "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", - "reference": "d86873af43b4aa9d1f39a3601cc0cfcf02b25266" + "reference": "628a481780561150481a9ec74709092b9759b3ec" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/d86873af43b4aa9d1f39a3601cc0cfcf02b25266", - "reference": "d86873af43b4aa9d1f39a3601cc0cfcf02b25266", + "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/628a481780561150481a9ec74709092b9759b3ec", + "reference": "628a481780561150481a9ec74709092b9759b3ec", "shasum": "" }, "require": { @@ -145,247 +145,7 @@ "phpcs", "standards" ], - "time": "2018-06-06T23:58:19+00:00" - }, - { - "name": "symfony/polyfill-ctype", - "version": "v1.8.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "7cc359f1b7b80fc25ed7796be7d96adc9b354bae" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/7cc359f1b7b80fc25ed7796be7d96adc9b354bae", - "reference": "7cc359f1b7b80fc25ed7796be7d96adc9b354bae", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.8-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Ctype\\": "" - }, - "files": [ - "bootstrap.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - }, - { - "name": "Gert de Pagter", - "email": "BackEndTea@gmail.com" - } - ], - "description": "Symfony polyfill for ctype functions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "ctype", - "polyfill", - "portable" - ], - "time": "2018-04-30T19:57:29+00:00" - }, - { - "name": "symfony/polyfill-mbstring", - "version": "v1.8.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "3296adf6a6454a050679cde90f95350ad604b171" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/3296adf6a6454a050679cde90f95350ad604b171", - "reference": "3296adf6a6454a050679cde90f95350ad604b171", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "suggest": { - "ext-mbstring": "For best performance" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.8-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Mbstring\\": "" - }, - "files": [ - "bootstrap.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill for the Mbstring extension", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "mbstring", - "polyfill", - "portable", - "shim" - ], - "time": "2018-04-26T10:06:28+00:00" - }, - { - "name": "symfony/yaml", - "version": "v4.1.2", - "source": { - "type": "git", - "url": "https://github.com/symfony/yaml.git", - "reference": "80e4bfa9685fc4a09acc4a857ec16974a9cd944e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/80e4bfa9685fc4a09acc4a857ec16974a9cd944e", - "reference": "80e4bfa9685fc4a09acc4a857ec16974a9cd944e", - "shasum": "" - }, - "require": { - "php": "^7.1.3", - "symfony/polyfill-ctype": "~1.8" - }, - "conflict": { - "symfony/console": "<3.4" - }, - "require-dev": { - "symfony/console": "~3.4|~4.0" - }, - "suggest": { - "symfony/console": "For validating YAML files using the lint command" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.1-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Component\\Yaml\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony Yaml Component", - "homepage": "https://symfony.com", - "time": "2018-05-30T07:26:09+00:00" - }, - { - "name": "twig/twig", - "version": "v2.5.0", - "source": { - "type": "git", - "url": "https://github.com/twigphp/Twig.git", - "reference": "6a5f676b77a90823c2d4eaf76137b771adf31323" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/twigphp/Twig/zipball/6a5f676b77a90823c2d4eaf76137b771adf31323", - "reference": "6a5f676b77a90823c2d4eaf76137b771adf31323", - "shasum": "" - }, - "require": { - "php": "^7.0", - "symfony/polyfill-ctype": "^1.8", - "symfony/polyfill-mbstring": "~1.0" - }, - "require-dev": { - "psr/container": "^1.0", - "symfony/debug": "^2.7", - "symfony/phpunit-bridge": "^3.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.5-dev" - } - }, - "autoload": { - "psr-0": { - "Twig_": "lib/" - }, - "psr-4": { - "Twig\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com", - "homepage": "http://fabien.potencier.org", - "role": "Lead Developer" - }, - { - "name": "Armin Ronacher", - "email": "armin.ronacher@active-4.com", - "role": "Project Founder" - }, - { - "name": "Twig Team", - "homepage": "https://twig.symfony.com/contributors", - "role": "Contributors" - } - ], - "description": "Twig, the flexible, fast, and secure template language for PHP", - "homepage": "https://twig.symfony.com", - "keywords": [ - "templating" - ], - "time": "2018-07-13T07:18:09+00:00" + "time": "2018-07-26T23:47:18+00:00" } ], "packages-dev": [], From a836d92a301d8999046974a6070833d49581a009 Mon Sep 17 00:00:00 2001 From: Paolo Greppi Date: Fri, 3 Aug 2018 10:55:44 +0200 Subject: [PATCH 53/69] refresh patches; patc php-saml to send optional attribute AttributeConsumingServiceIndex (required for #21) and AssertionConsumerServiceIndex instead of AssertionConsumerServiceURL + ProtocolBinding --- AuthnRequest.diff | 8 ++++++-- LogoutRequest.diff | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/AuthnRequest.diff b/AuthnRequest.diff index e11c2e7..e3265b4 100644 --- a/AuthnRequest.diff +++ b/AuthnRequest.diff @@ -1,9 +1,13 @@ -56,57c56 +72,73c72 < Format="{$nameIDPolicyFormat}" < AllowCreate="true" /> --- > Format="{$nameIDPolicyFormat}" /> -130c129 +143,145c142,144 +< ProtocolBinding="{$spData['assertionConsumerService']['binding']}" +< AssertionConsumerServiceURL="{$acsUrl}"> < {$spEntityId} --- +> AssertionConsumerServiceIndex="1" +> AttributeConsumingServiceIndex="1"> > {$spEntityId} diff --git a/LogoutRequest.diff b/LogoutRequest.diff index 19a77dc..782267d 100644 --- a/LogoutRequest.diff +++ b/LogoutRequest.diff @@ -1,4 +1,4 @@ -107c107 +128c128 < {$spEntityId} --- > {$spEntityId} From 2c7a36eb1eb3f5d14b0d2808875db99682384a4a Mon Sep 17 00:00:00 2001 From: Lorenzo Cattaneo Date: Fri, 3 Aug 2018 13:03:56 +0200 Subject: [PATCH 54/69] aggiunta cartella idp e lista idp supportati --- .gitignore | 3 +++ example/acs.php | 2 +- example/idps.php | 11 +++++++++ idp_metadata/put_idp_metadata_here | 0 src/Config/OneloginSamlConfig.php | 16 ++++++++---- src/Config/idp/testenv2.xml | 39 ------------------------------ src/Helpers/IdpHelper.php | 4 +-- src/SpidPHP.php | 3 ++- src/Strategy/PhpSamlOneLogin.php | 21 ++++++++++++---- 9 files changed, 46 insertions(+), 53 deletions(-) create mode 100644 example/idps.php create mode 100644 idp_metadata/put_idp_metadata_here delete mode 100644 src/Config/idp/testenv2.xml diff --git a/.gitignore b/.gitignore index 50b1cd9..1d93f62 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,6 @@ LogoutRequest.patched .vscode .DS_Store +src/Config/idp/*.xml + +idp_metadata/*.xml \ No newline at end of file diff --git a/example/acs.php b/example/acs.php index 25cf118..e83c9c3 100644 --- a/example/acs.php +++ b/example/acs.php @@ -15,7 +15,7 @@ $attributes = $onelogin->getAttributes(); echo "logged in !" . PHP_EOL; foreach ($attributes as $key => $attribute) { - echo $key .": " . $attribute . "\n"; + echo $key .": " . $attribute . "
"; } echo '

Logout

'; diff --git a/example/idps.php b/example/idps.php new file mode 100644 index 0000000..1406f0f --- /dev/null +++ b/example/idps.php @@ -0,0 +1,11 @@ +getSupportedIdps() as $key => $idp) { + echo $key . ' - ' . $idp . '
'; +} \ No newline at end of file diff --git a/idp_metadata/put_idp_metadata_here b/idp_metadata/put_idp_metadata_here new file mode 100644 index 0000000..e69de29 diff --git a/src/Config/OneloginSamlConfig.php b/src/Config/OneloginSamlConfig.php index bca06c8..8c9fabd 100644 --- a/src/Config/OneloginSamlConfig.php +++ b/src/Config/OneloginSamlConfig.php @@ -22,6 +22,10 @@ class OneloginSamlConfig var $idpSSO = null; var $idpSLO = null; var $idpCertValue = null; + + var $level = 1; + + var $idp_list = array(); private $is_not_updatable = ['spKeyFileValue', 'spCrtFileValue', 'idpEntityId', 'idpSSO', 'idpSLO', 'idpCertValue']; @@ -61,19 +65,19 @@ public function getSettings() "serviceDescription" => "Test Service", "requestedAttributes" => array( array ( - 'nameFormat' => \OneLogin\Saml2\Constants::ATTRNAME_FORMAT_URI, + 'nameFormat' => \OneLogin\Saml2\Constants::ATTRNAME_FORMAT_BASIC, 'isRequired' => true, 'name' => 'name', 'friendlyName' => 'Nome' ), array ( - 'nameFormat' => \OneLogin\Saml2\Constants::ATTRNAME_FORMAT_URI, + 'nameFormat' => \OneLogin\Saml2\Constants::ATTRNAME_FORMAT_BASIC, 'isRequired' => true, 'name' => 'familyName', 'friendlyName' => 'Cognome' ), array ( - 'nameFormat' => \OneLogin\Saml2\Constants::ATTRNAME_FORMAT_URI, + 'nameFormat' => \OneLogin\Saml2\Constants::ATTRNAME_FORMAT_BASIC, 'isRequired' => true, 'name' => 'fiscalNumber', 'friendlyName' => 'Codice Fiscale' @@ -97,14 +101,13 @@ public function getSettings() 'logoutResponseSigned' => true, 'signMetadata' => true, 'signatureAlgorithm' => 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256', - 'requestedAuthnContext' => array('https://www.spid.gov.it/SpidL1'), + 'requestedAuthnContext' => array('https://www.spid.gov.it/SpidL' . $this->level), ), ); } public function updateSettings($settings) { foreach ($settings as $key => $value) { - // do not update idp os sp cert file values, they are updated in their own method if (!property_exists(OneloginSamlConfig::class, $key)) { continue; } @@ -124,6 +127,9 @@ public function updateSettings($settings) { } public function updateIdpMetadata($idpName) { + if (!array_key_exists($idpName, $this->idp_list)) { + throw new Exception("Unsupported IDP provided", 1); + } $metadata = IdpHelper::getMetadata($idpName); foreach ($metadata as $key => $value) { if (property_exists(OneloginSamlConfig::class, $key) && strpos($key, "idp") !== false) { diff --git a/src/Config/idp/testenv2.xml b/src/Config/idp/testenv2.xml deleted file mode 100644 index 1030cb0..0000000 --- a/src/Config/idp/testenv2.xml +++ /dev/null @@ -1,39 +0,0 @@ - - - - - - - - MIIDiDCCAnCgAwIBAgIJAJnqANY7GvtNMA0GCSqGSIb3DQEBCwUAMFkxCzAJBgNV - BAYTAklUMQ4wDAYDVQQIDAVJdGFseTENMAsGA1UEBwwEUm9tZTERMA8GA1UECgwI - dGVzdGVudjIxGDAWBgNVBAMMDyR7YW5zaWJsZV9mcWRufTAeFw0xODA3MTkxMzEw - MDVaFw0xOTA3MTkxMzEwMDVaMFkxCzAJBgNVBAYTAklUMQ4wDAYDVQQIDAVJdGFs - eTENMAsGA1UEBwwEUm9tZTERMA8GA1UECgwIdGVzdGVudjIxGDAWBgNVBAMMDyR7 - YW5zaWJsZV9mcWRufTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALTP - ycItk7xvubL0WgegvbtCvhzeCq13WorzjYQdpWvUwY8YmL51h6ssKfWMfnSj96Ix - lJw2HTm+TgHK2/S7Iw7gh6xoaY+LDmkZGJKzUSFR/Hn7WbFNb3jytowiNDQdcBZd - hNhEnDvd2fLMGxD81qdlMx3y5XHP+p9Gc8bjp8aShGw+DQpWBXcfoDnCXz5ywmnR - oD66CnMFMQXmD0wZf79/0fY+Muwn83r7P0h6bJCFDjPdGiSeo7q5AJKkERoNoedc - HThCuT0uN36bDaVBIcSxVtPjvVhfcNIVN5JHaCLZT89aU8DAJlTUiO3u5Nj4aDjD - XAhxMWkwGbHXCznhjVMCAwEAAaNTMFEwHQYDVR0OBBYEFH1/ReU+04oYtVpMgD/z - VCPuagu6MB8GA1UdIwQYMBaAFH1/ReU+04oYtVpMgD/zVCPuagu6MA8GA1UdEwEB - /wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAAWT9+r+ojVmxUUZvq8/TumEX9Y0 - FxxQebTcPMeumA58mp9kDCeNK73PZ6cbQSGXzwbNmHw48N2kZMl3rS8ddYPG3nFx - EZO5Xi0De2SLGwLZX43lfD4BHhhqhTlnBK8cL+LvySQ32X1vL8aKly/UTez3/DCr - dTqFjp1V0PdY2q0Ni3UAiO90MpFGbP+aAQ1LGtI0EWpDCVAqqgAA2EA+s0AlwbC5 - cuDbQUjHA5dhkWT/4kvHB/E5zPZUtRV4d3mYBs33lfIyKXWjjgR4T8j01wMk2LWC - nBU8i4mCb1w3v94KcRbnK23bJLz+zNp6I3belhLknSahyjKPl+6FBSyW/GE= - - - - - - - - urn:oasis:names:tc:SAML:2.0:nameid-format:transient - - - - - diff --git a/src/Helpers/IdpHelper.php b/src/Helpers/IdpHelper.php index 23176f5..f4ca20e 100644 --- a/src/Helpers/IdpHelper.php +++ b/src/Helpers/IdpHelper.php @@ -6,11 +6,11 @@ class IdpHelper { public static function getMetadata($idpName) { - if (!file_exists(__DIR__ . "/../Config/idp/" . $idpName . ".xml")) { + if (!file_exists(__DIR__ . "/../../idp_metadata/" . $idpName . ".xml")) { throw new \Exception("Invalid IDP Requested", 1); } - $xml = simplexml_load_file(__DIR__ . "/../Config/idp/" . $idpName . '.xml'); + $xml = simplexml_load_file(__DIR__ . "/../../idp_metadata/" . $idpName . '.xml'); $metadata = array(); $metadata['idpEntityId'] = $xml->attributes()->entityID->__toString(); diff --git a/src/SpidPHP.php b/src/SpidPHP.php index 7f99141..253fe78 100644 --- a/src/SpidPHP.php +++ b/src/SpidPHP.php @@ -38,7 +38,8 @@ public function getSPMetadata() public function getSupportedIdps() { - return array(); + if (is_null($this->phpSaml)) return array(); + return $this->phpSaml->getSupportedIdps(); } public function isAuthenticated() diff --git a/src/Strategy/PhpSamlOneLogin.php b/src/Strategy/PhpSamlOneLogin.php index 48cac81..8d2d67b 100644 --- a/src/Strategy/PhpSamlOneLogin.php +++ b/src/Strategy/PhpSamlOneLogin.php @@ -46,6 +46,8 @@ private function init() } throw new \Exception($message, 1); } + // FI suer doesn't provide for an IDP mapping, a default one is provided + $this->settings['idp_list'] = $this->getSupportedIdps(); $settingsHelper->updateSettings($this->settings); } @@ -78,15 +80,24 @@ public function getSPMetadata() } public function getSupportedIdps() - { - return array(); + { + if (array_key_exists('idp_list', $this->settings) && is_array($this->settings['idp_list'])) { + return $this->settings['idp_list']; + } + $dir = __DIR__ . '/../../idp_metadata'; + $idp_files = glob( $dir . '*.{xml}', GLOB_BRACE); + $idps = array(); + foreach ($idp_files as $key => $value) { + $xml = simplexml_load_file($value); + $idps[basename($value, '.xml')] = $xml->attributes()->entityID->__toString(); + } + return $idps; } public function isAuthenticated() { if (isset($_SESSION) && isset($_SESSION['idpName'])) { $this->changeIdp($_SESSION['idpName']); - $this->authRequestID = $_SESSION['authReqID']; } if (isset($_SESSION) && isset($_SESSION['LogoutRequestID'])) { @@ -114,7 +125,6 @@ public function isAuthenticated() $this->userdata['samlNameId'] = $this->auth->getNameId(); $this->userdata['samlNameIdFormat'] = $this->auth->getNameIdFormat(); $this->userdata['samlSessionIndex'] = $this->auth->getSessionIndex(); - $_SESSION['userdata'] = $this->userdata; } if ($this->auth->isAuthenticated() === false) { @@ -125,6 +135,7 @@ public function isAuthenticated() public function login( $idpName, $redirectTo = '', $level = 1 ) { + $this->settings['level'] = $level; $this->changeIdp($idpName); if ($this->auth->isAuthenticated()) { @@ -161,7 +172,7 @@ public function logout() public function getAttributes() { - return $this->userdata; + return $this->userdata['attributes']; } } \ No newline at end of file From 576bc4102de6301e564bdf845a1639bd86cb7973 Mon Sep 17 00:00:00 2001 From: Lorenzo Cattaneo Date: Fri, 3 Aug 2018 13:05:38 +0200 Subject: [PATCH 55/69] fix exception --- src/Config/OneloginSamlConfig.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config/OneloginSamlConfig.php b/src/Config/OneloginSamlConfig.php index 8c9fabd..62b1863 100644 --- a/src/Config/OneloginSamlConfig.php +++ b/src/Config/OneloginSamlConfig.php @@ -128,7 +128,7 @@ public function updateSettings($settings) { public function updateIdpMetadata($idpName) { if (!array_key_exists($idpName, $this->idp_list)) { - throw new Exception("Unsupported IDP provided", 1); + throw new \Exception("Unsupported IDP provided", 1); } $metadata = IdpHelper::getMetadata($idpName); foreach ($metadata as $key => $value) { From 1b423bf60ccff9489c3cd406fa9c6fd6dc777b24 Mon Sep 17 00:00:00 2001 From: Lorenzo Cattaneo Date: Fri, 3 Aug 2018 13:25:38 +0200 Subject: [PATCH 56/69] better init strategy --- src/SpidPHP.php | 9 ++------- src/Strategy/PhpSamlOneLogin.php | 9 ++++++--- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/SpidPHP.php b/src/SpidPHP.php index 253fe78..53733bc 100644 --- a/src/SpidPHP.php +++ b/src/SpidPHP.php @@ -16,6 +16,8 @@ public function __construct($settings = null, $mode = 'onelogin') session_start(); $this->mode = $mode; $this->settings = $settings; + + $this->initStrategy(); } private function initStrategy($idpName = null) @@ -32,20 +34,16 @@ private function initStrategy($idpName = null) public function getSPMetadata() { - if (is_null($this->phpSaml)) $this->initStrategy(); return $this->phpSaml->getSPMetadata(); } public function getSupportedIdps() { - if (is_null($this->phpSaml)) return array(); return $this->phpSaml->getSupportedIdps(); } public function isAuthenticated() { - if (isset($_SESSION['idpName']) && is_null($this->phpSaml)) $this->initStrategy($_SESSION['idpName']); - if (is_null($this->phpSaml)) return false; return $this->phpSaml->isAuthenticated(); } @@ -57,14 +55,11 @@ public function login( $idpName, $redirectTo = '', $level = 1 ) public function logout() { - if (isset($_SESSION['idpName']) && is_null($this->phpSaml)) $this->initStrategy($_SESSION['idpName']); - if (is_null($this->phpSaml)) return false; return $this->phpSaml->logout(); } public function getAttributes() { - if (is_null($this->phpSaml)) return false; return $this->phpSaml->getAttributes(); } diff --git a/src/Strategy/PhpSamlOneLogin.php b/src/Strategy/PhpSamlOneLogin.php index 8d2d67b..940d4df 100644 --- a/src/Strategy/PhpSamlOneLogin.php +++ b/src/Strategy/PhpSamlOneLogin.php @@ -25,7 +25,7 @@ class PhpSamlOneLogin implements PhpSamlInterface function __construct($idpName = null, $settings) { - $this->idpName = $idpName ?? "testenv2"; + $this->idpName = $idpName; $this->settings = $settings; $this->init(); } @@ -50,7 +50,8 @@ private function init() $this->settings['idp_list'] = $this->getSupportedIdps(); $settingsHelper->updateSettings($this->settings); } - + reset($this->settings['idp_list']); + $this->idpName = is_null($this->idpName) ? key($this->settings['idp_list']) : $this->idpName; $this->settingsHelper->updateIdpMetadata($this->idpName); $this->oneloginSettings = new Settings($this->settingsHelper->getSettings()); $this->auth = new Auth($this->settingsHelper->getSettings()); @@ -84,6 +85,7 @@ public function getSupportedIdps() if (array_key_exists('idp_list', $this->settings) && is_array($this->settings['idp_list'])) { return $this->settings['idp_list']; } + return array(2,3,4); $dir = __DIR__ . '/../../idp_metadata'; $idp_files = glob( $dir . '*.{xml}', GLOB_BRACE); $idps = array(); @@ -171,7 +173,8 @@ public function logout() } public function getAttributes() - { + { + if (is_null($this->userdata) || !array_key_exists('attributes', $this->userdata)) return array(); return $this->userdata['attributes']; } From 8bb62a015415014d8ab160408d5795444994ba85 Mon Sep 17 00:00:00 2001 From: Lorenzo Cattaneo Date: Fri, 3 Aug 2018 18:56:33 +0200 Subject: [PATCH 57/69] made project folder independant --- example/idps.php | 1 + example/settings.php | 3 ++- src/Config/OneloginSamlConfig.php | 2 ++ src/Helpers/Constants.php | 6 ++++++ src/Helpers/IdpHelper.php | 6 ++++-- src/Strategy/PhpSamlOneLogin.php | 9 +++++++-- 6 files changed, 22 insertions(+), 5 deletions(-) create mode 100644 src/Helpers/Constants.php diff --git a/example/idps.php b/example/idps.php index 1406f0f..99a6af9 100644 --- a/example/idps.php +++ b/example/idps.php @@ -4,6 +4,7 @@ use SpidPHP\SpidPHP; + $onelogin = new SpidPHP($settings); foreach ($onelogin->getSupportedIdps() as $key => $idp) { diff --git a/example/settings.php b/example/settings.php index 054c3a3..f7879f8 100644 --- a/example/settings.php +++ b/example/settings.php @@ -7,5 +7,6 @@ 'spKeyFile' => __DIR__ . "/../sp.key", 'spCrtFile' => __DIR__ . "/../sp.crt", 'spAcsUrl' => $base."/acs.php", - 'spSloUrl' => $base."/logout.php" + 'spSloUrl' => $base."/logout.php", + "idpMetadataFolderPath" => "idp_metadata" ]; diff --git a/src/Config/OneloginSamlConfig.php b/src/Config/OneloginSamlConfig.php index 62b1863..ed1a403 100644 --- a/src/Config/OneloginSamlConfig.php +++ b/src/Config/OneloginSamlConfig.php @@ -22,11 +22,13 @@ class OneloginSamlConfig var $idpSSO = null; var $idpSLO = null; var $idpCertValue = null; + var $idpMetadataFolderPath= null; var $level = 1; var $idp_list = array(); + private $is_required = ['spBaseUrl', ]; private $is_not_updatable = ['spKeyFileValue', 'spCrtFileValue', 'idpEntityId', 'idpSSO', 'idpSLO', 'idpCertValue']; function __construct() diff --git a/src/Helpers/Constants.php b/src/Helpers/Constants.php new file mode 100644 index 0000000..28278cd --- /dev/null +++ b/src/Helpers/Constants.php @@ -0,0 +1,6 @@ +settings) && is_array($this->settings['idp_list'])) { + return $this->settings['idp_list']; } - return array(2,3,4); - $dir = __DIR__ . '/../../idp_metadata'; + + $dir = APP_PATH . $this->settings['idpMetadataFolderPath']; $idp_files = glob( $dir . '*.{xml}', GLOB_BRACE); $idps = array(); foreach ($idp_files as $key => $value) { $xml = simplexml_load_file($value); $idps[basename($value, '.xml')] = $xml->attributes()->entityID->__toString(); } + print_r($idp_files); + die; return $idps; } From b8f7d7c4ec9f4b65b29fbfc6d34ab8a287040c4b Mon Sep 17 00:00:00 2001 From: Lorenzo Cattaneo Date: Fri, 3 Aug 2018 19:20:26 +0200 Subject: [PATCH 58/69] fix root folder path --- example/settings.php | 2 +- src/Helpers/Constants.php | 12 +++++++++--- src/Helpers/IdpHelper.php | 6 ++---- src/Strategy/PhpSamlOneLogin.php | 4 ++-- 4 files changed, 14 insertions(+), 10 deletions(-) diff --git a/example/settings.php b/example/settings.php index f7879f8..0135ac0 100644 --- a/example/settings.php +++ b/example/settings.php @@ -8,5 +8,5 @@ 'spCrtFile' => __DIR__ . "/../sp.crt", 'spAcsUrl' => $base."/acs.php", 'spSloUrl' => $base."/logout.php", - "idpMetadataFolderPath" => "idp_metadata" + "idpMetadataFolderPath" => "../idp_metadata" ]; diff --git a/src/Helpers/Constants.php b/src/Helpers/Constants.php index 28278cd..9f47c6c 100644 --- a/src/Helpers/Constants.php +++ b/src/Helpers/Constants.php @@ -1,6 +1,12 @@ attributes()->entityID->__toString(); diff --git a/src/Strategy/PhpSamlOneLogin.php b/src/Strategy/PhpSamlOneLogin.php index 63920fa..8c0e788 100644 --- a/src/Strategy/PhpSamlOneLogin.php +++ b/src/Strategy/PhpSamlOneLogin.php @@ -6,12 +6,12 @@ use SpidPHP\Helpers\ArrayHelper; use SpidPHP\Helpers\IdpHelper; use SpidPHP\Helpers\SpHelper; -use const SpidPHP\Helpers\Constants\APP_PATH; use SpidPHP\Config\OneloginSamlConfig; use OneLogin\Saml2\Auth; use OneLogin\Saml2\Utils; use OneLogin\Saml2\Settings; +use SpidPHP\Helpers\Constants; class PhpSamlOneLogin implements PhpSamlInterface @@ -89,7 +89,7 @@ public function getSupportedIdps() return $this->settings['idp_list']; } - $dir = APP_PATH . $this->settings['idpMetadataFolderPath']; + $dir = Constants::APP_PATH . $this->settings['idpMetadataFolderPath']; $idp_files = glob( $dir . '*.{xml}', GLOB_BRACE); $idps = array(); foreach ($idp_files as $key => $value) { From de10e762dd784f80a997cd02585cb4a3b0ff467d Mon Sep 17 00:00:00 2001 From: Lorenzo Cattaneo Date: Fri, 3 Aug 2018 19:53:15 +0200 Subject: [PATCH 59/69] WIP : better handling of supported IDP list --- example/idps.php | 1 - example/settings.php | 2 +- src/Helpers/Constants.php | 1 + src/Helpers/PathHelper.php | 19 +++++++++++++++++++ src/Strategy/PhpSamlOneLogin.php | 6 ++---- 5 files changed, 23 insertions(+), 6 deletions(-) create mode 100644 src/Helpers/PathHelper.php diff --git a/example/idps.php b/example/idps.php index 99a6af9..8697ed9 100644 --- a/example/idps.php +++ b/example/idps.php @@ -6,7 +6,6 @@ $onelogin = new SpidPHP($settings); - foreach ($onelogin->getSupportedIdps() as $key => $idp) { echo $key . ' - ' . $idp . '
'; } \ No newline at end of file diff --git a/example/settings.php b/example/settings.php index 0135ac0..dd5a439 100644 --- a/example/settings.php +++ b/example/settings.php @@ -8,5 +8,5 @@ 'spCrtFile' => __DIR__ . "/../sp.crt", 'spAcsUrl' => $base."/acs.php", 'spSloUrl' => $base."/logout.php", - "idpMetadataFolderPath" => "../idp_metadata" + "idpMetadataFolderPath" => "spid-php2/idp_metadata" ]; diff --git a/src/Helpers/Constants.php b/src/Helpers/Constants.php index 9f47c6c..e337ef9 100644 --- a/src/Helpers/Constants.php +++ b/src/Helpers/Constants.php @@ -3,6 +3,7 @@ namespace SpidPHP\Helpers; class Constants { + // Project root folder, assuming the package has been installed with composer. const APP_PATH = __DIR__ . '/../../../'; public static function getAppPath() diff --git a/src/Helpers/PathHelper.php b/src/Helpers/PathHelper.php new file mode 100644 index 0000000..baa19dd --- /dev/null +++ b/src/Helpers/PathHelper.php @@ -0,0 +1,19 @@ +settings) && is_array($this->settings['idp_list'])) { - return $this->settings['idp_list']; } - $dir = Constants::APP_PATH . $this->settings['idpMetadataFolderPath']; + $dir = Constants::APP_PATH . PathHelper::fixPathSlashes($this->settings['idpMetadataFolderPath']); $idp_files = glob( $dir . '*.{xml}', GLOB_BRACE); $idps = array(); foreach ($idp_files as $key => $value) { $xml = simplexml_load_file($value); $idps[basename($value, '.xml')] = $xml->attributes()->entityID->__toString(); } - print_r($idp_files); - die; return $idps; } From cfd79633e0c21d21390ac76d7ac8548cea409bb8 Mon Sep 17 00:00:00 2001 From: Paolo Greppi Date: Thu, 9 Aug 2018 19:45:13 +0200 Subject: [PATCH 60/69] metadata endpoint OK --- bin/download_idp_metadata.php | 39 +++++++++++++++++++++++++++++++ example/settings.php | 13 ++++++++++- src/Config/OneloginSamlConfig.php | 8 +++---- src/Helpers/ArrayHelper.php | 24 ------------------- src/Helpers/Constants.php | 13 ----------- src/Helpers/IdpHelper.php | 16 ++++++++----- src/Helpers/PathHelper.php | 19 --------------- src/Strategy/PhpSamlOneLogin.php | 18 ++++++-------- 8 files changed, 72 insertions(+), 78 deletions(-) create mode 100755 bin/download_idp_metadata.php delete mode 100644 src/Helpers/ArrayHelper.php delete mode 100644 src/Helpers/Constants.php delete mode 100644 src/Helpers/PathHelper.php diff --git a/bin/download_idp_metadata.php b/bin/download_idp_metadata.php new file mode 100755 index 0000000..5c51844 --- /dev/null +++ b/bin/download_idp_metadata.php @@ -0,0 +1,39 @@ +#!/usr/bin/php + +// License: BSD 3-Clause + +$idp_list_url = 'https://registry.spid.gov.it/assets/data/idp.json'; +$ch = curl_init(); +curl_setopt($ch, CURLOPT_URL, $idp_list_url); +curl_setopt($ch, CURLOPT_FAILONERROR, 1); +curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1); +curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); +curl_setopt($ch, CURLOPT_TIMEOUT, 15); +echo "Contacting $idp_list_url" . PHP_EOL; +$json = curl_exec($ch); +curl_close($ch); +$idps = json_decode($json); + +foreach ($idps->data as $idp) { + $metadata_url = $idp->metadata_url; + $ipa_entity_code = $idp->ipa_entity_code; + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $metadata_url); + curl_setopt($ch, CURLOPT_FAILONERROR, 1); + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($ch, CURLOPT_TIMEOUT, 15); + echo "Contacting $metadata_url" . PHP_EOL; + $xml = curl_exec($ch); + curl_close($ch); + $file = "idp_metadata/$ipa_entity_code.xml"; + file_put_contents($file, $xml); +} diff --git a/example/settings.php b/example/settings.php index dd5a439..6906f82 100644 --- a/example/settings.php +++ b/example/settings.php @@ -8,5 +8,16 @@ 'spCrtFile' => __DIR__ . "/../sp.crt", 'spAcsUrl' => $base."/acs.php", 'spSloUrl' => $base."/logout.php", - "idpMetadataFolderPath" => "spid-php2/idp_metadata" + 'idpMetadataFolderPath' => "/srv/spid-php2/idp_metadata", + 'idpList' => array( + 'idp_1', + 'idp_2', + 'idp_3', + 'idp_4', + 'idp_5', + 'idp_6', + 'idp_7', + 'idp_8', + 'testenv2' + ) ]; diff --git a/src/Config/OneloginSamlConfig.php b/src/Config/OneloginSamlConfig.php index ed1a403..a24fea9 100644 --- a/src/Config/OneloginSamlConfig.php +++ b/src/Config/OneloginSamlConfig.php @@ -26,7 +26,7 @@ class OneloginSamlConfig var $level = 1; - var $idp_list = array(); + var $idpList = array(); private $is_required = ['spBaseUrl', ]; private $is_not_updatable = ['spKeyFileValue', 'spCrtFileValue', 'idpEntityId', 'idpSSO', 'idpSLO', 'idpCertValue']; @@ -129,10 +129,10 @@ public function updateSettings($settings) { } public function updateIdpMetadata($idpName) { - if (!array_key_exists($idpName, $this->idp_list)) { - throw new \Exception("Unsupported IDP provided", 1); + if (!in_array($idpName, $this->idpList)) { + throw new \Exception("Unsupported IDP $idpName", 1); } - $metadata = IdpHelper::getMetadata($idpName); + $metadata = IdpHelper::getMetadata($idpName, $this->idpMetadataFolderPath); foreach ($metadata as $key => $value) { if (property_exists(OneloginSamlConfig::class, $key) && strpos($key, "idp") !== false) { $this->{$key} = $value; diff --git a/src/Helpers/ArrayHelper.php b/src/Helpers/ArrayHelper.php deleted file mode 100644 index 29154a0..0000000 --- a/src/Helpers/ArrayHelper.php +++ /dev/null @@ -1,24 +0,0 @@ - $v) { - if (is_array($arr1[$k]) && is_array($arr2[$k])) { - $d = array_diff_key_recursive($arr1[$k], $arr2[$k]); - - if ($d) { - $diff[$k] = $d; - } - } - } - - return $diff; - } -} diff --git a/src/Helpers/Constants.php b/src/Helpers/Constants.php deleted file mode 100644 index e337ef9..0000000 --- a/src/Helpers/Constants.php +++ /dev/null @@ -1,13 +0,0 @@ -registerXPathNamespace('md', 'urn:oasis:names:tc:SAML:2.0:metadata'); + $xml->registerXPathNamespace('ds', 'http://www.w3.org/2000/09/xmldsig#'); + $metadata = array(); $metadata['idpEntityId'] = $xml->attributes()->entityID->__toString(); - $metadata['idpSSO'] = $xml->xpath('//SingleSignOnService')[0]->attributes()->Location->__toString(); - $metadata['idpSLO'] = $xml->xpath('//SingleLogoutService')[0]->attributes()->Location->__toString(); - $metadata['idpCertValue'] = $xml->xpath('//X509Certificate')[0]->__toString(); + $metadata['idpSSO'] = $xml->xpath('//md:SingleSignOnService')[0]->attributes()->Location->__toString(); + $metadata['idpSLO'] = $xml->xpath('//md:SingleLogoutService')[0]->attributes()->Location->__toString(); + $metadata['idpCertValue'] = $xml->xpath('//ds:X509Certificate')[0]->__toString(); return $metadata; } diff --git a/src/Helpers/PathHelper.php b/src/Helpers/PathHelper.php deleted file mode 100644 index baa19dd..0000000 --- a/src/Helpers/PathHelper.php +++ /dev/null @@ -1,19 +0,0 @@ -settingsHelper = $settingsHelper; if (!is_null($this->settings)) { - $diff = ArrayHelper::array_diff_key_recursive($this->settings, get_object_vars($settingsHelper)); + $diff = array_diff_key($this->settings, get_object_vars($settingsHelper)); if (!empty($diff)) { $message = "The following keys are invalid for the provided settings array: "; $first = true; @@ -49,12 +46,11 @@ private function init() } throw new \Exception($message, 1); } - // FI suer doesn't provide for an IDP mapping, a default one is provided - $this->settings['idp_list'] = $this->getSupportedIdps(); + // if the user doesn't supply a preferred IDP, a default one is used + $this->settings['idpList'] = $this->getSupportedIdps(); $settingsHelper->updateSettings($this->settings); } - reset($this->settings['idp_list']); - $this->idpName = is_null($this->idpName) ? key($this->settings['idp_list']) : $this->idpName; + $this->idpName = is_null($this->idpName) ? $this->settings['idpList'][1] : $this->idpName; $this->settingsHelper->updateIdpMetadata($this->idpName); $this->oneloginSettings = new Settings($this->settingsHelper->getSettings()); $this->auth = new Auth($this->settingsHelper->getSettings()); @@ -85,11 +81,11 @@ public function getSPMetadata() public function getSupportedIdps() { - if (array_key_exists('idp_list', $this->settings) && is_array($this->settings['idp_list'])) { - return $this->settings['idp_list']; + if (array_key_exists('idpList', $this->settings) && is_array($this->settings['idpList'])) { + return $this->settings['idpList']; } - $dir = Constants::APP_PATH . PathHelper::fixPathSlashes($this->settings['idpMetadataFolderPath']); + $dir = $this->settings['idpMetadataFolderPath']; $idp_files = glob( $dir . '*.{xml}', GLOB_BRACE); $idps = array(); foreach ($idp_files as $key => $value) { From 5944028403b50c37ca73da9f69f53d49c089a212 Mon Sep 17 00:00:00 2001 From: Paolo Greppi Date: Thu, 9 Aug 2018 19:52:41 +0200 Subject: [PATCH 61/69] PSR2 --- src/Config/OneloginSamlConfig.php | 43 +++++++++++--------- src/Helpers/IdpHelper.php | 2 +- src/Helpers/SpHelper.php | 1 - src/SpidPHP.php | 17 ++++---- src/Strategy/Interfaces/PhpSamlInterface.php | 7 ++-- src/Strategy/PhpSamlOneLogin.php | 27 ++++++------ 6 files changed, 51 insertions(+), 46 deletions(-) diff --git a/src/Config/OneloginSamlConfig.php b/src/Config/OneloginSamlConfig.php index a24fea9..ed24845 100644 --- a/src/Config/OneloginSamlConfig.php +++ b/src/Config/OneloginSamlConfig.php @@ -5,33 +5,32 @@ use SpidPHP\Helpers\SpHelper; use SpidPHP\Helpers\IdpHelper; - class OneloginSamlConfig { // Default values SP - var $spBaseUrl = ''; - var $spEntityId = null; - var $spKeyFile = 'sp.key'; - var $spCrtFile = 'sp.crt'; + public $spBaseUrl = ''; + public $spEntityId = null; + public $spKeyFile = 'sp.key'; + public $spCrtFile = 'sp.crt'; private $spKeyFileValue = null; private $spCrtFileValue = null; - var $spAcsUrl = null; - var $spSloUrl = null; + public $spAcsUrl = null; + public $spSloUrl = null; // Default values IDP - var $idpEntityId = null; - var $idpSSO = null; - var $idpSLO = null; - var $idpCertValue = null; - var $idpMetadataFolderPath= null; + public $idpEntityId = null; + public $idpSSO = null; + public $idpSLO = null; + public $idpCertValue = null; + public $idpMetadataFolderPath= null; - var $level = 1; + public $level = 1; - var $idpList = array(); + public $idpList = array(); private $is_required = ['spBaseUrl', ]; private $is_not_updatable = ['spKeyFileValue', 'spCrtFileValue', 'idpEntityId', 'idpSSO', 'idpSLO', 'idpCertValue']; - function __construct() + public function __construct() { // Default values $this->spAcsUrl = $this->spBaseUrl . '/index.php?acs'; @@ -108,7 +107,8 @@ public function getSettings() ); } - public function updateSettings($settings) { + public function updateSettings($settings) + { foreach ($settings as $key => $value) { if (!property_exists(OneloginSamlConfig::class, $key)) { continue; @@ -128,7 +128,8 @@ public function updateSettings($settings) { return $this->getSettings(); } - public function updateIdpMetadata($idpName) { + public function updateIdpMetadata($idpName) + { if (!in_array($idpName, $this->idpList)) { throw new \Exception("Unsupported IDP $idpName", 1); } @@ -141,11 +142,13 @@ public function updateIdpMetadata($idpName) { return $this->getSettings(); } - public function updateSpData($sp) { - if (!is_array($sp)) + public function updateSpData($sp) + { + if (!is_array($sp)) { throw new \Exception("Invalid SP certificate data provided", 1); + } - $this->spKeyFile = $sp['key']; + $this->spKeyFile = $sp['key']; $this->spCrtFile = $sp['cert']; return $this->getSettings(); diff --git a/src/Helpers/IdpHelper.php b/src/Helpers/IdpHelper.php index f360e3a..7ce1fb0 100644 --- a/src/Helpers/IdpHelper.php +++ b/src/Helpers/IdpHelper.php @@ -24,4 +24,4 @@ public static function getMetadata($idpName, $folder) return $metadata; } -} \ No newline at end of file +} diff --git a/src/Helpers/SpHelper.php b/src/Helpers/SpHelper.php index 8f3e0c0..fa9ce8d 100644 --- a/src/Helpers/SpHelper.php +++ b/src/Helpers/SpHelper.php @@ -26,4 +26,3 @@ private static function cleanOpenSsl($k) return $ck; } } - diff --git a/src/SpidPHP.php b/src/SpidPHP.php index 53733bc..e35a195 100644 --- a/src/SpidPHP.php +++ b/src/SpidPHP.php @@ -12,7 +12,7 @@ class SpidPHP implements PhpSamlInterface private $settings = null; public function __construct($settings = null, $mode = 'onelogin') - { + { session_start(); $this->mode = $mode; $this->settings = $settings; @@ -43,13 +43,15 @@ public function getSupportedIdps() } public function isAuthenticated() - { + { return $this->phpSaml->isAuthenticated(); } - public function login( $idpName, $redirectTo = '', $level = 1 ) - { - if (is_null($this->phpSaml)) $this->initStrategy($idpName); + public function login($idpName, $redirectTo = '', $level = 1) + { + if (is_null($this->phpSaml)) { + $this->initStrategy($idpName); + } return $this->phpSaml->login($idpName, $redirectTo); } @@ -59,8 +61,7 @@ public function logout() } public function getAttributes() - { + { return $this->phpSaml->getAttributes(); } - -} \ No newline at end of file +} diff --git a/src/Strategy/Interfaces/PhpSamlInterface.php b/src/Strategy/Interfaces/PhpSamlInterface.php index d21acf3..f54bb31 100644 --- a/src/Strategy/Interfaces/PhpSamlInterface.php +++ b/src/Strategy/Interfaces/PhpSamlInterface.php @@ -2,11 +2,12 @@ namespace SpidPHP\Strategy\Interfaces; -interface PhpSamlInterface { +interface PhpSamlInterface +{ public function getSPMetadata(); public function getSupportedIdps(); public function isAuthenticated(); - public function login( $idpName, $redirectTo = '', $level = 1 ); + public function login($idpName, $redirectTo = '', $level = 1); public function logout(); public function getAttributes(); -} \ No newline at end of file +} diff --git a/src/Strategy/PhpSamlOneLogin.php b/src/Strategy/PhpSamlOneLogin.php index c67d2bb..9d0da8c 100644 --- a/src/Strategy/PhpSamlOneLogin.php +++ b/src/Strategy/PhpSamlOneLogin.php @@ -11,7 +11,6 @@ use OneLogin\Saml2\Utils; use OneLogin\Saml2\Settings; - class PhpSamlOneLogin implements PhpSamlInterface { private $idpName = null; @@ -23,9 +22,9 @@ class PhpSamlOneLogin implements PhpSamlInterface private $oneloginSettings; private $userdata; - function __construct($idpName = null, $settings) + public function __construct($settings, $idpName = null) { - $this->idpName = $idpName; + $this->idpName = $idpName; $this->settings = $settings; $this->init(); } @@ -40,7 +39,9 @@ private function init() $message = "The following keys are invalid for the provided settings array: "; $first = true; foreach ($diff as $key => $value) { - if ($first) $message .= $key; + if ($first) { + $message .= $key; + } $first = false; $message .= ", " . $key; } @@ -80,13 +81,13 @@ public function getSPMetadata() } public function getSupportedIdps() - { + { if (array_key_exists('idpList', $this->settings) && is_array($this->settings['idpList'])) { - return $this->settings['idpList']; + return $this->settings['idpList']; } $dir = $this->settings['idpMetadataFolderPath']; - $idp_files = glob( $dir . '*.{xml}', GLOB_BRACE); + $idp_files = glob($dir . '*.{xml}', GLOB_BRACE); $idps = array(); foreach ($idp_files as $key => $value) { $xml = simplexml_load_file($value); @@ -126,7 +127,6 @@ public function isAuthenticated() $this->userdata['samlNameId'] = $this->auth->getNameId(); $this->userdata['samlNameIdFormat'] = $this->auth->getNameIdFormat(); $this->userdata['samlSessionIndex'] = $this->auth->getSessionIndex(); - } if ($this->auth->isAuthenticated() === false) { return false; @@ -134,7 +134,7 @@ public function isAuthenticated() return true; } - public function login( $idpName, $redirectTo = '', $level = 1 ) + public function login($idpName, $redirectTo = '', $level = 1) { $this->settings['level'] = $level; $this->changeIdp($idpName); @@ -172,9 +172,10 @@ public function logout() } public function getAttributes() - { - if (is_null($this->userdata) || !array_key_exists('attributes', $this->userdata)) return array(); + { + if (is_null($this->userdata) || !array_key_exists('attributes', $this->userdata)) { + return array(); + } return $this->userdata['attributes']; } - -} \ No newline at end of file +} From 2466c9988bb521423f03f101dd44ec25957c723f Mon Sep 17 00:00:00 2001 From: Paolo Greppi Date: Fri, 10 Aug 2018 06:14:56 +0200 Subject: [PATCH 62/69] cleanup and complete round-trip --- example/logout.php | 3 ++- example/settings.php | 13 +++++++------ src/Config/OneloginSamlConfig.php | 4 ++-- src/SpidPHP.php | 4 ++-- src/Strategy/PhpSamlOneLogin.php | 6 +++--- 5 files changed, 16 insertions(+), 14 deletions(-) diff --git a/example/logout.php b/example/logout.php index 6f56ac5..75fd166 100644 --- a/example/logout.php +++ b/example/logout.php @@ -14,5 +14,6 @@ if ($onelogin->isAuthenticated()) { $onelogin->logout(); } else { - echo "Logged out!"; + echo "Logged out!"; + echo '

Go back

'; } diff --git a/example/settings.php b/example/settings.php index 6906f82..c8a0297 100644 --- a/example/settings.php +++ b/example/settings.php @@ -1,14 +1,15 @@ $base, - 'spEntityId' => $base."/metadata.php", - 'spKeyFile' => __DIR__ . "/../sp.key", - 'spCrtFile' => __DIR__ . "/../sp.crt", - 'spAcsUrl' => $base."/acs.php", - 'spSloUrl' => $base."/logout.php", - 'idpMetadataFolderPath' => "/srv/spid-php2/idp_metadata", + 'spEntityId' => $base, + 'spKeyFile' => $home . "/sp.key", + 'spCrtFile' => $home . "/sp.crt", + 'spAcsUrl' => $base . "/acs.php", + 'spSloUrl' => $base . "/logout.php", + 'idpMetadataFolderPath' => $home . "/idp_metadata", 'idpList' => array( 'idp_1', 'idp_2', diff --git a/src/Config/OneloginSamlConfig.php b/src/Config/OneloginSamlConfig.php index ed24845..4da6c0b 100644 --- a/src/Config/OneloginSamlConfig.php +++ b/src/Config/OneloginSamlConfig.php @@ -27,7 +27,7 @@ class OneloginSamlConfig public $idpList = array(); - private $is_required = ['spBaseUrl', ]; + private $is_required = ['spBaseUrl']; private $is_not_updatable = ['spKeyFileValue', 'spCrtFileValue', 'idpEntityId', 'idpSSO', 'idpSLO', 'idpCertValue']; public function __construct() @@ -35,7 +35,7 @@ public function __construct() // Default values $this->spAcsUrl = $this->spBaseUrl . '/index.php?acs'; $this->spSloUrl = $this->spBaseUrl . '/index.php?sls'; - $this->spEntityId = $this->spBaseUrl . '/metadata.php'; + $this->spEntityId = $this->spBaseUrl; } public function getSettings() diff --git a/src/SpidPHP.php b/src/SpidPHP.php index e35a195..2d91b67 100644 --- a/src/SpidPHP.php +++ b/src/SpidPHP.php @@ -24,10 +24,10 @@ private function initStrategy($idpName = null) { switch ($this->mode) { case 'onelogin': - $this->phpSaml = new PhpSamlOneLogin($idpName, $this->settings); + $this->phpSaml = new PhpSamlOneLogin($this->settings, $idpName); break; default: - $this->phpSaml = new PhpSamlOneLogin($idpName, $this->settings); + $this->phpSaml = new PhpSamlOneLogin($this->settings, $idpName); break; } } diff --git a/src/Strategy/PhpSamlOneLogin.php b/src/Strategy/PhpSamlOneLogin.php index 9d0da8c..8e148db 100644 --- a/src/Strategy/PhpSamlOneLogin.php +++ b/src/Strategy/PhpSamlOneLogin.php @@ -51,7 +51,7 @@ private function init() $this->settings['idpList'] = $this->getSupportedIdps(); $settingsHelper->updateSettings($this->settings); } - $this->idpName = is_null($this->idpName) ? $this->settings['idpList'][1] : $this->idpName; + $this->idpName = is_null($this->idpName) ? $this->settings['idpList'][0] : $this->idpName; $this->settingsHelper->updateIdpMetadata($this->idpName); $this->oneloginSettings = new Settings($this->settingsHelper->getSettings()); $this->auth = new Auth($this->settingsHelper->getSettings()); @@ -173,9 +173,9 @@ public function logout() public function getAttributes() { - if (is_null($this->userdata) || !array_key_exists('attributes', $this->userdata)) { + if (is_null($this->userdata) || !array_key_exists('samlUserdata', $this->userdata)) { return array(); } - return $this->userdata['attributes']; + return $this->userdata['samlUserdata']; } } From d3ce513cb9934772e6f62d09f6eac119fe0a8ccd Mon Sep 17 00:00:00 2001 From: Paolo Greppi Date: Fri, 10 Aug 2018 06:45:35 +0200 Subject: [PATCH 63/69] make patches generic --- AuthnRequest.diff | 2 +- LogoutRequest.diff | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/AuthnRequest.diff b/AuthnRequest.diff index e3265b4..2aea923 100644 --- a/AuthnRequest.diff +++ b/AuthnRequest.diff @@ -10,4 +10,4 @@ --- > AssertionConsumerServiceIndex="1" > AttributeConsumingServiceIndex="1"> -> {$spEntityId} +> {$spEntityId} diff --git a/LogoutRequest.diff b/LogoutRequest.diff index 782267d..8e9f466 100644 --- a/LogoutRequest.diff +++ b/LogoutRequest.diff @@ -1,4 +1,4 @@ 128c128 < {$spEntityId} --- -> {$spEntityId} +> {$spEntityId} From e1152a94101ae5ed6ce04754445867b90548ab6a Mon Sep 17 00:00:00 2001 From: Paolo Greppi Date: Fri, 10 Aug 2018 06:46:06 +0200 Subject: [PATCH 64/69] unpack atttributes if they are sent nack as arrays --- src/Strategy/PhpSamlOneLogin.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Strategy/PhpSamlOneLogin.php b/src/Strategy/PhpSamlOneLogin.php index 8e148db..adf858f 100644 --- a/src/Strategy/PhpSamlOneLogin.php +++ b/src/Strategy/PhpSamlOneLogin.php @@ -127,6 +127,11 @@ public function isAuthenticated() $this->userdata['samlNameId'] = $this->auth->getNameId(); $this->userdata['samlNameIdFormat'] = $this->auth->getNameIdFormat(); $this->userdata['samlSessionIndex'] = $this->auth->getSessionIndex(); + foreach ($this->userdata['samlUserdata'] as $key => &$value) { + if (is_array($value)) { + $value = $value[0]; + } + } } if ($this->auth->isAuthenticated() === false) { return false; From ae88827df6a2eced3208b99aa9e76d587366abb7 Mon Sep 17 00:00:00 2001 From: Paolo Greppi Date: Fri, 10 Aug 2018 07:08:22 +0200 Subject: [PATCH 65/69] document settings and get rid of spBaseUrl --- example/settings.php | 15 ++++++++------- src/Config/OneloginSamlConfig.php | 8 +++----- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/example/settings.php b/example/settings.php index c8a0297..d023536 100644 --- a/example/settings.php +++ b/example/settings.php @@ -3,13 +3,14 @@ $base = "http://sp2.simevo.com:8000"; $home = "/srv/spid-php2"; $settings = [ - 'spBaseUrl' => $base, - 'spEntityId' => $base, - 'spKeyFile' => $home . "/sp.key", - 'spCrtFile' => $home . "/sp.crt", - 'spAcsUrl' => $base . "/acs.php", - 'spSloUrl' => $base . "/logout.php", - 'idpMetadataFolderPath' => $home . "/idp_metadata", + 'spEntityId' => $base, // preferred: https protocol, no path, no trailing slash + 'spAcsUrl' => $base . "/acs.php", // full url + 'spSloUrl' => $base . "/logout.php", // full url + 'spKeyFile' => $home . "/sp.key", // full path + 'spCrtFile' => $home . "/sp.crt", // full path + 'idpMetadataFolderPath' => $home . "/idp_metadata", // full path + // for each item in the idpList array, a file with the same name and xml extension + // must be present in the idpMetadataFolderPath directory 'idpList' => array( 'idp_1', 'idp_2', diff --git a/src/Config/OneloginSamlConfig.php b/src/Config/OneloginSamlConfig.php index 4da6c0b..1c29431 100644 --- a/src/Config/OneloginSamlConfig.php +++ b/src/Config/OneloginSamlConfig.php @@ -8,7 +8,6 @@ class OneloginSamlConfig { // Default values SP - public $spBaseUrl = ''; public $spEntityId = null; public $spKeyFile = 'sp.key'; public $spCrtFile = 'sp.crt'; @@ -27,15 +26,14 @@ class OneloginSamlConfig public $idpList = array(); - private $is_required = ['spBaseUrl']; + private $is_required = ['spEntityId']; private $is_not_updatable = ['spKeyFileValue', 'spCrtFileValue', 'idpEntityId', 'idpSSO', 'idpSLO', 'idpCertValue']; public function __construct() { // Default values - $this->spAcsUrl = $this->spBaseUrl . '/index.php?acs'; - $this->spSloUrl = $this->spBaseUrl . '/index.php?sls'; - $this->spEntityId = $this->spBaseUrl; + $this->spAcsUrl = $this->spEntityId . '/index.php?acs'; + $this->spSloUrl = $this->spEntityId . '/index.php?sls'; } public function getSettings() From 2c3448fb65c5fc12e4faef75d3642091dd6aab8a Mon Sep 17 00:00:00 2001 From: Paolo Greppi Date: Fri, 10 Aug 2018 07:16:59 +0200 Subject: [PATCH 66/69] polish example --- example/acs.php | 2 -- example/index.php | 1 + example/login.php | 3 +-- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/example/acs.php b/example/acs.php index e83c9c3..7338e65 100644 --- a/example/acs.php +++ b/example/acs.php @@ -23,5 +23,3 @@ echo "not logged in !" . PHP_EOL; echo '

Login

'; } - - diff --git a/example/index.php b/example/index.php index f9a3c71..5681d2b 100644 --- a/example/index.php +++ b/example/index.php @@ -2,5 +2,6 @@ echo '

Login

'; echo '

Show the SP metadata

'; +echo '

Show the supported IdPs

'; echo '

Logout

'; echo '

Assertion Consuming Service

'; diff --git a/example/login.php b/example/login.php index 3f24a21..6b91adb 100644 --- a/example/login.php +++ b/example/login.php @@ -14,6 +14,5 @@ if ($onelogin->isAuthenticated() === false) { $onelogin->login("testenv2"); } else { - echo "Already logged in!"; + echo "Already logged in!"; } - From ea7ef30b9ef0a1ccfc5575d87657ea93d1e04a21 Mon Sep 17 00:00:00 2001 From: Paolo Greppi Date: Fri, 10 Aug 2018 07:57:50 +0200 Subject: [PATCH 67/69] we have no scripts yet --- composer.json | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/composer.json b/composer.json index 9ddc39e..bd78975 100644 --- a/composer.json +++ b/composer.json @@ -4,18 +4,9 @@ "onelogin/php-saml": "3.0.0.x-dev", "squizlabs/php_codesniffer": "*" }, - - "scripts": { - "post-install-cmd": [ - "setup\\Configuration::setup" - ], - "post-update-cmd": [ - "setup\\Configuration::setup" - ] - }, "autoload": { "psr-4": { "SpidPHP\\": "src/" } } -} +} \ No newline at end of file From 72926874d48af2baba032f1cfe735400f78ebd08 Mon Sep 17 00:00:00 2001 From: Paolo Greppi Date: Fri, 10 Aug 2018 07:59:44 +0200 Subject: [PATCH 68/69] rename namespace and classes; move SpInterface.php to src/Interfaces --- composer.json | 2 +- example/acs.php | 8 ++---- example/idps.php | 9 ++---- example/login.php | 8 ++---- example/logout.php | 8 ++---- example/metadata.php | 6 ++-- example/settings.php | 1 + ...loginSamlConfig.php => OneLoginConfig.php} | 14 +++++----- src/Helpers/IdpHelper.php | 2 +- src/Helpers/SpHelper.php | 2 +- .../SpInterface.php} | 4 +-- src/{SpidPHP.php => Sp.php} | 28 +++++++++---------- .../{PhpSamlOneLogin.php => SpOneLogin.php} | 14 +++++----- 13 files changed, 48 insertions(+), 58 deletions(-) rename src/Config/{OneloginSamlConfig.php => OneLoginConfig.php} (93%) rename src/{Strategy/Interfaces/PhpSamlInterface.php => Interfaces/SpInterface.php} (79%) rename src/{SpidPHP.php => Sp.php} (55%) rename src/Strategy/{PhpSamlOneLogin.php => SpOneLogin.php} (95%) diff --git a/composer.json b/composer.json index bd78975..cf8a5be 100644 --- a/composer.json +++ b/composer.json @@ -6,7 +6,7 @@ }, "autoload": { "psr-4": { - "SpidPHP\\": "src/" + "Spid\\": "src/" } } } \ No newline at end of file diff --git a/example/acs.php b/example/acs.php index 7338e65..b38e009 100644 --- a/example/acs.php +++ b/example/acs.php @@ -7,12 +7,10 @@ require_once(__DIR__ . "/../vendor/autoload.php"); require_once(__DIR__ . "/settings.php"); -use SpidPHP\SpidPHP; +$sp = new Spid\Sp($settings); -$onelogin = new SpidPHP($settings); - -if ($onelogin->isAuthenticated()) { - $attributes = $onelogin->getAttributes(); +if ($sp->isAuthenticated()) { + $attributes = $sp->getAttributes(); echo "logged in !" . PHP_EOL; foreach ($attributes as $key => $attribute) { echo $key .": " . $attribute . "
"; diff --git a/example/idps.php b/example/idps.php index 8697ed9..d1e5a69 100644 --- a/example/idps.php +++ b/example/idps.php @@ -2,10 +2,7 @@ require_once(__DIR__ . "/../vendor/autoload.php"); require_once(__DIR__ . "/settings.php"); -use SpidPHP\SpidPHP; - - -$onelogin = new SpidPHP($settings); -foreach ($onelogin->getSupportedIdps() as $key => $idp) { +$sp = new Spid\Sp($settings); +foreach ($sp->getSupportedIdps() as $key => $idp) { echo $key . ' - ' . $idp . '
'; -} \ No newline at end of file +} diff --git a/example/login.php b/example/login.php index 6b91adb..0727e68 100644 --- a/example/login.php +++ b/example/login.php @@ -7,12 +7,10 @@ require_once(__DIR__ . "/../vendor/autoload.php"); require_once(__DIR__ . "/settings.php"); -use SpidPHP\SpidPHP; +$sp = new Spid\Sp($settings); -$onelogin = new SpidPHP($settings); - -if ($onelogin->isAuthenticated() === false) { - $onelogin->login("testenv2"); +if ($sp->isAuthenticated() === false) { + $sp->login("testenv2"); } else { echo "Already logged in!"; } diff --git a/example/logout.php b/example/logout.php index 75fd166..038c2c4 100644 --- a/example/logout.php +++ b/example/logout.php @@ -7,12 +7,10 @@ require_once(__DIR__ . "/../vendor/autoload.php"); require_once(__DIR__ . "/settings.php"); -use SpidPHP\SpidPHP; +$sp = new Spid\Sp($settings); -$onelogin = new SpidPHP($settings); - -if ($onelogin->isAuthenticated()) { - $onelogin->logout(); +if ($sp->isAuthenticated()) { + $sp->logout(); } else { echo "Logged out!"; echo '

Go back

'; diff --git a/example/metadata.php b/example/metadata.php index 3358573..f93d22a 100644 --- a/example/metadata.php +++ b/example/metadata.php @@ -7,11 +7,9 @@ require_once(__DIR__ . "/../vendor/autoload.php"); require_once(__DIR__ . "/settings.php"); -use SpidPHP\SpidPHP; +$sp = new Spid\Sp($settings); -$onelogin = new SpidPHP($settings); - -$metadata = $onelogin->getSPMetadata(); +$metadata = $sp->getSPMetadata(); header('Content-Type: text/xml'); echo $metadata; diff --git a/example/settings.php b/example/settings.php index d023536..6a6355b 100644 --- a/example/settings.php +++ b/example/settings.php @@ -23,3 +23,4 @@ 'testenv2' ) ]; + diff --git a/src/Config/OneloginSamlConfig.php b/src/Config/OneLoginConfig.php similarity index 93% rename from src/Config/OneloginSamlConfig.php rename to src/Config/OneLoginConfig.php index 1c29431..f56cafa 100644 --- a/src/Config/OneloginSamlConfig.php +++ b/src/Config/OneLoginConfig.php @@ -1,11 +1,11 @@ $value) { - if (!property_exists(OneloginSamlConfig::class, $key)) { + if (!property_exists(OneLoginConfig::class, $key)) { continue; } if (in_array($key, $this->is_not_updatable)) { @@ -119,7 +119,7 @@ public function updateSettings($settings) } // Get .key and .cert files content and add it to configuration if (!file_exists($this->spKeyFile) || !file_exists($this->spCrtFile)) { - throw new \Exception("The path for .key and .cert files is invalid", 1); + throw new \Exception("The path for .key ($this->spKeyFile) and .cert ($this->spCrtFile) files is invalid", 1); } $sp = SpHelper::getSpCert($this->spKeyFile, $this->spCrtFile); $this->updateSpData($sp); @@ -133,7 +133,7 @@ public function updateIdpMetadata($idpName) } $metadata = IdpHelper::getMetadata($idpName, $this->idpMetadataFolderPath); foreach ($metadata as $key => $value) { - if (property_exists(OneloginSamlConfig::class, $key) && strpos($key, "idp") !== false) { + if (property_exists(OneLoginConfig::class, $key) && strpos($key, "idp") !== false) { $this->{$key} = $value; } } diff --git a/src/Helpers/IdpHelper.php b/src/Helpers/IdpHelper.php index 7ce1fb0..1b5d263 100644 --- a/src/Helpers/IdpHelper.php +++ b/src/Helpers/IdpHelper.php @@ -1,6 +1,6 @@ mode) { case 'onelogin': - $this->phpSaml = new PhpSamlOneLogin($this->settings, $idpName); + $this->strategy = new SpOneLogin($this->settings, $idpName); break; default: - $this->phpSaml = new PhpSamlOneLogin($this->settings, $idpName); + $this->strategy = new SpOneLogin($this->settings, $idpName); break; } } public function getSPMetadata() { - return $this->phpSaml->getSPMetadata(); + return $this->strategy->getSPMetadata(); } public function getSupportedIdps() { - return $this->phpSaml->getSupportedIdps(); + return $this->strategy->getSupportedIdps(); } public function isAuthenticated() { - return $this->phpSaml->isAuthenticated(); + return $this->strategy->isAuthenticated(); } public function login($idpName, $redirectTo = '', $level = 1) { - if (is_null($this->phpSaml)) { + if (is_null($this->strategy)) { $this->initStrategy($idpName); } - return $this->phpSaml->login($idpName, $redirectTo); + return $this->strategy->login($idpName, $redirectTo); } public function logout() { - return $this->phpSaml->logout(); + return $this->strategy->logout(); } public function getAttributes() { - return $this->phpSaml->getAttributes(); + return $this->strategy->getAttributes(); } } diff --git a/src/Strategy/PhpSamlOneLogin.php b/src/Strategy/SpOneLogin.php similarity index 95% rename from src/Strategy/PhpSamlOneLogin.php rename to src/Strategy/SpOneLogin.php index adf858f..b56508d 100644 --- a/src/Strategy/PhpSamlOneLogin.php +++ b/src/Strategy/SpOneLogin.php @@ -1,17 +1,17 @@ settingsHelper = $settingsHelper; if (!is_null($this->settings)) { $diff = array_diff_key($this->settings, get_object_vars($settingsHelper)); From 10df3f7c9e3de39f085801332adae4c56ce8740d Mon Sep 17 00:00:00 2001 From: Paolo Greppi Date: Fri, 10 Aug 2018 16:21:02 +0200 Subject: [PATCH 69/69] move to Italia\Spid2 namespace; move idp_metadata inside example; brush up README --- .gitignore | 19 +- Makefile | 8 +- README.md | 175 +++++++++++++----- composer.json | 8 +- composer.lock | 7 +- example/acs.php | 6 +- .../idp_metadata}/put_idp_metadata_here | 0 example/idps.php | 4 +- example/login.php | 6 +- example/logout.php | 6 +- example/metadata.php | 6 +- example/settings.php | 11 +- images/screencast.gif | Bin 0 -> 448258 bytes src/Config/OneLoginConfig.php | 6 +- src/Helpers/IdpHelper.php | 2 +- src/Helpers/SpHelper.php | 2 +- src/Interfaces/SpInterface.php | 2 +- src/Sp.php | 7 +- src/Strategy/SpOneLogin.php | 12 +- 19 files changed, 173 insertions(+), 114 deletions(-) rename {idp_metadata => example/idp_metadata}/put_idp_metadata_here (100%) create mode 100644 images/screencast.gif diff --git a/.gitignore b/.gitignore index 1d93f62..cb2c4db 100644 --- a/.gitignore +++ b/.gitignore @@ -1,16 +1,9 @@ -sp.key -sp.crt -vendor -tmp -www/settings.php -www2/settings.php -config.yaml .directory -AuthnRequest.patched -LogoutRequest.patched .vscode .DS_Store - -src/Config/idp/*.xml - -idp_metadata/*.xml \ No newline at end of file +AuthnRequest.patched +LogoutRequest.patched +vendor +example/sp.key +example/sp.crt +example/idp_metadata/*.xml diff --git a/Makefile b/Makefile index ee5440e..e78cd76 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -all: sp.key AuthnRequest.patched LogoutRequest.patched +all: example/sp.key AuthnRequest.patched LogoutRequest.patched AuthnRequest.patched: TO_PATCH:=vendor/onelogin/php-saml/src/Saml2/AuthnRequest.php AuthnRequest.patched: AuthnRequest.diff @@ -12,10 +12,12 @@ LogoutRequest.patched: LogoutRequest.diff patch -N vendor/onelogin/php-saml/src/Saml2/LogoutRequest.php $< cp LogoutRequest.diff $@ -sp.key: - openssl req -x509 -nodes -sha256 -days 365 -newkey rsa:2048 -subj "/C=IT/ST=Italy/L=Rome/O=testenv2/CN=localhost" -keyout sp.key -out sp.crt +example/sp.key: + openssl req -x509 -nodes -sha256 -days 365 -newkey rsa:2048 -subj "/C=IT/ST=Italy/L=Rome/O=testenv2/CN=localhost" -keyout example/sp.key -out example/sp.crt clean: rm -rf vendor rm -f AuthnRequest.patched rm -f LogoutRequest.patched + rm -f example/idp_metadata/*.xml + rm -f example/sp.crt example/sp.key diff --git a/README.md b/README.md index c543283..dd3f84a 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,30 @@ +SPID + +[![Join the #spid-perl channel](https://img.shields.io/badge/Slack%20channel-%23spid--perl-blue.svg?logo=slack)](https://developersitalia.slack.com/messages/C7ESTMQDQ) +[![Get invited](https://slack.developers.italia.it/badge.svg)](https://slack.developers.italia.it/) +[![SPID on forum.italia.it](https://img.shields.io/badge/Forum-SPID-blue.svg)](https://forum.italia.it/c/spid) + +> ⚠️ **WORK IN PROGRESS (but should be useable)** ⚠️ + # spid-php2 +PHP package for SPID authentication based on [php-saml](https://github.com/onelogin/php-saml). -Work in progress. Do not merge this branch. +This PHP package is aimed at implementing SPID **Service Providers**. [SPID](https://www.spid.gov.it/) is the Italian digital identity system, which enables citizens to access all public services with a single set of credentials. This package provides a layer of abstraction over the SAML protocol by exposing just the subset required in order to implement SPID authentication in a web application. -Software Development Kit (SDK) for easy SPID SSO integration based on [php-saml](https://github.com/onelogin/php-saml). +Features: +- **routing-agnostic**, can be integrated in any web framework / CMS +- **sessionless** (apart from a short-lived internal session used to store the request ID and IdP name until the IdP responds) +- does not currently support Attribute Authority (AA). -This component acts as a SPID SP (Service Provider) and logs you in via an external IDP (IDentity Provider). It does not support Attribute Authority. +Alternatives for PHP: +- [spid-php](https://github.com/italia/spid-php) based on [SimpleSAMLphp](https://simplesamlphp.org/) +- [spid-php3](https://github.com/simevo/spid-php3), a lean implementation that does not rely on external SAML packages -Alternative SDK: [spid-php](https://github.com/italia/spid-php) based on [SimpleSAMLphp](https://simplesamlphp.org/). +Alternatives for other languages: +- [spid-perl](https://github.com/italia/spid-perl) +- [spid-ruby](https://github.com/italia/spid-ruby) -## Features +## Compliance |
_Compliance with [SPID regulations](http://www.agid.gov.it/sites/default/files/circolari/spid-regole_tecniche_v1.pdf) (for Service Providers)_|status (! = TODO)|comments| |:---|:---|:---| @@ -75,84 +91,143 @@ Alternative SDK: [spid-php](https://github.com/italia/spid-php) based on [Simple |generation of AttributeQuery XML||Attribute Authority is unsupported| |SOAP binding (client)||Attribute Authority is unsupported| -## Prerequisites +## Repository layout -Tested on Debian 10.x buster with PHP 7.2. +* [bin/](bin/) auxiliary scripts +* [example/](example/) contains a demo application +* [src/](src/) contains the implementation +* [test/](test/) will contain the unit tests -Perform these steps to install the prerequisites: -``` +## Getting Started + +Tested on Debian 9.5 (stretch, current stable) and 10 (buster, current unstable) with PHP 7-0-7.2. + +### Prerequisites + +```sh sudo apt install composer make openssl php-curl php-zip php-xml ``` -if you have PHP <= 7.1 (i.e. Debian 9.4 stretch or earlier), then you also need: -``` -apt install php-mcrypt -``` -Then install PHP dependencies; if you have PHP 7.2 (i.e. Debian 10.x buster): +### Configuring and Installing + +Before using this package, you must: + +1. Install prerequisites with composer + +2. Download and verify the Identity Provider (IdP) metadata files; it is advised to place them in a separate directory, for example [example/idp_metadata/](example/idp_metadata/). A convenience tool is provided for this purpose: [bin/download_idp_metadata.php](bin/download_idp_metadata.php). + +3. Generate key and certificate for the Service Provider (SP) and patch the php-saml package to comply with the SPID standard. To do that, you can use the provided [Makefile](Makefile). + +All steps can be performed with: +```sh +composer install --no-dev +pushd example && ../bin/download_idp_metadata.php && popd +make ``` -composer install + +**NOTE**: during testing, it is highly adviced to use the test Identity Provider [spid-testenv2](https://github.com/italia/spid-testenv2). + +### Usage + +All classes provided by this package reside in the `Italia\Spid2` namespace. + +Load them using the composer-generated autoloader: +```php +require_once(__DIR__ . "/../vendor/autoload.php"); ``` -if you have PHP <= 7.1 (i.e. Debian 9.4 stretch or earlier), then use the v2.x branch of php-saml: + +The main class is `Italia\Spid2\Sp` (service provider), sample instantiation: + +```php +$base = "http://localhost:8000"; +$settings = [ + 'spEntityId' => $base, + 'spAcsUrl' => $base . "/acs.php", + 'spSloUrl' => $base . "/logout.php", + 'spKeyFile' => "./sp.key", + 'spCrtFile' => "./sp.crt", + 'idpMetadataFolderPath' => $home . "/idp_metadata", + 'idpList' => array( + 'testenv2' + ) + ]; +$sp = new Italia\Spid2\Sp($settings); ``` -rm composer.* -composer require onelogin/php-saml -composer require twig/twig -composer require symfony/yaml + +The service provider is now ready for use, as in: +```php +$idp_name = 'idp_1'; +$return_to = 'https://example.com/return_to_url'; +$spid_level = 1; +$sp->login($idp_name, $return_to, $spid_level); +$attributes = $sp->getAttributes(); +var_dump($attributes); +$sp->logout(); ``` -## Demo +### Example -The demo is based on php-saml demo1. +A basic demo application is provided in the [example/](example/) directory. -To set it up and run it: +To use: -1. copy `config.yaml.example` to `config.yaml` and customize it as required (you should at least set `idp_metadata_url` to match your IDP metadata endpoint) +1. in `example/settings.php`: -2. auto-configure: - ``` - make - ``` + - adapt the base url (`$base`) to your needs (use am IP address or a hostname that is visible to the IdP) + - make sure the IdP metadata corresponding to the IdPs listed in the `idpList` key are present in `example/idp_metadata` + +2. in `example/login.php` change the IdP that will be used to login 3. Start PHP's builtin webserver in the root of the repo: - ``` - php -S localhost:8000 -t www - ``` - if you have php-saml v2.x (i.e. Debian 9.4 stretch), then run it from the www2 dir: - ``` - php -S localhost:8000 -t www2 + ```sh + php -S 0.0.0.0:8000 -t example ``` -4. visit http://localhost:8000/metadata.php to get the SP (Service Provider) metadata, then copy these over to the IDP +4. visit http://localhost:8000/metadata.php to get the SP (Service Provider) metadata, then copy these over to the IdP 5. visit: http://localhost:8000 and click `login`. +This screencast shows what you should see if all goes well: + +![img](images/screencast.gif) + ## Troubleshooting -- install a browser plugin to trace SAML messages: +It is advised to install a browser plugin to trace SAML messages: - - Firefox: +- Firefox: - - [SAML-tracer by Olav Morken, Jaime Perez](https://addons.mozilla.org/en-US/firefox/addon/saml-tracer/) - - [SAML Message Decoder by Magnus Suther](https://addons.mozilla.org/en-US/firefox/addon/saml-message-decoder-extension/) + - [SAML-tracer by Olav Morken, Jaime Perez](https://addons.mozilla.org/en-US/firefox/addon/saml-tracer/) + - [SAML Message Decoder by Magnus Suther](https://addons.mozilla.org/en-US/firefox/addon/saml-message-decoder-extension/) - - Chrome/Chromium: +- Chrome/Chromium: - - [SAML Message Decoder by Magnus Suther](https://chrome.google.com/webstore/detail/saml-message-decoder/mpabchoaimgbdbbjjieoaeiibojelbhm) - - [SAML Chrome Panel by MLai](https://chrome.google.com/webstore/detail/saml-chrome-panel/paijfdbeoenhembfhkhllainmocckace) - - [SAML DevTools extension by stefan.rasmusson.as](https://chrome.google.com/webstore/detail/saml-devtools-extension/jndllhgbinhiiddokbeoeepbppdnhhio) + - [SAML Message Decoder by Magnus Suther](https://chrome.google.com/webstore/detail/saml-message-decoder/mpabchoaimgbdbbjjieoaeiibojelbhm) + - [SAML Chrome Panel by MLai](https://chrome.google.com/webstore/detail/saml-chrome-panel/paijfdbeoenhembfhkhllainmocckace) + - [SAML DevTools extension by stefan.rasmusson.as](https://chrome.google.com/webstore/detail/saml-devtools-extension/jndllhgbinhiiddokbeoeepbppdnhhio) -- use the [SAML Developer Tools](https://www.samltool.com/online_tools.php) provided by onelogin to understand what is going on +In addition, you can use the [SAML Developer Tools](https://www.samltool.com/online_tools.php) provided by onelogin to understand what is going on -## Contributing +## Testing + +### Unit tests + +TODO + +Unit tests will be performed with PHPunit. -Your code **should** comply with the [PSR-2: Coding Style Guide](https://www.php-fig.org/psr/psr-2/). -Check your changes with: +### Linting + +This project complies with the [PSR-2: Coding Style Guide](https://www.php-fig.org/psr/psr-2/). + +Lint the code with: ``` -./vendor/bin/phpcs --standard=PSR2 bin/configure.php -... +./vendor/bin/phpcs --standard=PSR2 xxx.php ``` -You **must** use the [git-flow workflow](https://danielkummer.github.io/git-flow-cheatsheet/). +## Contributing + +For your contributions please use the [git-flow workflow](https://danielkummer.github.io/git-flow-cheatsheet/). ## Legalese diff --git a/composer.json b/composer.json index cf8a5be..50f2290 100644 --- a/composer.json +++ b/composer.json @@ -1,12 +1,14 @@ { "name": "italia/spid-php2", "require": { - "onelogin/php-saml": "3.0.0.x-dev", - "squizlabs/php_codesniffer": "*" + "onelogin/php-saml": "3.0.0.x-dev" + }, + "require-dev": { + "squizlabs/php_codesniffer": "^3.3" }, "autoload": { "psr-4": { - "Spid\\": "src/" + "Italia\\Spid2\\": "src/" } } } \ No newline at end of file diff --git a/composer.lock b/composer.lock index b2aabb3..8d61fef 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "9c614b6245789a0e2cf885b1e6fc7014", + "content-hash": "0e934f11b83adea144b2a84c424aaf82", "packages": [ { "name": "onelogin/php-saml", @@ -95,7 +95,9 @@ "xmldsig" ], "time": "2017-08-31T09:27:07+00:00" - }, + } + ], + "packages-dev": [ { "name": "squizlabs/php_codesniffer", "version": "3.3.1", @@ -148,7 +150,6 @@ "time": "2018-07-26T23:47:18+00:00" } ], - "packages-dev": [], "aliases": [], "minimum-stability": "stable", "stability-flags": { diff --git a/example/acs.php b/example/acs.php index b38e009..e10fc2d 100644 --- a/example/acs.php +++ b/example/acs.php @@ -1,13 +1,9 @@ isAuthenticated()) { $attributes = $sp->getAttributes(); diff --git a/idp_metadata/put_idp_metadata_here b/example/idp_metadata/put_idp_metadata_here similarity index 100% rename from idp_metadata/put_idp_metadata_here rename to example/idp_metadata/put_idp_metadata_here diff --git a/example/idps.php b/example/idps.php index d1e5a69..78793b7 100644 --- a/example/idps.php +++ b/example/idps.php @@ -1,8 +1,10 @@ getSupportedIdps() as $key => $idp) { echo $key . ' - ' . $idp . '
'; } diff --git a/example/login.php b/example/login.php index 0727e68..ea97e8e 100644 --- a/example/login.php +++ b/example/login.php @@ -1,13 +1,9 @@ isAuthenticated() === false) { $sp->login("testenv2"); diff --git a/example/logout.php b/example/logout.php index 038c2c4..293c6ad 100644 --- a/example/logout.php +++ b/example/logout.php @@ -1,13 +1,9 @@ isAuthenticated()) { $sp->logout(); diff --git a/example/metadata.php b/example/metadata.php index f93d22a..f752505 100644 --- a/example/metadata.php +++ b/example/metadata.php @@ -1,13 +1,9 @@ getSPMetadata(); diff --git a/example/settings.php b/example/settings.php index 6a6355b..a7592b6 100644 --- a/example/settings.php +++ b/example/settings.php @@ -1,14 +1,14 @@ $base, // preferred: https protocol, no path, no trailing slash 'spAcsUrl' => $base . "/acs.php", // full url 'spSloUrl' => $base . "/logout.php", // full url - 'spKeyFile' => $home . "/sp.key", // full path - 'spCrtFile' => $home . "/sp.crt", // full path - 'idpMetadataFolderPath' => $home . "/idp_metadata", // full path + 'spKeyFile' => "./sp.key", // full or relative path + 'spCrtFile' => "./sp.crt", // full or relative path + 'idpMetadataFolderPath' => "./idp_metadata", // full or relative path // for each item in the idpList array, a file with the same name and xml extension // must be present in the idpMetadataFolderPath directory 'idpList' => array( @@ -23,4 +23,3 @@ 'testenv2' ) ]; - diff --git a/images/screencast.gif b/images/screencast.gif new file mode 100644 index 0000000000000000000000000000000000000000..daed10e98df533d0a02025fa764715b3c2fe9560 GIT binary patch literal 448258 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