diff --git a/composer.json b/composer.json index ad83957..d7e47e8 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": { @@ -36,18 +37,20 @@ "php": "^8.1", "ext-dom": "*", - "robrichards/xmlseclibs": "^3.1", - "simplesamlphp/assert": "^1.0", + "beste/clock": "^3.0", + "psr/clock": "^1.0", + "simplesamlphp/assert": "^1.1", "simplesamlphp/saml11": "^1.0", "simplesamlphp/saml2": "^5@dev", - "simplesamlphp/simplesamlphp": "^2.0", - "simplesamlphp/xml-common": "^1.12", - "simplesamlphp/ws-security": "^1.0", + "simplesamlphp/simplesamlphp": "dev-feature/adfs-upgrade", + "simplesamlphp/ws-security": "^1.6", + "simplesamlphp/xml-common": "^1.16", + "simplesamlphp/xml-security": "^1.9", "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", 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 @@ +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' => Constants::BINDING_HTTP_REDIRECT, - 'Location' => $adfs_service_location, - ], - ], - 'SingleLogoutService' => [ - 0 => [ - 'Binding' => Constants::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', - Constants::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'); - } + $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'; - 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'); + $metaxml = $document->ownerDocument->saveXML(); $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; } @@ -229,10 +106,10 @@ 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'); + if ($request->get('wa', null) !== null) { + $wa = $request->get('wa'); if ($wa === 'wsignout1.0') { return new StreamedResponse( function () use ($idp) { @@ -243,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; diff --git a/src/IdP/ADFS.php b/src/IdP/ADFS.php index 22492aa..790205d 100644 --- a/src/IdP/ADFS.php +++ b/src/IdP/ADFS.php @@ -4,22 +4,47 @@ namespace SimpleSAML\Module\adfs\IdP; +use DateInterval; +use DateTimeImmutable; +use DateTimeZone; use Exception; -use RobRichards\XMLSecLibs\XMLSecurityDSig; -use RobRichards\XMLSecLibs\XMLSecurityKey; use SimpleSAML\Configuration; use SimpleSAML\Error; use SimpleSAML\IdP; use SimpleSAML\Logger; use SimpleSAML\Metadata\MetaDataStorageHandler; use SimpleSAML\Module; -use SimpleSAML\SAML2\Constants; +use SimpleSAML\SAML11\Constants as C; +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\Utils; +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_200502\RequestSecurityToken; +use SimpleSAML\WSSecurity\XML\wst_200502\RequestSecurityTokenResponse; use SimpleSAML\XHTML\Template; -use SimpleSAML\XML\DOMDocumentFactory; +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 Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\StreamedResponse; +use function base64_encode; +use function chunk_split; +use function trim; + class ADFS { /** @@ -38,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'); } @@ -74,52 +100,49 @@ 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, array $attributes, int $assertionLifetime, - ): string { + ): Assertion { $httpUtils = new Utils\HTTP(); $randomUtils = new Utils\Random(); $timeUtils = new Utils\Time(); $issueInstant = $timeUtils->generateTimestamp(); - $notBefore = $timeUtils->generateTimestamp(time() - 30); - $assertionExpire = $timeUtils->generateTimestamp(time() + $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); + $now = new DateTimeImmutable('now', new DateTimeZone('Z')); if ($httpUtils->isHTTPS()) { - $method = Constants::AC_PASSWORD_PROTECTED_TRANSPORT; + $method = 'urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport'; } else { - $method = Constants::AC_PASSWORD; + $method = C::AC_PASSWORD; } - $result = << - - - - - $target - - - - - $nameid - - - - - $nameid - -MSG; + $audience = new Audience($target); + $audienceRestrictionCondition = new AudienceRestrictionCondition([$audience]); + $conditions = new Conditions( + [$audienceRestrictionCondition], + [], + [], + $now->sub($notBefore), + $now->add($notOnOrAfter), + ); + + $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 +153,64 @@ 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, $attrs); + + 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."); - } + ): Assertion { + $key = PrivateKey::fromFile($key, $passphrase); + $pubkey = PublicKey::fromFile($cert); + $keyInfo = new KeyInfo([ + new X509Data( + [new X509Certificate( + trim(chunk_split(base64_encode($pubkey->getPEM()->data()))), + )], + ), + ]); - $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(); + $assertion->sign($signer, C::C14N_EXCLUSIVE_WITHOUT_COMMENTS, $keyInfo); + return $assertion; } @@ -235,32 +236,68 @@ 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. - * + * @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 */ - 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' => [], ]; @@ -400,19 +437,32 @@ public static function sendResponse(array $state): void $assertionLifetime = $idpMetadata->getOptionalInteger('assertion.lifetime', 300); } - $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')); - $certificateFile = $configUtils->getCertPath($idpMetadata->getString('certificate')); - $passphrase = $idpMetadata->getOptionalString('privatekey_pass', null); + $privateKeyCfg = $idpMetadata->getOptionalString('privatekey', null); + $certificateCfg = $idpMetadata->getOptionalString('certificate', null); - $algo = $spMetadata->getOptionalString('signature.algorithm', null); - if ($algo === null) { - $algo = $idpMetadata->getOptionalString('signature.algorithm', XMLSecurityKey::RSA_SHA256); + 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); + } + + $assertion = ADFS::signAssertion($assertion, $privateKeyFile, $certificateFile, $algo, $passphrase); + $assertion = Assertion::fromXML($assertion->toXML()); } - $wresult = ADFS::signResponse($response, $privateKeyFile, $certificateFile, $algo, $passphrase); + $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']; $wreply = $state['adfs:wreply'] ? : $spMetadata->getValue('prp'); ADFS::postResponse($wreply, $wresult, $wctx); diff --git a/src/IdP/MetadataBuilder.php b/src/IdP/MetadataBuilder.php new file mode 100644 index 0000000..1c68dc3 --- /dev/null +++ b/src/IdP/MetadataBuilder.php @@ -0,0 +1,353 @@ +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_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: [ + 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, + ); + } +} 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; - } -} 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); } }