diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 59090e1..c2e1375 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,7 +18,7 @@ jobs: strategy: matrix: operating-system: [ubuntu-latest] - php-versions: ['7.4', '8.0', '8.1'] + php-versions: ['7.1', '8.2'] name: PHP ${{ matrix.php-versions }} Test on ${{ matrix.operating-system }} @@ -50,7 +50,10 @@ jobs: run: bin/download_idp_metadata.php example/idp_metadata - name: Coding standard - run: find . | grep 'php$' | grep -v vendor | grep -v tests | xargs ./vendor/bin/phpcs --standard=PSR2 + run: composer code-style + + - name: Coding quality + run: composer static-analysis - name: Run tests run: ./vendor/bin/phpunit --stderr --testdox tests diff --git a/README.md b/README.md index 7a5c3c4..4fe698b 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,7 @@ -SPID - -[![Join the #spid-php channel](https://img.shields.io/badge/Slack%20channel-%23spid--php-blue.svg?logo=slack)](https://developersitalia.slack.com/messages/CB6DCK274) -[![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) -[![Build Status](https://travis-ci.org/italia/spid-php-lib.svg?branch=master)](https://travis-ci.org/italia/spid-php-lib) - -> **CURRENT VERSION: v0.35** +# Notice: Unofficial Release +Main differences with official release: +- implements logging +- uses PSR-12 style guide # spid-php-lib PHP package for SPID authentication. diff --git a/composer.json b/composer.json index faf69d4..2dd06ce 100644 --- a/composer.json +++ b/composer.json @@ -1,5 +1,5 @@ { - "name": "italia/spid-php-lib", + "name": "intervieweb/spid-php-lib", "description": "PHP package for SPID authentication", "type": "library", "license": "BSD-3-Clause", @@ -10,15 +10,36 @@ { "name": "Paolo Greppi", "email": "paolo.greppi@libpf.com" + }, + { + "name": "Nico Caprioli", + "email": "nico.caprioli@gmail.com" } ], "require": { + "ext-dom": "*", + "ext-openssl": "*", + "ext-simplexml": "*", + "ext-zlib": "*", "robrichards/xmlseclibs": "^3.0", - "php": "^7.4 || ^8.0" + "php": "^7.1 || ^8.0", + "psr/log": "1.1.4 || ^3.0" }, "require-dev": { - "squizlabs/php_codesniffer": "^3.3", - "phpunit/phpunit": "^9.5" + "squizlabs/php_codesniffer": "*", + "phpunit/phpunit": "^10", + "phpstan/phpstan": "^1.10" + }, + "scripts": { + "test": [ + "./vendor/phpunit/phpunit/phpunit --stderr --testdox tests" + ], + "code-style": [ + "./vendor/squizlabs/php_codesniffer/bin/phpcs --standard=PSR12 -q ./src/" + ], + "static-analysis": [ + "./vendor/phpstan/phpstan/phpstan analyze -c phpstan.neon" + ] }, "autoload": { "psr-4": { diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..71969dd --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,5 @@ +parameters: + level: 3 + paths: + - src + - tests \ No newline at end of file diff --git a/phpunit.xml b/phpunit.xml index a9ef9da..6f7e0bc 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,17 +1,16 @@ - - - - ./tests - - - - - src - - - + + + + + ./tests + + + + src + + diff --git a/src/Sp.php b/src/Sp.php index 11e0f4e..bb77649 100644 --- a/src/Sp.php +++ b/src/Sp.php @@ -2,6 +2,12 @@ namespace Italia\Spid; +use Exception; +use Italia\Spid\Spid\Interfaces\LoggerSelector; + +/** + * @mixin Spid\Saml + */ class Sp { /* @@ -13,25 +19,31 @@ class Sp */ private $protocol; - public function __construct(array $settings, String $protocol = null, $autoconfigure = true) + /** + * @throws Exception + */ + public function __construct(LoggerSelector $logger, array $settings, string $protocol = null, $autoconfigure = true) { if (session_status() == PHP_SESSION_NONE) { session_start(); } switch ($protocol) { case 'saml': - $this->protocol = new Spid\Saml($settings, $autoconfigure); + $this->protocol = new Spid\Saml($logger, $settings, $autoconfigure); break; default: - $this->protocol = new Spid\Saml($settings, $autoconfigure); + $this->protocol = new Spid\Saml($logger, $settings, $autoconfigure); } } + /** + * @throws Exception + */ public function __call($method, $arguments) { $methods_implemented = get_class_methods($this->protocol); if (!in_array($method, $methods_implemented)) { - throw new \Exception("Invalid method [$method] requested", 1); + throw new Exception("Invalid method [$method] requested", 1); } return call_user_func_array(array($this->protocol, $method), $arguments); } diff --git a/src/Spid/Exceptions/SpidException.php b/src/Spid/Exceptions/SpidException.php new file mode 100644 index 0000000..74026e5 --- /dev/null +++ b/src/Spid/Exceptions/SpidException.php @@ -0,0 +1,24 @@ +context = $context; + } + + public function getContext() + { + return $this->context; + } +} diff --git a/src/Spid/Interfaces/IdpInterface.php b/src/Spid/Interfaces/IdpInterface.php index f118b41..dfbaab2 100644 --- a/src/Spid/Interfaces/IdpInterface.php +++ b/src/Spid/Interfaces/IdpInterface.php @@ -1,5 +1,7 @@ entityID (used for spid-smart-button) // if no IdPs are found returns an empty array - public function getIdpList() : array; + public function getIdpList(): array; // alias of loadIdpFromFile public function getIdp(string $filename); // returns SP metadata as a string - public function getSPMetadata() : string; - + public function getSPMetadata(): string; + // performs login with REDIRECT binding // $idpFilename: shortname of IdP, same as the name of corresponding IdP metadata file, without .xml extension // $assertID: index of assertion consumer service as per the SP metadata @@ -70,11 +71,11 @@ public function getSPMetadata() : string; // $returnTo: url to redirect to after login // $shouldRedirect: tells if the function should emit headers and redirect to login URL or return the URL as string // returns false is already logged in - // returns an empty string if $shouldRedirect = true, the login URL otherwhise + // returns an empty string if $shouldRedirect = true, the login URL otherwise public function login( - string $idpFilename, - int $assertID, - int $attrID, + string $idpName, + int $assertId, + int $attrId, $level = 1, string $redirectTo = null, $shouldRedirect = true @@ -83,9 +84,9 @@ public function login( // performs login with POST Binding // uses the same parameters and return values as login public function loginPost( - string $idpFilename, - int $assertID, - int $attrID, + string $idpName, + int $assertId, + int $attrId, $level = 1, string $redirectTo = null, $shouldRedirect = true @@ -101,14 +102,14 @@ public function loginPost( // SESSION AND STORING USER ATTRIBUTES. // SIMILARLY, AFTER A LOGOUT() CALLING THIS METHOD WILL VALIDATE THE RESULT AND DESTROY THE SESSION. // LOGIN() AND LOGOUT() ALONE INTERACT WITH THE IDP, BUT DON'T CHECK FOR RESULTS AND UPDATE THE SP - public function isAuthenticated() : bool; + public function isAuthenticated(): bool; // performs logout with REDIRECT binding // $slo: index of the singlelogout service as per the SP metadata // $returnTo: url to redirect to after logout // $shouldRedirect: tells if the function should emit headers and redirect to logout URL or return the URL as string // returns false if not logged in - // returns an empty string if $shouldRedirect = true, the logout URL otherwhise + // returns an empty string if $shouldRedirect = true, the logout URL otherwise public function logout(int $slo, string $redirectTo = null, $shouldRedirect = true); // performs logout with POST Binding @@ -117,5 +118,5 @@ public function logoutPost(int $slo, string $redirectTo = null, $shouldRedirect // returns attributes as an array or an empty array if not authenticated // example: array('name' => 'Franco', 'familyName' => 'Rossi', 'fiscalNumber' => 'FFFRRR88A12T4441R',) - public function getAttributes() : array; + public function getAttributes(): array; } diff --git a/src/Spid/Logging/AbstractLoggerSelector.php b/src/Spid/Logging/AbstractLoggerSelector.php new file mode 100644 index 0000000..7d56c9c --- /dev/null +++ b/src/Spid/Logging/AbstractLoggerSelector.php @@ -0,0 +1,72 @@ + 'Autenticazione fallita per ripetuta sottomissione di credenziali errate', + 20 => 'Utente privo di credenziali compatibili con il livello richiesto dal fornitore del servizio', + 21 => "Timeout durante l'autenticazione dell'utente", + 22 => "Utente nega il consenso all'invio di dati al SP in caso di sessione vigente", + 23 => 'Utente con identità sospesa/revocata o con credenziali bloccate', + 25 => "Processo di autenticazione annullato dall'utente", + 30 => "Tentativo dell'utente di utilizzare una tipologia di identità digitale " . + 'diversa da quanto richiesto dal service provider' + ]; + + public const GENERIC_ERROR = 'Accesso temporaneamente non disponibile, si prega di riprovare.'; + + abstract public function getPermanentLogger(): ?LoggerInterface; + + abstract public function getTemporaryLogger(): ?LoggerInterface; + + private function getErrorCodeFromXml(\DOMDocument $xml): int + { + $errorCode = -1; + $statusMessage = $xml->getElementsByTagName('StatusMessage'); + if ($statusMessage->item(0) && $statusMessage->item(0)->nodeValue) { + $errorString = $statusMessage->item(0)->nodeValue; + $errorCode = intval(str_replace('ErrorCode nr', '', $errorString)) ?: -1; + } + return $errorCode; + } + + public static function getErrorLevel(int $code): string + { + if (array_key_exists($code, self::WARNINGS)) { + return LogLevel::WARNING; + } + return LogLevel::ERROR; + } + + public static function getErrorMessage(int $code): string + { + if (array_key_exists($code, self::WARNINGS)) { + return self::WARNINGS[$code]; + } + return self::GENERIC_ERROR; + } + + public function logAndThrow(\DOMDocument $xml, $message): void + { + $errorCode = self::getErrorCodeFromXml($xml); + $xmlString = $xml->saveXML(); + $logger = $this->getPermanentLogger(); + if ($logger) { + $logger->log( + self::getErrorLevel($errorCode), + $message, + ['xml' => $xmlString, 'error_message' => self::getErrorMessage($errorCode)] + ); + } + throw new SpidException($message, $errorCode, null, $xmlString); + } +} diff --git a/src/Spid/Saml.php b/src/Spid/Saml.php index 1fb7d62..ab00628 100644 --- a/src/Spid/Saml.php +++ b/src/Spid/Saml.php @@ -2,25 +2,39 @@ namespace Italia\Spid\Spid; -use Italia\Spid\Spid\Saml\Idp; -use Italia\Spid\Spid\Saml\In\BaseResponse; -use Italia\Spid\Spid\Saml\Settings; -use Italia\Spid\Spid\Saml\SignatureUtils; -use Italia\Spid\Spid\Interfaces\SAMLInterface; -use Italia\Spid\Spid\Session; + use DOMDocument; + use Exception; + use InvalidArgumentException; + use Italia\Spid\Spid\Exceptions\SpidException; + use Italia\Spid\Spid\Interfaces\LoggerSelector; + use Italia\Spid\Spid\Interfaces\SAMLInterface; + use Italia\Spid\Spid\Logging\AbstractLoggerSelector; + use Italia\Spid\Spid\Saml\Idp; + use Italia\Spid\Spid\Saml\In\BaseResponse; + use Italia\Spid\Spid\Saml\Settings; + use Italia\Spid\Spid\Saml\SignatureUtils; + use Psr\Log\LogLevel; class Saml implements SAMLInterface { public $settings; private $idps = []; // contains filename -> Idp object array private $session; // Session object + /** + * @var LoggerSelector + */ + private $logger; - public function __construct(array $settings, $autoconfigure = true) + /** + * @throws Exception + */ + public function __construct(LoggerSelector $logger, array $settings, $autoconfigure = true) { Settings::validateSettings($settings); $this->settings = $settings; + $this->logger = $logger; - // Do not attemp autoconfiguration if key and cert values have not been set + // Do not attempt autoconfiguration if key and cert values have not been set if (!array_key_exists('sp_key_cert_values', $this->settings)) { $autoconfigure = false; } @@ -29,6 +43,9 @@ public function __construct(array $settings, $autoconfigure = true) } } + /** + * @throws Exception + */ public function loadIdpFromFile(string $filename) { if (empty($filename)) { @@ -42,27 +59,35 @@ public function loadIdpFromFile(string $filename) return $idp; } - public function getIdpList() : array + /** + * @throws Exception + */ + public function getIdpList(): array { $files = glob($this->settings['idp_metadata_folder'] . "*.xml"); if (is_array($files)) { - $mapping = array(); + $mapping = []; foreach ($files as $filename) { $idp = $this->loadIdpFromFile($filename); - $mapping[basename($filename, ".xml")] = $idp->metadata['idpEntityId']; } return $mapping; } - return array(); + return []; } + /** + * @throws Exception + */ public function getIdp(string $filename) { return $this->loadIdpFromFile($filename); } + /** + * @throws Exception + */ public function getSPMetadata(): string { if (!is_readable($this->settings['sp_cert_file'])) { @@ -70,20 +95,23 @@ public function getSPMetadata(): string Your SP certificate file is not readable. Please check file permissions. XML; } - + $entityID = htmlspecialchars($this->settings['sp_entityid'], ENT_XML1); $id = preg_replace('/[^a-z0-9_-]/', '_', $entityID); $cert = Settings::cleanOpenSsl($this->settings['sp_cert_file']); - $sloLocationArray = $this->settings['sp_singlelogoutservice'] ?? array(); - $assertcsArray = $this->settings['sp_assertionconsumerservice'] ?? array(); - $attrcsArray = $this->settings['sp_attributeconsumingservice'] ?? array(); + $sloLocationArray = $this->settings['sp_singlelogoutservice'] ?? []; + $assertcsArray = $this->settings['sp_assertionconsumerservice'] ?? []; + $attrcsArray = $this->settings['sp_attributeconsumingservice'] ?? []; $xml = << - + + $cert @@ -112,8 +140,9 @@ public function getSPMetadata(): string $xml .= << + isDefault="true" + Location="$location" + Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"/> XML; } for ($i = 0; $i < count($attrcsArray); $i++) { @@ -132,7 +161,6 @@ public function getSPMetadata(): string } $xml .= ''; - if (array_key_exists('sp_org_name', $this->settings)) { $orgName = $this->settings['sp_org_name']; $orgDisplayName = $this->settings['sp_org_display_name']; @@ -144,11 +172,65 @@ public function getSPMetadata(): string XML; } + + if (array_key_exists('sp_contact_aggregator_person_type', $this->settings)) { + $aggregatorContactCompany = $this->settings['sp_contact_aggregator_company']; + $aggregatorContactPersonType = $this->settings['sp_contact_aggregator_person_type']; + $aggregatorContactPersonVATNumber = $this->settings['sp_contact_aggregator_person_vat_number']; + $aggregatorContactPersonFiscalCode = $this->settings['sp_contact_aggregator_person_fiscal_code']; + $aggregatorContactPersonEmail = $this->settings['sp_contact_aggregator_person_email']; + $aggregatorContactPersonPhone = $this->settings['sp_contact_aggregator_person_phone']; + $xml .= << + + $aggregatorContactPersonVATNumber + $aggregatorContactPersonFiscalCode + + + $aggregatorContactCompany + $aggregatorContactPersonEmail + $aggregatorContactPersonPhone + +XML; + } + + if (array_key_exists('sp_contact_aggregate_person_type', $this->settings)) { + $aggregateContactIpaCode = $this->settings['sp_contact_aggregate_ipa_code']; + $aggregateContactPersonCompany = $this->settings['sp_contact_aggregate_company']; + $aggregateContactPersonType = $this->settings['sp_contact_aggregate_person_type']; + $xml .= << + + $aggregateContactIpaCode + + + $aggregateContactPersonCompany +XML; + if (array_key_exists('sp_contact_aggregate_email', $this->settings)) { + $aggregateEmail = $this->settings['sp_contact_aggregate_email']; + $xml .= "$aggregateEmail"; + } + + if (array_key_exists('sp_contact_aggregate_telephone', $this->settings)) { + $aggregateTelephone = $this->settings['sp_contact_aggregate_telephone']; + $xml .= "$aggregateTelephone"; + } + + $xml .= ''; + } + $xml .= ''; return SignatureUtils::signXml($xml, $this->settings); } + /** + * @throws Exception + */ public function login( string $idpName, int $assertId, @@ -161,6 +243,9 @@ public function login( return $this->baseLogin(Settings::BINDING_REDIRECT, ...$args); } + /** + * @throws Exception + */ public function loginPost( string $idpName, int $assertId, @@ -173,6 +258,10 @@ public function loginPost( return $this->baseLogin(Settings::BINDING_POST, ...$args); } + /** + * @throws SpidException + * @throws Exception + */ private function baseLogin( $binding, $idpName, @@ -186,11 +275,11 @@ private function baseLogin( return false; } if (!array_key_exists($assertId, $this->settings['sp_assertionconsumerservice'])) { - throw new \Exception("Invalid Assertion Consumer Service ID"); + throw new Exception("Invalid Assertion Consumer Service ID"); } if (isset($this->settings['sp_attributeconsumingservice'])) { if (!isset($this->settings['sp_attributeconsumingservice'][$attrId])) { - throw new \Exception("Invalid Attribute Consuming Service ID"); + throw new Exception("Invalid Attribute Consuming Service ID"); } } else { $attrId = null; @@ -200,43 +289,62 @@ private function baseLogin( return $idp->authnRequest($assertId, $attrId, $binding, $level, $redirectTo, $shouldRedirect); } - public function isAuthenticated() : bool + /** + * @throws SpidException + * @throws Exception + */ + public function isAuthenticated(): bool { $selectedIdp = $_SESSION['idpName'] ?? $_SESSION['spidSession']['idp'] ?? null; if (is_null($selectedIdp)) { + $this->logAuthenticationErrors("session error"); return false; } $idp = $this->loadIdpFromFile($selectedIdp); $response = new BaseResponse($this); if (!empty($idp) && !$response->validate($idp->metadata['idpCertValue'])) { + $this->logAuthenticationErrors("invalid metadata"); return false; } - if (isset($_SESSION) && isset($_SESSION['inResponseTo'])) { + if (isset($_SESSION['inResponseTo'])) { $idp->logoutResponse(); + $this->logAuthenticationErrors("isset inResponseTo"); return false; } - if (isset($_SESSION) && isset($_SESSION['spidSession'])) { + if (isset($_SESSION['spidSession'])) { $session = new Session($_SESSION['spidSession']); if ($session->isValid()) { $this->session = $session; return true; } } + + $this->logAuthenticationErrors("unknown case"); return false; } + /** + * @throws SpidException + */ public function logout(int $slo, string $redirectTo = null, $shouldRedirect = true) { $args = func_get_args(); return $this->baseLogout(Settings::BINDING_REDIRECT, ...$args); } + /** + * @throws SpidException + */ public function logoutPost(int $slo, string $redirectTo = null, $shouldRedirect = true) { $args = func_get_args(); return $this->baseLogout(Settings::BINDING_POST, ...$args); } + /** + * @throws SpidException + * @throws Exception + */ private function baseLogout($binding, $slo, $redirectTo = null, $shouldRedirect = true) { if (!$this->isAuthenticated()) { @@ -246,18 +354,22 @@ private function baseLogout($binding, $slo, $redirectTo = null, $shouldRedirect return $idp->logoutRequest($this->session, $slo, $binding, $redirectTo, $shouldRedirect); } - public function getAttributes() : array + /** + * @throws SpidException + */ + public function getAttributes(): array { if ($this->isAuthenticated() === false) { - return array(); + return []; } - return isset($this->session->attributes) && is_array($this->session->attributes) ? $this->session->attributes : - array(); + return isset($this->session->attributes) && is_array($this->session->attributes) + ? $this->session->attributes + : []; } - + // returns true if the SP certificates are found where the settings says they are, and they are valid // (i.e. the library has been configured correctly - private function isConfigured() : bool + private function isConfigured(): bool { if (!is_readable($this->settings['sp_key_file'])) { return false; @@ -287,15 +399,63 @@ private function configure() $keyCert = SignatureUtils::generateKeyCert($this->settings); $dir = dirname($this->settings['sp_key_file']); if (!is_dir($dir)) { - throw new \InvalidArgumentException('The directory you selected for sp_key_file does not exist. ' . - 'Please create ' . $dir); + throw new InvalidArgumentException( + "The directory you selected for sp_key_file does not exist. Please create $dir" + ); } $dir = dirname($this->settings['sp_cert_file']); if (!is_dir($dir)) { - throw new \InvalidArgumentException('The directory you selected for sp_cert_file does not exist.' . - 'Please create ' . $dir); + throw new InvalidArgumentException( + "The directory you selected for sp_cert_file does not exist. Please create $dir" + ); } file_put_contents($this->settings['sp_key_file'], $keyCert['key']); file_put_contents($this->settings['sp_cert_file'], $keyCert['cert']); } + + private function logAuthenticationErrors(string $errorMessage): void + { + $xml = null; + if (isset($_GET['SAMLResponse'])) { + $xml = gzinflate(base64_decode($_GET['SAMLResponse'])); + } elseif (isset($_POST['SAMLResponse'])) { + $xml = base64_decode($_POST['SAMLResponse']); + } + + $errorLevel = LogLevel::ERROR; + $additionalErrorInfo = ''; + + if ($xml) { + $dom = new DOMDocument(); + $dom->loadXML($xml); + $statusMessageElement = $dom->getElementsByTagName('StatusMessage'); + if ($statusMessageElement->item(0)->nodeValue) { + $errorString = $statusMessageElement->item(0)->nodeValue; + $errorCode = intval(str_replace('ErrorCode nr', '', $errorString)); + $errorLevel = AbstractLoggerSelector::getErrorLevel($errorCode); + $additionalErrorInfo = ' ' . AbstractLoggerSelector::getErrorMessage($errorCode) ; + } + } + + if ($this->logger->getTemporaryLogger()) { + $this->logger->getTemporaryLogger()->log( + $errorLevel, + "Saml::isAuthenticated error{$additionalErrorInfo}: {$errorMessage}" + . PHP_EOL + . "SESSION: " . var_export($_SESSION, true) + ); + } + if ($this->logger->getPermanentLogger()) { + $this->logger->getPermanentLogger()->log( + $errorLevel, + "Saml::isAuthenticated error{$additionalErrorInfo}", + ['xml' => $xml, 'error_message' => $additionalErrorInfo] + ); + } + } + + public function getLogger(): LoggerSelector + { + return $this->logger; + } } diff --git a/src/Spid/Saml/Idp.php b/src/Spid/Saml/Idp.php index a4b0327..6563c20 100644 --- a/src/Spid/Saml/Idp.php +++ b/src/Spid/Saml/Idp.php @@ -2,7 +2,10 @@ namespace Italia\Spid\Spid\Saml; +use Exception; +use Italia\Spid\Sp; use Italia\Spid\Spid\Interfaces\IdpInterface; +use Italia\Spid\Spid\Saml; use Italia\Spid\Spid\Saml\Out\AuthnRequest; use Italia\Spid\Spid\Saml\Out\LogoutRequest; use Italia\Spid\Spid\Session; @@ -18,12 +21,18 @@ class Idp implements IdpInterface public $level = 1; public $session; + /** + * @param Saml|Sp $sp + */ public function __construct($sp) { $this->sp = $sp; } - public function loadFromXml($xmlFile) + /** + * @throws Exception + */ + public function loadFromXml($xmlFile): self { if (strpos($xmlFile, $this->sp->settings['idp_metadata_folder']) !== false) { $fileName = $xmlFile; @@ -31,24 +40,24 @@ public function loadFromXml($xmlFile) $fileName = $this->sp->settings['idp_metadata_folder'] . $xmlFile . ".xml"; } if (!file_exists($fileName)) { - throw new \Exception("Metadata file $fileName not found", 1); + throw new Exception("Metadata file $fileName not found", 1); } if (!is_readable($fileName)) { - throw new \Exception("Metadata file $fileName is not readable. Please check file permissions.", 1); + throw new Exception("Metadata file $fileName is not readable. Please check file permissions.", 1); } $xml = simplexml_load_file($fileName); $xml->registerXPathNamespace('md', 'urn:oasis:names:tc:SAML:2.0:metadata'); $xml->registerXPathNamespace('ds', 'http://www.w3.org/2000/09/xmldsig#'); - $metadata = array(); - $idpSSO = array(); + $metadata = []; + $idpSSO = []; foreach ($xml->xpath('//md:SingleSignOnService') as $index => $item) { $idpSSO[$index]['location'] = $item->attributes()->Location->__toString(); $idpSSO[$index]['binding'] = $item->attributes()->Binding->__toString(); } - $idpSLO = array(); + $idpSLO = []; foreach ($xml->xpath('//md:SingleLogoutService') as $index => $item) { $idpSLO[$index]['location'] = $item->attributes()->Location->__toString(); $idpSLO[$index]['binding'] = $item->attributes()->Binding->__toString(); @@ -57,14 +66,25 @@ public function loadFromXml($xmlFile) $metadata['idpEntityId'] = $xml->attributes()->entityID->__toString(); $metadata['idpSSO'] = $idpSSO; $metadata['idpSLO'] = $idpSLO; - $metadata['idpCertValue'] = self::formatCert($xml->xpath('//ds:X509Certificate')[0]->__toString()); + $excludedIdps = + (strpos($metadata['idpEntityId'], 'lepida') != false) || + (strpos($metadata['idpEntityId'], 'tim') != false) || + (strpos($metadata['idpEntityId'], 'posteid') != false); + + if ($excludedIdps) { + $metadata['idpCertValue'] = self::formatCert($xml->xpath('//ds:X509Certificate')[0]->__toString()); + } else { + $metadata['idpCertValue'] = self::formatCert( + $xml->xpath('//md:IDPSSODescriptor//ds:X509Certificate')[0]->__toString() + ); + } $this->idpFileName = $xmlFile; $this->metadata = $metadata; return $this; } - private static function formatCert($cert, $heads = true) + private static function formatCert($cert) { //$cert = str_replace(" ", "\n", $cert); $x509cert = str_replace(array("\x0D", "\r", "\n"), "", $cert); @@ -72,25 +92,27 @@ private static function formatCert($cert, $heads = true) $x509cert = str_replace('-----BEGIN CERTIFICATE-----', "", $x509cert); $x509cert = str_replace('-----END CERTIFICATE-----', "", $x509cert); $x509cert = str_replace(' ', '', $x509cert); - - if ($heads) { - $x509cert = "-----BEGIN CERTIFICATE-----\n" . - chunk_split($x509cert, 64, "\n") . - "-----END CERTIFICATE-----\n"; - } + $x509cert = "-----BEGIN CERTIFICATE-----\n" . + chunk_split($x509cert, 64, "\n") . + "-----END CERTIFICATE-----\n"; } return $x509cert; } - public function authnRequest($ass, $attr, $binding, $level = 1, $redirectTo = null, $shouldRedirect = true) : string + + /** + * @throws Exception + */ + public function authnRequest($ass, $attr, $binding, $level = 1, $redirectTo = null, $shouldRedirect = true): string { $this->assertID = $ass; $this->attrID = $attr; $this->level = $level; - $authn = new AuthnRequest($this); + $authn = new AuthnRequest($this, $this->sp->getLogger()); $url = $binding == Settings::BINDING_REDIRECT ? $authn->redirectUrl($redirectTo) : $authn->httpPost($redirectTo); + $_SESSION['RequestID'] = $authn->id; $_SESSION['idpName'] = $this->idpFileName; $_SESSION['idpEntityId'] = $this->metadata['idpEntityId']; @@ -106,7 +128,10 @@ public function authnRequest($ass, $attr, $binding, $level = 1, $redirectTo = nu exit(""); } - public function logoutRequest(Session $session, $slo, $binding, $redirectTo = null, $shouldRedirect = true) : string + /** + * @throws Exception + */ + public function logoutRequest(Session $session, $slo, $binding, $redirectTo = null, $shouldRedirect = true): string { $this->session = $session; @@ -131,7 +156,10 @@ public function logoutRequest(Session $session, $slo, $binding, $redirectTo = nu exit(""); } - public function logoutResponse() : string + /** + * @throws Exception + */ + public function logoutResponse(): string { $binding = Settings::BINDING_POST; $redirectTo = $this->sp->settings['sp_entityid']; @@ -141,7 +169,7 @@ public function logoutResponse() : string $logoutResponse->redirectUrl($redirectTo) : $logoutResponse->httpPost($redirectTo); unset($_SESSION); - + if ($binding == Settings::BINDING_POST) { return $url; exit; diff --git a/src/Spid/Saml/In/BaseResponse.php b/src/Spid/Saml/In/BaseResponse.php index e8fb0ff..c4c9283 100644 --- a/src/Spid/Saml/In/BaseResponse.php +++ b/src/Spid/Saml/In/BaseResponse.php @@ -2,18 +2,21 @@ namespace Italia\Spid\Spid\Saml\In; +use DOMDocument; +use Exception; +use Italia\Spid\Spid\Exceptions\SpidException; use Italia\Spid\Spid\Saml\SignatureUtils; use Italia\Spid\Spid\Saml; /* * Generates the proper response object at runtime by reading the input XML. * Validates the response and the signature -* Specific response may complete other tasks upon succesful validation +* Specific response may complete other tasks upon successful validation * such as creating a login session for Response, or destroying the session -* for Logout resposnes. +* for Logout response. * The only case in which a Request is validated instead of a response is -* for Idp Initiated Logout. In this case the input is not a response to a requese +* for Idp Initiated Logout. In this case the input is not a response to a request * to a request sent by the SP, but rather a request started by the Idp */ class BaseResponse @@ -21,20 +24,26 @@ class BaseResponse private $response; private $xml; private $root; + private $xmlString; + private $logger; - public function __construct(Saml $saml = null) + /** + * @throws SpidException + */ + public function __construct(?Saml $saml = null) { - if ((!isset($_POST) || !isset($_POST['SAMLResponse'])) && - (!isset($_GET) || !isset($_GET['SAMLResponse'])) - ) { + if (!isset($_POST['SAMLResponse']) && !isset($_GET['SAMLResponse'])) { return; } - $xmlString = isset($_GET['SAMLResponse']) ? + + $this->logger = $saml->getLogger(); + + $this->xmlString = isset($_GET['SAMLResponse']) ? gzinflate(base64_decode($_GET['SAMLResponse'])) : base64_decode($_POST['SAMLResponse']); - - $this->xml = new \DOMDocument(); - $this->xml->loadXML($xmlString); + + $this->xml = new DOMDocument(); + $this->xml->loadXML($this->xmlString); $ns_samlp = 'urn:oasis:names:tc:SAML:2.0:protocol'; $this->root = $this->xml->getElementsByTagNameNS($ns_samlp, '*')->item(0)->localName; @@ -61,30 +70,43 @@ public function __construct(Saml $saml = null) $this->response = new LogoutRequest($saml); break; default: - throw new \Exception('No valid response found'); - break; + $printable = <<logger->logAndThrow($this->xml, $printable); } } - public function validate($cert) : bool + /** + * @throws SpidException + * @throws Exception + */ + public function validate($cert): bool { if (is_null($this->response)) { return true; } - + $ns_saml = 'urn:oasis:names:tc:SAML:2.0:assertion'; $hasAssertion = $this->xml->getElementsByTagNameNS($ns_saml, 'Assertion')->length > 0; $ns_signature = 'http://www.w3.org/2000/09/xmldsig#'; $signatures = $this->xml->getElementsByTagNameNS($ns_signature, 'Signature'); if ($hasAssertion && $signatures->length == 0) { - throw new \Exception("Invalid Response. Response must contain at least one signature"); + $this->logger->logAndThrow( + $this->xml, + 'Invalid Response. Response must contain at least one signature' + ); } $responseSignature = null; $assertionSignature = null; if ($signatures->length > 0) { - foreach ($signatures as $key => $item) { + foreach ($signatures as $item) { if ($item->parentNode->localName == 'Assertion') { $assertionSignature = $item; } @@ -93,13 +115,25 @@ public function validate($cert) : bool } } if ($hasAssertion && is_null($assertionSignature)) { - throw new \Exception("Invalid Response. Assertion must be signed"); + $this->logger->logAndThrow($this->xml, 'Invalid Response. Assertion must be signed'); } } - if (!SignatureUtils::validateXmlSignature($responseSignature, $cert) || - !SignatureUtils::validateXmlSignature($assertionSignature, $cert)) { - throw new \Exception("Invalid Response. Signature validation failed"); + + if (is_null($responseSignature)) { + $this->logger->logAndThrow($this->xml, 'Invalid Response. responseSignature is empty'); + } + if (is_null($assertionSignature)) { + $this->logger->logAndThrow($this->xml, 'Invalid Response. assertionSignature is empty'); + } + + if (!SignatureUtils::validateXmlSignature($responseSignature, $cert, $this->logger)) { + $this->logger->logAndThrow($this->xml, 'Invalid Response. responseSignature validation failed'); + } + + if (!SignatureUtils::validateXmlSignature($assertionSignature, $cert, $this->logger)) { + $this->logger->logAndThrow($this->xml, 'Invalid Response. assertionSignature validation failed'); } + return $this->response->validate($this->xml, $hasAssertion); } } diff --git a/src/Spid/Saml/In/LogoutRequest.php b/src/Spid/Saml/In/LogoutRequest.php index 1a8509e..41cdff6 100644 --- a/src/Spid/Saml/In/LogoutRequest.php +++ b/src/Spid/Saml/In/LogoutRequest.php @@ -2,12 +2,13 @@ namespace Italia\Spid\Spid\Saml\In; +use DOMDocument; +use Exception; use Italia\Spid\Spid\Interfaces\ResponseInterface; use Italia\Spid\Spid\Saml; class LogoutRequest implements ResponseInterface { - private $saml; public function __construct(Saml $saml) @@ -15,54 +16,59 @@ public function __construct(Saml $saml) $this->saml = $saml; } - public function validate($xml, $hasAssertion) : bool + /** + * @throws Exception + */ + public function validate(DOMDocument $xml, $hasAssertion): bool { $root = $xml->getElementsByTagName('LogoutRequest')->item(0); if ($xml->getElementsByTagName('Issuer')->length == 0) { - throw new \Exception("Invalid Response. Missing Issuer element"); + throw new Exception("Invalid Response. Missing Issuer element"); } if ($xml->getElementsByTagName('NameID')->length == 0) { - throw new \Exception("Invalid Response. Missing NameID element"); + throw new Exception("Invalid Response. Missing NameID element"); } if ($xml->getElementsByTagName('SessionIndex')->length == 0) { - throw new \Exception("Invalid Response. Missing SessionIndex element"); + throw new Exception("Invalid Response. Missing SessionIndex element"); } - + + $issuer = $xml->getElementsByTagName('Issuer')->item(0); if ($issuer->getAttribute('Destination') == "") { - throw new \Exception("Missing Destination attribute"); + throw new Exception("Missing Destination attribute"); } elseif ($issuer->getAttribute('Destination') != $this->saml->settings['sp_entityid']) { - throw new \Exception("Invalid ForDestinationmat attribute"); + throw new Exception("Invalid ForDestinationmat attribute"); } - $issuer = $xml->getElementsByTagName('Issuer')->item(0); - $nameId = $xml->getElementsByTagName('NameID')->item(0); - $sessionIndex = $xml->getElementsByTagName('SessionIndex')->item(0); if ($issuer->getAttribute('Format') == "") { - throw new \Exception("Missing Format attribute"); + throw new Exception("Missing Format attribute"); } elseif ($issuer->getAttribute('Format') != "urn:oasis:names:tc:SAML:2.0:nameid-format:entity") { - throw new \Exception("Invalid Format attribute"); + throw new Exception("Invalid Format attribute"); } if ($issuer->getAttribute('NameQualifier') == "") { - throw new \Exception("Missing NameQualifier attribute"); + throw new Exception("Missing NameQualifier attribute"); } elseif ($issuer->getAttribute('NameQualifier') != $_SESSION['spidSession']->idpEntityID) { - throw new \Exception("Invalid NameQualifier attribute"); + throw new Exception("Invalid NameQualifier attribute"); } + $nameId = $xml->getElementsByTagName('NameID')->item(0); + $sessionIndex = $xml->getElementsByTagName('SessionIndex')->item(0); if ($nameId->getAttribute('Format') == "") { - throw new \Exception("Missing NameID Format attribute"); + throw new Exception("Missing NameID Format attribute"); } elseif ($nameId->getAttribute('Format') != "“urn:oasis:names:tc:SAML:2.0:nameidformat:transient") { - throw new \Exception("Invalid NameID Format attribute"); + throw new Exception("Invalid NameID Format attribute"); } if ($nameId->getAttribute('NameQualifier') == "") { - throw new \Exception("Missing NameID NameQualifier attribute"); + throw new Exception("Missing NameID NameQualifier attribute"); } elseif ($nameId->getAttribute('NameQualifier') != $_SESSION['spidSession']->idpEntityID) { - throw new \Exception("Invalid NameID NameQualifier attribute"); + throw new Exception("Invalid NameID NameQualifier attribute"); } - + if ($sessionIndex->nodeValue != $_SESSION['spidSession']->sessionID) { - throw new \Exception("Invalid SessionID, expected " . $_SESSION['spidSession']->sessionID . - " but received " . $sessionIndex->nodeValue); + throw new Exception( + "Invalid SessionID, expected " . $_SESSION['spidSession']->sessionID . + " but received " . $sessionIndex->nodeValue + ); } $_SESSION['inResponseTo'] = $root->getAttribute('ID'); return true; diff --git a/src/Spid/Saml/In/LogoutResponse.php b/src/Spid/Saml/In/LogoutResponse.php index 538c125..4297889 100644 --- a/src/Spid/Saml/In/LogoutResponse.php +++ b/src/Spid/Saml/In/LogoutResponse.php @@ -2,47 +2,54 @@ namespace Italia\Spid\Spid\Saml\In; +use DOMDocument; +use Exception; use Italia\Spid\Spid\Interfaces\ResponseInterface; class LogoutResponse implements ResponseInterface { - public function validate($xml, $hasAssertion) : bool + /** + * @throws Exception + */ + public function validate(DOMDocument $xml, $hasAssertion): bool { $root = $xml->getElementsByTagName('LogoutResponse')->item(0); if ($root->getAttribute('ID') == "") { - throw new \Exception("missing ID attribute"); + throw new Exception("missing ID attribute"); } if ($root->getAttribute('Version') == "") { - throw new \Exception("missing Version attribute"); + throw new Exception("missing Version attribute"); } elseif ($root->getAttribute('Version') != '2.0') { - throw new \Exception("Invalid Version attribute"); + throw new Exception("Invalid Version attribute"); } if ($root->getAttribute('IssueInstant') == "") { - throw new \Exception("Missing IssueInstant attribute"); + throw new Exception("Missing IssueInstant attribute"); } if ($root->getAttribute('InResponseTo') == "" || !isset($_SESSION['RequestID'])) { - throw new \Exception("Missing InResponseTo attribute, or request ID was not saved correctly " . + throw new Exception("Missing InResponseTo attribute, or request ID was not saved correctly " . "for comparison"); } elseif ($root->getAttribute('InResponseTo') != $_SESSION['RequestID']) { - throw new \Exception("Invalid InResponseTo attribute, expected " . $_SESSION['RequestID']); + throw new Exception("Invalid InResponseTo attribute, expected " . $_SESSION['RequestID']); } if ($root->getAttribute('Destination') == "") { - throw new \Exception("Missing Destination attribute"); + throw new Exception("Missing Destination attribute"); } elseif ($root->getAttribute('Destination') != $_SESSION['sloUrl']) { - throw new \Exception("Invalid Destination attribute, expected " . $_SESSION['sloUrl'] . + throw new Exception("Invalid Destination attribute, expected " . $_SESSION['sloUrl'] . " but received " . $root->getAttribute('Destination')); } if ($xml->getElementsByTagName('Issuer')->length == 0) { - throw new \Exception("Missing Issuer attribute"); + throw new Exception("Missing Issuer attribute"); } elseif ($xml->getElementsByTagName('Issuer')->item(0)->nodeValue != $_SESSION['idpEntityId']) { - throw new \Exception("Invalid Issuer attribute, expected " . $_SESSION['idpEntityId'] . + throw new Exception("Invalid Issuer attribute, expected " . $_SESSION['idpEntityId'] . " but received " . $xml->getElementsByTagName('Response')->item(0)->nodeValue); } if ($xml->getElementsByTagName('Status')->length <= 0) { - throw new \Exception("Missing Status element"); - } elseif ($xml->getElementsByTagName('StatusCode')->item(0)->getAttribute('Value') != - 'urn:oasis:names:tc:SAML:2.0:status:Success') { + throw new Exception("Missing Status element"); + } elseif ( + $xml->getElementsByTagName('StatusCode')->item(0)->getAttribute('Value') != + 'urn:oasis:names:tc:SAML:2.0:status:Success' + ) { // Status code != success return false; } diff --git a/src/Spid/Saml/In/Response.php b/src/Spid/Saml/In/Response.php index b3738fe..85a5104 100644 --- a/src/Spid/Saml/In/Response.php +++ b/src/Spid/Saml/In/Response.php @@ -2,13 +2,14 @@ namespace Italia\Spid\Spid\Saml\In; +use DOMDocument; +use Italia\Spid\Spid\Exceptions\SpidException; use Italia\Spid\Spid\Interfaces\ResponseInterface; use Italia\Spid\Spid\Session; use Italia\Spid\Spid\Saml; class Response implements ResponseInterface { - private $saml; public function __construct(Saml $saml) @@ -16,184 +17,218 @@ public function __construct(Saml $saml) $this->saml = $saml; } - public function validate($xml, $hasAssertion): bool + /** + * @throws SpidException + */ + public function validate(DOMDocument $xml, $hasAssertion): bool { - $accepted_clock_skew_seconds = isset($this->saml->settings['accepted_clock_skew_seconds']) ? - $this->saml->settings['accepted_clock_skew_seconds'] : 0; + $logger = $this->saml->getLogger(); + + $acceptedClockSkewSeconds = $this->saml->settings['accepted_clock_skew_seconds'] ?? 0; + $minTime = strtotime('now') - $acceptedClockSkewSeconds; + $maxTime = strtotime('now') + $acceptedClockSkewSeconds; + $samlUrn = 'urn:oasis:names:tc:SAML:2.0:'; $root = $xml->getElementsByTagName('Response')->item(0); if ($root->getAttribute('Version') == "") { - throw new \Exception("Missing Version attribute"); + $logger->logAndThrow($xml, "Missing Version attribute"); } elseif ($root->getAttribute('Version') != '2.0') { - throw new \Exception("Invalid Version attribute"); + $logger->logAndThrow($xml, "Invalid Version attribute"); } - if ($root->getAttribute('IssueInstant') == "") { - throw new \Exception("Missing IssueInstant attribute on Response"); - } elseif (!$this->validateDate($root->getAttribute('IssueInstant'))) { - throw new \Exception("Invalid IssueInstant attribute on Response"); - } elseif (strtotime($root->getAttribute('IssueInstant')) > strtotime('now') + $accepted_clock_skew_seconds) { - throw new \Exception("IssueInstant attribute on Response is in the future"); + + $issueInstant = $root->getAttribute('IssueInstant'); + if ($issueInstant == "") { + $logger->logAndThrow($xml, "Missing IssueInstant attribute on Response"); + } elseif (!$this->validateDate($issueInstant)) { + $logger->logAndThrow($xml, "Invalid IssueInstant attribute on Response"); + } elseif (strtotime($issueInstant) > strtotime('now') + $acceptedClockSkewSeconds) { + $logger->logAndThrow($xml, "IssueInstant attribute on Response is in the future"); } - if ($root->getAttribute('InResponseTo') == "" || !isset($_SESSION['RequestID'])) { - throw new \Exception("Missing InResponseTo attribute, or request ID was not saved correctly " . - "for comparison"); - } elseif ($root->getAttribute('InResponseTo') != $_SESSION['RequestID']) { - throw new \Exception("Invalid InResponseTo attribute, expected " . $_SESSION['RequestID'] . - " but received " . $root->getAttribute('InResponseTo')); + $inResponseTo = $root->getAttribute('InResponseTo'); + if ($inResponseTo == "" || !isset($_SESSION['RequestID'])) { + $logger->logAndThrow( + $xml, + "Missing InResponseTo attribute, or request ID was not saved correctly for comparison" + ); + } elseif ($inResponseTo != $_SESSION['RequestID']) { + $logger->logAndThrow( + $xml, + "Invalid InResponseTo attribute, expected {$_SESSION['RequestID']} but received " . $inResponseTo + ); } - if ($root->getAttribute('Destination') == "") { - throw new \Exception("Missing Destination attribute"); - } elseif ($root->getAttribute('Destination') != $_SESSION['acsUrl']) { - throw new \Exception("Invalid Destination attribute, expected " . $_SESSION['acsUrl'] . - " but received " . $root->getAttribute('Destination')); + $destination = $root->getAttribute('Destination'); + if ($destination == "") { + $logger->logAndThrow($xml, "Missing Destination attribute"); + } elseif ($destination != $_SESSION['acsUrl']) { + $logger->logAndThrow( + $xml, + "Invalid Destination attribute, expected {$_SESSION['acsUrl']} but received " . $destination + ); } - if ($xml->getElementsByTagName('Issuer')->length == 0) { - throw new \Exception("Missing Issuer attribute"); + $issuer = $xml->getElementsByTagName('Issuer'); + if ($issuer->length == 0) { + $logger->logAndThrow($xml, "Missing Issuer attribute"); //check item 0, this the Issuer element child of Response - } elseif ($xml->getElementsByTagName('Issuer')->item(0)->nodeValue != $_SESSION['idpEntityId']) { - throw new \Exception("Invalid Issuer attribute, expected " . $_SESSION['idpEntityId'] . - " but received " . $xml->getElementsByTagName('Issuer')->item(0)->nodeValue); - } elseif ($xml->getElementsByTagName('Issuer')->item(0)->getAttribute('Format') != - 'urn:oasis:names:tc:SAML:2.0:nameid-format:entity') { - throw new \Exception("Invalid Issuer attribute, expected 'urn:oasis:names:tc:SAML:2.0:nameid-format:" . - "entity'" . " but received " . $xml->getElementsByTagName('Issuer')->item(0)->getAttribute('Format')); + } elseif ($issuer->item(0)->nodeValue != $_SESSION['idpEntityId']) { + $logger->logAndThrow( + $xml, + "Invalid Issuer attribute, expected {$_SESSION['idpEntityId']} but received " . + $issuer->item(0)->nodeValue + ); + } elseif ( + $issuer->item(0)->hasAttribute('Format') + && $issuer->item(0)->getAttribute('Format') != $samlUrn . 'nameid-format:entity' + ) { + $logger->logAndThrow( + $xml, + "Invalid Issuer attribute, expected '{$samlUrn}nameid-format:entity' but received " . + $issuer->item(0)->getAttribute('Format') + ); } if ($hasAssertion) { - if ($xml->getElementsByTagName('Assertion')->item(0)->getAttribute('ID') == "" || - $xml->getElementsByTagName('Assertion')->item(0)->getAttribute('ID') == null) { - throw new \Exception("Missing ID attribute on Assertion"); - } elseif ($xml->getElementsByTagName('Assertion')->item(0)->getAttribute('Version') != '2.0') { - throw new \Exception("Invalid Version attribute on Assertion"); - } elseif ($xml->getElementsByTagName('Assertion')->item(0)->getAttribute('IssueInstant') == "") { - throw new \Exception("Invalid IssueInstant attribute on Assertion"); - } elseif (!$this->validateDate( - $xml->getElementsByTagName('Assertion')->item(0)->getAttribute('IssueInstant') - )) { - throw new \Exception("Invalid IssueInstant attribute on Assertion"); - } elseif (strtotime($xml->getElementsByTagName('Assertion')->item(0)->getAttribute('IssueInstant')) > - strtotime('now') + $accepted_clock_skew_seconds) { - throw new \Exception("IssueInstant attribute on Assertion is in the future"); + $assertion = $xml->getElementsByTagName('Assertion')->item(0); + if ($assertion->getAttribute('ID') == "" || $assertion->getAttribute('ID') == null) { + $logger->logAndThrow($xml, "Missing ID attribute on Assertion"); + } elseif ($assertion->getAttribute('Version') != '2.0') { + $logger->logAndThrow($xml, "Invalid Version attribute on Assertion"); + } elseif ($assertion->getAttribute('IssueInstant') == "") { + $logger->logAndThrow($xml, "Invalid IssueInstant attribute on Assertion"); + } elseif (!$this->validateDate($assertion->getAttribute('IssueInstant'))) { + $logger->logAndThrow($xml, "Invalid IssueInstant attribute on Assertion"); + } elseif (strtotime($assertion->getAttribute('IssueInstant')) > $maxTime) { + $logger->logAndThrow($xml, "IssueInstant attribute on Assertion is in the future"); } // check item 1, this must be the Issuer element child of Assertion - if ($hasAssertion && $xml->getElementsByTagName('Issuer')->item(1)->nodeValue != $_SESSION['idpEntityId']) { - throw new \Exception("Invalid Issuer attribute, expected " . $_SESSION['idpEntityId'] . - " but received " . $xml->getElementsByTagName('Issuer')->item(1)->nodeValue); - } elseif ($xml->getElementsByTagName('Issuer')->item(1)->getAttribute('Format') != - 'urn:oasis:names:tc:SAML:2.0:nameid-format:entity') { - throw new \Exception("Invalid Issuer attribute, expected 'urn:oasis:names:tc:SAML:2.0:nameid-format:" . - "entity'" . " but received " . $xml->getElementsByTagName('Issuer')->item(1)->getAttribute('Format')); + if ($issuer->item(1)->nodeValue != $_SESSION['idpEntityId']) { + $logger->logAndThrow( + $xml, + "Invalid Issuer attribute, expected {$_SESSION['idpEntityId']} but received " . + $issuer->item(1)->nodeValue + ); + } elseif ($issuer->item(1)->getAttribute('Format') != $samlUrn . 'nameid-format:entity') { + $logger->logAndThrow( + $xml, + "Invalid Issuer attribute, expected '{$samlUrn}nameid-format:entity'" . + ' but received ' . $issuer->item(1)->getAttribute('Format') + ); } - if ($xml->getElementsByTagName('Conditions')->length == 0) { - throw new \Exception("Missing Conditions attribute"); - } elseif ($xml->getElementsByTagName('Conditions')->item(0)->getAttribute('NotBefore') == "") { - throw new \Exception("Missing NotBefore attribute"); - } elseif (!$this->validateDate( - $xml->getElementsByTagName('Conditions')->item(0)->getAttribute('NotBefore') - )) { - throw new \Exception("Invalid NotBefore attribute"); - } elseif (strtotime($xml->getElementsByTagName('Conditions')->item(0)->getAttribute('NotBefore')) > - strtotime('now') + $accepted_clock_skew_seconds) { - throw new \Exception("NotBefore attribute is in the future"); - } elseif ($xml->getElementsByTagName('Conditions')->item(0)->getAttribute('NotOnOrAfter') == "") { - throw new \Exception("Missing NotOnOrAfter attribute"); - } elseif (!$this->validateDate( - $xml->getElementsByTagName('Conditions')->item(0)->getAttribute('NotOnOrAfter') - )) { - throw new \Exception("Invalid NotOnOrAfter attribute"); - } elseif (strtotime($xml->getElementsByTagName('Conditions')->item(0)->getAttribute('NotOnOrAfter')) <= - strtotime('now') - $accepted_clock_skew_seconds) { - throw new \Exception("NotOnOrAfter attribute is in the past"); + $conditions = $xml->getElementsByTagName('Conditions'); + if ($conditions->length == 0) { + $logger->logAndThrow($xml, "Missing Conditions attribute"); + } elseif ($conditions->item(0)->getAttribute('NotBefore') == "") { + $logger->logAndThrow($xml, "Missing NotBefore attribute"); + } elseif (!$this->validateDate($conditions->item(0)->getAttribute('NotBefore'))) { + $logger->logAndThrow($xml, "Invalid NotBefore attribute"); + } elseif (strtotime($conditions->item(0)->getAttribute('NotBefore')) > $maxTime) { + $logger->logAndThrow($xml, "NotBefore attribute is in the future"); + } elseif ($conditions->item(0)->getAttribute('NotOnOrAfter') == "") { + $logger->logAndThrow($xml, "Missing NotOnOrAfter attribute"); + } elseif (!$this->validateDate($conditions->item(0)->getAttribute('NotOnOrAfter'))) { + $logger->logAndThrow($xml, "Invalid NotOnOrAfter attribute"); + } elseif (strtotime($conditions->item(0)->getAttribute('NotOnOrAfter')) <= $minTime) { + $logger->logAndThrow($xml, "NotOnOrAfter attribute is in the past"); } if ($xml->getElementsByTagName('AudienceRestriction')->length == 0) { - throw new \Exception("Missing AudienceRestriction attribute"); + $logger->logAndThrow($xml, "Missing AudienceRestriction attribute"); } - if ($xml->getElementsByTagName('Audience')->length == 0) { - throw new \Exception("Missing Audience attribute"); - } elseif ($xml->getElementsByTagName('Audience')->item(0)->nodeValue != - $this->saml->settings['sp_entityid']) { - throw new \Exception("Invalid Audience attribute, expected " . $this->saml->settings['sp_entityid'] . - " but received " . $xml->getElementsByTagName('Audience')->item(0)->nodeValue); + $audience = $xml->getElementsByTagName('Audience'); + if ($audience->length == 0) { + $logger->logAndThrow($xml, "Missing Audience attribute"); + } elseif ($audience->item(0)->nodeValue != $this->saml->settings['sp_entityid']) { + $logger->logAndThrow( + $xml, + "Invalid Audience attribute, expected " . $this->saml->settings['sp_entityid'] . + " but received " . $audience->item(0)->nodeValue + ); } - if ($xml->getElementsByTagName('NameID')->length == 0) { - throw new \Exception("Missing NameID attribute"); - } elseif ($xml->getElementsByTagName('NameID')->item(0)->getAttribute('Format') != - 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient') { - throw new \Exception("Invalid NameID attribute, expected " . - "'urn:oasis:names:tc:SAML:2.0:nameid-format:transient'" . " but received " . - $xml->getElementsByTagName('NameID')->item(0)->getAttribute('Format')); - } elseif ($xml->getElementsByTagName('NameID')->item(0)->getAttribute('NameQualifier') != - $_SESSION['idpEntityId']) { - throw new \Exception("Invalid NameQualifier attribute, expected " . $_SESSION['idpEntityId'] . - " but received " . $xml->getElementsByTagName('NameID')->item(0)->getAttribute('NameQualifier')); + $nameId = $xml->getElementsByTagName('NameID'); + if ($nameId->length == 0) { + $logger->logAndThrow($xml, "Missing NameID attribute"); + } elseif ($nameId->item(0)->getAttribute('Format') != $samlUrn . 'nameid-format:transient') { + $logger->logAndThrow( + $xml, + "Invalid NameID attribute, expected '{$samlUrn}nameid-format:transient'" . + " but received " . $nameId->item(0)->getAttribute('Format') + ); + } elseif ($nameId->item(0)->getAttribute('NameQualifier') != $_SESSION['idpEntityId']) { + $logger->logAndThrow( + $xml, + "Invalid NameQualifier attribute, expected {$_SESSION['idpEntityId']} but received " . + $nameId->item(0)->getAttribute('NameQualifier') + ); } - if ($xml->getElementsByTagName('SubjectConfirmationData')->length == 0) { - throw new \Exception("Missing SubjectConfirmationData attribute"); - } elseif ($xml->getElementsByTagName('SubjectConfirmationData')->item(0)->getAttribute('InResponseTo') != - $_SESSION['RequestID']) { - throw new \Exception("Invalid SubjectConfirmationData attribute, expected " . $_SESSION['RequestID'] . - " but received " . - $xml->getElementsByTagName('SubjectConfirmationData')->item(0)->getAttribute('InResponseTo')); - } elseif (strtotime( - $xml->getElementsByTagName('SubjectConfirmationData')->item(0)->getAttribute('NotOnOrAfter') - ) <= strtotime('now') - $accepted_clock_skew_seconds) { - throw new \Exception("Invalid NotOnOrAfter attribute"); - } elseif ($xml->getElementsByTagName('SubjectConfirmationData')->item(0)->getAttribute('Recipient') != - $_SESSION['acsUrl']) { - throw new \Exception("Invalid Recipient attribute, expected " . $_SESSION['acsUrl'] . - " but received " . - $xml->getElementsByTagName('SubjectConfirmationData')->item(0)->getAttribute('Recipient')); - } elseif ($xml->getElementsByTagName('SubjectConfirmation')->item(0)->getAttribute('Method') != - 'urn:oasis:names:tc:SAML:2.0:cm:bearer') { - throw new \Exception("Invalid Method attribute, expected 'urn:oasis:names:tc:SAML:2.0:cm:bearer'" . - " but received " . - $xml->getElementsByTagName('SubjectConfirmation')->item(0)->getAttribute('Method')); + $subjectConfirmation = $xml->getElementsByTagName('SubjectConfirmation')->item(0); + $subjectConfirmationData = $subjectConfirmation->getElementsByTagName('SubjectConfirmationData'); + if ($subjectConfirmationData->length == 0) { + $logger->logAndThrow($xml, "Missing SubjectConfirmationData attribute"); + } elseif ($subjectConfirmationData->item(0)->getAttribute('InResponseTo') != $_SESSION['RequestID']) { + $logger->logAndThrow( + $xml, + "Invalid SubjectConfirmationData attribute, expected {$_SESSION['RequestID']} but received " . + $subjectConfirmationData->item(0)->getAttribute('InResponseTo') + ); + } elseif (strtotime($subjectConfirmationData->item(0)->getAttribute('NotOnOrAfter')) <= $minTime) { + $logger->logAndThrow($xml, "Invalid NotOnOrAfter attribute"); + } elseif ($subjectConfirmationData->item(0)->getAttribute('Recipient') != $_SESSION['acsUrl']) { + $logger->logAndThrow( + $xml, + "Invalid Recipient attribute, expected {$_SESSION['acsUrl']} but received " . + $subjectConfirmationData->item(0)->getAttribute('Recipient') + ); + } elseif ($subjectConfirmation->getAttribute('Method') != $samlUrn . 'cm:bearer') { + $logger->logAndThrow( + $xml, + "Invalid Method attribute, expected '{$samlUrn}cm:bearer' but received " . + $subjectConfirmation->getAttribute('Method') + ); } if ($xml->getElementsByTagName('Attribute')->length == 0) { - throw new \Exception("Missing Attribute Element"); + $logger->logAndThrow($xml, "Missing Attribute Element"); } if ($xml->getElementsByTagName('AttributeValue')->length == 0) { - throw new \Exception("Missing AttributeValue Element"); + $logger->logAndThrow($xml, "Missing AttributeValue Element"); } } - if ($xml->getElementsByTagName('Status')->length <= 0) { - throw new \Exception("Missing Status element"); - } elseif ($xml->getElementsByTagName('Status')->item(0) == null) { - throw new \Exception("Missing Status element"); - } elseif ($xml->getElementsByTagName('StatusCode')->item(0) == null) { - throw new \Exception("Missing StatusCode element"); - } elseif ($xml->getElementsByTagName('StatusCode')->item(0)->getAttribute('Value') == - 'urn:oasis:names:tc:SAML:2.0:status:Success') { + $status = $xml->getElementsByTagName('Status'); + if ($status->length <= 0) { + $logger->logAndThrow($xml, "Missing Status element"); + } elseif ($status->item(0) == null) { + $logger->logAndThrow($xml, "Missing Status element"); + } + + $statusCode = $xml->getElementsByTagName('StatusCode'); + if ($statusCode->item(0) == null) { + $logger->logAndThrow($xml, "Missing StatusCode element"); + } elseif ($statusCode->item(0)->getAttribute('Value') == $samlUrn . 'status:Success') { if ($hasAssertion && $xml->getElementsByTagName('AuthnStatement')->length <= 0) { - throw new \Exception("Missing AuthnStatement element"); + $logger->logAndThrow($xml, "Missing AuthnStatement element"); } - } elseif ($xml->getElementsByTagName('StatusCode')->item(0)->getAttribute('Value') != - 'urn:oasis:names:tc:SAML:2.0:status:Success') { + } elseif ($statusCode->item(0)->getAttribute('Value') != $samlUrn . 'status:Success') { if ($xml->getElementsByTagName('StatusMessage')->item(0) != null) { - $StatusMessage = ' [message: ' . $xml->getElementsByTagName('StatusMessage')->item(0)->nodeValue . ']'; + $errorString = $xml->getElementsByTagName('StatusMessage')->item(0)->nodeValue; + $logger->logAndThrow($xml, "StatusCode is not Success [message: {$errorString}]"); } else { - $StatusMessage = ""; + $logger->logAndThrow($xml, "StatusCode is not Success"); } - throw new \Exception("StatusCode is not Success" . $StatusMessage); - } elseif ($xml->getElementsByTagName('StatusCode')->item(1)->getAttribute('Value') == - 'urn:oasis:names:tc:SAML:2.0:status:AuthnFailed') { - throw new \Exception("AuthnFailed AuthnStatement element"); + } elseif ($statusCode->item(1)->getAttribute('Value') == $samlUrn . 'status:AuthnFailed') { + $logger->logAndThrow($xml, "AuthnFailed AuthnStatement element"); } else { // Status code != success - return false; + $logger->logAndThrow($xml, "Generic error"); } // Response OK @@ -206,9 +241,9 @@ public function validate($xml, $hasAssertion): bool return true; } - private function validateDate($date) + private function validateDate($date): bool { - if (preg_match('/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(\.\d+)?Z$/', $date, $parts) == true) { + if (preg_match('/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(\.\d+)?Z$/', $date, $parts)) { $time = gmmktime($parts[4], $parts[5], $parts[6], $parts[2], $parts[3], $parts[1]); $input_time = strtotime($date); @@ -222,17 +257,17 @@ private function validateDate($date) } } - private function spidSession(\DOMDocument $xml) + private function spidSession(DOMDocument $xml): Session { $session = new Session(); - $attributes = array(); + $attributes = []; $attributeStatements = $xml->getElementsByTagName('AttributeStatement'); if ($attributeStatements->length > 0) { foreach ($attributeStatements->item(0)->childNodes as $attr) { if ($attr->hasAttributes()) { - $attributes[$attr->attributes->getNamedItem('Name')->value] = trim($attr->nodeValue); + $attributes[$attr->attributes->getNamedItem('Name')->nodeValue] = trim($attr->nodeValue); } } } diff --git a/src/Spid/Saml/Out/AuthnRequest.php b/src/Spid/Saml/Out/AuthnRequest.php index 95d9f96..8b438c7 100644 --- a/src/Spid/Saml/Out/AuthnRequest.php +++ b/src/Spid/Saml/Out/AuthnRequest.php @@ -2,12 +2,27 @@ namespace Italia\Spid\Spid\Saml\Out; +use Exception; +use Italia\Spid\Spid\Interfaces\LoggerSelector; use Italia\Spid\Spid\Interfaces\RequestInterface; +use Italia\Spid\Spid\Saml\Idp; use Italia\Spid\Spid\Saml\Settings; use Italia\Spid\Spid\Saml\SignatureUtils; +use SimpleXMLElement; class AuthnRequest extends Base implements RequestInterface { + private $logger; + + public function __construct(Idp $idp, LoggerSelector $logger) + { + parent::__construct($idp); + $this->logger = $logger; + } + + /** + * @throws Exception + */ public function generateXml() { $id = $this->generateID(); @@ -18,13 +33,9 @@ public function generateXml() $assertID = $this->idp->assertID; $attrID = $this->idp->attrID; $level = $this->idp->level; - if (isset($this->idp->sp->settings['sp_comparison'])) { - $comparison = $this->idp->sp->settings['sp_comparison']; - } else { - $comparison = "exact"; - } + $comparison = $this->idp->sp->settings['sp_comparison'] ?? "exact"; $force = ($level > 1 || $comparison == "minimum") ? "true" : "false"; - + $authnRequestXml = << XML; - $xml = new \SimpleXMLElement($authnRequestXml); + $xml = new SimpleXMLElement($authnRequestXml); if (!is_null($attrID)) { $xml->addAttribute('AttributeConsumingServiceIndex', $attrID); } $this->xml = $xml->asXML(); + $logger = $this->logger->getPermanentLogger(); + if ($logger) { + $logger->info('AuthnRequest', [ + 'xml' => $this->xml, + 'schema' => 'AuthnRequest' + ]); + } } - public function redirectUrl($redirectTo = null) : string + /** + * @throws Exception + */ + public function redirectUrl($redirectTo = null): string { $location = parent::getBindingLocation(Settings::BINDING_REDIRECT); if (is_null($this->xml)) { @@ -61,7 +82,10 @@ public function redirectUrl($redirectTo = null) : string return parent::redirect($location, $redirectTo); } - public function httpPost($redirectTo = null) : string + /** + * @throws Exception + */ + public function httpPost($redirectTo = null): string { $location = parent::getBindingLocation(Settings::BINDING_POST); if (is_null($this->xml)) { diff --git a/src/Spid/Saml/Out/Base.php b/src/Spid/Saml/Out/Base.php index 25ddde7..bcd5444 100644 --- a/src/Spid/Saml/Out/Base.php +++ b/src/Spid/Saml/Out/Base.php @@ -2,6 +2,7 @@ namespace Italia\Spid\Spid\Saml\Out; +use Exception; use Italia\Spid\Spid\Saml\Idp; use Italia\Spid\Spid\Saml\SignatureUtils; @@ -17,7 +18,10 @@ public function __construct(Idp $idp) $this->idp = $idp; } - public function generateID() + /** + * @throws Exception + */ + public function generateID(): string { $this->id = '_' . bin2hex(random_bytes(16)); return $this->id; @@ -29,13 +33,18 @@ public function generateIssueInstant() return $this->issueInstant; } - public function redirect($url, $redirectTo = null) + /** + * @throws Exception + */ + public function redirect($url, $redirectTo = null): string { $compressed = gzdeflate($this->xml); $parameters['SAMLRequest'] = base64_encode($compressed); - $parameters['RelayState'] = is_null($redirectTo) ? (isset($_SERVER['HTTPS']) - && $_SERVER['HTTPS'] === 'on' ? "https" : "http") . - "//{$_SERVER['HTTP_HOST']}{$_SERVER['REQUEST_URI']}" : $redirectTo; + $schema = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? "https" : "http"; + $relayState = is_null($redirectTo) + ? "{$schema}://{$_SERVER['HTTP_HOST']}{$_SERVER['REQUEST_URI']}" + : $redirectTo; + $parameters['RelayState'] = base64_encode($relayState); $parameters['SigAlg'] = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256'; $parameters['Signature'] = SignatureUtils::signUrl( $parameters['SAMLRequest'], @@ -48,14 +57,15 @@ public function redirect($url, $redirectTo = null) return $url; } - public function postForm($url, $redirectTo = null) + public function postForm($url, $redirectTo = null): string { $SAMLRequest = base64_encode($this->xml); - $relayState = is_null($redirectTo) ? (isset($_SERVER['HTTPS']) && - $_SERVER['HTTPS'] === 'on' ? "https" : "http") . - "//{$_SERVER['HTTP_HOST']}{$_SERVER['REQUEST_URI']}" : $redirectTo; - $relayState = null; + $schema = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? "https" : "http"; + $relayState = is_null($redirectTo) + ? "{$schema}://{$_SERVER['HTTP_HOST']}{$_SERVER['REQUEST_URI']}" + : $redirectTo; + $relayState = base64_encode($relayState); return << @@ -68,6 +78,9 @@ public function postForm($url, $redirectTo = null) HTML; } + /** + * @throws Exception + */ protected function getBindingLocation($binding, $service = 'SSO') { $location = null; @@ -78,7 +91,7 @@ protected function getBindingLocation($binding, $service = 'SSO') } }); if (is_null($location)) { - throw new \Exception("No location found for binding " . $binding); + throw new Exception("No location found for binding " . $binding); } return $location; } diff --git a/src/Spid/Saml/Out/LogoutRequest.php b/src/Spid/Saml/Out/LogoutRequest.php index 973f8a9..4284520 100644 --- a/src/Spid/Saml/Out/LogoutRequest.php +++ b/src/Spid/Saml/Out/LogoutRequest.php @@ -2,12 +2,16 @@ namespace Italia\Spid\Spid\Saml\Out; +use Exception; use Italia\Spid\Spid\Interfaces\RequestInterface; use Italia\Spid\Spid\Saml\Settings; use Italia\Spid\Spid\Saml\SignatureUtils; class LogoutRequest extends Base implements RequestInterface { + /** + * @throws Exception + */ public function generateXml() { $id = $this->generateID(); @@ -31,7 +35,10 @@ public function generateXml() $this->xml = $xml; } - public function redirectUrl($redirectTo = null) : string + /** + * @throws Exception + */ + public function redirectUrl($redirectTo = null): string { $location = parent::getBindingLocation(Settings::BINDING_REDIRECT, 'SLO'); if (is_null($this->xml)) { @@ -40,13 +47,16 @@ public function redirectUrl($redirectTo = null) : string return parent::redirect($location, $redirectTo); } - public function httpPost($redirectTo = null) : string + /** + * @throws Exception + */ + public function httpPost($redirectTo = null): string { $location = parent::getBindingLocation(Settings::BINDING_POST, 'SLO'); if (is_null($this->xml)) { $this->generateXml(); } - + $this->xml = SignatureUtils::signXml($this->xml, $this->idp->sp->settings); return parent::postForm($location, $redirectTo); } diff --git a/src/Spid/Saml/Out/LogoutResponse.php b/src/Spid/Saml/Out/LogoutResponse.php index f4f852f..caa8661 100644 --- a/src/Spid/Saml/Out/LogoutResponse.php +++ b/src/Spid/Saml/Out/LogoutResponse.php @@ -2,14 +2,16 @@ namespace Italia\Spid\Spid\Saml\Out; +use Exception; use Italia\Spid\Spid\Interfaces\RequestInterface; use Italia\Spid\Spid\Saml\Settings; -use Italia\Spid\Spid\Saml\Idp; -use Italia\Spid\Spid\Saml\In\LogoutRequest; use Italia\Spid\Spid\Saml\SignatureUtils; class LogoutResponse extends Base implements RequestInterface { + /** + * @throws Exception + */ public function generateXml() { $id = $this->generateID(); @@ -31,7 +33,10 @@ public function generateXml() $this->xml = $xml; } - public function redirectUrl($redirectTo = null) : string + /** + * @throws Exception + */ + public function redirectUrl($redirectTo = null): string { $location = parent::getBindingLocation(Settings::BINDING_REDIRECT, 'SLO'); if (is_null($this->xml)) { @@ -40,7 +45,10 @@ public function redirectUrl($redirectTo = null) : string return parent::redirect($location, $redirectTo); } - public function httpPost($redirectTo = null) : string + /** + * @throws Exception + */ + public function httpPost($redirectTo = null): string { $location = parent::getBindingLocation(Settings::BINDING_POST, 'SLO'); if (is_null($this->xml)) { diff --git a/src/Spid/Saml/Settings.php b/src/Spid/Saml/Settings.php index 825aea2..9f4d545 100644 --- a/src/Spid/Saml/Settings.php +++ b/src/Spid/Saml/Settings.php @@ -2,13 +2,15 @@ namespace Italia\Spid\Spid\Saml; +use Exception; +use InvalidArgumentException; + class Settings { - const BINDING_REDIRECT = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect'; - const BINDING_POST = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST'; - - const REQUIRED = 1; - const NOT_REQUIRED = 0; + public const BINDING_REDIRECT = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect'; + public const BINDING_POST = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST'; + public const REQUIRED = 1; + public const NOT_REQUIRED = 0; // Settings with value 1 are mandatory private static $validSettings = [ 'sp_entityid' => self::REQUIRED, @@ -30,7 +32,18 @@ class Settings ] ], 'idp_metadata_folder' => self::REQUIRED, - 'accepted_clock_skew_seconds' => self::NOT_REQUIRED + 'accepted_clock_skew_seconds' => self::NOT_REQUIRED, + // aggregator + 'sp_contact_aggregator_company' => self::NOT_REQUIRED, + 'sp_contact_aggregator_person_type' => self::NOT_REQUIRED, + 'sp_contact_aggregator_person_vat_number' => self::NOT_REQUIRED, + 'sp_contact_aggregator_person_fiscal_code' => self::NOT_REQUIRED, + 'sp_contact_aggregator_person_email' => self::NOT_REQUIRED, + 'sp_contact_aggregator_person_phone' => self::NOT_REQUIRED, + // aggregate + 'sp_contact_aggregate_ipa_code' => self::NOT_REQUIRED, + 'sp_contact_aggregate_person_type' => self::NOT_REQUIRED, + 'sp_contact_aggregate_company' => self::NOT_REQUIRED ]; private static $validAttributeFields = [ @@ -53,9 +66,12 @@ class Settings "digitalAddress" ]; + /** + * @throws Exception + */ public static function validateSettings(array $settings) { - $missingSettings = array(); + $missingSettings = []; $msg = 'Missing settings fields: '; array_walk(self::$validSettings, function ($v, $k) use (&$missingSettings, &$settings) { $settingRequired = self::$validSettings[$k]; @@ -78,12 +94,12 @@ public static function validateSettings(array $settings) $msg .= $k . ', '; } if (count($missingSettings) > 0) { - throw new \Exception($msg); + throw new Exception($msg); } $invalidFields = array_diff_key($settings, self::$validSettings); // Check for settings that have child values - array_walk(self::$validSettings, function ($v, $k) use (&$invalidFields) { + array_walk(self::$validSettings, function ($v, $k) use (&$invalidFields, $settings) { // Child values found, check if settings array is set for that key if (is_array($v) && isset($settings[$k])) { // $v has at most 2 keys, self::REQUIRED and self::NOT_REQUIRED @@ -97,19 +113,22 @@ public static function validateSettings(array $settings) $msg .= $k . ', '; } if (count($invalidFields) > 0) { - throw new \Exception($msg); + throw new Exception($msg); } self::checkSettingsValues($settings); } - public static function cleanOpenSsl($file, $isCert = false) + /** + * @throws Exception + */ + public static function cleanOpenSsl($file, $isCert = false): string { if ($isCert) { $k = $file; } else { if (!is_readable($file)) { - throw new \Exception('File '.$file.' is not readable. Please check file permissions.'); + throw new Exception("File $file is not readable. Please check file permissions."); } $k = file_get_contents($file); } @@ -122,128 +141,139 @@ public static function cleanOpenSsl($file, $isCert = false) return $ck; } + /** + * @throws InvalidArgumentException + */ private static function checkSettingsValues($settings) { if (filter_var($settings['sp_entityid'], FILTER_VALIDATE_URL) === false) { - throw new \InvalidArgumentException('Invalid SP Entity ID provided'); + throw new InvalidArgumentException('Invalid SP Entity ID provided'); } // Save entity id host url for other checks $host = parse_url($settings['sp_entityid'], PHP_URL_HOST); if (!is_readable($settings['idp_metadata_folder'])) { - throw new \InvalidArgumentException('Idp metadata folder does not exist or is not readable.'); + throw new InvalidArgumentException('Idp metadata folder does not exist or is not readable.'); } if (isset($settings['sp_attributeconsumingservice'])) { if (!is_array($settings['sp_attributeconsumingservice'])) { - throw new \InvalidArgumentException('sp_attributeconsumingservice should be an array'); + throw new InvalidArgumentException('sp_attributeconsumingservice should be an array'); } array_walk($settings['sp_attributeconsumingservice'], function ($acs) { if (!is_array($acs)) { - throw new \InvalidArgumentException('sp_attributeconsumingservice elements should be an arrays'); + throw new InvalidArgumentException('sp_attributeconsumingservice elements should be an arrays'); } if (count($acs) == 0) { - throw new \InvalidArgumentException( + throw new InvalidArgumentException( 'sp_attributeconsumingservice elements should contain at least one element' ); } array_walk($acs, function ($field) { if (!in_array($field, self::$validAttributeFields)) { - throw new \InvalidArgumentException('Invalid Attribute field '. $field .' requested'); + throw new InvalidArgumentException("Invalid Attribute field $field requested"); } }); }); } if (!is_array($settings['sp_assertionconsumerservice'])) { - throw new \InvalidArgumentException('sp_assertionconsumerservice should be an array'); + throw new InvalidArgumentException('sp_assertionconsumerservice should be an array'); } if (count($settings['sp_assertionconsumerservice']) == 0) { - throw new \InvalidArgumentException('sp_assertionconsumerservice should contain at least one element'); + throw new InvalidArgumentException('sp_assertionconsumerservice should contain at least one element'); } array_walk($settings['sp_assertionconsumerservice'], function ($acs) use ($host) { if (strpos($acs, $host) === false) { - throw new \InvalidArgumentException( - 'sp_assertionconsumerservice elements Location domain should be ' . $host . ', got ' . + throw new InvalidArgumentException( + "sp_assertionconsumerservice elements Location domain should be $host, got " . parse_url($acs, PHP_URL_HOST) . ' instead' ); } }); if (!is_array($settings['sp_singlelogoutservice'])) { - throw new \InvalidArgumentException('sp_singlelogoutservice should be an array'); + throw new InvalidArgumentException('sp_singlelogoutservice should be an array'); } if (count($settings['sp_singlelogoutservice']) == 0) { - throw new \InvalidArgumentException('sp_singlelogoutservice should contain at least one element'); + throw new InvalidArgumentException('sp_singlelogoutservice should contain at least one element'); } array_walk($settings['sp_singlelogoutservice'], function ($slo) use ($host) { if (!is_array($slo)) { - throw new \InvalidArgumentException('sp_singlelogoutservice elements should be arrays'); + throw new InvalidArgumentException('sp_singlelogoutservice elements should be arrays'); } if (count($slo) != 2) { - throw new \InvalidArgumentException( - 'sp_singlelogoutservice array elements should contain exactly 2 elements, in order SLO Location ' . - 'and Binding' + throw new InvalidArgumentException( + 'sp_singlelogoutservice array elements should contain exactly 2 elements, ' . + 'in order SLO Location and Binding' ); } if (!is_string($slo[0]) || !is_string($slo[1])) { - throw new \InvalidArgumentException( - 'sp_singlelogoutservice array elements should contain 2 string values, in order SLO Location ' . - 'and Binding' + throw new InvalidArgumentException( + 'sp_singlelogoutservice array elements should contain 2 string values, ' . + 'in order SLO Location and Binding' ); } - if (strcasecmp($slo[1], "POST") != 0 && + if ( + strcasecmp($slo[1], "POST") != 0 && strcasecmp($slo[1], "REDIRECT") != 0 && - strcasecmp($slo[1], "") != 0) { - throw new \InvalidArgumentException('sp_singlelogoutservice elements Binding value should be one of '. - '"POST", "REDIRECT", or "" (empty string, defaults to POST)'); + strcasecmp($slo[1], "") != 0 + ) { + throw new InvalidArgumentException( + 'sp_singlelogoutservice elements Binding value should be one of "POST", "REDIRECT", ' . + 'or "" (empty string, defaults to POST)' + ); } if (strpos($slo[0], $host) === false) { - throw new \InvalidArgumentException( - 'sp_singlelogoutservice elements Location domain should be ' . $host . - ', got ' . parse_url($slo[0], PHP_URL_HOST) . 'instead' + throw new InvalidArgumentException( + "sp_singlelogoutservice elements Location domain should be $host, got " . + parse_url($slo[0], PHP_URL_HOST) . 'instead' ); } }); if (isset($settings['sp_key_cert_values'])) { if (!is_array($settings['sp_key_cert_values'])) { - throw new \Exception('sp_key_cert_values should be an array'); + throw new InvalidArgumentException('sp_key_cert_values should be an array'); } if (count($settings['sp_key_cert_values']) != 5) { - throw new \Exception( - 'sp_key_cert_values should contain 5 values: countryName, stateOrProvinceName, localityName, ' . - 'commonName, emailAddress' + throw new InvalidArgumentException( + 'sp_key_cert_values should contain 5 values: ' . + 'countryName, stateOrProvinceName, localityName, commonName, emailAddress' ); } foreach ($settings['sp_key_cert_values'] as $key => $value) { if (!is_string($value)) { - throw new \Exception( - 'sp_key_cert_values values should be strings. Valued provided for key ' . $key . - ' is not a string' + throw new InvalidArgumentException( + "sp_key_cert_values values should be strings. Valued provided for key $key is not a string" ); } } if (strlen($settings['sp_key_cert_values']['countryName']) != 2) { - throw new \Exception('sp_key_cert_values countryName should be a 2 characters country code'); + throw new InvalidArgumentException( + 'sp_key_cert_values countryName should be a 2 characters country code' + ); } } if (isset($settings['accepted_clock_skew_seconds'])) { if (!is_numeric($settings['accepted_clock_skew_seconds'])) { - throw new \InvalidArgumentException('accepted_clock_skew_seconds should be a number'); + throw new InvalidArgumentException('accepted_clock_skew_seconds should be a number'); } if ($settings['accepted_clock_skew_seconds'] < 0) { - throw new \InvalidArgumentException('accepted_clock_skew_seconds should be at least 0 seconds'); + throw new InvalidArgumentException('accepted_clock_skew_seconds should be at least 0 seconds'); } if ($settings['accepted_clock_skew_seconds'] > 300) { - throw new \InvalidArgumentException('accepted_clock_skew_seconds should be at most 300 seconds'); + throw new InvalidArgumentException('accepted_clock_skew_seconds should be at most 300 seconds'); } } if (isset($settings['sp_comparison'])) { - if (strcasecmp($settings['sp_comparison'], "exact") != 0 && + if ( + strcasecmp($settings['sp_comparison'], "exact") != 0 && strcasecmp($settings['sp_comparison'], "minimum") != 0 && strcasecmp($settings['sp_comparison'], "better") != 0 && - strcasecmp($settings['sp_comparison'], "maximum") != 0) { - throw new \InvalidArgumentException('sp_comparison value should be one of:' . - '"exact", "minimum", "better" or "maximum"'); + strcasecmp($settings['sp_comparison'], "maximum") != 0 + ) { + throw new InvalidArgumentException( + 'sp_comparison value should be one of: "exact", "minimum", "better" or "maximum"' + ); } } } diff --git a/src/Spid/Saml/SignatureUtils.php b/src/Spid/Saml/SignatureUtils.php index cec7a57..8654851 100644 --- a/src/Spid/Saml/SignatureUtils.php +++ b/src/Spid/Saml/SignatureUtils.php @@ -2,26 +2,33 @@ namespace Italia\Spid\Spid\Saml; +use DOMDocument; +use DOMXPath; +use Exception; +use Italia\Spid\Spid\Interfaces\LoggerSelector; +use RobRichards\XMLSecLibs\XMLSecEnc; use RobRichards\XMLSecLibs\XMLSecurityDSig; use RobRichards\XMLSecLibs\XMLSecurityKey; -use RobRichards\XMLSecLibs\XMLSecEnc; class SignatureUtils { - public static function signXml($xml, $settings) : string + /** + * @throws Exception + */ + public static function signXml($xml, $settings): string { if (!is_readable($settings['sp_key_file'])) { - throw new \Exception('Your SP key file is not readable. Please check file permissions.'); + throw new Exception('Your SP key file is not readable. Please check file permissions.'); } if (!is_readable($settings['sp_cert_file'])) { - throw new \Exception('Your SP certificate file is not readable. Please check file permissions.'); + throw new Exception('Your SP certificate file is not readable. Please check file permissions.'); } $key = file_get_contents($settings['sp_key_file']); $key = openssl_get_privatekey($key, ""); $cert = file_get_contents($settings['sp_cert_file']); - $dom = new \DOMDocument(); + $dom = new DOMDocument(); $dom->loadXML($xml); - + $objKey = new XMLSecurityKey('http://www.w3.org/2001/04/xmldsig-more#rsa-sha256', array('type' => 'private')); $objKey->loadKey($key, false); @@ -40,7 +47,7 @@ public static function signXml($xml, $settings) : string $insertBefore = $rootNode->firstChild; $messageTypes = array('AuthnRequest', 'Response', 'LogoutRequest', 'LogoutResponse'); if (in_array($rootNode->localName, $messageTypes)) { - $issuerNodes = self::query($dom, '/' . $rootNode->tagName . '/saml:Issuer'); + $issuerNodes = self::query($dom, '/' . $rootNode->localName . '/saml:Issuer'); if ($issuerNodes->length == 1) { $insertBefore = $issuerNodes->item(0)->nextSibling; } @@ -50,10 +57,13 @@ public static function signXml($xml, $settings) : string return $dom->saveXML(); } - public static function signUrl($samlRequest, $relayState, $signatureAlgo, $keyFile) + /** + * @throws Exception + */ + public static function signUrl($samlRequest, $relayState, $signatureAlgo, $keyFile): string { if (!is_readable($keyFile)) { - throw new \Exception('Your SP key file is not readable. Please check file permissions.'); + throw new Exception('Your SP key file is not readable. Please check file permissions.'); } $key = file_get_contents($keyFile); $key = openssl_get_privatekey($key, ""); @@ -68,7 +78,10 @@ public static function signUrl($samlRequest, $relayState, $signatureAlgo, $keyFi return base64_encode($signature); } - public static function validateXmlSignature($xml, $cert) : bool + /** + * @throws Exception + */ + public static function validateXmlSignature($xml, $cert, LoggerSelector $logger): bool { if (is_null($xml)) { return true; @@ -81,6 +94,19 @@ public static function validateXmlSignature($xml, $cert) : bool true ); if ($signCertFingerprint != $certFingerprint) { + $logger = $logger->getTemporaryLogger(); + if ($logger) { + $errorMessage = <<error($errorMessage, [ + 'signCertFingerprint' => var_export($signCertFingerprint, true), + 'certFingerprint' => var_export($certFingerprint, true) + ]); + } return false; } @@ -92,14 +118,10 @@ public static function validateXmlSignature($xml, $cert) : bool $objXMLSecDSig->canonicalizeSignedInfo(); - try { - $retVal = $objXMLSecDSig->validateReference(); - } catch (Exception $e) { - throw $e; - } + $objXMLSecDSig->validateReference(); XMLSecEnc::staticLocateKeyInfo($objKey, $objDSig); - + $objKey->loadKey($cert, false, true); if ($objXMLSecDSig->verify($objKey) === 1) { return true; @@ -107,12 +129,12 @@ public static function validateXmlSignature($xml, $cert) : bool return false; } - public static function certDNEquals($cert, $settings) + public static function certDNEquals($cert, $settings): bool { $parsed = openssl_x509_parse($cert); $dn = $parsed['subject']; - $newDN = array(); + $newDN = []; $newDN[] = $settings['sp_org_name'] ?? []; $newDN[] = $settings['sp_org_display_name'] ?? []; $newDN = array_merge($newDN, $settings['sp_key_cert_values'] ?? []); @@ -125,7 +147,7 @@ public static function certDNEquals($cert, $settings) return false; } - public static function generateKeyCert($settings) : array + public static function generateKeyCert($settings): array { $numberofdays = 3652 * 2; $privkey = openssl_pkey_new(array( @@ -136,7 +158,7 @@ public static function generateKeyCert($settings) : array "countryName" => $settings['sp_key_cert_values']['countryName'], "stateOrProvinceName" => $settings['sp_key_cert_values']['stateOrProvinceName'], "localityName" => $settings['sp_key_cert_values']['localityName'], - "organizationName" => $orgName = $settings['sp_org_name'], + "organizationName" => $settings['sp_org_name'], "organizationalUnitName" => $settings['sp_org_display_name'], "commonName" => $settings['sp_key_cert_values']['commonName'], "emailAddress" => $settings['sp_key_cert_values']['emailAddress'] @@ -145,17 +167,17 @@ public static function generateKeyCert($settings) : array $myserial = (int) hexdec(bin2hex(openssl_random_pseudo_bytes(8))); $configArgs = array("digest_alg" => "sha256"); $sscert = openssl_csr_sign($csr, null, $privkey, $numberofdays, $configArgs, $myserial); - openssl_x509_export($sscert, $publickey); - openssl_pkey_export($privkey, $privatekey); + openssl_x509_export($sscert, $publicKey); + openssl_pkey_export($privkey, $privateKey); return [ - 'key' => $privatekey, - 'cert' => $publickey + 'key' => $privateKey, + 'cert' => $publicKey ]; } - private static function query(\DOMDocument $dom, $query, \DOMElement $context = null) + private static function query(DOMDocument $dom, $query) { - $xpath = new \DOMXPath($dom); + $xpath = new DOMXPath($dom); $xpath->registerNamespace('samlp', 'urn:oasis:names:tc:SAML:2.0:protocol'); $xpath->registerNamespace('saml', 'urn:oasis:names:tc:SAML:2.0:assertion'); @@ -164,12 +186,6 @@ private static function query(\DOMDocument $dom, $query, \DOMElement $context = $xpath->registerNamespace('xsi', 'http://www.w3.org/2001/XMLSchema-instance'); $xpath->registerNamespace('xs', 'http://www.w3.org/2001/XMLSchema'); $xpath->registerNamespace('md', 'urn:oasis:names:tc:SAML:2.0:metadata'); - - if (isset($context)) { - $res = $xpath->query($query, $context); - } else { - $res = $xpath->query($query); - } - return $res; + return $xpath->query($query); } } diff --git a/src/Spid/Session.php b/src/Spid/Session.php index c87c0d7..cd069b6 100644 --- a/src/Spid/Session.php +++ b/src/Spid/Session.php @@ -22,9 +22,10 @@ public function __construct(array $values = null) } } - public function isValid() + public function isValid(): bool { - if (empty($this->sessionID) || + if ( + empty($this->sessionID) || empty($this->idp) || empty($this->idpEntityID) || empty($this->level) diff --git a/tests/IdpTest.php b/tests/IdpTest.php index 80bab35..65e6f2a 100644 --- a/tests/IdpTest.php +++ b/tests/IdpTest.php @@ -1,6 +1,9 @@ assertInstanceOf( + $sp = new Italia\Spid\Sp(new LoggerMock(), IdpTest::$settings); + $this->assertInstanceOf( Italia\Spid\Spid\Saml\Idp::class, new Italia\Spid\Spid\Saml\Idp($sp) ); @@ -48,7 +51,7 @@ public function testCanLoadFromValidXML() { $result = self::setupIdps(); - $sp = new Italia\Spid\Spid\Saml(IdpTest::$settings); + $sp = new Italia\Spid\Spid\Saml(new LoggerMock(), IdpTest::$settings); $idp = new Italia\Spid\Spid\Saml\Idp($sp); $loaded = $idp->loadFromXml(self::$idps[0]); $this->assertInstanceOf( @@ -66,20 +69,20 @@ public function testCanLoadFromValidXML() public function testCanLoadFromValidXMLFullPath() { - $sp = new Italia\Spid\Spid\Saml(IdpTest::$settings); + $sp = new Italia\Spid\Spid\Saml(new LoggerMock(), IdpTest::$settings); $idp = new Italia\Spid\Spid\Saml\Idp($sp); $loaded = $idp->loadFromXml(self::$idps[0]); $this->assertInstanceOf( Italia\Spid\Spid\Saml\Idp::class, $loaded ); - $this->assertNotEmpty($idp->idpFileName); - $this->assertNotEmpty($idp->metadata); + $this->assertNotEmpty($idp->idpFileName); + $this->assertNotEmpty($idp->metadata); } public function testLoadXMLWIthWrongFilePath() { - $sp = new Italia\Spid\Spid\Saml(IdpTest::$settings); + $sp = new Italia\Spid\Spid\Saml(new LoggerMock(), IdpTest::$settings); $idp = new Italia\Spid\Spid\Saml\Idp($sp); $sp->settings['idp_metadata_folder'] = '/wrong/path/to/metadata/'; diff --git a/tests/LoggerMock.php b/tests/LoggerMock.php new file mode 100644 index 0000000..1a477a5 --- /dev/null +++ b/tests/LoggerMock.php @@ -0,0 +1,23 @@ +assertInstanceOf( Italia\Spid\Sp::class, - new Italia\Spid\Sp(SpTest::$settings) + new Italia\Spid\Sp(new LoggerMock(), SpTest::$settings) ); $this->assertIsReadable(self::$settings['sp_key_file']); $this->assertIsReadable(self::$settings['sp_cert_file']); @@ -62,7 +65,7 @@ public function testCanBeCreatedWithoutAutoconfigure() $settings['sp_cert_file'] = './some/location/sp.crt'; $this->assertInstanceOf( Italia\Spid\Sp::class, - new Italia\Spid\Sp(SpTest::$settings, null, false) + new Italia\Spid\Sp(new LoggerMock(), SpTest::$settings, null, false) ); $this->assertFalse(is_readable($settings['sp_key_file'])); $this->assertFalse(is_readable($settings['sp_cert_file'])); @@ -77,7 +80,7 @@ private function validateXml($xmlString, $schemaFile, $valid = true) public function testMetatadaValid() { - $sp = new Italia\Spid\Sp(SpTest::$settings); + $sp = new Italia\Spid\Sp(new LoggerMock(), SpTest::$settings); $metadata = $sp->getSPMetadata(); $this->validateXml($metadata, "./tests/schemas/saml-schema-metadata-SPID-SP.xsd"); } @@ -87,7 +90,7 @@ public function testSettingsWithoutEntityId() $settings1 = SpTest::$settings; unset($settings1['sp_entityid']); $this->expectException(\Exception::class); - $sp = new Italia\Spid\Sp($settings1); + $sp = new Italia\Spid\Sp(new LoggerMock(), $settings1); } public function testSettingsWithoutSpKeyFile() @@ -95,7 +98,7 @@ public function testSettingsWithoutSpKeyFile() $settings1 = SpTest::$settings; unset($settings1['sp_key_file']); $this->expectException(\Exception::class); - $sp = new Italia\Spid\Sp($settings1); + $sp = new Italia\Spid\Sp(new LoggerMock(), $settings1); } public function testSettingsWithoutSpCertFile() @@ -103,7 +106,7 @@ public function testSettingsWithoutSpCertFile() $settings1 = SpTest::$settings; unset($settings1['sp_cert_file']); $this->expectException(\Exception::class); - $sp = new Italia\Spid\Sp($settings1); + $sp = new Italia\Spid\Sp(new LoggerMock(), $settings1); } public function testSettingsWithoutAssertionConsumerService() @@ -111,7 +114,7 @@ public function testSettingsWithoutAssertionConsumerService() $settings1 = SpTest::$settings; unset($settings1['sp_assertionconsumerservice']); $this->expectException(\Exception::class); - $sp = new Italia\Spid\Sp($settings1); + $sp = new Italia\Spid\Sp(new LoggerMock(), $settings1); } public function testSettingsWithoutSingleLogoutService() @@ -119,7 +122,7 @@ public function testSettingsWithoutSingleLogoutService() $settings1 = SpTest::$settings; unset($settings1['sp_singlelogoutservice']); $this->expectException(\Exception::class); - $sp = new Italia\Spid\Sp($settings1); + $sp = new Italia\Spid\Sp(new LoggerMock(), $settings1); } public function testSettingsWithoutIdpMetadataFolder() @@ -127,7 +130,7 @@ public function testSettingsWithoutIdpMetadataFolder() $settings1 = SpTest::$settings; unset($settings1['idp_metadata_folder']); $this->expectException(\Exception::class); - $sp = new Italia\Spid\Sp($settings1); + $sp = new Italia\Spid\Sp(new LoggerMock(), $settings1); } public function testSettingsWithInvalidSPEntityid() @@ -135,7 +138,7 @@ public function testSettingsWithInvalidSPEntityid() $this->expectException(InvalidArgumentException::class); $settings = self::$settings; $settings['sp_entityid'] = "htp:/simevo"; - new Italia\Spid\Sp($settings); + new Italia\Spid\Sp(new LoggerMock(), $settings); } public function testSettingsWithInvalidComparison() @@ -143,7 +146,7 @@ public function testSettingsWithInvalidComparison() $settings = self::$settings; $this->expectException(InvalidArgumentException::class); $settings['sp_comparison'] = "invalid"; - new Italia\Spid\Sp($settings); + new Italia\Spid\Sp(new LoggerMock(), $settings); } public function testSettingsWithInvalidSpACS() @@ -151,17 +154,17 @@ public function testSettingsWithInvalidSpACS() $settings = self::$settings; $this->expectException(InvalidArgumentException::class); $settings['sp_assertionconsumerservice'] = "not an array"; - new Italia\Spid\Sp($settings); + new Italia\Spid\Sp(new LoggerMock(), $settings); $this->expectException(InvalidArgumentException::class); $settings['sp_assertionconsumerservice'] = []; - new Italia\Spid\Sp($settings); + new Italia\Spid\Sp(new LoggerMock(), $settings); $this->expectException(InvalidArgumentException::class); $settings['sp_assertionconsumerservice'] = [ 'http://wrong.url.com/acs' ]; - new Italia\Spid\Sp($settings); + new Italia\Spid\Sp(new LoggerMock(), $settings); } public function testSettingsWithInvalidSpSLO() @@ -169,43 +172,43 @@ public function testSettingsWithInvalidSpSLO() $settings = self::$settings; $this->expectException(InvalidArgumentException::class); $settings['sp_singlelogoutservice'] = "not an array"; - new Italia\Spid\Sp($settings); + new Italia\Spid\Sp(new LoggerMock(), $settings); $this->expectException(InvalidArgumentException::class); $settings['sp_singlelogoutservice'] = [ 'not an array' ]; - new Italia\Spid\Sp($settings); + new Italia\Spid\Sp(new LoggerMock(), $settings); $this->expectException(InvalidArgumentException::class); $settings['sp_singlelogoutservice'] = [ [] ]; - new Italia\Spid\Sp($settings); + new Italia\Spid\Sp(new LoggerMock(), $settings); $this->expectException(InvalidArgumentException::class); $settings['sp_singlelogoutservice'] = [ ['too', 'many', 'elements'] ]; - new Italia\Spid\Sp($settings); + new Italia\Spid\Sp(new LoggerMock(), $settings); $this->expectException(InvalidArgumentException::class); $settings['sp_singlelogoutservice'] = [ ['both elements should be strings', 1] ]; - new Italia\Spid\Sp($settings); + new Italia\Spid\Sp(new LoggerMock(), $settings); $this->expectException(InvalidArgumentException::class); $settings['sp_singlelogoutservice'] = [ ['http://wrong.url.com', ''] ]; - new Italia\Spid\Sp($settings); + new Italia\Spid\Sp(new LoggerMock(), $settings); $this->expectException(InvalidArgumentException::class); $settings['sp_singlelogoutservice'] = [ ['http://sp3.simevo.com/slo', 'invalid binding'] ]; - new Italia\Spid\Sp($settings); + new Italia\Spid\Sp(new LoggerMock(), $settings); } public function testSettingsWithInvalidSpAttrCS() @@ -213,31 +216,31 @@ public function testSettingsWithInvalidSpAttrCS() $settings = self::$settings; $this->expectException(InvalidArgumentException::class); $settings['sp_attributeconsumingservice'] = "not an array"; - new Italia\Spid\Sp($settings); + new Italia\Spid\Sp(new LoggerMock(), $settings); $this->expectException(InvalidArgumentException::class); $settings['sp_attributeconsumingservice'] = [ 'not an array' ]; - new Italia\Spid\Sp($settings); + new Italia\Spid\Sp(new LoggerMock(), $settings); $this->expectException(InvalidArgumentException::class); $settings['sp_attributeconsumingservice'] = [ 'not an array' ]; - new Italia\Spid\Sp($settings); + new Italia\Spid\Sp(new LoggerMock(), $settings); $this->expectException(InvalidArgumentException::class); $settings['sp_attributeconsumingservice'] = [ [] ]; - new Italia\Spid\Sp($settings); + new Italia\Spid\Sp(new LoggerMock(), $settings); $this->expectException(InvalidArgumentException::class); $settings['sp_attributeconsumingservice'] = [ ['invalid name'] ]; - new Italia\Spid\Sp($settings); + new Italia\Spid\Sp(new LoggerMock(), $settings); } public function testSettingsWithInvalidKey() @@ -245,7 +248,7 @@ public function testSettingsWithInvalidKey() $settings = self::$settings; $this->expectException(InvalidArgumentException::class); $settings['sp_key_file'] = "/invalid/path/sp.key"; - new Italia\Spid\Sp($settings); + new Italia\Spid\Sp(new LoggerMock(), $settings); } public function testSettingsWithInvalidCert() @@ -253,7 +256,7 @@ public function testSettingsWithInvalidCert() $settings = self::$settings; $this->expectException(InvalidArgumentException::class); $settings['sp_cert_file'] = "/invalid/path/sp.cert"; - new Italia\Spid\Sp($settings); + new Italia\Spid\Sp(new LoggerMock(), $settings); } public function testSettingsWithInvalidIdpMetaFolder() @@ -261,7 +264,7 @@ public function testSettingsWithInvalidIdpMetaFolder() $settings = self::$settings; $this->expectException(InvalidArgumentException::class); $settings['idp_metadata_folder'] = "/invalid/path/idp_metadata"; - new Italia\Spid\Sp($settings); + new Italia\Spid\Sp(new LoggerMock(), $settings); } public function testSettingsWithCrapAcss() @@ -269,7 +272,7 @@ public function testSettingsWithCrapAcss() $settings = self::$settings; $this->expectException(InvalidArgumentException::class); $settings['accepted_clock_skew_seconds'] = 'zero'; - new Italia\Spid\Sp($settings); + new Italia\Spid\Sp(new LoggerMock(), $settings); } public function testSettingsWithNegativeAcss() @@ -277,7 +280,7 @@ public function testSettingsWithNegativeAcss() $settings = self::$settings; $this->expectException(InvalidArgumentException::class); $settings['accepted_clock_skew_seconds'] = -1; - new Italia\Spid\Sp($settings); + new Italia\Spid\Sp(new LoggerMock(), $settings); } public function testSettingsWithLudicrousAcss() @@ -285,25 +288,30 @@ public function testSettingsWithLudicrousAcss() $settings = self::$settings; $this->expectException(InvalidArgumentException::class); $settings['accepted_clock_skew_seconds'] = 3000; - new Italia\Spid\Sp($settings); + new Italia\Spid\Sp(new LoggerMock(),$settings); } public function testCanLoadAllIdpMetadata() { - $sp = new Italia\Spid\Sp(SpTest::$settings); + $sp = new Italia\Spid\Sp(new LoggerMock(),SpTest::$settings); $result = self::setupIdps(); foreach (self::$idps as $idp) { $retrievedIdp = $sp->loadIdpFromFile($idp); $this->assertEquals($retrievedIdp->idpFileName, $idp); $idpEntityId = $retrievedIdp->metadata['idpEntityId']; $host = parse_url($idpEntityId, PHP_URL_HOST); + $host = implode('.', array_slice(explode('.', $host), -2, 2)); $idpSSOArray = $retrievedIdp->metadata['idpSSO']; - foreach ($idpSSOArray as $key => $idpSSO) { - $this->assertStringContainsString($host, $idpSSO['location']); + foreach ($idpSSOArray as $idpSSO) { + $location = parse_url($idpSSO['location'], PHP_URL_HOST); + $location = implode('.', array_slice(explode('.', $location), -2, 2)); + $this->assertStringContainsString($host, $location); } $idpSLOArray = $retrievedIdp->metadata['idpSLO']; - foreach ($idpSLOArray as $key => $idpSLO) { - $this->assertStringContainsString($host, $idpSLO['location']); + foreach ($idpSLOArray as $idpSLO) { + $location = parse_url($idpSLO['location'], PHP_URL_HOST); + $location = implode('.', array_slice(explode('.', $location), -2, 2)); + $this->assertStringContainsString($host, $location); } } // If IDPs were downloaded for testing purposes, then delete them @@ -314,14 +322,14 @@ public function testCanLoadAllIdpMetadata() public function testIsAuthenticatedNoIDP() { - $sp = new Italia\Spid\Sp(SpTest::$settings); + $sp = new Italia\Spid\Sp(new LoggerMock(),SpTest::$settings); $this->assertFalse($sp->isAuthenticated()); } public function testIsAuthenticatedInvalidIDP() { unset($_SESSION); - $sp = new Italia\Spid\Sp(SpTest::$settings); + $sp = new Italia\Spid\Sp(new LoggerMock(),SpTest::$settings); $_SESSION['idpName'] = null; $this->assertFalse($sp->isAuthenticated()); } @@ -331,7 +339,7 @@ public function testIsAuthenticatedInvalidSession() unset($_SESSION); $result = self::setupIdps(); - $sp = new Italia\Spid\Sp(SpTest::$settings); + $sp = new Italia\Spid\Sp(new LoggerMock(),SpTest::$settings); $session = new Italia\Spid\Spid\Session(); $session->idp = self::$idps[0]; // IF these values are not set, the session is invalid @@ -350,7 +358,7 @@ public function testIsAuthenticatedInvalidSession() public function testIsAuthenticatedInvalidResponse() { unset($_SESSION); - $sp = new Italia\Spid\Sp(SpTest::$settings); + $sp = new Italia\Spid\Sp(new LoggerMock(),SpTest::$settings); $_POST['SAMLResponse'] = ""; $this->assertFalse($sp->isAuthenticated()); unset($_POST['SAMLResponse']); @@ -361,7 +369,7 @@ public function testIsAuthenticatedLogoutResponse() unset($_SESSION); $result = self::setupIdps(); - $sp = new Italia\Spid\Sp(SpTest::$settings); + $sp = new Italia\Spid\Sp(new LoggerMock(),SpTest::$settings); $_SESSION['idpName'] = self::$idps[0]; $_SESSION['inResponseTo'] = "PROVA"; $this->assertFalse($sp->isAuthenticated()); @@ -377,7 +385,7 @@ public function testIsAuthenticated() unset($_SESSION); $result = self::setupIdps(); - $sp = new Italia\Spid\Sp(SpTest::$settings); + $sp = new Italia\Spid\Sp(new LoggerMock(), SpTest::$settings); $session = new Italia\Spid\Spid\Session(); $session->idp = self::$idps[0]; $session->idpEntityID = 'https:/sp.example.com/'; @@ -395,7 +403,7 @@ public function testIsAuthenticated() public function testGetAttributesNoAuth() { unset($_SESSION); - $sp = new Italia\Spid\Sp(SpTest::$settings); + $sp = new Italia\Spid\Sp(new LoggerMock(), SpTest::$settings); $this->assertFalse($sp->isAuthenticated()); $this->assertEquals([], $sp->getAttributes()); } @@ -407,7 +415,7 @@ public function testGetAttributes() $result = self::setupIdps(); // Authenticate first - $sp = new Italia\Spid\Sp(SpTest::$settings); + $sp = new Italia\Spid\Sp(new LoggerMock(), SpTest::$settings); $session = new Italia\Spid\Spid\Session(); $session->idp = self::$idps[0]; $session->idpEntityID = 'https:/sp.example.com/'; @@ -417,7 +425,7 @@ public function testGetAttributes() $_SESSION['spidSession'] = (array)$session; $this->assertTrue($sp->isAuthenticated()); // Authentication completed, request attributes - $sp = new Italia\Spid\Sp(SpTest::$settings); + $sp = new Italia\Spid\Sp(new LoggerMock(), SpTest::$settings); $this->assertIsArray($sp->getAttributes()); $this->assertCount(0, $sp->getAttributes()); // No test with attributes requested @@ -439,7 +447,7 @@ public function testLoginInvalidACS() unset($_SESSION); $result = self::setupIdps(); - $sp = new Italia\Spid\Sp(SpTest::$settings); + $sp = new Italia\Spid\Sp(new LoggerMock(), SpTest::$settings); $this->expectException(\Exception::class); $sp->login(self::$idps[0], 12, 0); @@ -450,7 +458,7 @@ public function testLoginInvalidAttrCS() unset($_SESSION); $result = self::setupIdps(); - $sp = new Italia\Spid\Sp(SpTest::$settings); + $sp = new Italia\Spid\Sp(new LoggerMock(), SpTest::$settings); $this->expectException(\Exception::class); $sp->login(self::$idps[0], 0, 12); @@ -461,7 +469,7 @@ public function testLoginAlreadyAuthenticated() unset($_SESSION); $result = self::setupIdps(); - $sp = new Italia\Spid\Sp(SpTest::$settings); + $sp = new Italia\Spid\Sp(new LoggerMock(), SpTest::$settings); $session = new Italia\Spid\Spid\Session(); $session->idp = self::$idps[0]; $session->idpEntityID = 'https:/sp.example.com/';