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 @@
-
-
-[](https://developersitalia.slack.com/messages/CB6DCK274)
-[](https://slack.developers.italia.it/)
-[](https://forum.italia.it/c/spid)
-[](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/';