From 7d98fbd0412ee7333490d02a20e979c0841acfb0 Mon Sep 17 00:00:00 2001 From: Tim van Dijen Date: Sun, 24 Mar 2024 23:32:12 +0100 Subject: [PATCH 01/45] Migrate module to use saml11 + ws-security libraries --- src/Controller/Adfs.php | 9 +- src/IdP/ADFS.php | 169 +++++++++--------- src/SAML2/XML/fed/Endpoint.php | 37 ---- .../XML/fed/SecurityTokenServiceType.php | 93 ---------- src/SAML2/XML/fed/TokenTypesOffered.php | 34 ---- 5 files changed, 87 insertions(+), 255 deletions(-) delete mode 100644 src/SAML2/XML/fed/Endpoint.php delete mode 100644 src/SAML2/XML/fed/SecurityTokenServiceType.php delete mode 100644 src/SAML2/XML/fed/TokenTypesOffered.php diff --git a/src/Controller/Adfs.php b/src/Controller/Adfs.php index 8755fc6..117fd89 100644 --- a/src/Controller/Adfs.php +++ b/src/Controller/Adfs.php @@ -13,7 +13,7 @@ use SimpleSAML\Metadata; use SimpleSAML\Module; use SimpleSAML\Module\adfs\IdP\ADFS as ADFS_IDP; -use SimpleSAML\SAML2\Constants; +use SimpleSAML\SAML2\Constants as C; use SimpleSAML\Session; use SimpleSAML\Utils; use Symfony\Component\HttpFoundation\Request; @@ -200,12 +200,7 @@ public function metadata(Request $request): Response $response = new Response(); $response->setEtag(hash('sha256', $metaxml)); - $response->setCache([ - 'no_cache' => $protectedMetadata === true, - 'public' => $protectedMetadata === false, - 'private' => $protectedMetadata === true, - ]); - + $response->setPublic(); if ($response->isNotModified($request)) { return $response; } diff --git a/src/IdP/ADFS.php b/src/IdP/ADFS.php index aaa6b39..7b84eff 100644 --- a/src/IdP/ADFS.php +++ b/src/IdP/ADFS.php @@ -4,17 +4,40 @@ namespace SimpleSAML\Module\adfs\IdP; +use DateInterval; +use DateTimeImmutable; +use DateTimeZone; use Exception; -use RobRichards\XMLSecLibs\XMLSecurityDSig; -use RobRichards\XMLSecLibs\XMLSecurityKey; +use SimpleSAML\Assert\Assert; use SimpleSAML\Configuration; use SimpleSAML\Error; use SimpleSAML\IdP; use SimpleSAML\Logger; use SimpleSAML\Metadata\MetaDataStorageHandler; use SimpleSAML\Module; +use SimpleSAML\SAML11\Attribute; +use SimpleSAML\SAML11\AttributeStatement; +use SimpleSAML\SAML11\AttributeValue; +use SimpleSAML\SAML11\Audience; +use SimpleSAML\SAML11\AudienceRestrictionCondition; +use SimpleSAML\SAML11\AuthenticationStatement; +use SimpleSAML\SAML11\Conditions; +use SimpleSAML\SAML11\NameIdentifier; +use SimpleSAML\SAML11\Subject; use SimpleSAML\SAML2\Constants; use SimpleSAML\Utils; +use SimpleSAML\XMLSecurity\Constants as C; +use SimpleSAML\XMLSecurity\Alg\Signature\SignatureAlgorithmFactory; +use SimpleSAML\XMLSecurity\Key\PrivateKey; +use SimpleSAML\XMLSecurity\Key\X509Certificate as PublicKey; +use SimpleSAML\XMLSecurity\XML\ds\KeyInfo; +use SimpleSAML\XMLSecurity\XML\ds\X509Certificate; +use SimpleSAML\XMLSecurity\XML\ds\X509Data; +use SimpleSAML\WSSecurity\XML\wsa\Address; +use SimpleSAML\WSSecurity\XML\wsa\EndpointReference; +use SimpleSAML\WSSecurity\XML\wsp\AppliesTo; +use SimpleSAML\WSSecurity\XML\wst\RequestSecurityTokenResponse; +use SimpleSAML\WSSecurity\XML\wst\RequestSecurityToken; use SimpleSAML\XHTML\Template; use SimpleSAML\XML\DOMDocumentFactory; use Symfony\Component\HttpFoundation\Request; @@ -66,9 +89,9 @@ function () use ($idp, &$state) { * @param string $nameid * @param array $attributes * @param int $assertionLifetime - * @return string + * @return \SimpleSAML\SAML11\XML\saml\Assertion */ - private static function generateResponse( + private static function generateAssertion( string $issuer, string $target, string $nameid, @@ -80,11 +103,12 @@ private static function generateResponse( $timeUtils = new Utils\Time(); $issueInstant = $timeUtils->generateTimestamp(); - $notBefore = $timeUtils->generateTimestamp(time() - 30); - $assertionExpire = $timeUtils->generateTimestamp(time() + $assertionLifetime); + $notBefore = new DateInterval('PT30S'); + $notOnOrAfter = new DateInterval($assertionLifetime); $assertionID = $randomUtils->generateID(); $nameidFormat = 'http://schemas.xmlsoap.org/claims/UPN'; $nameid = htmlspecialchars($nameid); + $now = new DateTimeImmutable('now', new DateTimeZone('Z')); if ($httpUtils->isHTTPS()) { $method = Constants::AC_PASSWORD_PROTECTED_TRANSPORT; @@ -92,26 +116,22 @@ private static function generateResponse( $method = Constants::AC_PASSWORD; } - $result = << - - - - - $target - - - - - $nameid - - - - - $nameid - -MSG; + $audience = new Audience($target); + $audienceRestrictionCondition = new AudienceRestrictionCondition([$audience]); + $conditions = new Conditions( + $audience, + [], + [], + $now->sub($notBefore), + $now->add($assertionLifetime), + ); + + $nameIdentifier = new NameIdentifier($nameid, null, $nameidFormat); + $subject = new Subject(null, $nameIdentifier); + + $authenticationStatement = new AuthenticationStatement($subject, $method, $now); + $attrs = []; $attrUtils = new Utils\Attributes(); foreach ($attributes as $name => $values) { if ((!is_array($values)) || (count($values) == 0)) { @@ -122,86 +142,61 @@ private static function generateResponse( $name, 'http://schemas.xmlsoap.org/claims', ); + $namespace = htmlspecialchars($namespace); $name = htmlspecialchars($name); + $attrValue = []; foreach ($values as $value) { if ((!isset($value)) || ($value === '')) { continue; } - $value = htmlspecialchars($value); - - $result .= << - $value - -MSG; + $attrValue[] = new AttributeValue($value); } + $attrs[] = new Attribute($name, $namespace, $attrValue); } - - $result .= << - - - - - $target - - - -MSG; - - return $result; + $attributeStatement = new AttributeStatement($subject, $attributes); + + return new Assertion( + $assertionID, + $issuer, + $now, + $conditions, + null, // Advice + [$authenticationStatement, $attributeStatement], + ); } /** - * @param string $response + * @param \SimpleSAML\SAML11\XML\saml\Assertion $assertion * @param string $key * @param string $cert * @param string $algo * @param string|null $passphrase - * @return string + * @return \SimpleSAML\SAML11\XML\saml\Assertion */ - private static function signResponse( - string $response, + private static function signAssertion( + Assertion $assertion, string $key, string $cert, string $algo, #[\SensitiveParameter] string $passphrase = null, ): string { - $objXMLSecDSig = new XMLSecurityDSig(); - $objXMLSecDSig->idKeys = ['AssertionID']; - $objXMLSecDSig->setCanonicalMethod(XMLSecurityDSig::EXC_C14N); - $responsedom = DOMDocumentFactory::fromString(str_replace("\r", "", $response)); - $firstassertionroot = $responsedom->getElementsByTagName('Assertion')->item(0); - - if (is_null($firstassertionroot)) { - throw new Exception("No assertion found in response."); - } + $key = PrivateKey::fromFile($key, $passphrase); + $pubkey = PublicKey::fromFile($cert); + $keyInfo = new KeyInfo([ + new X509Data( + [new X509Certificate($pubkey->getPEM()->getData())], + ), + ]); - $objXMLSecDSig->addReferenceList( - [$firstassertionroot], - XMLSecurityDSig::SHA256, - ['http://www.w3.org/2000/09/xmldsig#enveloped-signature', XMLSecurityDSig::EXC_C14N], - ['id_name' => 'AssertionID'], + $signer = (new SignatureAlgorithmFactory())->getAlgorithm( + $algo, + $key, ); - $objKey = new XMLSecurityKey($algo, ['type' => 'private']); - if (is_string($passphrase)) { - $objKey->passphrase = $passphrase; - } - $objKey->loadKey($key, true); - $objXMLSecDSig->sign($objKey); - if ($cert) { - $public_cert = file_get_contents($cert); - $objXMLSecDSig->add509Cert($public_cert, true); - } - - /** @var \DOMElement $objXMLSecDSig->sigNode */ - $newSig = $responsedom->importNode($objXMLSecDSig->sigNode, true); - $firstassertionroot->appendChild($newSig); - return $responsedom->saveXML(); + return $assertion->sign($signer, C::C14N_EXCLUSIVE_WITHOUT_COMMENTS, $keyInfo); } @@ -387,12 +382,13 @@ public static function sendResponse(array $state): void 'adfs:entityID' => $spEntityId, ]); - $assertionLifetime = $spMetadata->getOptionalInteger('assertion.lifetime', null); + $assertionLifetime = $spMetadata->getOptionalString('assertion.lifetime', null); if ($assertionLifetime === null) { - $assertionLifetime = $idpMetadata->getOptionalInteger('assertion.lifetime', 300); + $assertionLifetime = $idpMetadata->getOptionalString('assertion.lifetime', 'PT300S'); } + Assert::nullOrValidDuration($assertionLifetime); - $response = ADFS::generateResponse($idpEntityId, $spEntityId, $nameid, $attributes, $assertionLifetime); + $assertion = ADFS::generateAssertion($idpEntityId, $spEntityId, $nameid, $attributes, $assertionLifetime); $configUtils = new Utils\Config(); $privateKeyFile = $configUtils->getCertPath($idpMetadata->getString('privatekey')); @@ -401,10 +397,15 @@ public static function sendResponse(array $state): void $algo = $spMetadata->getOptionalString('signature.algorithm', null); if ($algo === null) { - $algo = $idpMetadata->getOptionalString('signature.algorithm', XMLSecurityKey::RSA_SHA256); + $algo = $idpMetadata->getOptionalString('signature.algorithm', C::SIG_RSA_SHA256); } - $wresult = ADFS::signResponse($response, $privateKeyFile, $certificateFile, $algo, $passphrase); + $assertion = ADFS::signAssertion($assertion, $privateKeyFile, $certificateFile, $algo, $passphrase); + + $requestSecurityToken = new RequestSecurityToken(null, [$assertion]); + $appliesTo = new AppliesTo([new EndpointReference(new Address($target))]); + $requestSecurityTokenResponse = RequestSecurityTokenResponse(null, [$requestSecurityToken, $appliesTo]); + $wresult = $requestSecurityTokenResponse->saveXML();; $wctx = $state['adfs:wctx']; $wreply = $state['adfs:wreply'] ? : $spMetadata->getValue('prp'); ADFS::postResponse($wreply, $wresult, $wctx); diff --git a/src/SAML2/XML/fed/Endpoint.php b/src/SAML2/XML/fed/Endpoint.php deleted file mode 100644 index 6a65cab..0000000 --- a/src/SAML2/XML/fed/Endpoint.php +++ /dev/null @@ -1,37 +0,0 @@ -ownerDocument->createElement($name); - $parent->appendChild($e); - - $endpoint = new EndpointReference(new Address($address)); - $endpoint->toXML($parent); - - return $e; - } -} diff --git a/src/SAML2/XML/fed/SecurityTokenServiceType.php b/src/SAML2/XML/fed/SecurityTokenServiceType.php deleted file mode 100644 index 23da53e..0000000 --- a/src/SAML2/XML/fed/SecurityTokenServiceType.php +++ /dev/null @@ -1,93 +0,0 @@ -protocolSupportEnumeration); - - if ($xml === null) { - return; - } - } - - /** - * Convert this SecurityTokenServiceType RoleDescriptor to XML. - * - * @param \DOMElement $parent The element we should add this contact to. - * @return \DOMElement The new ContactPerson-element. - */ - public function toXML(DOMElement $parent): DOMElement - { - Assert::notEmpty($this->Location, 'Location not set'); - - $e = parent::toXML($parent); - $e->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:fed', C::NS_FED); - $e->setAttributeNS(C::NS_XSI, 'xsi:type', 'fed:SecurityTokenServiceType'); - TokenTypesOffered::appendXML($e); - Endpoint::appendXML($e, 'SecurityTokenServiceEndpoint', $this->Location); - Endpoint::appendXML($e, 'fed:PassiveRequestorEndpoint', $this->Location); - - return $e; - } - - - /** - * Get the location of this service. - * - * @return string The full URL where this service can be reached. - */ - public function getLocation(): string - { - Assert::notEmpty($this->Location, 'Location not set'); - - return $this->Location; - } - - - /** - * Set the location of this service. - * - * @param string $location The full URL where this service can be reached. - */ - public function setLocation(string $location): void - { - $this->Location = $location; - } -} diff --git a/src/SAML2/XML/fed/TokenTypesOffered.php b/src/SAML2/XML/fed/TokenTypesOffered.php deleted file mode 100644 index b5bca25..0000000 --- a/src/SAML2/XML/fed/TokenTypesOffered.php +++ /dev/null @@ -1,34 +0,0 @@ -ownerDocument->createElementNS(C::NS_FED, 'fed:TokenTypesOffered'); - $parent->appendChild($e); - - $tokentype = $parent->ownerDocument->createElementNS(C::NS_FED, 'fed:TokenType'); - $tokentype->setAttribute('Uri', 'urn:oasis:names:tc:SAML:1.0:assertion'); - $e->appendChild($tokentype); - - return $e; - } -} From f018c8e26e71c9022f6fca07c986ff4ae8245cc6 Mon Sep 17 00:00:00 2001 From: Tim van Dijen Date: Thu, 28 Mar 2024 21:15:54 +0100 Subject: [PATCH 02/45] Drop dependency on robrichards/xmlseclibs --- composer.json | 1 - 1 file changed, 1 deletion(-) diff --git a/composer.json b/composer.json index 000f5f6..5998e3e 100644 --- a/composer.json +++ b/composer.json @@ -36,7 +36,6 @@ "php": "^8.1", "ext-dom": "*", - "robrichards/xmlseclibs": "^3.1", "simplesamlphp/assert": "^1.0", "simplesamlphp/saml11": "^1.0", "simplesamlphp/simplesamlphp": "^2.0", From 9b7e1384d0ef79582cca1167d1754135e195095c Mon Sep 17 00:00:00 2001 From: monkeyiq Date: Mon, 29 Apr 2024 17:25:16 +1000 Subject: [PATCH 03/45] allow hosted metadata again. This lets me see it in the admin ui (#18) --- src/Controller/Adfs.php | 3 -- src/IdP/ADFS.php | 81 +++++++++++++++++++++++++++++------------ 2 files changed, 58 insertions(+), 26 deletions(-) diff --git a/src/Controller/Adfs.php b/src/Controller/Adfs.php index 117fd89..8263776 100644 --- a/src/Controller/Adfs.php +++ b/src/Controller/Adfs.php @@ -5,15 +5,12 @@ namespace SimpleSAML\Module\adfs\Controller; use Exception; -use SimpleSAML\Assert\Assert; use SimpleSAML\Configuration; use SimpleSAML\Error as SspError; use SimpleSAML\IdP; use SimpleSAML\Logger; use SimpleSAML\Metadata; -use SimpleSAML\Module; use SimpleSAML\Module\adfs\IdP\ADFS as ADFS_IDP; -use SimpleSAML\SAML2\Constants as C; use SimpleSAML\Session; use SimpleSAML\Utils; use Symfony\Component\HttpFoundation\Request; diff --git a/src/IdP/ADFS.php b/src/IdP/ADFS.php index 7b84eff..8c35edc 100644 --- a/src/IdP/ADFS.php +++ b/src/IdP/ADFS.php @@ -25,21 +25,20 @@ use SimpleSAML\SAML11\NameIdentifier; use SimpleSAML\SAML11\Subject; use SimpleSAML\SAML2\Constants; +use SimpleSAML\SAML2\Constants as C; use SimpleSAML\Utils; -use SimpleSAML\XMLSecurity\Constants as C; +use SimpleSAML\WSSecurity\XML\wsa\Address; +use SimpleSAML\WSSecurity\XML\wsa\EndpointReference; +use SimpleSAML\WSSecurity\XML\wsp\AppliesTo; +use SimpleSAML\WSSecurity\XML\wst\RequestSecurityToken; +use SimpleSAML\WSSecurity\XML\wst\RequestSecurityTokenResponse; +use SimpleSAML\XHTML\Template; use SimpleSAML\XMLSecurity\Alg\Signature\SignatureAlgorithmFactory; use SimpleSAML\XMLSecurity\Key\PrivateKey; use SimpleSAML\XMLSecurity\Key\X509Certificate as PublicKey; use SimpleSAML\XMLSecurity\XML\ds\KeyInfo; use SimpleSAML\XMLSecurity\XML\ds\X509Certificate; use SimpleSAML\XMLSecurity\XML\ds\X509Data; -use SimpleSAML\WSSecurity\XML\wsa\Address; -use SimpleSAML\WSSecurity\XML\wsa\EndpointReference; -use SimpleSAML\WSSecurity\XML\wsp\AppliesTo; -use SimpleSAML\WSSecurity\XML\wst\RequestSecurityTokenResponse; -use SimpleSAML\WSSecurity\XML\wst\RequestSecurityToken; -use SimpleSAML\XHTML\Template; -use SimpleSAML\XML\DOMDocumentFactory; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\StreamedResponse; @@ -224,30 +223,66 @@ private static function postResponse(string $wreply, string $wresult, ?string $w * @param string $entityid The entity ID of the hosted ADFS IdP whose metadata we want to fetch. * * @return array + * @param MetaDataStorageHandler $handler Optionally the metadata storage to use, + * if omitted the configured handler will be used. * @throws \SimpleSAML\Error\Exception * @throws \SimpleSAML\Error\MetadataNotFound */ - public static function getHostedMetadata(string $entityid): array + public static function getHostedMetadata(string $entityid, MetaDataStorageHandler $handler = null): array { - $handler = MetaDataStorageHandler::getMetadataHandler(); $cryptoUtils = new Utils\Crypto(); + + $globalConfig = Configuration::getInstance(); + if ($handler === null) { + $handler = MetaDataStorageHandler::getMetadataHandler($globalConfig); + } $config = $handler->getMetaDataConfig($entityid, 'adfs-idp-hosted'); - $endpoint = Module::getModuleURL('adfs/idp/prp.php'); + $host = Module::getModuleURL('adfs/idp/prp.php'); + + // configure endpoints + $ssob = $handler->getGenerated('SingleSignOnServiceBinding', 'adfs-idp-hosted', $host); + $slob = $handler->getGenerated('SingleLogoutServiceBinding', 'adfs-idp-hosted', $host); + $ssol = $handler->getGenerated('SingleSignOnService', 'adfs-idp-hosted', $host); + $slol = $handler->getGenerated('SingleLogoutService', 'adfs-idp-hosted', $host); + + $sso = []; + if (is_array($ssob)) { + foreach ($ssob as $binding) { + $sso[] = [ + 'Binding' => $binding, + 'Location' => $ssol, + ]; + } + } else { + $sso[] = [ + 'Binding' => $ssob, + 'Location' => $ssol, + ]; + } + + $slo = []; + if (is_array($slob)) { + foreach ($slob as $binding) { + $slo[] = [ + 'Binding' => $binding, + 'Location' => $slol, + ]; + } + } else { + $slo[] = [ + 'Binding' => $slob, + 'Location' => $slol, + ]; + } + + $metadata = [ 'metadata-set' => 'adfs-idp-hosted', 'entityid' => $entityid, - 'SingleSignOnService' => [ - [ - 'Binding' => Constants::BINDING_HTTP_REDIRECT, - 'Location' => $endpoint, - ], - ], - 'SingleLogoutService' => [ - 'Binding' => Constants::BINDING_HTTP_REDIRECT, - 'Location' => $endpoint, - ], - 'NameIDFormat' => $config->getOptionalString('NameIDFormat', Constants::NAMEID_TRANSIENT), + 'SingleSignOnService' => $sso, + 'SingleLogoutService' => $slo, + 'NameIDFormat' => $config->getOptionalArrayizeString('NameIDFormat', [C::NAMEID_TRANSIENT]), 'contacts' => [], ]; @@ -405,7 +440,7 @@ public static function sendResponse(array $state): void $appliesTo = new AppliesTo([new EndpointReference(new Address($target))]); $requestSecurityTokenResponse = RequestSecurityTokenResponse(null, [$requestSecurityToken, $appliesTo]); - $wresult = $requestSecurityTokenResponse->saveXML();; + $wresult = $requestSecurityTokenResponse->saveXML(); $wctx = $state['adfs:wctx']; $wreply = $state['adfs:wreply'] ? : $spMetadata->getValue('prp'); ADFS::postResponse($wreply, $wresult, $wctx); From ec55f61128917f9a0b6a51c2c15bfa89343fb65c Mon Sep 17 00:00:00 2001 From: Tim van Dijen Date: Sat, 30 Mar 2024 17:07:12 +0100 Subject: [PATCH 04/45] Depend on feature-branch of SSP --- composer.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/composer.json b/composer.json index 5998e3e..2f2c2ca 100644 --- a/composer.json +++ b/composer.json @@ -36,16 +36,16 @@ "php": "^8.1", "ext-dom": "*", - "simplesamlphp/assert": "^1.0", + "simplesamlphp/assert": "^1.1", "simplesamlphp/saml11": "^1.0", - "simplesamlphp/simplesamlphp": "^2.0", - "simplesamlphp/xml-common": "^1.12", + "simplesamlphp/simplesamlphp": "dev-saml2v5_metadata", + "simplesamlphp/xml-common": "^1.16", "simplesamlphp/ws-security": "^1.0", "symfony/http-foundation": "^6.4" }, "require-dev": { - "simplesamlphp/simplesamlphp-test-framework": "^1.5", - "simplesamlphp/xml-security": "^1.6" + "simplesamlphp/simplesamlphp-test-framework": "^1.6", + "simplesamlphp/xml-security": "^1.7" }, "support": { "issues": "https://github.com/simplesamlphp/simplesamlphp-module-adfs/issues", From 76bf046fa08caea442705ddee3ea84307fd56a8b Mon Sep 17 00:00:00 2001 From: Tim van Dijen Date: Mon, 29 Apr 2024 20:56:44 +0200 Subject: [PATCH 05/45] Fix coding style --- src/IdP/ADFS.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/IdP/ADFS.php b/src/IdP/ADFS.php index 8c35edc..f093ddf 100644 --- a/src/IdP/ADFS.php +++ b/src/IdP/ADFS.php @@ -221,10 +221,10 @@ private static function postResponse(string $wreply, string $wresult, ?string $w * Get the metadata of a given hosted ADFS IdP. * * @param string $entityid The entity ID of the hosted ADFS IdP whose metadata we want to fetch. - * - * @return array - * @param MetaDataStorageHandler $handler Optionally the metadata storage to use, + * @param \SimpleSAML\Metadata\MetaDataStorageHandler $handler Optionally the metadata storage to use, * if omitted the configured handler will be used. + * @return array + * * @throws \SimpleSAML\Error\Exception * @throws \SimpleSAML\Error\MetadataNotFound */ From 6a20079e93e168b812088f6bb1396cd6e4b65716 Mon Sep 17 00:00:00 2001 From: monkeyiq Date: Wed, 8 May 2024 18:34:48 +1000 Subject: [PATCH 06/45] A hook to generate metadata (#20) * A hook to generate metadata * Fix coding style & strict comparison --------- Co-authored-by: Tim van Dijen --- hooks/hook_generate_metadata.php | 33 ++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 hooks/hook_generate_metadata.php diff --git a/hooks/hook_generate_metadata.php b/hooks/hook_generate_metadata.php new file mode 100644 index 0000000..f2e9d0c --- /dev/null +++ b/hooks/hook_generate_metadata.php @@ -0,0 +1,33 @@ + Date: Sat, 31 Aug 2024 20:41:37 +0200 Subject: [PATCH 07/45] Fix constant --- src/IdP/ADFS.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/IdP/ADFS.php b/src/IdP/ADFS.php index f093ddf..cf47de0 100644 --- a/src/IdP/ADFS.php +++ b/src/IdP/ADFS.php @@ -24,7 +24,6 @@ use SimpleSAML\SAML11\Conditions; use SimpleSAML\SAML11\NameIdentifier; use SimpleSAML\SAML11\Subject; -use SimpleSAML\SAML2\Constants; use SimpleSAML\SAML2\Constants as C; use SimpleSAML\Utils; use SimpleSAML\WSSecurity\XML\wsa\Address; @@ -110,9 +109,9 @@ private static function generateAssertion( $now = new DateTimeImmutable('now', new DateTimeZone('Z')); if ($httpUtils->isHTTPS()) { - $method = Constants::AC_PASSWORD_PROTECTED_TRANSPORT; + $method = C::AC_PASSWORD_PROTECTED_TRANSPORT; } else { - $method = Constants::AC_PASSWORD; + $method = C::AC_PASSWORD; } $audience = new Audience($target); From 1b52f19ae612c5719d8438356b9baa1ba8b95acd Mon Sep 17 00:00:00 2001 From: Tim van Dijen Date: Sat, 31 Aug 2024 20:50:26 +0200 Subject: [PATCH 08/45] Fix unit test --- tests/src/Controller/AdfsControllerTest.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/src/Controller/AdfsControllerTest.php b/tests/src/Controller/AdfsControllerTest.php index f4e788e..e6c4cce 100644 --- a/tests/src/Controller/AdfsControllerTest.php +++ b/tests/src/Controller/AdfsControllerTest.php @@ -8,10 +8,9 @@ use PHPUnit\Framework\TestCase; use SimpleSAML\Configuration; use SimpleSAML\Error; -use SimpleSAML\HTTP\RunnableResponse; use SimpleSAML\Module\adfs\Controller; use SimpleSAML\Session; -use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\{Request, StreamedResponse}; use function dirname; @@ -108,6 +107,6 @@ public function testValidRequest(): void $response = $c->prp($request); $this->assertTrue($response->isSuccessful()); - $this->assertInstanceOf(RunnableResponse::class, $response); + $this->assertInstanceOf(StreamedResponse::class, $response); } } From b10eb1cd946cf44cffb3b92af1bd6b89a0d054c2 Mon Sep 17 00:00:00 2001 From: Tim van Dijen Date: Sat, 31 Aug 2024 21:07:08 +0200 Subject: [PATCH 09/45] Back to SSP release-branch --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 2f2c2ca..5205e33 100644 --- a/composer.json +++ b/composer.json @@ -38,7 +38,7 @@ "simplesamlphp/assert": "^1.1", "simplesamlphp/saml11": "^1.0", - "simplesamlphp/simplesamlphp": "dev-saml2v5_metadata", + "simplesamlphp/simplesamlphp": "^2.3", "simplesamlphp/xml-common": "^1.16", "simplesamlphp/ws-security": "^1.0", "symfony/http-foundation": "^6.4" From 8f5cd028afa1a5eec5208eaf89ce12f4cd85b4a6 Mon Sep 17 00:00:00 2001 From: Tim van Dijen Date: Sun, 1 Sep 2024 00:57:24 +0200 Subject: [PATCH 10/45] Fix constants --- src/Controller/Adfs.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Controller/Adfs.php b/src/Controller/Adfs.php index 8263776..bb82c08 100644 --- a/src/Controller/Adfs.php +++ b/src/Controller/Adfs.php @@ -10,7 +10,9 @@ use SimpleSAML\IdP; use SimpleSAML\Logger; use SimpleSAML\Metadata; +use SimpleSAML\Module; use SimpleSAML\Module\adfs\IdP\ADFS as ADFS_IDP; +use SimpleSAML\SAML2\Constants as C; use SimpleSAML\Session; use SimpleSAML\Utils; use Symfony\Component\HttpFoundation\Request; @@ -123,13 +125,13 @@ public function metadata(Request $request): Response 'entityid' => $idpentityid, 'SingleSignOnService' => [ 0 => [ - 'Binding' => Constants::BINDING_HTTP_REDIRECT, + 'Binding' => C::BINDING_HTTP_REDIRECT, 'Location' => $adfs_service_location, ], ], 'SingleLogoutService' => [ 0 => [ - 'Binding' => Constants::BINDING_HTTP_REDIRECT, + 'Binding' => C::BINDING_HTTP_REDIRECT, 'Location' => $adfs_service_location, ], ], @@ -143,7 +145,7 @@ public function metadata(Request $request): Response $metaArray['NameIDFormat'] = $idpmeta->getOptionalString( 'NameIDFormat', - Constants::NAMEID_TRANSIENT, + C::NAMEID_TRANSIENT, ); if ($idpmeta->hasValue('OrganizationName')) { From 9cdf489b44e7594731d6bbd71107b23eeefd1f35 Mon Sep 17 00:00:00 2001 From: Tim van Dijen Date: Sun, 1 Sep 2024 11:38:11 +0200 Subject: [PATCH 11/45] Fix pre-filled username --- src/IdP/ADFS.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/IdP/ADFS.php b/src/IdP/ADFS.php index cf47de0..d476344 100644 --- a/src/IdP/ADFS.php +++ b/src/IdP/ADFS.php @@ -59,6 +59,10 @@ public static function receiveAuthnRequest(Request $request, IdP $idp): Streamed Logger::info('ADFS - IdP.prp: Incoming Authentication request: ' . $issuer . ' id ' . $requestid); + if ($request->query->has('username')) { + $username = (string) $request->query->get('username'); + } + $state = [ 'Responder' => [ADFS::class, 'sendResponse'], 'SPMetadata' => $spMetadata->toArray(), @@ -68,6 +72,10 @@ public static function receiveAuthnRequest(Request $request, IdP $idp): Streamed 'adfs:wreply' => false, ]; + if ($username !== null) { + $state['core:username'] = $username; + } + if (isset($query['wreply']) && !empty($query['wreply'])) { $httpUtils = new Utils\HTTP(); $state['adfs:wreply'] = $httpUtils->checkURLAllowed($query['wreply']); From dd24db543ff80225549648daed86ff2602dd13eb Mon Sep 17 00:00:00 2001 From: Tim van Dijen Date: Sun, 1 Sep 2024 11:47:21 +0200 Subject: [PATCH 12/45] Attempted fix for installing on SSP v2.3 --- composer.json | 1 + 1 file changed, 1 insertion(+) diff --git a/composer.json b/composer.json index 5205e33..682b22e 100644 --- a/composer.json +++ b/composer.json @@ -38,6 +38,7 @@ "simplesamlphp/assert": "^1.1", "simplesamlphp/saml11": "^1.0", + "simplesamlphp/saml2": "^5@dev", "simplesamlphp/simplesamlphp": "^2.3", "simplesamlphp/xml-common": "^1.16", "simplesamlphp/ws-security": "^1.0", From 233ed25162229162be21a45fe69e2dedc1aedf07 Mon Sep 17 00:00:00 2001 From: Tim van Dijen Date: Sun, 1 Sep 2024 13:32:24 +0200 Subject: [PATCH 13/45] Fixes --- src/Controller/Adfs.php | 2 +- src/IdP/ADFS.php | 57 ++++++++++++++++++++++++----------------- 2 files changed, 34 insertions(+), 25 deletions(-) diff --git a/src/Controller/Adfs.php b/src/Controller/Adfs.php index bb82c08..2596602 100644 --- a/src/Controller/Adfs.php +++ b/src/Controller/Adfs.php @@ -223,7 +223,7 @@ public function prp(Request $request): Response Logger::info('ADFS - IdP.prp: Accessing ADFS IdP endpoint prp'); $idpEntityId = $this->metadata->getMetaDataCurrentEntityID('adfs-idp-hosted'); - $idp = IdP::getById($this->config, 'adfs:' . $idpEntityId); + $idp = IdP::getById('adfs:' . $idpEntityId); if ($request->query->has('wa')) { $wa = $request->query->get('wa'); diff --git a/src/IdP/ADFS.php b/src/IdP/ADFS.php index d476344..05e9d32 100644 --- a/src/IdP/ADFS.php +++ b/src/IdP/ADFS.php @@ -15,15 +15,16 @@ use SimpleSAML\Logger; use SimpleSAML\Metadata\MetaDataStorageHandler; use SimpleSAML\Module; -use SimpleSAML\SAML11\Attribute; -use SimpleSAML\SAML11\AttributeStatement; -use SimpleSAML\SAML11\AttributeValue; -use SimpleSAML\SAML11\Audience; -use SimpleSAML\SAML11\AudienceRestrictionCondition; -use SimpleSAML\SAML11\AuthenticationStatement; -use SimpleSAML\SAML11\Conditions; -use SimpleSAML\SAML11\NameIdentifier; -use SimpleSAML\SAML11\Subject; +use SimpleSAML\SAML11\XML\saml\Assertion; +use SimpleSAML\SAML11\XML\saml\Attribute; +use SimpleSAML\SAML11\XML\saml\AttributeStatement; +use SimpleSAML\SAML11\XML\saml\AttributeValue; +use SimpleSAML\SAML11\XML\saml\Audience; +use SimpleSAML\SAML11\XML\saml\AudienceRestrictionCondition; +use SimpleSAML\SAML11\XML\saml\AuthenticationStatement; +use SimpleSAML\SAML11\XML\saml\Conditions; +use SimpleSAML\SAML11\XML\saml\NameIdentifier; +use SimpleSAML\SAML11\XML\saml\Subject; use SimpleSAML\SAML2\Constants as C; use SimpleSAML\Utils; use SimpleSAML\WSSecurity\XML\wsa\Address; @@ -41,6 +42,10 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\StreamedResponse; +use function base64_encode; +use function chunk_split; +use function trim; + class ADFS { /** @@ -103,14 +108,14 @@ private static function generateAssertion( string $nameid, array $attributes, int $assertionLifetime, - ): string { + ): Assertion { $httpUtils = new Utils\HTTP(); $randomUtils = new Utils\Random(); $timeUtils = new Utils\Time(); $issueInstant = $timeUtils->generateTimestamp(); - $notBefore = new DateInterval('PT30S'); - $notOnOrAfter = new DateInterval($assertionLifetime); + $notBefore = DateInterval::createFromDateString('30 seconds'); + $notOnOrAfter = DateInterval::createFromDateString(sprintf('%d seconds', $assertionLifetime)); $assertionID = $randomUtils->generateID(); $nameidFormat = 'http://schemas.xmlsoap.org/claims/UPN'; $nameid = htmlspecialchars($nameid); @@ -125,11 +130,11 @@ private static function generateAssertion( $audience = new Audience($target); $audienceRestrictionCondition = new AudienceRestrictionCondition([$audience]); $conditions = new Conditions( - $audience, + [$audienceRestrictionCondition], [], [], $now->sub($notBefore), - $now->add($assertionLifetime), + $now->add($notOnOrAfter), ); $nameIdentifier = new NameIdentifier($nameid, null, $nameidFormat); @@ -160,7 +165,7 @@ private static function generateAssertion( } $attrs[] = new Attribute($name, $namespace, $attrValue); } - $attributeStatement = new AttributeStatement($subject, $attributes); + $attributeStatement = new AttributeStatement($subject, $attrs); return new Assertion( $assertionID, @@ -188,12 +193,14 @@ private static function signAssertion( string $algo, #[\SensitiveParameter] string $passphrase = null, - ): string { + ): Assertion { $key = PrivateKey::fromFile($key, $passphrase); $pubkey = PublicKey::fromFile($cert); $keyInfo = new KeyInfo([ new X509Data( - [new X509Certificate($pubkey->getPEM()->getData())], + [new X509Certificate( + trim(chunk_split(base64_encode($pubkey->getPEM()->data()))), + )], ), ]); @@ -202,7 +209,8 @@ private static function signAssertion( $key, ); - return $assertion->sign($signer, C::C14N_EXCLUSIVE_WITHOUT_COMMENTS, $keyInfo); + $assertion->sign($signer, C::C14N_EXCLUSIVE_WITHOUT_COMMENTS, $keyInfo); + return $assertion; } @@ -424,11 +432,10 @@ public static function sendResponse(array $state): void 'adfs:entityID' => $spEntityId, ]); - $assertionLifetime = $spMetadata->getOptionalString('assertion.lifetime', null); + $assertionLifetime = $spMetadata->getOptionalInteger('assertion.lifetime', null); if ($assertionLifetime === null) { - $assertionLifetime = $idpMetadata->getOptionalString('assertion.lifetime', 'PT300S'); + $assertionLifetime = $idpMetadata->getOptionalInteger('assertion.lifetime', 300); } - Assert::nullOrValidDuration($assertionLifetime); $assertion = ADFS::generateAssertion($idpEntityId, $spEntityId, $nameid, $attributes, $assertionLifetime); @@ -444,10 +451,12 @@ public static function sendResponse(array $state): void $assertion = ADFS::signAssertion($assertion, $privateKeyFile, $certificateFile, $algo, $passphrase); $requestSecurityToken = new RequestSecurityToken(null, [$assertion]); - $appliesTo = new AppliesTo([new EndpointReference(new Address($target))]); - $requestSecurityTokenResponse = RequestSecurityTokenResponse(null, [$requestSecurityToken, $appliesTo]); + $appliesTo = new AppliesTo([new EndpointReference(new Address($spEntityId))]); + $requestSecurityTokenResponse = new RequestSecurityTokenResponse(null, [$requestSecurityToken, $appliesTo]); + - $wresult = $requestSecurityTokenResponse->saveXML(); + $xmlResponse = $requestSecurityTokenResponse->toXML(); + $wresult = $xmlResponse->ownerDocument->saveXML($xmlResponse); $wctx = $state['adfs:wctx']; $wreply = $state['adfs:wreply'] ? : $spMetadata->getValue('prp'); ADFS::postResponse($wreply, $wresult, $wctx); From 4d5b782cff3b8903b7c78aee21e20f576fc2f511 Mon Sep 17 00:00:00 2001 From: Tim van Dijen Date: Sun, 1 Sep 2024 17:19:36 +0200 Subject: [PATCH 14/45] Do not rely on SimpleSAMLphp for metadata-building --- src/Controller/Adfs.php | 30 ++-- src/IdP/MetadataBuilder.php | 338 ++++++++++++++++++++++++++++++++++++ 2 files changed, 354 insertions(+), 14 deletions(-) create mode 100644 src/IdP/MetadataBuilder.php diff --git a/src/Controller/Adfs.php b/src/Controller/Adfs.php index 2596602..2057cbc 100644 --- a/src/Controller/Adfs.php +++ b/src/Controller/Adfs.php @@ -5,19 +5,12 @@ namespace SimpleSAML\Module\adfs\Controller; use Exception; -use SimpleSAML\Configuration; +use SimpleSAML\{Configuration, IdP, Logger, Metadata, Module, Session, Utils}; use SimpleSAML\Error as SspError; -use SimpleSAML\IdP; -use SimpleSAML\Logger; -use SimpleSAML\Metadata; -use SimpleSAML\Module; use SimpleSAML\Module\adfs\IdP\ADFS as ADFS_IDP; +use SimpleSAML\Module\adfs\IdP\MetadataBuilder; use SimpleSAML\SAML2\Constants as C; -use SimpleSAML\Session; -use SimpleSAML\Utils; -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\HttpFoundation\StreamedResponse; +use Symfony\Component\HttpFoundation\{Request, Response, StreamedResponse}; /** * Controller class for the adfs module. @@ -79,6 +72,8 @@ public function metadata(Request $request): Response } $idpmeta = $this->metadata->getMetaDataConfig($idpentityid, 'adfs-idp-hosted'); + $builder = new MetadataBuilder($this->config, Configuration::fromArray($idpmeta)); +/* $availableCerts = []; $keys = []; $certInfo = $this->cryptoUtils->loadPublicKey($idpmeta, false, 'new_'); @@ -95,9 +90,9 @@ public function metadata(Request $request): Response } else { $hasNewCert = false; } - +*/ /** @var array $certInfo */ - $certInfo = $this->cryptoUtils->loadPublicKey($idpmeta, true); +/* $certInfo = $this->cryptoUtils->loadPublicKey($idpmeta, true); $availableCerts['idp.crt'] = $certInfo; $keys[] = [ 'type' => 'X509Certificate', @@ -107,7 +102,9 @@ public function metadata(Request $request): Response ]; if ($idpmeta->hasValue('https.certificate')) { +*/ /** @var array $httpsCert */ +/* $httpsCert = $this->cryptoUtils->loadPublicKey($idpmeta, true, 'https.'); Assert::keyExists($httpsCert, 'certData'); $availableCerts['https.crt'] = $httpsCert; @@ -193,9 +190,14 @@ public function metadata(Request $request): Response ])); } $metaxml = $metaBuilder->getEntityDescriptorText(); - +*/ // sign the metadata if enabled - $metaxml = Metadata\Signer::sign($metaxml, $idpmeta->toArray(), 'ADFS IdP'); +// $metaxml = Metadata\Signer::sign($metaxml, $idpmeta->toArray(), 'ADFS IdP'); + $document = $builder->buildDocument()->toXML(); + $document->ownerDocument->formatOutput = true; + $document->ownerDocument->encoding = 'UTF-8'; + +$metaxml = $document->ownerDocument->saveXML(); $response = new Response(); $response->setEtag(hash('sha256', $metaxml)); diff --git a/src/IdP/MetadataBuilder.php b/src/IdP/MetadataBuilder.php new file mode 100644 index 0000000..de7aa06 --- /dev/null +++ b/src/IdP/MetadataBuilder.php @@ -0,0 +1,338 @@ +clock = LocalizedClock::in('Z'); + } + + + /** + * Build a metadata document + * + * @return \SimpleSAML\SAML2\XML\md\EntityDescriptor + */ + public function buildDocument(): EntityDescriptor + { + $entityId = $this->metadata->getString('entityid'); + $contactPerson = $this->getContactPerson(); + $organization = $this->getOrganization(); + $roleDescriptor = $this->getRoleDescriptor(); + + $entityDescriptor = new EntityDescriptor( + entityId: $entityId, + contactPerson: $contactPerson, + organization: $organization, + roleDescriptor: $roleDescriptor, + ); + + if ($this->config->getOptionalBoolean('metadata.sign.enable', false) === true) { + $this->signDocument($entityDescriptor); + } + + return $entityDescriptor; + } + + + /** + * @param \SimpleSAML\SAML2\XML\md\AbstractMetadataDocument $document + * @return \SimpleSAML\SAML2\XML\md\AbstractMetadataDocument + */ + protected function signDocument(AbstractMetadataDocument $document): AbstractMetadataDocument + { + $cryptoUtils = new Utils\Crypto(); + + /** @var array $keyArray */ + $keyArray = $cryptoUtils->loadPrivateKey($this->config, true, 'metadata.sign.'); + $certArray = $cryptoUtils->loadPublicKey($this->config, false, 'metadata.sign.'); + $algo = $this->config->getOptionalString('metadata.sign.algorithm', C::SIG_RSA_SHA256); + + $key = PrivateKey::fromFile($keyArray['PEM'], $keyArray['password'] ?? ''); + $signer = (new SignatureAlgorithmFactory())->getAlgorithm($algo, $key); + + $keyInfo = null; + if ($certArray !== null) { + $keyInfo = new KeyInfo([ + new X509Data([ + new X509Certificate($certArray['certData']), + ]), + ]); + } + + $document->sign($signer, C::C14N_EXCLUSIVE_WITHOUT_COMMENTS, $keyInfo); + return $document; + } + + + /** + * This method builds the md:Organization element, if any + */ + private function getOrganization(): ?Organization + { + if ( + !$this->metadata->hasValue('OrganizationName') || + !$this->metadata->hasValue('OrganizationDisplayName') || + !$this->metadata->hasValue('OrganizationURL') + ) { + // empty or incomplete organization information + return null; + } + + $arrayUtils = new Utils\Arrays(); + $org = null; + + try { + $org = Organization::fromArray([ + 'OrganizationName' => $arrayUtils->arrayize($this->metadata->getArray('OrganizationName'), 'en'), + 'OrganizationDisplayName' => $arrayUtils->arrayize($this->metadata->getArray('OrganizationDisplayName'), 'en'), + 'OrganizationURL' => $arrayUtils->arrayize($this->metadata->getArray('OrganizationURL'), 'en'), + ]); + } catch (ArrayValidationException $e) { + Logger::error('Federation: invalid content found in contact: ' . $e->getMessage()); + } + + return $org; + } + + + /** + * This method builds the role descriptor elements + */ + private function getRoleDescriptor(): array + { + $descriptors = []; + + $set = $this->metadata->getString('metadata-set'); + switch ($set) { + case 'adfs-idp-hosted': + $descriptors[] = $this->getSecurityTokenService(); + break; + default: + throw new Exception('Not implemented'); + } + + return $descriptors; + } + + + /** + * This method builds the SecurityTokenService element + */ + private function getSecurityTokenService(): SecurityTokenService + { + $defaultEndpoint = $this->metadata->getDefaultEndpoint('SingleSignOnService'); + + return new SecurityTokenServiceType( + protocolSupportEnumeration: [C::NS_TRUST, C::NS_FED], + keyDescriptors: $this->getKeyDescriptor(), + tokenTypesOffered: new TokenTypesOffered([new TokenType('urn:oasis:names:tc:SAML:1.0:assertion')]), + securityTokenServiceEndpoint: [ + new SecurityTokenServiceEndpoint([ + new EndpointReference(new Address($defaultEndpoint['Location'])), + ]), + ], + passiveRequestorEndpoint: [ + new PassiveRequestorEndpoint([ + new EndpointReference(new Address($defaultEndpoint['Location'])), + ]), + ], + ); + } + + + /** + * This method builds the md:KeyDescriptor elements, if any + */ + private function getKeyDescriptor(): array + { + $keyDescriptor = []; + + $keys = $this->metadata->getPublicKeys(); + foreach ($keys as $key) { + if ($key['type'] !== 'X509Certificate') { + continue; + } + if (!isset($key['signing']) || $key['signing'] === true) { + $keyDescriptor[] = self::buildKeyDescriptor('signing', $key['X509Certificate'], $key['name'] ?? null); + } + if (!isset($key['encryption']) || $key['encryption'] === true) { + $keyDescriptor[] = self::buildKeyDescriptor('encryption', $key['X509Certificate'], $key['name'] ?? null); + } + } + + if ($this->metadata->hasValue('https.certData')) { + $keyDescriptor[] = self::buildKeyDescriptor('signing', $this->metadata->getString('https.certData'), null); + } + + return $keyDescriptor; + } + + + /** + * This method builds the md:ContactPerson elements, if any + */ + private function getContactPerson(): array + { + $contacts = []; + + foreach ($this->metadata->getOptionalArray('contacts', []) as $contact) { + if (array_key_exists('ContactType', $contact) && array_key_exists('EmailAddress', $contact)) { + $contacts[] = ContactPerson::fromArray($contact); + } + } + + return $contacts; + } + + + /** + * This method builds the md:Extensions, if any + */ + private function getExtensions(): ?Extensions + { + $extensions = []; + + if ($this->metadata->hasValue('scope')) { + foreach ($this->metadata->getArray('scope') as $scopetext) { + $isRegexpScope = (1 === preg_match('/[\$\^\)\(\*\|\\\\]/', $scopetext)); + $extensions[] = new Scope($scopetext, $isRegexpScope); + } + } + + if ($this->metadata->hasValue('EntityAttributes')) { + $attr = []; + foreach ($this->metadata->getArray('EntityAttributes') as $attributeName => $attributeValues) { + $attrValues = []; + foreach ($attributeValues as $attributeValue) { + $attrValues[] = new AttributeValue($attributeValue); + } + + // Attribute names that is not URI is prefixed as this: '{nameformat}name' + if (preg_match('/^\{(.*?)\}(.*)$/', $attributeName, $matches)) { + $attr[] = new Attribute( + name: $matches[2], + nameFormat: $matches[1] === C::NAMEFORMAT_UNSPECIFIED ? null : $matches[1], + attributeValue: $attrValues, + ); + } else { + $attr[] = new Attribute( + name: $attributeName, + nameFormat: C::NAMEFORMAT_UNSPECIFIED, + attributeValue: $attrValues, + ); + } + } + + $extensions[] = new EntityAttributes($attr); + } + + if ($this->metadata->hasValue('saml:Extensions')) { + $chunks = $this->metadata->getArray('saml:Extensions'); + Assert::allIsInstanceOf($chunks, Chunk::class); + $extensions = array_merge($extensions, $chunks); + } + + if ($this->metadata->hasValue('RegistrationInfo')) { + try { + $extensions[] = RegistrationInfo::fromArray($this->metadata->getArray('RegistrationInfo')); + } catch (ArrayValidationException $err) { + Logger::error('Metadata: invalid content found in RegistrationInfo: ' . $err->getMessage()); + } + } + + if ($this->metadata->hasValue('UIInfo')) { + try { + $extensions[] = UIInfo::fromArray($this->metadata->getArray('UIInfo')); + } catch (ArrayValidationException $err) { + Logger::error('Metadata: invalid content found in UIInfo: ' . $err->getMessage()); + } + } + + if ($this->metadata->hasValue('DiscoHints')) { + try { + $extensions[] = DiscoHints::fromArray($this->metadata->getArray('DiscoHints')); + } catch (ArrayValidationException $err) { + Logger::error('Metadata: invalid content found in DiscoHints: ' . $err->getMessage()); + } + } + + if ($extensions !== []) { + return new Extensions($extensions); + } + + return null; + } + + + private static function buildKeyDescriptor(string $use, string $x509Cert, ?string $keyName): KeyDescriptor + { + Assert::oneOf($use, ['encryption', 'signing']); + $info = [ + new X509Data([ + new X509Certificate($x509Cert), + ]), + ]; + + if ($keyName !== null) { + $info[] = new KeyName($keyName); + } + + return new KeyDescriptor( + new KeyInfo($info), + $use, + ); + } +} From 5d69d3dc2a64fd6360010f1f9abaab53c3cc73b7 Mon Sep 17 00:00:00 2001 From: Tim van Dijen Date: Sun, 1 Sep 2024 17:26:27 +0200 Subject: [PATCH 15/45] Fix --- src/Controller/Adfs.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Controller/Adfs.php b/src/Controller/Adfs.php index 2057cbc..7b10239 100644 --- a/src/Controller/Adfs.php +++ b/src/Controller/Adfs.php @@ -72,7 +72,7 @@ public function metadata(Request $request): Response } $idpmeta = $this->metadata->getMetaDataConfig($idpentityid, 'adfs-idp-hosted'); - $builder = new MetadataBuilder($this->config, Configuration::fromArray($idpmeta)); + $builder = new MetadataBuilder($this->config, $idpmeta); /* $availableCerts = []; $keys = []; From d3bd32fb9367fb878a06ff0a75f72aafa879fcd2 Mon Sep 17 00:00:00 2001 From: Tim van Dijen Date: Sun, 1 Sep 2024 17:26:36 +0200 Subject: [PATCH 16/45] Introduce a clokc --- composer.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/composer.json b/composer.json index 682b22e..12ce0da 100644 --- a/composer.json +++ b/composer.json @@ -36,6 +36,8 @@ "php": "^8.1", "ext-dom": "*", + "beste/clock": "^3.0", + "psr/clock": "^1.0", "simplesamlphp/assert": "^1.1", "simplesamlphp/saml11": "^1.0", "simplesamlphp/saml2": "^5@dev", From 2f890b8cc08a21737f37817992f8c8557d1fef92 Mon Sep 17 00:00:00 2001 From: Tim van Dijen Date: Sun, 1 Sep 2024 17:37:52 +0200 Subject: [PATCH 17/45] Generate ID --- src/IdP/MetadataBuilder.php | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/IdP/MetadataBuilder.php b/src/IdP/MetadataBuilder.php index de7aa06..204d203 100644 --- a/src/IdP/MetadataBuilder.php +++ b/src/IdP/MetadataBuilder.php @@ -7,7 +7,7 @@ use Beste\Clock\LocalizedClock; use Exception; use Psr\Clock\ClockInterface; -use SimpleSAML\{Configuration, Logger, Utils}; +use SimpleSAML\{Configuration, Logger, Module, Utils}; use SimpleSAML\Assert\Assert; use SimpleSAML\SAML2\Exception\ArrayValidationException; use SimpleSAML\SAML2\XML\md\AbstractMetadataDocument; @@ -72,7 +72,9 @@ public function buildDocument(): EntityDescriptor $organization = $this->getOrganization(); $roleDescriptor = $this->getRoleDescriptor(); + $randomUtils = new Utils\Random(); $entityDescriptor = new EntityDescriptor( + id: $randomUtils->generateID(), entityId: $entityId, contactPerson: $contactPerson, organization: $organization, @@ -171,9 +173,9 @@ private function getRoleDescriptor(): array /** * This method builds the SecurityTokenService element */ - private function getSecurityTokenService(): SecurityTokenService + public function getSecurityTokenService(): SecurityTokenServiceType { - $defaultEndpoint = $this->metadata->getDefaultEndpoint('SingleSignOnService'); + $defaultEndpoint = Module::getModuleURL('adfs') . '/idp/prp.php'; return new SecurityTokenServiceType( protocolSupportEnumeration: [C::NS_TRUST, C::NS_FED], @@ -181,12 +183,12 @@ private function getSecurityTokenService(): SecurityTokenService tokenTypesOffered: new TokenTypesOffered([new TokenType('urn:oasis:names:tc:SAML:1.0:assertion')]), securityTokenServiceEndpoint: [ new SecurityTokenServiceEndpoint([ - new EndpointReference(new Address($defaultEndpoint['Location'])), + new EndpointReference(new Address($defaultEndpoint)), ]), ], passiveRequestorEndpoint: [ new PassiveRequestorEndpoint([ - new EndpointReference(new Address($defaultEndpoint['Location'])), + new EndpointReference(new Address($defaultEndpoint)), ]), ], ); From f128f1a5cb30e203b7ecddacbcfd551c9c7c39a4 Mon Sep 17 00:00:00 2001 From: Tim van Dijen Date: Sun, 24 Mar 2024 23:32:12 +0100 Subject: [PATCH 18/45] Migrate module to use saml11 + ws-security libraries --- src/Controller/Adfs.php | 9 +- src/IdP/ADFS.php | 169 +++++++++--------- src/SAML2/XML/fed/Endpoint.php | 37 ---- .../XML/fed/SecurityTokenServiceType.php | 93 ---------- src/SAML2/XML/fed/TokenTypesOffered.php | 34 ---- 5 files changed, 87 insertions(+), 255 deletions(-) delete mode 100644 src/SAML2/XML/fed/Endpoint.php delete mode 100644 src/SAML2/XML/fed/SecurityTokenServiceType.php delete mode 100644 src/SAML2/XML/fed/TokenTypesOffered.php diff --git a/src/Controller/Adfs.php b/src/Controller/Adfs.php index 8755fc6..117fd89 100644 --- a/src/Controller/Adfs.php +++ b/src/Controller/Adfs.php @@ -13,7 +13,7 @@ use SimpleSAML\Metadata; use SimpleSAML\Module; use SimpleSAML\Module\adfs\IdP\ADFS as ADFS_IDP; -use SimpleSAML\SAML2\Constants; +use SimpleSAML\SAML2\Constants as C; use SimpleSAML\Session; use SimpleSAML\Utils; use Symfony\Component\HttpFoundation\Request; @@ -200,12 +200,7 @@ public function metadata(Request $request): Response $response = new Response(); $response->setEtag(hash('sha256', $metaxml)); - $response->setCache([ - 'no_cache' => $protectedMetadata === true, - 'public' => $protectedMetadata === false, - 'private' => $protectedMetadata === true, - ]); - + $response->setPublic(); if ($response->isNotModified($request)) { return $response; } diff --git a/src/IdP/ADFS.php b/src/IdP/ADFS.php index 22492aa..6cacd5b 100644 --- a/src/IdP/ADFS.php +++ b/src/IdP/ADFS.php @@ -4,17 +4,40 @@ namespace SimpleSAML\Module\adfs\IdP; +use DateInterval; +use DateTimeImmutable; +use DateTimeZone; use Exception; -use RobRichards\XMLSecLibs\XMLSecurityDSig; -use RobRichards\XMLSecLibs\XMLSecurityKey; +use SimpleSAML\Assert\Assert; use SimpleSAML\Configuration; use SimpleSAML\Error; use SimpleSAML\IdP; use SimpleSAML\Logger; use SimpleSAML\Metadata\MetaDataStorageHandler; use SimpleSAML\Module; +use SimpleSAML\SAML11\Attribute; +use SimpleSAML\SAML11\AttributeStatement; +use SimpleSAML\SAML11\AttributeValue; +use SimpleSAML\SAML11\Audience; +use SimpleSAML\SAML11\AudienceRestrictionCondition; +use SimpleSAML\SAML11\AuthenticationStatement; +use SimpleSAML\SAML11\Conditions; +use SimpleSAML\SAML11\NameIdentifier; +use SimpleSAML\SAML11\Subject; use SimpleSAML\SAML2\Constants; use SimpleSAML\Utils; +use SimpleSAML\XMLSecurity\Constants as C; +use SimpleSAML\XMLSecurity\Alg\Signature\SignatureAlgorithmFactory; +use SimpleSAML\XMLSecurity\Key\PrivateKey; +use SimpleSAML\XMLSecurity\Key\X509Certificate as PublicKey; +use SimpleSAML\XMLSecurity\XML\ds\KeyInfo; +use SimpleSAML\XMLSecurity\XML\ds\X509Certificate; +use SimpleSAML\XMLSecurity\XML\ds\X509Data; +use SimpleSAML\WSSecurity\XML\wsa\Address; +use SimpleSAML\WSSecurity\XML\wsa\EndpointReference; +use SimpleSAML\WSSecurity\XML\wsp\AppliesTo; +use SimpleSAML\WSSecurity\XML\wst\RequestSecurityTokenResponse; +use SimpleSAML\WSSecurity\XML\wst\RequestSecurityToken; use SimpleSAML\XHTML\Template; use SimpleSAML\XML\DOMDocumentFactory; use Symfony\Component\HttpFoundation\Request; @@ -74,9 +97,9 @@ function () use ($idp, &$state) { * @param string $nameid * @param array $attributes * @param int $assertionLifetime - * @return string + * @return \SimpleSAML\SAML11\XML\saml\Assertion */ - private static function generateResponse( + private static function generateAssertion( string $issuer, string $target, string $nameid, @@ -88,11 +111,12 @@ private static function generateResponse( $timeUtils = new Utils\Time(); $issueInstant = $timeUtils->generateTimestamp(); - $notBefore = $timeUtils->generateTimestamp(time() - 30); - $assertionExpire = $timeUtils->generateTimestamp(time() + $assertionLifetime); + $notBefore = new DateInterval('PT30S'); + $notOnOrAfter = new DateInterval($assertionLifetime); $assertionID = $randomUtils->generateID(); $nameidFormat = 'http://schemas.xmlsoap.org/claims/UPN'; $nameid = htmlspecialchars($nameid); + $now = new DateTimeImmutable('now', new DateTimeZone('Z')); if ($httpUtils->isHTTPS()) { $method = Constants::AC_PASSWORD_PROTECTED_TRANSPORT; @@ -100,26 +124,22 @@ private static function generateResponse( $method = Constants::AC_PASSWORD; } - $result = << - - - - - $target - - - - - $nameid - - - - - $nameid - -MSG; + $audience = new Audience($target); + $audienceRestrictionCondition = new AudienceRestrictionCondition([$audience]); + $conditions = new Conditions( + $audience, + [], + [], + $now->sub($notBefore), + $now->add($assertionLifetime), + ); + + $nameIdentifier = new NameIdentifier($nameid, null, $nameidFormat); + $subject = new Subject(null, $nameIdentifier); + + $authenticationStatement = new AuthenticationStatement($subject, $method, $now); + $attrs = []; $attrUtils = new Utils\Attributes(); foreach ($attributes as $name => $values) { if ((!is_array($values)) || (count($values) == 0)) { @@ -130,86 +150,61 @@ private static function generateResponse( $name, 'http://schemas.xmlsoap.org/claims', ); + $namespace = htmlspecialchars($namespace); $name = htmlspecialchars($name); + $attrValue = []; foreach ($values as $value) { if ((!isset($value)) || ($value === '')) { continue; } - $value = htmlspecialchars($value); - - $result .= << - $value - -MSG; + $attrValue[] = new AttributeValue($value); } + $attrs[] = new Attribute($name, $namespace, $attrValue); } - - $result .= << - - - - - $target - - - -MSG; - - return $result; + $attributeStatement = new AttributeStatement($subject, $attributes); + + return new Assertion( + $assertionID, + $issuer, + $now, + $conditions, + null, // Advice + [$authenticationStatement, $attributeStatement], + ); } /** - * @param string $response + * @param \SimpleSAML\SAML11\XML\saml\Assertion $assertion * @param string $key * @param string $cert * @param string $algo * @param string|null $passphrase - * @return string + * @return \SimpleSAML\SAML11\XML\saml\Assertion */ - private static function signResponse( - string $response, + private static function signAssertion( + Assertion $assertion, string $key, string $cert, string $algo, #[\SensitiveParameter] string $passphrase = null, ): string { - $objXMLSecDSig = new XMLSecurityDSig(); - $objXMLSecDSig->idKeys = ['AssertionID']; - $objXMLSecDSig->setCanonicalMethod(XMLSecurityDSig::EXC_C14N); - $responsedom = DOMDocumentFactory::fromString(str_replace("\r", "", $response)); - $firstassertionroot = $responsedom->getElementsByTagName('Assertion')->item(0); - - if (is_null($firstassertionroot)) { - throw new Exception("No assertion found in response."); - } + $key = PrivateKey::fromFile($key, $passphrase); + $pubkey = PublicKey::fromFile($cert); + $keyInfo = new KeyInfo([ + new X509Data( + [new X509Certificate($pubkey->getPEM()->getData())], + ), + ]); - $objXMLSecDSig->addReferenceList( - [$firstassertionroot], - XMLSecurityDSig::SHA256, - ['http://www.w3.org/2000/09/xmldsig#enveloped-signature', XMLSecurityDSig::EXC_C14N], - ['id_name' => 'AssertionID'], + $signer = (new SignatureAlgorithmFactory())->getAlgorithm( + $algo, + $key, ); - $objKey = new XMLSecurityKey($algo, ['type' => 'private']); - if (is_string($passphrase)) { - $objKey->passphrase = $passphrase; - } - $objKey->loadKey($key, true); - $objXMLSecDSig->sign($objKey); - if ($cert) { - $public_cert = file_get_contents($cert); - $objXMLSecDSig->add509Cert($public_cert, true); - } - - /** @var \DOMElement $objXMLSecDSig->sigNode */ - $newSig = $responsedom->importNode($objXMLSecDSig->sigNode, true); - $firstassertionroot->appendChild($newSig); - return $responsedom->saveXML(); + return $assertion->sign($signer, C::C14N_EXCLUSIVE_WITHOUT_COMMENTS, $keyInfo); } @@ -395,12 +390,13 @@ public static function sendResponse(array $state): void 'adfs:entityID' => $spEntityId, ]); - $assertionLifetime = $spMetadata->getOptionalInteger('assertion.lifetime', null); + $assertionLifetime = $spMetadata->getOptionalString('assertion.lifetime', null); if ($assertionLifetime === null) { - $assertionLifetime = $idpMetadata->getOptionalInteger('assertion.lifetime', 300); + $assertionLifetime = $idpMetadata->getOptionalString('assertion.lifetime', 'PT300S'); } + Assert::nullOrValidDuration($assertionLifetime); - $response = ADFS::generateResponse($idpEntityId, $spEntityId, $nameid, $attributes, $assertionLifetime); + $assertion = ADFS::generateAssertion($idpEntityId, $spEntityId, $nameid, $attributes, $assertionLifetime); $configUtils = new Utils\Config(); $privateKeyFile = $configUtils->getCertPath($idpMetadata->getString('privatekey')); @@ -409,10 +405,15 @@ public static function sendResponse(array $state): void $algo = $spMetadata->getOptionalString('signature.algorithm', null); if ($algo === null) { - $algo = $idpMetadata->getOptionalString('signature.algorithm', XMLSecurityKey::RSA_SHA256); + $algo = $idpMetadata->getOptionalString('signature.algorithm', C::SIG_RSA_SHA256); } - $wresult = ADFS::signResponse($response, $privateKeyFile, $certificateFile, $algo, $passphrase); + $assertion = ADFS::signAssertion($assertion, $privateKeyFile, $certificateFile, $algo, $passphrase); + + $requestSecurityToken = new RequestSecurityToken(null, [$assertion]); + $appliesTo = new AppliesTo([new EndpointReference(new Address($target))]); + $requestSecurityTokenResponse = RequestSecurityTokenResponse(null, [$requestSecurityToken, $appliesTo]); + $wresult = $requestSecurityTokenResponse->saveXML();; $wctx = $state['adfs:wctx']; $wreply = $state['adfs:wreply'] ? : $spMetadata->getValue('prp'); ADFS::postResponse($wreply, $wresult, $wctx); diff --git a/src/SAML2/XML/fed/Endpoint.php b/src/SAML2/XML/fed/Endpoint.php deleted file mode 100644 index 6a65cab..0000000 --- a/src/SAML2/XML/fed/Endpoint.php +++ /dev/null @@ -1,37 +0,0 @@ -ownerDocument->createElement($name); - $parent->appendChild($e); - - $endpoint = new EndpointReference(new Address($address)); - $endpoint->toXML($parent); - - return $e; - } -} diff --git a/src/SAML2/XML/fed/SecurityTokenServiceType.php b/src/SAML2/XML/fed/SecurityTokenServiceType.php deleted file mode 100644 index b3dbaa7..0000000 --- a/src/SAML2/XML/fed/SecurityTokenServiceType.php +++ /dev/null @@ -1,93 +0,0 @@ -protocolSupportEnumeration); - - if ($xml === null) { - return; - } - } - - /** - * Convert this SecurityTokenServiceType RoleDescriptor to XML. - * - * @param \DOMElement $parent The element we should add this contact to. - * @return \DOMElement The new ContactPerson-element. - */ - public function toXML(DOMElement $parent): DOMElement - { - Assert::notEmpty($this->Location, 'Location not set'); - - $e = parent::toXML($parent); - $e->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:fed', C::NS_FED); - $e->setAttributeNS(C::NS_XSI, 'xsi:type', 'fed:SecurityTokenServiceType'); - TokenTypesOffered::appendXML($e); - Endpoint::appendXML($e, 'fed:SecurityTokenServiceEndpoint', $this->Location); - Endpoint::appendXML($e, 'fed:PassiveRequestorEndpoint', $this->Location); - - return $e; - } - - - /** - * Get the location of this service. - * - * @return string The full URL where this service can be reached. - */ - public function getLocation(): string - { - Assert::notEmpty($this->Location, 'Location not set'); - - return $this->Location; - } - - - /** - * Set the location of this service. - * - * @param string $location The full URL where this service can be reached. - */ - public function setLocation(string $location): void - { - $this->Location = $location; - } -} diff --git a/src/SAML2/XML/fed/TokenTypesOffered.php b/src/SAML2/XML/fed/TokenTypesOffered.php deleted file mode 100644 index b5bca25..0000000 --- a/src/SAML2/XML/fed/TokenTypesOffered.php +++ /dev/null @@ -1,34 +0,0 @@ -ownerDocument->createElementNS(C::NS_FED, 'fed:TokenTypesOffered'); - $parent->appendChild($e); - - $tokentype = $parent->ownerDocument->createElementNS(C::NS_FED, 'fed:TokenType'); - $tokentype->setAttribute('Uri', 'urn:oasis:names:tc:SAML:1.0:assertion'); - $e->appendChild($tokentype); - - return $e; - } -} From c8d890a7e8baa085a3256927fe32739cd050d299 Mon Sep 17 00:00:00 2001 From: Tim van Dijen Date: Thu, 28 Mar 2024 21:15:54 +0100 Subject: [PATCH 19/45] Drop dependency on robrichards/xmlseclibs --- composer.json | 1 - 1 file changed, 1 deletion(-) diff --git a/composer.json b/composer.json index 000f5f6..5998e3e 100644 --- a/composer.json +++ b/composer.json @@ -36,7 +36,6 @@ "php": "^8.1", "ext-dom": "*", - "robrichards/xmlseclibs": "^3.1", "simplesamlphp/assert": "^1.0", "simplesamlphp/saml11": "^1.0", "simplesamlphp/simplesamlphp": "^2.0", From 21701e381d9e97e6101918316d3e5edab44f4794 Mon Sep 17 00:00:00 2001 From: monkeyiq Date: Mon, 29 Apr 2024 17:25:16 +1000 Subject: [PATCH 20/45] allow hosted metadata again. This lets me see it in the admin ui (#18) --- src/Controller/Adfs.php | 3 -- src/IdP/ADFS.php | 81 +++++++++++++++++++++++++++++------------ 2 files changed, 58 insertions(+), 26 deletions(-) diff --git a/src/Controller/Adfs.php b/src/Controller/Adfs.php index 117fd89..8263776 100644 --- a/src/Controller/Adfs.php +++ b/src/Controller/Adfs.php @@ -5,15 +5,12 @@ namespace SimpleSAML\Module\adfs\Controller; use Exception; -use SimpleSAML\Assert\Assert; use SimpleSAML\Configuration; use SimpleSAML\Error as SspError; use SimpleSAML\IdP; use SimpleSAML\Logger; use SimpleSAML\Metadata; -use SimpleSAML\Module; use SimpleSAML\Module\adfs\IdP\ADFS as ADFS_IDP; -use SimpleSAML\SAML2\Constants as C; use SimpleSAML\Session; use SimpleSAML\Utils; use Symfony\Component\HttpFoundation\Request; diff --git a/src/IdP/ADFS.php b/src/IdP/ADFS.php index 6cacd5b..b70b487 100644 --- a/src/IdP/ADFS.php +++ b/src/IdP/ADFS.php @@ -25,21 +25,20 @@ use SimpleSAML\SAML11\NameIdentifier; use SimpleSAML\SAML11\Subject; use SimpleSAML\SAML2\Constants; +use SimpleSAML\SAML2\Constants as C; use SimpleSAML\Utils; -use SimpleSAML\XMLSecurity\Constants as C; +use SimpleSAML\WSSecurity\XML\wsa\Address; +use SimpleSAML\WSSecurity\XML\wsa\EndpointReference; +use SimpleSAML\WSSecurity\XML\wsp\AppliesTo; +use SimpleSAML\WSSecurity\XML\wst\RequestSecurityToken; +use SimpleSAML\WSSecurity\XML\wst\RequestSecurityTokenResponse; +use SimpleSAML\XHTML\Template; use SimpleSAML\XMLSecurity\Alg\Signature\SignatureAlgorithmFactory; use SimpleSAML\XMLSecurity\Key\PrivateKey; use SimpleSAML\XMLSecurity\Key\X509Certificate as PublicKey; use SimpleSAML\XMLSecurity\XML\ds\KeyInfo; use SimpleSAML\XMLSecurity\XML\ds\X509Certificate; use SimpleSAML\XMLSecurity\XML\ds\X509Data; -use SimpleSAML\WSSecurity\XML\wsa\Address; -use SimpleSAML\WSSecurity\XML\wsa\EndpointReference; -use SimpleSAML\WSSecurity\XML\wsp\AppliesTo; -use SimpleSAML\WSSecurity\XML\wst\RequestSecurityTokenResponse; -use SimpleSAML\WSSecurity\XML\wst\RequestSecurityToken; -use SimpleSAML\XHTML\Template; -use SimpleSAML\XML\DOMDocumentFactory; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\StreamedResponse; @@ -232,30 +231,66 @@ private static function postResponse(string $wreply, string $wresult, ?string $w * @param string $entityid The entity ID of the hosted ADFS IdP whose metadata we want to fetch. * * @return array + * @param MetaDataStorageHandler $handler Optionally the metadata storage to use, + * if omitted the configured handler will be used. * @throws \SimpleSAML\Error\Exception * @throws \SimpleSAML\Error\MetadataNotFound */ - public static function getHostedMetadata(string $entityid): array + public static function getHostedMetadata(string $entityid, MetaDataStorageHandler $handler = null): array { - $handler = MetaDataStorageHandler::getMetadataHandler(); $cryptoUtils = new Utils\Crypto(); + + $globalConfig = Configuration::getInstance(); + if ($handler === null) { + $handler = MetaDataStorageHandler::getMetadataHandler($globalConfig); + } $config = $handler->getMetaDataConfig($entityid, 'adfs-idp-hosted'); - $endpoint = Module::getModuleURL('adfs/idp/prp.php'); + $host = Module::getModuleURL('adfs/idp/prp.php'); + + // configure endpoints + $ssob = $handler->getGenerated('SingleSignOnServiceBinding', 'adfs-idp-hosted', $host); + $slob = $handler->getGenerated('SingleLogoutServiceBinding', 'adfs-idp-hosted', $host); + $ssol = $handler->getGenerated('SingleSignOnService', 'adfs-idp-hosted', $host); + $slol = $handler->getGenerated('SingleLogoutService', 'adfs-idp-hosted', $host); + + $sso = []; + if (is_array($ssob)) { + foreach ($ssob as $binding) { + $sso[] = [ + 'Binding' => $binding, + 'Location' => $ssol, + ]; + } + } else { + $sso[] = [ + 'Binding' => $ssob, + 'Location' => $ssol, + ]; + } + + $slo = []; + if (is_array($slob)) { + foreach ($slob as $binding) { + $slo[] = [ + 'Binding' => $binding, + 'Location' => $slol, + ]; + } + } else { + $slo[] = [ + 'Binding' => $slob, + 'Location' => $slol, + ]; + } + + $metadata = [ 'metadata-set' => 'adfs-idp-hosted', 'entityid' => $entityid, - 'SingleSignOnService' => [ - [ - 'Binding' => Constants::BINDING_HTTP_REDIRECT, - 'Location' => $endpoint, - ], - ], - 'SingleLogoutService' => [ - 'Binding' => Constants::BINDING_HTTP_REDIRECT, - 'Location' => $endpoint, - ], - 'NameIDFormat' => $config->getOptionalString('NameIDFormat', Constants::NAMEID_TRANSIENT), + 'SingleSignOnService' => $sso, + 'SingleLogoutService' => $slo, + 'NameIDFormat' => $config->getOptionalArrayizeString('NameIDFormat', [C::NAMEID_TRANSIENT]), 'contacts' => [], ]; @@ -413,7 +448,7 @@ public static function sendResponse(array $state): void $appliesTo = new AppliesTo([new EndpointReference(new Address($target))]); $requestSecurityTokenResponse = RequestSecurityTokenResponse(null, [$requestSecurityToken, $appliesTo]); - $wresult = $requestSecurityTokenResponse->saveXML();; + $wresult = $requestSecurityTokenResponse->saveXML(); $wctx = $state['adfs:wctx']; $wreply = $state['adfs:wreply'] ? : $spMetadata->getValue('prp'); ADFS::postResponse($wreply, $wresult, $wctx); From 05f50a10fd4105e50f08cbb09664785bba72f94b Mon Sep 17 00:00:00 2001 From: Tim van Dijen Date: Sat, 30 Mar 2024 17:07:12 +0100 Subject: [PATCH 21/45] Depend on feature-branch of SSP --- composer.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/composer.json b/composer.json index 5998e3e..2f2c2ca 100644 --- a/composer.json +++ b/composer.json @@ -36,16 +36,16 @@ "php": "^8.1", "ext-dom": "*", - "simplesamlphp/assert": "^1.0", + "simplesamlphp/assert": "^1.1", "simplesamlphp/saml11": "^1.0", - "simplesamlphp/simplesamlphp": "^2.0", - "simplesamlphp/xml-common": "^1.12", + "simplesamlphp/simplesamlphp": "dev-saml2v5_metadata", + "simplesamlphp/xml-common": "^1.16", "simplesamlphp/ws-security": "^1.0", "symfony/http-foundation": "^6.4" }, "require-dev": { - "simplesamlphp/simplesamlphp-test-framework": "^1.5", - "simplesamlphp/xml-security": "^1.6" + "simplesamlphp/simplesamlphp-test-framework": "^1.6", + "simplesamlphp/xml-security": "^1.7" }, "support": { "issues": "https://github.com/simplesamlphp/simplesamlphp-module-adfs/issues", From b4c921457b6e7e003e3a53a82f7eeda5654a1b10 Mon Sep 17 00:00:00 2001 From: Tim van Dijen Date: Mon, 29 Apr 2024 20:56:44 +0200 Subject: [PATCH 22/45] Fix coding style --- src/IdP/ADFS.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/IdP/ADFS.php b/src/IdP/ADFS.php index b70b487..46aa449 100644 --- a/src/IdP/ADFS.php +++ b/src/IdP/ADFS.php @@ -229,10 +229,10 @@ private static function postResponse(string $wreply, string $wresult, ?string $w * Get the metadata of a given hosted ADFS IdP. * * @param string $entityid The entity ID of the hosted ADFS IdP whose metadata we want to fetch. - * - * @return array - * @param MetaDataStorageHandler $handler Optionally the metadata storage to use, + * @param \SimpleSAML\Metadata\MetaDataStorageHandler $handler Optionally the metadata storage to use, * if omitted the configured handler will be used. + * @return array + * * @throws \SimpleSAML\Error\Exception * @throws \SimpleSAML\Error\MetadataNotFound */ From 332e6d898341da778e65b67f337d4c35f79226d4 Mon Sep 17 00:00:00 2001 From: monkeyiq Date: Wed, 8 May 2024 18:34:48 +1000 Subject: [PATCH 23/45] A hook to generate metadata (#20) * A hook to generate metadata * Fix coding style & strict comparison --------- Co-authored-by: Tim van Dijen --- hooks/hook_generate_metadata.php | 33 ++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 hooks/hook_generate_metadata.php diff --git a/hooks/hook_generate_metadata.php b/hooks/hook_generate_metadata.php new file mode 100644 index 0000000..f2e9d0c --- /dev/null +++ b/hooks/hook_generate_metadata.php @@ -0,0 +1,33 @@ + Date: Sat, 31 Aug 2024 20:41:37 +0200 Subject: [PATCH 24/45] Fix constant --- src/IdP/ADFS.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/IdP/ADFS.php b/src/IdP/ADFS.php index 46aa449..d476344 100644 --- a/src/IdP/ADFS.php +++ b/src/IdP/ADFS.php @@ -24,7 +24,6 @@ use SimpleSAML\SAML11\Conditions; use SimpleSAML\SAML11\NameIdentifier; use SimpleSAML\SAML11\Subject; -use SimpleSAML\SAML2\Constants; use SimpleSAML\SAML2\Constants as C; use SimpleSAML\Utils; use SimpleSAML\WSSecurity\XML\wsa\Address; @@ -118,9 +117,9 @@ private static function generateAssertion( $now = new DateTimeImmutable('now', new DateTimeZone('Z')); if ($httpUtils->isHTTPS()) { - $method = Constants::AC_PASSWORD_PROTECTED_TRANSPORT; + $method = C::AC_PASSWORD_PROTECTED_TRANSPORT; } else { - $method = Constants::AC_PASSWORD; + $method = C::AC_PASSWORD; } $audience = new Audience($target); From bb2cb62ebbe6c93de3fdb1fa020f6730fea1b6e2 Mon Sep 17 00:00:00 2001 From: Tim van Dijen Date: Sat, 31 Aug 2024 20:50:26 +0200 Subject: [PATCH 25/45] Fix unit test --- tests/src/Controller/AdfsControllerTest.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/src/Controller/AdfsControllerTest.php b/tests/src/Controller/AdfsControllerTest.php index f4e788e..e6c4cce 100644 --- a/tests/src/Controller/AdfsControllerTest.php +++ b/tests/src/Controller/AdfsControllerTest.php @@ -8,10 +8,9 @@ use PHPUnit\Framework\TestCase; use SimpleSAML\Configuration; use SimpleSAML\Error; -use SimpleSAML\HTTP\RunnableResponse; use SimpleSAML\Module\adfs\Controller; use SimpleSAML\Session; -use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\{Request, StreamedResponse}; use function dirname; @@ -108,6 +107,6 @@ public function testValidRequest(): void $response = $c->prp($request); $this->assertTrue($response->isSuccessful()); - $this->assertInstanceOf(RunnableResponse::class, $response); + $this->assertInstanceOf(StreamedResponse::class, $response); } } From dc582095da14ffee3bb3ab586942aa5258225e47 Mon Sep 17 00:00:00 2001 From: Tim van Dijen Date: Sat, 31 Aug 2024 21:07:08 +0200 Subject: [PATCH 26/45] Back to SSP release-branch --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 2f2c2ca..5205e33 100644 --- a/composer.json +++ b/composer.json @@ -38,7 +38,7 @@ "simplesamlphp/assert": "^1.1", "simplesamlphp/saml11": "^1.0", - "simplesamlphp/simplesamlphp": "dev-saml2v5_metadata", + "simplesamlphp/simplesamlphp": "^2.3", "simplesamlphp/xml-common": "^1.16", "simplesamlphp/ws-security": "^1.0", "symfony/http-foundation": "^6.4" From b77a6177964ade6ee06b2446542cb5014ab5908c Mon Sep 17 00:00:00 2001 From: Tim van Dijen Date: Sun, 1 Sep 2024 00:57:24 +0200 Subject: [PATCH 27/45] Fix constants --- src/Controller/Adfs.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Controller/Adfs.php b/src/Controller/Adfs.php index 8263776..bb82c08 100644 --- a/src/Controller/Adfs.php +++ b/src/Controller/Adfs.php @@ -10,7 +10,9 @@ use SimpleSAML\IdP; use SimpleSAML\Logger; use SimpleSAML\Metadata; +use SimpleSAML\Module; use SimpleSAML\Module\adfs\IdP\ADFS as ADFS_IDP; +use SimpleSAML\SAML2\Constants as C; use SimpleSAML\Session; use SimpleSAML\Utils; use Symfony\Component\HttpFoundation\Request; @@ -123,13 +125,13 @@ public function metadata(Request $request): Response 'entityid' => $idpentityid, 'SingleSignOnService' => [ 0 => [ - 'Binding' => Constants::BINDING_HTTP_REDIRECT, + 'Binding' => C::BINDING_HTTP_REDIRECT, 'Location' => $adfs_service_location, ], ], 'SingleLogoutService' => [ 0 => [ - 'Binding' => Constants::BINDING_HTTP_REDIRECT, + 'Binding' => C::BINDING_HTTP_REDIRECT, 'Location' => $adfs_service_location, ], ], @@ -143,7 +145,7 @@ public function metadata(Request $request): Response $metaArray['NameIDFormat'] = $idpmeta->getOptionalString( 'NameIDFormat', - Constants::NAMEID_TRANSIENT, + C::NAMEID_TRANSIENT, ); if ($idpmeta->hasValue('OrganizationName')) { From 11824b829481f18a11fbed6ec45d23dd4e3b9a94 Mon Sep 17 00:00:00 2001 From: Tim van Dijen Date: Sun, 1 Sep 2024 11:47:21 +0200 Subject: [PATCH 28/45] Attempted fix for installing on SSP v2.3 --- composer.json | 1 + 1 file changed, 1 insertion(+) diff --git a/composer.json b/composer.json index 5205e33..682b22e 100644 --- a/composer.json +++ b/composer.json @@ -38,6 +38,7 @@ "simplesamlphp/assert": "^1.1", "simplesamlphp/saml11": "^1.0", + "simplesamlphp/saml2": "^5@dev", "simplesamlphp/simplesamlphp": "^2.3", "simplesamlphp/xml-common": "^1.16", "simplesamlphp/ws-security": "^1.0", From 162d15555f940f8272472855710363922d0dfcf1 Mon Sep 17 00:00:00 2001 From: Tim van Dijen Date: Sun, 1 Sep 2024 13:32:24 +0200 Subject: [PATCH 29/45] Fixes --- src/Controller/Adfs.php | 2 +- src/IdP/ADFS.php | 57 ++++++++++++++++++++++++----------------- 2 files changed, 34 insertions(+), 25 deletions(-) diff --git a/src/Controller/Adfs.php b/src/Controller/Adfs.php index bb82c08..2596602 100644 --- a/src/Controller/Adfs.php +++ b/src/Controller/Adfs.php @@ -223,7 +223,7 @@ public function prp(Request $request): Response Logger::info('ADFS - IdP.prp: Accessing ADFS IdP endpoint prp'); $idpEntityId = $this->metadata->getMetaDataCurrentEntityID('adfs-idp-hosted'); - $idp = IdP::getById($this->config, 'adfs:' . $idpEntityId); + $idp = IdP::getById('adfs:' . $idpEntityId); if ($request->query->has('wa')) { $wa = $request->query->get('wa'); diff --git a/src/IdP/ADFS.php b/src/IdP/ADFS.php index d476344..05e9d32 100644 --- a/src/IdP/ADFS.php +++ b/src/IdP/ADFS.php @@ -15,15 +15,16 @@ use SimpleSAML\Logger; use SimpleSAML\Metadata\MetaDataStorageHandler; use SimpleSAML\Module; -use SimpleSAML\SAML11\Attribute; -use SimpleSAML\SAML11\AttributeStatement; -use SimpleSAML\SAML11\AttributeValue; -use SimpleSAML\SAML11\Audience; -use SimpleSAML\SAML11\AudienceRestrictionCondition; -use SimpleSAML\SAML11\AuthenticationStatement; -use SimpleSAML\SAML11\Conditions; -use SimpleSAML\SAML11\NameIdentifier; -use SimpleSAML\SAML11\Subject; +use SimpleSAML\SAML11\XML\saml\Assertion; +use SimpleSAML\SAML11\XML\saml\Attribute; +use SimpleSAML\SAML11\XML\saml\AttributeStatement; +use SimpleSAML\SAML11\XML\saml\AttributeValue; +use SimpleSAML\SAML11\XML\saml\Audience; +use SimpleSAML\SAML11\XML\saml\AudienceRestrictionCondition; +use SimpleSAML\SAML11\XML\saml\AuthenticationStatement; +use SimpleSAML\SAML11\XML\saml\Conditions; +use SimpleSAML\SAML11\XML\saml\NameIdentifier; +use SimpleSAML\SAML11\XML\saml\Subject; use SimpleSAML\SAML2\Constants as C; use SimpleSAML\Utils; use SimpleSAML\WSSecurity\XML\wsa\Address; @@ -41,6 +42,10 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\StreamedResponse; +use function base64_encode; +use function chunk_split; +use function trim; + class ADFS { /** @@ -103,14 +108,14 @@ private static function generateAssertion( string $nameid, array $attributes, int $assertionLifetime, - ): string { + ): Assertion { $httpUtils = new Utils\HTTP(); $randomUtils = new Utils\Random(); $timeUtils = new Utils\Time(); $issueInstant = $timeUtils->generateTimestamp(); - $notBefore = new DateInterval('PT30S'); - $notOnOrAfter = new DateInterval($assertionLifetime); + $notBefore = DateInterval::createFromDateString('30 seconds'); + $notOnOrAfter = DateInterval::createFromDateString(sprintf('%d seconds', $assertionLifetime)); $assertionID = $randomUtils->generateID(); $nameidFormat = 'http://schemas.xmlsoap.org/claims/UPN'; $nameid = htmlspecialchars($nameid); @@ -125,11 +130,11 @@ private static function generateAssertion( $audience = new Audience($target); $audienceRestrictionCondition = new AudienceRestrictionCondition([$audience]); $conditions = new Conditions( - $audience, + [$audienceRestrictionCondition], [], [], $now->sub($notBefore), - $now->add($assertionLifetime), + $now->add($notOnOrAfter), ); $nameIdentifier = new NameIdentifier($nameid, null, $nameidFormat); @@ -160,7 +165,7 @@ private static function generateAssertion( } $attrs[] = new Attribute($name, $namespace, $attrValue); } - $attributeStatement = new AttributeStatement($subject, $attributes); + $attributeStatement = new AttributeStatement($subject, $attrs); return new Assertion( $assertionID, @@ -188,12 +193,14 @@ private static function signAssertion( string $algo, #[\SensitiveParameter] string $passphrase = null, - ): string { + ): Assertion { $key = PrivateKey::fromFile($key, $passphrase); $pubkey = PublicKey::fromFile($cert); $keyInfo = new KeyInfo([ new X509Data( - [new X509Certificate($pubkey->getPEM()->getData())], + [new X509Certificate( + trim(chunk_split(base64_encode($pubkey->getPEM()->data()))), + )], ), ]); @@ -202,7 +209,8 @@ private static function signAssertion( $key, ); - return $assertion->sign($signer, C::C14N_EXCLUSIVE_WITHOUT_COMMENTS, $keyInfo); + $assertion->sign($signer, C::C14N_EXCLUSIVE_WITHOUT_COMMENTS, $keyInfo); + return $assertion; } @@ -424,11 +432,10 @@ public static function sendResponse(array $state): void 'adfs:entityID' => $spEntityId, ]); - $assertionLifetime = $spMetadata->getOptionalString('assertion.lifetime', null); + $assertionLifetime = $spMetadata->getOptionalInteger('assertion.lifetime', null); if ($assertionLifetime === null) { - $assertionLifetime = $idpMetadata->getOptionalString('assertion.lifetime', 'PT300S'); + $assertionLifetime = $idpMetadata->getOptionalInteger('assertion.lifetime', 300); } - Assert::nullOrValidDuration($assertionLifetime); $assertion = ADFS::generateAssertion($idpEntityId, $spEntityId, $nameid, $attributes, $assertionLifetime); @@ -444,10 +451,12 @@ public static function sendResponse(array $state): void $assertion = ADFS::signAssertion($assertion, $privateKeyFile, $certificateFile, $algo, $passphrase); $requestSecurityToken = new RequestSecurityToken(null, [$assertion]); - $appliesTo = new AppliesTo([new EndpointReference(new Address($target))]); - $requestSecurityTokenResponse = RequestSecurityTokenResponse(null, [$requestSecurityToken, $appliesTo]); + $appliesTo = new AppliesTo([new EndpointReference(new Address($spEntityId))]); + $requestSecurityTokenResponse = new RequestSecurityTokenResponse(null, [$requestSecurityToken, $appliesTo]); + - $wresult = $requestSecurityTokenResponse->saveXML(); + $xmlResponse = $requestSecurityTokenResponse->toXML(); + $wresult = $xmlResponse->ownerDocument->saveXML($xmlResponse); $wctx = $state['adfs:wctx']; $wreply = $state['adfs:wreply'] ? : $spMetadata->getValue('prp'); ADFS::postResponse($wreply, $wresult, $wctx); From c7e80baa5e80c9b3fab34842feb1faf499e45056 Mon Sep 17 00:00:00 2001 From: Tim van Dijen Date: Sun, 1 Sep 2024 18:27:07 +0200 Subject: [PATCH 30/45] Feature/standalone metadata (#21) * Do not rely on SimpleSAMLphp for metadata-building * Fix * Introduce a clokc * Generate ID * Cleanup --- composer.json | 2 + src/Controller/Adfs.php | 135 +------------- src/IdP/ADFS.php | 1 - src/IdP/MetadataBuilder.php | 347 ++++++++++++++++++++++++++++++++++++ 4 files changed, 358 insertions(+), 127 deletions(-) create mode 100644 src/IdP/MetadataBuilder.php diff --git a/composer.json b/composer.json index 682b22e..12ce0da 100644 --- a/composer.json +++ b/composer.json @@ -36,6 +36,8 @@ "php": "^8.1", "ext-dom": "*", + "beste/clock": "^3.0", + "psr/clock": "^1.0", "simplesamlphp/assert": "^1.1", "simplesamlphp/saml11": "^1.0", "simplesamlphp/saml2": "^5@dev", diff --git a/src/Controller/Adfs.php b/src/Controller/Adfs.php index 2596602..f733fd8 100644 --- a/src/Controller/Adfs.php +++ b/src/Controller/Adfs.php @@ -5,19 +5,11 @@ namespace SimpleSAML\Module\adfs\Controller; use Exception; -use SimpleSAML\Configuration; +use SimpleSAML\{Configuration, IdP, Logger, Metadata, Module, Session, Utils}; use SimpleSAML\Error as SspError; -use SimpleSAML\IdP; -use SimpleSAML\Logger; -use SimpleSAML\Metadata; -use SimpleSAML\Module; use SimpleSAML\Module\adfs\IdP\ADFS as ADFS_IDP; -use SimpleSAML\SAML2\Constants as C; -use SimpleSAML\Session; -use SimpleSAML\Utils; -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\HttpFoundation\StreamedResponse; +use SimpleSAML\Module\adfs\IdP\MetadataBuilder; +use Symfony\Component\HttpFoundation\{Request, Response, StreamedResponse}; /** * Controller class for the adfs module. @@ -79,123 +71,14 @@ public function metadata(Request $request): Response } $idpmeta = $this->metadata->getMetaDataConfig($idpentityid, 'adfs-idp-hosted'); - $availableCerts = []; - $keys = []; - $certInfo = $this->cryptoUtils->loadPublicKey($idpmeta, false, 'new_'); + $builder = new MetadataBuilder($this->config, $idpmeta); - if ($certInfo !== null) { - $availableCerts['new_idp.crt'] = $certInfo; - $keys[] = [ - 'type' => 'X509Certificate', - 'signing' => true, - 'encryption' => true, - 'X509Certificate' => $certInfo['certData'], - ]; - $hasNewCert = true; - } else { - $hasNewCert = false; - } - - /** @var array $certInfo */ - $certInfo = $this->cryptoUtils->loadPublicKey($idpmeta, true); - $availableCerts['idp.crt'] = $certInfo; - $keys[] = [ - 'type' => 'X509Certificate', - 'signing' => true, - 'encryption' => ($hasNewCert ? false : true), - 'X509Certificate' => $certInfo['certData'], - ]; - - if ($idpmeta->hasValue('https.certificate')) { - /** @var array $httpsCert */ - $httpsCert = $this->cryptoUtils->loadPublicKey($idpmeta, true, 'https.'); - Assert::keyExists($httpsCert, 'certData'); - $availableCerts['https.crt'] = $httpsCert; - $keys[] = [ - 'type' => 'X509Certificate', - 'signing' => true, - 'encryption' => false, - 'X509Certificate' => $httpsCert['certData'], - ]; - } - - $adfs_service_location = Module::getModuleURL('adfs') . '/idp/prp.php'; - $metaArray = [ - 'metadata-set' => 'adfs-idp-remote', - 'entityid' => $idpentityid, - 'SingleSignOnService' => [ - 0 => [ - 'Binding' => C::BINDING_HTTP_REDIRECT, - 'Location' => $adfs_service_location, - ], - ], - 'SingleLogoutService' => [ - 0 => [ - 'Binding' => C::BINDING_HTTP_REDIRECT, - 'Location' => $adfs_service_location, - ], - ], - ]; - - if (count($keys) === 1) { - $metaArray['certData'] = $keys[0]['X509Certificate']; - } else { - $metaArray['keys'] = $keys; - } - - $metaArray['NameIDFormat'] = $idpmeta->getOptionalString( - 'NameIDFormat', - C::NAMEID_TRANSIENT, - ); - - if ($idpmeta->hasValue('OrganizationName')) { - $metaArray['OrganizationName'] = $idpmeta->getLocalizedString('OrganizationName'); - $metaArray['OrganizationDisplayName'] = $idpmeta->getOptionalLocalizedString( - 'OrganizationDisplayName', - $metaArray['OrganizationName'], - ); - - if (!$idpmeta->hasValue('OrganizationURL')) { - throw new SspError\Exception('If OrganizationName is set, OrganizationURL must also be set.'); - } - $metaArray['OrganizationURL'] = $idpmeta->getLocalizedString('OrganizationURL'); - } - - if ($idpmeta->hasValue('scope')) { - $metaArray['scope'] = $idpmeta->getArray('scope'); - } - - if ($idpmeta->hasValue('EntityAttributes')) { - $metaArray['EntityAttributes'] = $idpmeta->getArray('EntityAttributes'); - } - - if ($idpmeta->hasValue('UIInfo')) { - $metaArray['UIInfo'] = $idpmeta->getArray('UIInfo'); - } - - if ($idpmeta->hasValue('DiscoHints')) { - $metaArray['DiscoHints'] = $idpmeta->getArray('DiscoHints'); - } - - if ($idpmeta->hasValue('RegistrationInfo')) { - $metaArray['RegistrationInfo'] = $idpmeta->getArray('RegistrationInfo'); - } - - $metaBuilder = new Metadata\SAMLBuilder($idpentityid); - $metaBuilder->addSecurityTokenServiceType($metaArray); - $metaBuilder->addOrganizationInfo($metaArray); - $technicalContactEmail = $this->config->getOptionalString('technicalcontact_email', null); - if ($technicalContactEmail !== null && $technicalContactEmail !== 'na@example.org') { - $metaBuilder->addContact(Utils\Config\Metadata::getContact([ - 'emailAddress' => $technicalContactEmail, - 'givenName' => $this->config->getOptionalString('technicalcontact_name', null), - 'contactType' => 'technical', - ])); - } - $metaxml = $metaBuilder->getEntityDescriptorText(); + $document = $builder->buildDocument()->toXML(); + // Some products like DirX are known to break on pretty-printed XML + $document->ownerDocument->formatOutput = false; + $document->ownerDocument->encoding = 'UTF-8'; - // sign the metadata if enabled - $metaxml = Metadata\Signer::sign($metaxml, $idpmeta->toArray(), 'ADFS IdP'); + $metaxml = $document->ownerDocument->saveXML(); $response = new Response(); $response->setEtag(hash('sha256', $metaxml)); diff --git a/src/IdP/ADFS.php b/src/IdP/ADFS.php index 05e9d32..94732e2 100644 --- a/src/IdP/ADFS.php +++ b/src/IdP/ADFS.php @@ -8,7 +8,6 @@ use DateTimeImmutable; use DateTimeZone; use Exception; -use SimpleSAML\Assert\Assert; use SimpleSAML\Configuration; use SimpleSAML\Error; use SimpleSAML\IdP; diff --git a/src/IdP/MetadataBuilder.php b/src/IdP/MetadataBuilder.php new file mode 100644 index 0000000..c572608 --- /dev/null +++ b/src/IdP/MetadataBuilder.php @@ -0,0 +1,347 @@ +clock = LocalizedClock::in('Z'); + } + + + /** + * Build a metadata document + * + * @return \SimpleSAML\SAML2\XML\md\EntityDescriptor + */ + public function buildDocument(): EntityDescriptor + { + $entityId = $this->metadata->getString('entityid'); + $contactPerson = $this->getContactPerson(); + $organization = $this->getOrganization(); + $roleDescriptor = $this->getRoleDescriptor(); + + $randomUtils = new Utils\Random(); + $entityDescriptor = new EntityDescriptor( + id: $randomUtils->generateID(), + entityId: $entityId, + contactPerson: $contactPerson, + organization: $organization, + roleDescriptor: $roleDescriptor, + ); + + if ($this->config->getOptionalBoolean('metadata.sign.enable', false) === true) { + $this->signDocument($entityDescriptor); + } + + return $entityDescriptor; + } + + + /** + * @param \SimpleSAML\SAML2\XML\md\AbstractMetadataDocument $document + * @return \SimpleSAML\SAML2\XML\md\AbstractMetadataDocument + */ + protected function signDocument(AbstractMetadataDocument $document): AbstractMetadataDocument + { + $cryptoUtils = new Utils\Crypto(); + + /** @var array $keyArray */ + $keyArray = $cryptoUtils->loadPrivateKey($this->config, true, 'metadata.sign.'); + $certArray = $cryptoUtils->loadPublicKey($this->config, false, 'metadata.sign.'); + $algo = $this->config->getOptionalString('metadata.sign.algorithm', C::SIG_RSA_SHA256); + + $key = PrivateKey::fromFile($keyArray['PEM'], $keyArray['password'] ?? ''); + $signer = (new SignatureAlgorithmFactory())->getAlgorithm($algo, $key); + + $keyInfo = null; + if ($certArray !== null) { + $keyInfo = new KeyInfo([ + new X509Data([ + new X509Certificate($certArray['certData']), + ]), + ]); + } + + $document->sign($signer, C::C14N_EXCLUSIVE_WITHOUT_COMMENTS, $keyInfo); + return $document; + } + + + /** + * This method builds the md:Organization element, if any + */ + private function getOrganization(): ?Organization + { + if ( + !$this->metadata->hasValue('OrganizationName') || + !$this->metadata->hasValue('OrganizationDisplayName') || + !$this->metadata->hasValue('OrganizationURL') + ) { + // empty or incomplete organization information + return null; + } + + $arrayUtils = new Utils\Arrays(); + $org = null; + + try { + $org = Organization::fromArray([ + 'OrganizationName' => $arrayUtils->arrayize($this->metadata->getArray('OrganizationName'), 'en'), + 'OrganizationDisplayName' => $arrayUtils->arrayize( + $this->metadata->getArray('OrganizationDisplayName'), + 'en', + ), + 'OrganizationURL' => $arrayUtils->arrayize($this->metadata->getArray('OrganizationURL'), 'en'), + ]); + } catch (ArrayValidationException $e) { + Logger::error('Federation: invalid content found in contact: ' . $e->getMessage()); + } + + return $org; + } + + + /** + * This method builds the role descriptor elements + */ + private function getRoleDescriptor(): array + { + $descriptors = []; + + $set = $this->metadata->getString('metadata-set'); + switch ($set) { + case 'adfs-idp-hosted': + $descriptors[] = $this->getSecurityTokenService(); + break; + default: + throw new Exception('Not implemented'); + } + + return $descriptors; + } + + + /** + * This method builds the SecurityTokenService element + */ + public function getSecurityTokenService(): SecurityTokenServiceType + { + $defaultEndpoint = Module::getModuleURL('adfs') . '/idp/prp.php'; + + return new SecurityTokenServiceType( + protocolSupportEnumeration: [C::NS_TRUST, C::NS_FED], + keyDescriptors: $this->getKeyDescriptor(), + tokenTypesOffered: new TokenTypesOffered([new TokenType('urn:oasis:names:tc:SAML:1.0:assertion')]), + securityTokenServiceEndpoint: [ + new SecurityTokenServiceEndpoint([ + new EndpointReference(new Address($defaultEndpoint)), + ]), + ], + passiveRequestorEndpoint: [ + new PassiveRequestorEndpoint([ + new EndpointReference(new Address($defaultEndpoint)), + ]), + ], + ); + } + + + /** + * This method builds the md:KeyDescriptor elements, if any + */ + private function getKeyDescriptor(): array + { + $keyDescriptor = []; + + $keys = $this->metadata->getPublicKeys(); + foreach ($keys as $key) { + if ($key['type'] !== 'X509Certificate') { + continue; + } + if (!isset($key['signing']) || $key['signing'] === true) { + $keyDescriptor[] = self::buildKeyDescriptor( + 'signing', + $key['X509Certificate'], + $key['name'] ?? null, + ); + } + if (!isset($key['encryption']) || $key['encryption'] === true) { + $keyDescriptor[] = self::buildKeyDescriptor( + 'encryption', + $key['X509Certificate'], + $key['name'] ?? null, + )); + } + } + + if ($this->metadata->hasValue('https.certData')) { + $keyDescriptor[] = self::buildKeyDescriptor('signing', $this->metadata->getString('https.certData'), null); + } + + return $keyDescriptor; + } + + + /** + * This method builds the md:ContactPerson elements, if any + */ + private function getContactPerson(): array + { + $contacts = []; + + foreach ($this->metadata->getOptionalArray('contacts', []) as $contact) { + if (array_key_exists('ContactType', $contact) && array_key_exists('EmailAddress', $contact)) { + $contacts[] = ContactPerson::fromArray($contact); + } + } + + return $contacts; + } + + + /** + * This method builds the md:Extensions, if any + */ + private function getExtensions(): ?Extensions + { + $extensions = []; + + if ($this->metadata->hasValue('scope')) { + foreach ($this->metadata->getArray('scope') as $scopetext) { + $isRegexpScope = (1 === preg_match('/[\$\^\)\(\*\|\\\\]/', $scopetext)); + $extensions[] = new Scope($scopetext, $isRegexpScope); + } + } + + if ($this->metadata->hasValue('EntityAttributes')) { + $attr = []; + foreach ($this->metadata->getArray('EntityAttributes') as $attributeName => $attributeValues) { + $attrValues = []; + foreach ($attributeValues as $attributeValue) { + $attrValues[] = new AttributeValue($attributeValue); + } + + // Attribute names that is not URI is prefixed as this: '{nameformat}name' + if (preg_match('/^\{(.*?)\}(.*)$/', $attributeName, $matches)) { + $attr[] = new Attribute( + name: $matches[2], + nameFormat: $matches[1] === C::NAMEFORMAT_UNSPECIFIED ? null : $matches[1], + attributeValue: $attrValues, + ); + } else { + $attr[] = new Attribute( + name: $attributeName, + nameFormat: C::NAMEFORMAT_UNSPECIFIED, + attributeValue: $attrValues, + ); + } + } + + $extensions[] = new EntityAttributes($attr); + } + + if ($this->metadata->hasValue('saml:Extensions')) { + $chunks = $this->metadata->getArray('saml:Extensions'); + Assert::allIsInstanceOf($chunks, Chunk::class); + $extensions = array_merge($extensions, $chunks); + } + + if ($this->metadata->hasValue('RegistrationInfo')) { + try { + $extensions[] = RegistrationInfo::fromArray($this->metadata->getArray('RegistrationInfo')); + } catch (ArrayValidationException $err) { + Logger::error('Metadata: invalid content found in RegistrationInfo: ' . $err->getMessage()); + } + } + + if ($this->metadata->hasValue('UIInfo')) { + try { + $extensions[] = UIInfo::fromArray($this->metadata->getArray('UIInfo')); + } catch (ArrayValidationException $err) { + Logger::error('Metadata: invalid content found in UIInfo: ' . $err->getMessage()); + } + } + + if ($this->metadata->hasValue('DiscoHints')) { + try { + $extensions[] = DiscoHints::fromArray($this->metadata->getArray('DiscoHints')); + } catch (ArrayValidationException $err) { + Logger::error('Metadata: invalid content found in DiscoHints: ' . $err->getMessage()); + } + } + + if ($extensions !== []) { + return new Extensions($extensions); + } + + return null; + } + + + private static function buildKeyDescriptor(string $use, string $x509Cert, ?string $keyName): KeyDescriptor + { + Assert::oneOf($use, ['encryption', 'signing']); + $info = [ + new X509Data([ + new X509Certificate($x509Cert), + ]), + ]; + + if ($keyName !== null) { + $info[] = new KeyName($keyName); + } + + return new KeyDescriptor( + new KeyInfo($info), + $use, + ); + } +} From 07a817cc0458a9713c4af777bd42d7b7748873f1 Mon Sep 17 00:00:00 2001 From: Tim van Dijen Date: Sun, 1 Sep 2024 18:26:40 +0200 Subject: [PATCH 31/45] Cleanup --- src/Controller/Adfs.php | 129 ++---------------------------------- src/IdP/ADFS.php | 1 - src/IdP/MetadataBuilder.php | 23 ++++--- 3 files changed, 20 insertions(+), 133 deletions(-) diff --git a/src/Controller/Adfs.php b/src/Controller/Adfs.php index 7b10239..f733fd8 100644 --- a/src/Controller/Adfs.php +++ b/src/Controller/Adfs.php @@ -9,7 +9,6 @@ use SimpleSAML\Error as SspError; use SimpleSAML\Module\adfs\IdP\ADFS as ADFS_IDP; use SimpleSAML\Module\adfs\IdP\MetadataBuilder; -use SimpleSAML\SAML2\Constants as C; use Symfony\Component\HttpFoundation\{Request, Response, StreamedResponse}; /** @@ -73,131 +72,13 @@ public function metadata(Request $request): Response $idpmeta = $this->metadata->getMetaDataConfig($idpentityid, 'adfs-idp-hosted'); $builder = new MetadataBuilder($this->config, $idpmeta); -/* - $availableCerts = []; - $keys = []; - $certInfo = $this->cryptoUtils->loadPublicKey($idpmeta, false, 'new_'); - if ($certInfo !== null) { - $availableCerts['new_idp.crt'] = $certInfo; - $keys[] = [ - 'type' => 'X509Certificate', - 'signing' => true, - 'encryption' => true, - 'X509Certificate' => $certInfo['certData'], - ]; - $hasNewCert = true; - } else { - $hasNewCert = false; - } -*/ - /** @var array $certInfo */ -/* $certInfo = $this->cryptoUtils->loadPublicKey($idpmeta, true); - $availableCerts['idp.crt'] = $certInfo; - $keys[] = [ - 'type' => 'X509Certificate', - 'signing' => true, - 'encryption' => ($hasNewCert ? false : true), - 'X509Certificate' => $certInfo['certData'], - ]; - - if ($idpmeta->hasValue('https.certificate')) { -*/ - /** @var array $httpsCert */ -/* - $httpsCert = $this->cryptoUtils->loadPublicKey($idpmeta, true, 'https.'); - Assert::keyExists($httpsCert, 'certData'); - $availableCerts['https.crt'] = $httpsCert; - $keys[] = [ - 'type' => 'X509Certificate', - 'signing' => true, - 'encryption' => false, - 'X509Certificate' => $httpsCert['certData'], - ]; - } - - $adfs_service_location = Module::getModuleURL('adfs') . '/idp/prp.php'; - $metaArray = [ - 'metadata-set' => 'adfs-idp-remote', - 'entityid' => $idpentityid, - 'SingleSignOnService' => [ - 0 => [ - 'Binding' => C::BINDING_HTTP_REDIRECT, - 'Location' => $adfs_service_location, - ], - ], - 'SingleLogoutService' => [ - 0 => [ - 'Binding' => C::BINDING_HTTP_REDIRECT, - 'Location' => $adfs_service_location, - ], - ], - ]; - - if (count($keys) === 1) { - $metaArray['certData'] = $keys[0]['X509Certificate']; - } else { - $metaArray['keys'] = $keys; - } - - $metaArray['NameIDFormat'] = $idpmeta->getOptionalString( - 'NameIDFormat', - C::NAMEID_TRANSIENT, - ); - - if ($idpmeta->hasValue('OrganizationName')) { - $metaArray['OrganizationName'] = $idpmeta->getLocalizedString('OrganizationName'); - $metaArray['OrganizationDisplayName'] = $idpmeta->getOptionalLocalizedString( - 'OrganizationDisplayName', - $metaArray['OrganizationName'], - ); - - if (!$idpmeta->hasValue('OrganizationURL')) { - throw new SspError\Exception('If OrganizationName is set, OrganizationURL must also be set.'); - } - $metaArray['OrganizationURL'] = $idpmeta->getLocalizedString('OrganizationURL'); - } - - if ($idpmeta->hasValue('scope')) { - $metaArray['scope'] = $idpmeta->getArray('scope'); - } - - if ($idpmeta->hasValue('EntityAttributes')) { - $metaArray['EntityAttributes'] = $idpmeta->getArray('EntityAttributes'); - } - - if ($idpmeta->hasValue('UIInfo')) { - $metaArray['UIInfo'] = $idpmeta->getArray('UIInfo'); - } - - if ($idpmeta->hasValue('DiscoHints')) { - $metaArray['DiscoHints'] = $idpmeta->getArray('DiscoHints'); - } - - if ($idpmeta->hasValue('RegistrationInfo')) { - $metaArray['RegistrationInfo'] = $idpmeta->getArray('RegistrationInfo'); - } - - $metaBuilder = new Metadata\SAMLBuilder($idpentityid); - $metaBuilder->addSecurityTokenServiceType($metaArray); - $metaBuilder->addOrganizationInfo($metaArray); - $technicalContactEmail = $this->config->getOptionalString('technicalcontact_email', null); - if ($technicalContactEmail !== null && $technicalContactEmail !== 'na@example.org') { - $metaBuilder->addContact(Utils\Config\Metadata::getContact([ - 'emailAddress' => $technicalContactEmail, - 'givenName' => $this->config->getOptionalString('technicalcontact_name', null), - 'contactType' => 'technical', - ])); - } - $metaxml = $metaBuilder->getEntityDescriptorText(); -*/ - // sign the metadata if enabled -// $metaxml = Metadata\Signer::sign($metaxml, $idpmeta->toArray(), 'ADFS IdP'); - $document = $builder->buildDocument()->toXML(); - $document->ownerDocument->formatOutput = true; - $document->ownerDocument->encoding = 'UTF-8'; + $document = $builder->buildDocument()->toXML(); + // Some products like DirX are known to break on pretty-printed XML + $document->ownerDocument->formatOutput = false; + $document->ownerDocument->encoding = 'UTF-8'; -$metaxml = $document->ownerDocument->saveXML(); + $metaxml = $document->ownerDocument->saveXML(); $response = new Response(); $response->setEtag(hash('sha256', $metaxml)); diff --git a/src/IdP/ADFS.php b/src/IdP/ADFS.php index 05e9d32..94732e2 100644 --- a/src/IdP/ADFS.php +++ b/src/IdP/ADFS.php @@ -8,7 +8,6 @@ use DateTimeImmutable; use DateTimeZone; use Exception; -use SimpleSAML\Assert\Assert; use SimpleSAML\Configuration; use SimpleSAML\Error; use SimpleSAML\IdP; diff --git a/src/IdP/MetadataBuilder.php b/src/IdP/MetadataBuilder.php index 204d203..b4a1b42 100644 --- a/src/IdP/MetadataBuilder.php +++ b/src/IdP/MetadataBuilder.php @@ -13,12 +13,8 @@ use SimpleSAML\SAML2\XML\md\AbstractMetadataDocument; use SimpleSAML\SAML2\XML\md\ContactPerson; use SimpleSAML\SAML2\XML\md\EntityDescriptor; -use SimpleSAML\SAML2\XML\md\NameIDFormat; use SimpleSAML\SAML2\XML\md\KeyDescriptor; use SimpleSAML\SAML2\XML\md\Organization; -use SimpleSAML\SAML2\XML\md\RequestedAttribute; -use SimpleSAML\SAML2\XML\md\ServiceDescription; -use SimpleSAML\SAML2\XML\md\ServiceName; use SimpleSAML\XML\Chunk; use SimpleSAML\XMLSecurity\Alg\Signature\SignatureAlgorithmFactory; use SimpleSAML\XMLSecurity\Key\PrivateKey; @@ -54,7 +50,7 @@ class MetadataBuilder */ public function __construct( protected Configuration $config, - protected Configuration $metadata + protected Configuration $metadata, ) { $this->clock = LocalizedClock::in('Z'); } @@ -139,7 +135,10 @@ private function getOrganization(): ?Organization try { $org = Organization::fromArray([ 'OrganizationName' => $arrayUtils->arrayize($this->metadata->getArray('OrganizationName'), 'en'), - 'OrganizationDisplayName' => $arrayUtils->arrayize($this->metadata->getArray('OrganizationDisplayName'), 'en'), + 'OrganizationDisplayName' => $arrayUtils->arrayize( + $this->metadata->getArray('OrganizationDisplayName'), + 'en', + ), 'OrganizationURL' => $arrayUtils->arrayize($this->metadata->getArray('OrganizationURL'), 'en'), ]); } catch (ArrayValidationException $e) { @@ -208,10 +207,18 @@ private function getKeyDescriptor(): array continue; } if (!isset($key['signing']) || $key['signing'] === true) { - $keyDescriptor[] = self::buildKeyDescriptor('signing', $key['X509Certificate'], $key['name'] ?? null); + $keyDescriptor[] = self::buildKeyDescriptor( + 'signing', + $key['X509Certificate'], + $key['name'] ?? null, + ); } if (!isset($key['encryption']) || $key['encryption'] === true) { - $keyDescriptor[] = self::buildKeyDescriptor('encryption', $key['X509Certificate'], $key['name'] ?? null); + $keyDescriptor[] = self::buildKeyDescriptor( + 'encryption', + $key['X509Certificate'], + $key['name'] ?? null, + ); } } From f4aa39bc0f31d883a79e62d29a1f08eb8309486a Mon Sep 17 00:00:00 2001 From: Tim van Dijen Date: Tue, 3 Sep 2024 21:05:04 +0200 Subject: [PATCH 32/45] Fix syntax error --- src/IdP/MetadataBuilder.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/IdP/MetadataBuilder.php b/src/IdP/MetadataBuilder.php index c572608..b4a1b42 100644 --- a/src/IdP/MetadataBuilder.php +++ b/src/IdP/MetadataBuilder.php @@ -218,7 +218,7 @@ private function getKeyDescriptor(): array 'encryption', $key['X509Certificate'], $key['name'] ?? null, - )); + ); } } From 42572df182de47e432b5d6ecf518a090ece65a31 Mon Sep 17 00:00:00 2001 From: Tim van Dijen Date: Mon, 23 Sep 2024 15:11:18 +0200 Subject: [PATCH 33/45] Fix undeclared var --- composer.json | 5 +++-- src/IdP/ADFS.php | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 12ce0da..2d83773 100644 --- a/composer.json +++ b/composer.json @@ -19,7 +19,8 @@ "composer/package-versions-deprecated": true, "dealerdirect/phpcodesniffer-composer-installer": true, "phpstan/extension-installer": true, - "simplesamlphp/composer-module-installer": true + "simplesamlphp/composer-module-installer": true, + "simplesamlphp/composer-xmlprovider-installer": true } }, "autoload": { @@ -41,7 +42,7 @@ "simplesamlphp/assert": "^1.1", "simplesamlphp/saml11": "^1.0", "simplesamlphp/saml2": "^5@dev", - "simplesamlphp/simplesamlphp": "^2.3", + "simplesamlphp/simplesamlphp": "dev-feature/adfs-upgrade", "simplesamlphp/xml-common": "^1.16", "simplesamlphp/ws-security": "^1.0", "symfony/http-foundation": "^6.4" diff --git a/src/IdP/ADFS.php b/src/IdP/ADFS.php index 94732e2..5a1a482 100644 --- a/src/IdP/ADFS.php +++ b/src/IdP/ADFS.php @@ -63,6 +63,7 @@ public static function receiveAuthnRequest(Request $request, IdP $idp): Streamed Logger::info('ADFS - IdP.prp: Incoming Authentication request: ' . $issuer . ' id ' . $requestid); + $username = null; if ($request->query->has('username')) { $username = (string) $request->query->get('username'); } From 3c38b613897587d8a84035251b55cd2e78975ea9 Mon Sep 17 00:00:00 2001 From: Tim van Dijen Date: Mon, 23 Sep 2024 15:18:49 +0200 Subject: [PATCH 34/45] Use the correct version of WS addressing to mimic ADFS --- src/IdP/ADFS.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/IdP/ADFS.php b/src/IdP/ADFS.php index 5a1a482..dfcfc73 100644 --- a/src/IdP/ADFS.php +++ b/src/IdP/ADFS.php @@ -26,8 +26,8 @@ use SimpleSAML\SAML11\XML\saml\Subject; use SimpleSAML\SAML2\Constants as C; use SimpleSAML\Utils; -use SimpleSAML\WSSecurity\XML\wsa\Address; -use SimpleSAML\WSSecurity\XML\wsa\EndpointReference; +use SimpleSAML\WSSecurity\XML\wsa_200508\Address; +use SimpleSAML\WSSecurity\XML\wsa_200508\EndpointReference; use SimpleSAML\WSSecurity\XML\wsp\AppliesTo; use SimpleSAML\WSSecurity\XML\wst\RequestSecurityToken; use SimpleSAML\WSSecurity\XML\wst\RequestSecurityTokenResponse; From 0710f56e5e23e98459b25f0dbcf7d47388b5248f Mon Sep 17 00:00:00 2001 From: Tim van Dijen Date: Mon, 23 Sep 2024 15:26:53 +0200 Subject: [PATCH 35/45] Drop dependency on saml2-lib for constants --- src/IdP/ADFS.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/IdP/ADFS.php b/src/IdP/ADFS.php index dfcfc73..585090f 100644 --- a/src/IdP/ADFS.php +++ b/src/IdP/ADFS.php @@ -24,7 +24,7 @@ use SimpleSAML\SAML11\XML\saml\Conditions; use SimpleSAML\SAML11\XML\saml\NameIdentifier; use SimpleSAML\SAML11\XML\saml\Subject; -use SimpleSAML\SAML2\Constants as C; +use SimpleSAML\SAML11\Constants as C; use SimpleSAML\Utils; use SimpleSAML\WSSecurity\XML\wsa_200508\Address; use SimpleSAML\WSSecurity\XML\wsa_200508\EndpointReference; @@ -122,7 +122,7 @@ private static function generateAssertion( $now = new DateTimeImmutable('now', new DateTimeZone('Z')); if ($httpUtils->isHTTPS()) { - $method = C::AC_PASSWORD_PROTECTED_TRANSPORT; + $method = 'urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport'; } else { $method = C::AC_PASSWORD; } From bc8ea2bb391f2be7e08acd395fc83dad67916556 Mon Sep 17 00:00:00 2001 From: Tim van Dijen Date: Mon, 23 Sep 2024 15:34:42 +0200 Subject: [PATCH 36/45] Fix namespace --- src/IdP/MetadataBuilder.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/IdP/MetadataBuilder.php b/src/IdP/MetadataBuilder.php index b4a1b42..3d706a9 100644 --- a/src/IdP/MetadataBuilder.php +++ b/src/IdP/MetadataBuilder.php @@ -27,7 +27,7 @@ TokenTypesOffered, TokenType, }; -use SimpleSAML\WSSecurity\XML\wsa\{Address, EndpointReference}; +use SimpleSAML\WSSecurity\XML\wsa_200508\{Address, EndpointReference}; use function array_key_exists; use function preg_match; From f7717bd176b3e46d2bebc598d8aba2c89a8b4a23 Mon Sep 17 00:00:00 2001 From: Tim van Dijen Date: Wed, 2 Oct 2024 23:19:53 +0200 Subject: [PATCH 37/45] Update ws-security lib --- composer.json | 2 +- src/IdP/ADFS.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index 2d83773..e54c2fe 100644 --- a/composer.json +++ b/composer.json @@ -44,7 +44,7 @@ "simplesamlphp/saml2": "^5@dev", "simplesamlphp/simplesamlphp": "dev-feature/adfs-upgrade", "simplesamlphp/xml-common": "^1.16", - "simplesamlphp/ws-security": "^1.0", + "simplesamlphp/ws-security": "^1.6", "symfony/http-foundation": "^6.4" }, "require-dev": { diff --git a/src/IdP/ADFS.php b/src/IdP/ADFS.php index 585090f..30aebda 100644 --- a/src/IdP/ADFS.php +++ b/src/IdP/ADFS.php @@ -29,8 +29,8 @@ use SimpleSAML\WSSecurity\XML\wsa_200508\Address; use SimpleSAML\WSSecurity\XML\wsa_200508\EndpointReference; use SimpleSAML\WSSecurity\XML\wsp\AppliesTo; -use SimpleSAML\WSSecurity\XML\wst\RequestSecurityToken; -use SimpleSAML\WSSecurity\XML\wst\RequestSecurityTokenResponse; +use SimpleSAML\WSSecurity\XML\wst_200502\RequestSecurityToken; +use SimpleSAML\WSSecurity\XML\wst_200502\RequestSecurityTokenResponse; use SimpleSAML\XHTML\Template; use SimpleSAML\XMLSecurity\Alg\Signature\SignatureAlgorithmFactory; use SimpleSAML\XMLSecurity\Key\PrivateKey; From bb735c107976de7f4db79009ef88d4f726afd11e Mon Sep 17 00:00:00 2001 From: Tim van Dijen Date: Wed, 2 Oct 2024 23:30:53 +0200 Subject: [PATCH 38/45] Fix dependencies --- composer.json | 1 + src/IdP/MetadataBuilder.php | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/composer.json b/composer.json index e54c2fe..9cecdf1 100644 --- a/composer.json +++ b/composer.json @@ -44,6 +44,7 @@ "simplesamlphp/saml2": "^5@dev", "simplesamlphp/simplesamlphp": "dev-feature/adfs-upgrade", "simplesamlphp/xml-common": "^1.16", + "simplesamlphp/xml-security": "^1.9", "simplesamlphp/ws-security": "^1.6", "symfony/http-foundation": "^6.4" }, diff --git a/src/IdP/MetadataBuilder.php b/src/IdP/MetadataBuilder.php index 3d706a9..fea753e 100644 --- a/src/IdP/MetadataBuilder.php +++ b/src/IdP/MetadataBuilder.php @@ -13,8 +13,14 @@ use SimpleSAML\SAML2\XML\md\AbstractMetadataDocument; use SimpleSAML\SAML2\XML\md\ContactPerson; use SimpleSAML\SAML2\XML\md\EntityDescriptor; +use SimpleSAML\SAML2\XML\md\Extensions; use SimpleSAML\SAML2\XML\md\KeyDescriptor; use SimpleSAML\SAML2\XML\md\Organization; +use SimpleSAML\SAML2\XML\mdattr\EntityAttributes; +use SimpleSAML\SAML2\XML\mdrpi\RegistrationInfo; +use SimpleSAML\SAML2\XML\mdui\{DiscoHints, UIInfo}; +use SimpleSAML\SAML2\XML\saml\{Attribute, AttributeValue}; +use SimpleSAML\SAML2\XML\shibmd\Scope; use SimpleSAML\XML\Chunk; use SimpleSAML\XMLSecurity\Alg\Signature\SignatureAlgorithmFactory; use SimpleSAML\XMLSecurity\Key\PrivateKey; From 2741e5a3566352421f5a4eebe4077c3da5742585 Mon Sep 17 00:00:00 2001 From: Tim van Dijen Date: Wed, 2 Oct 2024 23:33:15 +0200 Subject: [PATCH 39/45] Fix codesniffer issue --- src/IdP/ADFS.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/IdP/ADFS.php b/src/IdP/ADFS.php index 30aebda..3636295 100644 --- a/src/IdP/ADFS.php +++ b/src/IdP/ADFS.php @@ -14,6 +14,7 @@ use SimpleSAML\Logger; use SimpleSAML\Metadata\MetaDataStorageHandler; use SimpleSAML\Module; +use SimpleSAML\SAML11\Constants as C; use SimpleSAML\SAML11\XML\saml\Assertion; use SimpleSAML\SAML11\XML\saml\Attribute; use SimpleSAML\SAML11\XML\saml\AttributeStatement; @@ -24,7 +25,6 @@ use SimpleSAML\SAML11\XML\saml\Conditions; use SimpleSAML\SAML11\XML\saml\NameIdentifier; use SimpleSAML\SAML11\XML\saml\Subject; -use SimpleSAML\SAML11\Constants as C; use SimpleSAML\Utils; use SimpleSAML\WSSecurity\XML\wsa_200508\Address; use SimpleSAML\WSSecurity\XML\wsa_200508\EndpointReference; From e82387a25775cc94091eafbe54e431616345fcaa Mon Sep 17 00:00:00 2001 From: Tim van Dijen Date: Wed, 2 Oct 2024 23:42:25 +0200 Subject: [PATCH 40/45] Fix constants --- src/IdP/MetadataBuilder.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/IdP/MetadataBuilder.php b/src/IdP/MetadataBuilder.php index fea753e..1c68dc3 100644 --- a/src/IdP/MetadataBuilder.php +++ b/src/IdP/MetadataBuilder.php @@ -183,7 +183,7 @@ public function getSecurityTokenService(): SecurityTokenServiceType $defaultEndpoint = Module::getModuleURL('adfs') . '/idp/prp.php'; return new SecurityTokenServiceType( - protocolSupportEnumeration: [C::NS_TRUST, C::NS_FED], + protocolSupportEnumeration: [C::NS_TRUST_200512, C::NS_TRUST_200502, C::NS_FED], keyDescriptors: $this->getKeyDescriptor(), tokenTypesOffered: new TokenTypesOffered([new TokenType('urn:oasis:names:tc:SAML:1.0:assertion')]), securityTokenServiceEndpoint: [ From fae939a543a1a60532dd1a33ffad235436a3cecb Mon Sep 17 00:00:00 2001 From: Tim van Dijen Date: Tue, 8 Oct 2024 22:23:50 +0200 Subject: [PATCH 41/45] Fix signed assertion --- src/IdP/ADFS.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/IdP/ADFS.php b/src/IdP/ADFS.php index 3636295..374916f 100644 --- a/src/IdP/ADFS.php +++ b/src/IdP/ADFS.php @@ -449,12 +449,12 @@ public static function sendResponse(array $state): void $algo = $idpMetadata->getOptionalString('signature.algorithm', C::SIG_RSA_SHA256); } $assertion = ADFS::signAssertion($assertion, $privateKeyFile, $certificateFile, $algo, $passphrase); + $assertion = Assertion::fromXML($assertion->toXML()); $requestSecurityToken = new RequestSecurityToken(null, [$assertion]); $appliesTo = new AppliesTo([new EndpointReference(new Address($spEntityId))]); $requestSecurityTokenResponse = new RequestSecurityTokenResponse(null, [$requestSecurityToken, $appliesTo]); - $xmlResponse = $requestSecurityTokenResponse->toXML(); $wresult = $xmlResponse->ownerDocument->saveXML($xmlResponse); $wctx = $state['adfs:wctx']; From eb2f7055995d38ecb9967b73fac36b30b33e4929 Mon Sep 17 00:00:00 2001 From: Tim van Dijen Date: Tue, 15 Oct 2024 23:29:13 +0200 Subject: [PATCH 42/45] Don't try to sign if we don't have keys --- src/IdP/ADFS.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/IdP/ADFS.php b/src/IdP/ADFS.php index 94732e2..cc838f0 100644 --- a/src/IdP/ADFS.php +++ b/src/IdP/ADFS.php @@ -447,7 +447,10 @@ public static function sendResponse(array $state): void if ($algo === null) { $algo = $idpMetadata->getOptionalString('signature.algorithm', C::SIG_RSA_SHA256); } - $assertion = ADFS::signAssertion($assertion, $privateKeyFile, $certificateFile, $algo, $passphrase); + + if ($privateKeyFile !== null && $certificateFile !== null && $algo !== null) { + $assertion = ADFS::signAssertion($assertion, $privateKeyFile, $certificateFile, $algo, $passphrase); + } $requestSecurityToken = new RequestSecurityToken(null, [$assertion]); $appliesTo = new AppliesTo([new EndpointReference(new Address($spEntityId))]); From 8f9a9d4f8efe7fca01027cfa6dbe5837159a6692 Mon Sep 17 00:00:00 2001 From: Tim van Dijen Date: Fri, 18 Oct 2024 00:57:38 +0200 Subject: [PATCH 43/45] Fix --- src/IdP/ADFS.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/IdP/ADFS.php b/src/IdP/ADFS.php index bb7a679..450f1ee 100644 --- a/src/IdP/ADFS.php +++ b/src/IdP/ADFS.php @@ -440,8 +440,8 @@ public static function sendResponse(array $state): void $assertion = ADFS::generateAssertion($idpEntityId, $spEntityId, $nameid, $attributes, $assertionLifetime); $configUtils = new Utils\Config(); - $privateKeyFile = $configUtils->getCertPath($idpMetadata->getString('privatekey')); - $certificateFile = $configUtils->getCertPath($idpMetadata->getString('certificate')); + $privateKeyFile = $configUtils->getCertPath($idpMetadata->getOptionalString('privatekey', null)); + $certificateFile = $configUtils->getCertPath($idpMetadata->getOptionalString('certificate', null)); $passphrase = $idpMetadata->getOptionalString('privatekey_pass', null); $algo = $spMetadata->getOptionalString('signature.algorithm', null); From 9612d64d3320de3109efb80d18a2b06b32567cc5 Mon Sep 17 00:00:00 2001 From: Tim van Dijen Date: Tue, 22 Oct 2024 10:28:34 +0200 Subject: [PATCH 44/45] Fix --- src/IdP/ADFS.php | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/IdP/ADFS.php b/src/IdP/ADFS.php index 450f1ee..790205d 100644 --- a/src/IdP/ADFS.php +++ b/src/IdP/ADFS.php @@ -439,17 +439,20 @@ public static function sendResponse(array $state): void $assertion = ADFS::generateAssertion($idpEntityId, $spEntityId, $nameid, $attributes, $assertionLifetime); - $configUtils = new Utils\Config(); - $privateKeyFile = $configUtils->getCertPath($idpMetadata->getOptionalString('privatekey', null)); - $certificateFile = $configUtils->getCertPath($idpMetadata->getOptionalString('certificate', null)); - $passphrase = $idpMetadata->getOptionalString('privatekey_pass', null); - - $algo = $spMetadata->getOptionalString('signature.algorithm', null); - if ($algo === null) { - $algo = $idpMetadata->getOptionalString('signature.algorithm', C::SIG_RSA_SHA256); - } + $privateKeyCfg = $idpMetadata->getOptionalString('privatekey', null); + $certificateCfg = $idpMetadata->getOptionalString('certificate', null); + + if ($privateKeyCfg !== null && $certificateCfg !== null) { + $configUtils = new Utils\Config(); + $privateKeyFile = $configUtils->getCertPath($privateKeyCfg); + $certificateFile = $configUtils->getCertPath($certificateCfg); + $passphrase = $idpMetadata->getOptionalString('privatekey_pass', null); + + $algo = $spMetadata->getOptionalString('signature.algorithm', null); + if ($algo === null) { + $algo = $idpMetadata->getOptionalString('signature.algorithm', C::SIG_RSA_SHA256); + } - if ($privateKeyFile !== null && $certificateFile !== null && $algo !== null) { $assertion = ADFS::signAssertion($assertion, $privateKeyFile, $certificateFile, $algo, $passphrase); $assertion = Assertion::fromXML($assertion->toXML()); } From 0be6586b2a71a25b4968d31e3836247516bae1d6 Mon Sep 17 00:00:00 2001 From: Tim van Dijen Date: Tue, 22 Oct 2024 16:28:01 +0200 Subject: [PATCH 45/45] Allow both active and passive flow (GET or POST) --- routing/routes/routes.yml | 4 ++-- src/Controller/Adfs.php | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/routing/routes/routes.yml b/routing/routes/routes.yml index 01d2fc8..c03345d 100644 --- a/routing/routes/routes.yml +++ b/routing/routes/routes.yml @@ -12,7 +12,7 @@ adfs-prp: defaults: { _controller: 'SimpleSAML\Module\adfs\Controller\Adfs::prp' } - methods: [GET] + methods: [GET, POST] adfs-metadata-legacy: path: /idp/metadata.php @@ -26,4 +26,4 @@ adfs-prp-legacy: defaults: { _controller: 'SimpleSAML\Module\adfs\Controller\Adfs::prp' } - methods: [GET] + methods: [GET, POST] diff --git a/src/Controller/Adfs.php b/src/Controller/Adfs.php index f733fd8..df999e6 100644 --- a/src/Controller/Adfs.php +++ b/src/Controller/Adfs.php @@ -108,8 +108,8 @@ public function prp(Request $request): Response $idpEntityId = $this->metadata->getMetaDataCurrentEntityID('adfs-idp-hosted'); $idp = IdP::getById('adfs:' . $idpEntityId); - if ($request->query->has('wa')) { - $wa = $request->query->get('wa'); + if ($request->get('wa', null) !== null) { + $wa = $request->get('wa'); if ($wa === 'wsignout1.0') { return new StreamedResponse( function () use ($idp) { @@ -120,12 +120,12 @@ function () use ($idp) { return ADFS_IDP::receiveAuthnRequest($request, $idp); } throw new SspError\BadRequest("Unsupported value for 'wa' specified in request."); - } elseif ($request->query->has('assocId')) { + } elseif ($request->get('assocId', null) !== null) { // logout response from ADFS SP // Association ID of the SP that sent the logout response - $assocId = $request->query->get('assocId'); + $assocId = $request->get('assocId'); // Data that was sent in the logout request to the SP. Can be null - $relayState = $request->query->get('relayState'); + $relayState = $request->get('relayState'); // null on success, or an instance of a \SimpleSAML\Error\Exception on failure. $logoutError = null;