1<?php 2 3use RobRichards\XMLSecLibs\XMLSecurityKey; 4use SAML2\SOAP; 5 6/** 7 * IdP implementation for SAML 2.0 protocol. 8 * 9 * @package SimpleSAMLphp 10 */ 11class sspmod_saml_IdP_SAML2 12{ 13 14 /** 15 * Send a response to the SP. 16 * 17 * @param array $state The authentication state. 18 */ 19 public static function sendResponse(array $state) 20 { 21 assert(isset($state['Attributes'])); 22 assert(isset($state['SPMetadata'])); 23 assert(isset($state['saml:ConsumerURL'])); 24 assert(array_key_exists('saml:RequestId', $state)); // Can be NULL 25 assert(array_key_exists('saml:RelayState', $state)); // Can be NULL. 26 27 $spMetadata = $state["SPMetadata"]; 28 $spEntityId = $spMetadata['entityid']; 29 $spMetadata = SimpleSAML_Configuration::loadFromArray( 30 $spMetadata, 31 '$metadata['.var_export($spEntityId, true).']' 32 ); 33 34 SimpleSAML\Logger::info('Sending SAML 2.0 Response to '.var_export($spEntityId, true)); 35 36 $requestId = $state['saml:RequestId']; 37 $relayState = $state['saml:RelayState']; 38 $consumerURL = $state['saml:ConsumerURL']; 39 $protocolBinding = $state['saml:Binding']; 40 41 $idp = SimpleSAML_IdP::getByState($state); 42 43 $idpMetadata = $idp->getConfig(); 44 45 $assertion = self::buildAssertion($idpMetadata, $spMetadata, $state); 46 47 if (isset($state['saml:AuthenticatingAuthority'])) { 48 $assertion->setAuthenticatingAuthority($state['saml:AuthenticatingAuthority']); 49 } 50 51 // create the session association (for logout) 52 $association = array( 53 'id' => 'saml:'.$spEntityId, 54 'Handler' => 'sspmod_saml_IdP_SAML2', 55 'Expires' => $assertion->getSessionNotOnOrAfter(), 56 'saml:entityID' => $spEntityId, 57 'saml:NameID' => $state['saml:idp:NameID'], 58 'saml:SessionIndex' => $assertion->getSessionIndex(), 59 ); 60 61 // maybe encrypt the assertion 62 $assertion = self::encryptAssertion($idpMetadata, $spMetadata, $assertion); 63 64 // create the response 65 $ar = self::buildResponse($idpMetadata, $spMetadata, $consumerURL); 66 $ar->setInResponseTo($requestId); 67 $ar->setRelayState($relayState); 68 $ar->setAssertions(array($assertion)); 69 70 // register the session association with the IdP 71 $idp->addAssociation($association); 72 73 $statsData = array( 74 'spEntityID' => $spEntityId, 75 'idpEntityID' => $idpMetadata->getString('entityid'), 76 'protocol' => 'saml2', 77 ); 78 if (isset($state['saml:AuthnRequestReceivedAt'])) { 79 $statsData['logintime'] = microtime(true) - $state['saml:AuthnRequestReceivedAt']; 80 } 81 SimpleSAML_Stats::log('saml:idp:Response', $statsData); 82 83 // send the response 84 $binding = \SAML2\Binding::getBinding($protocolBinding); 85 $binding->send($ar); 86 } 87 88 89 /** 90 * Handle authentication error. 91 * 92 * SimpleSAML_Error_Exception $exception The exception. 93 * 94 * @param array $state The error state. 95 */ 96 public static function handleAuthError(SimpleSAML_Error_Exception $exception, array $state) 97 { 98 assert(isset($state['SPMetadata'])); 99 assert(isset($state['saml:ConsumerURL'])); 100 assert(array_key_exists('saml:RequestId', $state)); // Can be NULL. 101 assert(array_key_exists('saml:RelayState', $state)); // Can be NULL. 102 103 $spMetadata = $state["SPMetadata"]; 104 $spEntityId = $spMetadata['entityid']; 105 $spMetadata = SimpleSAML_Configuration::loadFromArray( 106 $spMetadata, 107 '$metadata['.var_export($spEntityId, true).']' 108 ); 109 110 $requestId = $state['saml:RequestId']; 111 $relayState = $state['saml:RelayState']; 112 $consumerURL = $state['saml:ConsumerURL']; 113 $protocolBinding = $state['saml:Binding']; 114 115 $idp = SimpleSAML_IdP::getByState($state); 116 117 $idpMetadata = $idp->getConfig(); 118 119 $error = sspmod_saml_Error::fromException($exception); 120 121 SimpleSAML\Logger::warning("Returning error to SP with entity ID '".var_export($spEntityId, true)."'."); 122 $exception->log(SimpleSAML\Logger::WARNING); 123 124 $ar = self::buildResponse($idpMetadata, $spMetadata, $consumerURL); 125 $ar->setInResponseTo($requestId); 126 $ar->setRelayState($relayState); 127 128 $status = array( 129 'Code' => $error->getStatus(), 130 'SubCode' => $error->getSubStatus(), 131 'Message' => $error->getStatusMessage(), 132 ); 133 $ar->setStatus($status); 134 135 $statsData = array( 136 'spEntityID' => $spEntityId, 137 'idpEntityID' => $idpMetadata->getString('entityid'), 138 'protocol' => 'saml2', 139 'error' => $status, 140 ); 141 if (isset($state['saml:AuthnRequestReceivedAt'])) { 142 $statsData['logintime'] = microtime(true) - $state['saml:AuthnRequestReceivedAt']; 143 } 144 SimpleSAML_Stats::log('saml:idp:Response:error', $statsData); 145 146 $binding = \SAML2\Binding::getBinding($protocolBinding); 147 $binding->send($ar); 148 } 149 150 151 /** 152 * Find SP AssertionConsumerService based on parameter in AuthnRequest. 153 * 154 * @param array $supportedBindings The bindings we allow for the response. 155 * @param SimpleSAML_Configuration $spMetadata The metadata for the SP. 156 * @param string|NULL $AssertionConsumerServiceURL AssertionConsumerServiceURL from request. 157 * @param string|NULL $ProtocolBinding ProtocolBinding from request. 158 * @param int|NULL $AssertionConsumerServiceIndex AssertionConsumerServiceIndex from request. 159 * 160 * @return array Array with the Location and Binding we should use for the response. 161 */ 162 private static function getAssertionConsumerService( 163 array $supportedBindings, 164 SimpleSAML_Configuration $spMetadata, 165 $AssertionConsumerServiceURL, 166 $ProtocolBinding, 167 $AssertionConsumerServiceIndex 168 ) { 169 assert(is_string($AssertionConsumerServiceURL) || $AssertionConsumerServiceURL === null); 170 assert(is_string($ProtocolBinding) || $ProtocolBinding === null); 171 assert(is_int($AssertionConsumerServiceIndex) || $AssertionConsumerServiceIndex === null); 172 173 /* We want to pick the best matching endpoint in the case where for example 174 * only the ProtocolBinding is given. We therefore pick endpoints with the 175 * following priority: 176 * 1. isDefault="true" 177 * 2. isDefault unset 178 * 3. isDefault="false" 179 */ 180 $firstNotFalse = null; 181 $firstFalse = null; 182 foreach ($spMetadata->getEndpoints('AssertionConsumerService') as $ep) { 183 if ($AssertionConsumerServiceURL !== null && $ep['Location'] !== $AssertionConsumerServiceURL) { 184 continue; 185 } 186 if ($ProtocolBinding !== null && $ep['Binding'] !== $ProtocolBinding) { 187 continue; 188 } 189 if ($AssertionConsumerServiceIndex !== null && $ep['index'] !== $AssertionConsumerServiceIndex) { 190 continue; 191 } 192 193 if (!in_array($ep['Binding'], $supportedBindings, true)) { 194 /* The endpoint has an unsupported binding. */ 195 continue; 196 } 197 198 // we have an endpoint that matches all our requirements. Check if it is the best one 199 200 if (array_key_exists('isDefault', $ep)) { 201 if ($ep['isDefault'] === true) { 202 // this is the first matching endpoint with isDefault set to true 203 return $ep; 204 } 205 // isDefault is set to FALSE, but the endpoint is still usable 206 if ($firstFalse === null) { 207 // this is the first endpoint that we can use 208 $firstFalse = $ep; 209 } 210 } else { 211 if ($firstNotFalse === null) { 212 // this is the first endpoint without isDefault set 213 $firstNotFalse = $ep; 214 } 215 } 216 } 217 218 if ($firstNotFalse !== null) { 219 return $firstNotFalse; 220 } elseif ($firstFalse !== null) { 221 return $firstFalse; 222 } 223 224 SimpleSAML\Logger::warning('Authentication request specifies invalid AssertionConsumerService:'); 225 if ($AssertionConsumerServiceURL !== null) { 226 SimpleSAML\Logger::warning('AssertionConsumerServiceURL: '.var_export($AssertionConsumerServiceURL, true)); 227 } 228 if ($ProtocolBinding !== null) { 229 SimpleSAML\Logger::warning('ProtocolBinding: '.var_export($ProtocolBinding, true)); 230 } 231 if ($AssertionConsumerServiceIndex !== null) { 232 SimpleSAML\Logger::warning( 233 'AssertionConsumerServiceIndex: '.var_export($AssertionConsumerServiceIndex, true) 234 ); 235 } 236 237 // we have no good endpoints. Our last resort is to just use the default endpoint 238 return $spMetadata->getDefaultEndpoint('AssertionConsumerService', $supportedBindings); 239 } 240 241 242 /** 243 * Receive an authentication request. 244 * 245 * @param SimpleSAML_IdP $idp The IdP we are receiving it for. 246 * @throws SimpleSAML_Error_BadRequest In case an error occurs when trying to receive the request. 247 */ 248 public static function receiveAuthnRequest(SimpleSAML_IdP $idp) 249 { 250 251 $metadata = SimpleSAML_Metadata_MetaDataStorageHandler::getMetadataHandler(); 252 $idpMetadata = $idp->getConfig(); 253 254 $supportedBindings = array(\SAML2\Constants::BINDING_HTTP_POST); 255 if ($idpMetadata->getBoolean('saml20.sendartifact', false)) { 256 $supportedBindings[] = \SAML2\Constants::BINDING_HTTP_ARTIFACT; 257 } 258 if ($idpMetadata->getBoolean('saml20.hok.assertion', false)) { 259 $supportedBindings[] = \SAML2\Constants::BINDING_HOK_SSO; 260 } 261 if ($idpMetadata->getBoolean('saml20.ecp', false)) { 262 $supportedBindings[] = \SAML2\Constants::BINDING_PAOS; 263 } 264 265 if (isset($_REQUEST['spentityid'])) { 266 /* IdP initiated authentication. */ 267 268 if (isset($_REQUEST['cookieTime'])) { 269 $cookieTime = (int) $_REQUEST['cookieTime']; 270 if ($cookieTime + 5 > time()) { 271 /* 272 * Less than five seconds has passed since we were 273 * here the last time. Cookies are probably disabled. 274 */ 275 \SimpleSAML\Utils\HTTP::checkSessionCookie(\SimpleSAML\Utils\HTTP::getSelfURL()); 276 } 277 } 278 279 $spEntityId = (string) $_REQUEST['spentityid']; 280 $spMetadata = $metadata->getMetaDataConfig($spEntityId, 'saml20-sp-remote'); 281 282 if (isset($_REQUEST['RelayState'])) { 283 $relayState = (string) $_REQUEST['RelayState']; 284 } else { 285 $relayState = null; 286 } 287 288 if (isset($_REQUEST['binding'])) { 289 $protocolBinding = (string) $_REQUEST['binding']; 290 } else { 291 $protocolBinding = null; 292 } 293 294 if (isset($_REQUEST['NameIDFormat'])) { 295 $nameIDFormat = (string) $_REQUEST['NameIDFormat']; 296 } else { 297 $nameIDFormat = null; 298 } 299 300 $requestId = null; 301 $IDPList = array(); 302 $ProxyCount = null; 303 $RequesterID = null; 304 $forceAuthn = false; 305 $isPassive = false; 306 $consumerURL = null; 307 $consumerIndex = null; 308 $extensions = null; 309 $allowCreate = true; 310 $authnContext = null; 311 $binding = null; 312 313 $idpInit = true; 314 315 SimpleSAML\Logger::info( 316 'SAML2.0 - IdP.SSOService: IdP initiated authentication: '.var_export($spEntityId, true) 317 ); 318 } else { 319 $binding = \SAML2\Binding::getCurrentBinding(); 320 $request = $binding->receive(); 321 322 if (!($request instanceof \SAML2\AuthnRequest)) { 323 throw new SimpleSAML_Error_BadRequest( 324 'Message received on authentication request endpoint wasn\'t an authentication request.' 325 ); 326 } 327 328 $spEntityId = $request->getIssuer(); 329 if ($spEntityId === null) { 330 throw new SimpleSAML_Error_BadRequest( 331 'Received message on authentication request endpoint without issuer.' 332 ); 333 } 334 $spMetadata = $metadata->getMetaDataConfig($spEntityId, 'saml20-sp-remote'); 335 336 sspmod_saml_Message::validateMessage($spMetadata, $idpMetadata, $request); 337 338 $relayState = $request->getRelayState(); 339 340 $requestId = $request->getId(); 341 $IDPList = $request->getIDPList(); 342 $ProxyCount = $request->getProxyCount(); 343 if ($ProxyCount !== null) { 344 $ProxyCount--; 345 } 346 $RequesterID = $request->getRequesterID(); 347 $forceAuthn = $request->getForceAuthn(); 348 $isPassive = $request->getIsPassive(); 349 $consumerURL = $request->getAssertionConsumerServiceURL(); 350 $protocolBinding = $request->getProtocolBinding(); 351 $consumerIndex = $request->getAssertionConsumerServiceIndex(); 352 $extensions = $request->getExtensions(); 353 $authnContext = $request->getRequestedAuthnContext(); 354 355 $nameIdPolicy = $request->getNameIdPolicy(); 356 if (isset($nameIdPolicy['Format'])) { 357 $nameIDFormat = $nameIdPolicy['Format']; 358 } else { 359 $nameIDFormat = null; 360 } 361 if (isset($nameIdPolicy['AllowCreate'])) { 362 $allowCreate = $nameIdPolicy['AllowCreate']; 363 } else { 364 $allowCreate = false; 365 } 366 367 $idpInit = false; 368 369 SimpleSAML\Logger::info( 370 'SAML2.0 - IdP.SSOService: incoming authentication request: '.var_export($spEntityId, true) 371 ); 372 } 373 374 SimpleSAML_Stats::log('saml:idp:AuthnRequest', array( 375 'spEntityID' => $spEntityId, 376 'idpEntityID' => $idpMetadata->getString('entityid'), 377 'forceAuthn' => $forceAuthn, 378 'isPassive' => $isPassive, 379 'protocol' => 'saml2', 380 'idpInit' => $idpInit, 381 )); 382 383 $acsEndpoint = self::getAssertionConsumerService( 384 $supportedBindings, 385 $spMetadata, 386 $consumerURL, 387 $protocolBinding, 388 $consumerIndex 389 ); 390 391 $IDPList = array_unique(array_merge($IDPList, $spMetadata->getArrayizeString('IDPList', array()))); 392 if ($ProxyCount === null) { 393 $ProxyCount = $spMetadata->getInteger('ProxyCount', null); 394 } 395 396 if (!$forceAuthn) { 397 $forceAuthn = $spMetadata->getBoolean('ForceAuthn', false); 398 } 399 400 $sessionLostParams = array( 401 'spentityid' => $spEntityId, 402 'cookieTime' => time(), 403 ); 404 if ($relayState !== null) { 405 $sessionLostParams['RelayState'] = $relayState; 406 } 407 408 $sessionLostURL = \SimpleSAML\Utils\HTTP::addURLParameters( 409 \SimpleSAML\Utils\HTTP::getSelfURLNoQuery(), 410 $sessionLostParams 411 ); 412 413 $state = array( 414 'Responder' => array('sspmod_saml_IdP_SAML2', 'sendResponse'), 415 SimpleSAML_Auth_State::EXCEPTION_HANDLER_FUNC => array('sspmod_saml_IdP_SAML2', 'handleAuthError'), 416 SimpleSAML_Auth_State::RESTART => $sessionLostURL, 417 418 'SPMetadata' => $spMetadata->toArray(), 419 'saml:RelayState' => $relayState, 420 'saml:RequestId' => $requestId, 421 'saml:IDPList' => $IDPList, 422 'saml:ProxyCount' => $ProxyCount, 423 'saml:RequesterID' => $RequesterID, 424 'ForceAuthn' => $forceAuthn, 425 'isPassive' => $isPassive, 426 'saml:ConsumerURL' => $acsEndpoint['Location'], 427 'saml:Binding' => $acsEndpoint['Binding'], 428 'saml:NameIDFormat' => $nameIDFormat, 429 'saml:AllowCreate' => $allowCreate, 430 'saml:Extensions' => $extensions, 431 'saml:AuthnRequestReceivedAt' => microtime(true), 432 'saml:RequestedAuthnContext' => $authnContext, 433 ); 434 435 // ECP AuthnRequests need to supply credentials 436 if ($binding instanceof SOAP) { 437 self::processSOAPAuthnRequest($state); 438 } 439 440 $idp->handleAuthenticationRequest($state); 441 } 442 443 public static function processSOAPAuthnRequest(array &$state) 444 { 445 if (!isset($_SERVER['PHP_AUTH_USER']) || !isset($_SERVER['PHP_AUTH_PW'])) { 446 SimpleSAML\Logger::error("ECP AuthnRequest did not contain Basic Authentication header"); 447 // TODO Throw some sort of ECP-specific exception / convert this to SOAP fault 448 throw new SimpleSAML_Error_Error("WRONGUSERPASS"); 449 } 450 451 $state['core:auth:username'] = $_SERVER['PHP_AUTH_USER']; 452 $state['core:auth:password'] = $_SERVER['PHP_AUTH_PW']; 453 } 454 455 /** 456 * Send a logout request to a given association. 457 * 458 * @param SimpleSAML_IdP $idp The IdP we are sending a logout request from. 459 * @param array $association The association that should be terminated. 460 * @param string|NULL $relayState An id that should be carried across the logout. 461 */ 462 public static function sendLogoutRequest(SimpleSAML_IdP $idp, array $association, $relayState) 463 { 464 assert(is_string($relayState) || $relayState === null); 465 466 SimpleSAML\Logger::info('Sending SAML 2.0 LogoutRequest to: '.var_export($association['saml:entityID'], true)); 467 468 $metadata = SimpleSAML_Metadata_MetaDataStorageHandler::getMetadataHandler(); 469 $idpMetadata = $idp->getConfig(); 470 $spMetadata = $metadata->getMetaDataConfig($association['saml:entityID'], 'saml20-sp-remote'); 471 472 SimpleSAML_Stats::log('saml:idp:LogoutRequest:sent', array( 473 'spEntityID' => $association['saml:entityID'], 474 'idpEntityID' => $idpMetadata->getString('entityid'), 475 )); 476 477 $dst = $spMetadata->getEndpointPrioritizedByBinding( 478 'SingleLogoutService', 479 array( 480 \SAML2\Constants::BINDING_HTTP_REDIRECT, 481 \SAML2\Constants::BINDING_HTTP_POST 482 ) 483 ); 484 $binding = \SAML2\Binding::getBinding($dst['Binding']); 485 $lr = self::buildLogoutRequest($idpMetadata, $spMetadata, $association, $relayState); 486 $lr->setDestination($dst['Location']); 487 488 $binding->send($lr); 489 } 490 491 492 /** 493 * Send a logout response. 494 * 495 * @param SimpleSAML_IdP $idp The IdP we are sending a logout request from. 496 * @param array &$state The logout state array. 497 */ 498 public static function sendLogoutResponse(SimpleSAML_IdP $idp, array $state) 499 { 500 assert(isset($state['saml:SPEntityId'])); 501 assert(isset($state['saml:RequestId'])); 502 assert(array_key_exists('saml:RelayState', $state)); // Can be NULL. 503 504 $spEntityId = $state['saml:SPEntityId']; 505 506 $metadata = SimpleSAML_Metadata_MetaDataStorageHandler::getMetadataHandler(); 507 $idpMetadata = $idp->getConfig(); 508 $spMetadata = $metadata->getMetaDataConfig($spEntityId, 'saml20-sp-remote'); 509 510 $lr = sspmod_saml_Message::buildLogoutResponse($idpMetadata, $spMetadata); 511 $lr->setInResponseTo($state['saml:RequestId']); 512 $lr->setRelayState($state['saml:RelayState']); 513 514 if (isset($state['core:Failed']) && $state['core:Failed']) { 515 $partial = true; 516 $lr->setStatus(array( 517 'Code' => \SAML2\Constants::STATUS_SUCCESS, 518 'SubCode' => \SAML2\Constants::STATUS_PARTIAL_LOGOUT, 519 )); 520 SimpleSAML\Logger::info('Sending logout response for partial logout to SP '.var_export($spEntityId, true)); 521 } else { 522 $partial = false; 523 SimpleSAML\Logger::debug('Sending logout response to SP '.var_export($spEntityId, true)); 524 } 525 526 SimpleSAML_Stats::log('saml:idp:LogoutResponse:sent', array( 527 'spEntityID' => $spEntityId, 528 'idpEntityID' => $idpMetadata->getString('entityid'), 529 'partial' => $partial 530 )); 531 $dst = $spMetadata->getEndpointPrioritizedByBinding( 532 'SingleLogoutService', 533 array( 534 \SAML2\Constants::BINDING_HTTP_REDIRECT, 535 \SAML2\Constants::BINDING_HTTP_POST 536 ) 537 ); 538 $binding = \SAML2\Binding::getBinding($dst['Binding']); 539 if (isset($dst['ResponseLocation'])) { 540 $dst = $dst['ResponseLocation']; 541 } else { 542 $dst = $dst['Location']; 543 } 544 $lr->setDestination($dst); 545 546 $binding->send($lr); 547 } 548 549 550 /** 551 * Receive a logout message. 552 * 553 * @param SimpleSAML_IdP $idp The IdP we are receiving it for. 554 * @throws SimpleSAML_Error_BadRequest In case an error occurs while trying to receive the logout message. 555 */ 556 public static function receiveLogoutMessage(SimpleSAML_IdP $idp) 557 { 558 559 $binding = \SAML2\Binding::getCurrentBinding(); 560 $message = $binding->receive(); 561 562 $spEntityId = $message->getIssuer(); 563 if ($spEntityId === null) { 564 /* Without an issuer we have no way to respond to the message. */ 565 throw new SimpleSAML_Error_BadRequest('Received message on logout endpoint without issuer.'); 566 } 567 568 $metadata = SimpleSAML_Metadata_MetaDataStorageHandler::getMetadataHandler(); 569 $idpMetadata = $idp->getConfig(); 570 $spMetadata = $metadata->getMetaDataConfig($spEntityId, 'saml20-sp-remote'); 571 572 sspmod_saml_Message::validateMessage($spMetadata, $idpMetadata, $message); 573 574 if ($message instanceof \SAML2\LogoutResponse) { 575 SimpleSAML\Logger::info('Received SAML 2.0 LogoutResponse from: '.var_export($spEntityId, true)); 576 $statsData = array( 577 'spEntityID' => $spEntityId, 578 'idpEntityID' => $idpMetadata->getString('entityid'), 579 ); 580 if (!$message->isSuccess()) { 581 $statsData['error'] = $message->getStatus(); 582 } 583 SimpleSAML_Stats::log('saml:idp:LogoutResponse:recv', $statsData); 584 585 $relayState = $message->getRelayState(); 586 587 if (!$message->isSuccess()) { 588 $logoutError = sspmod_saml_Message::getResponseError($message); 589 SimpleSAML\Logger::warning('Unsuccessful logout. Status was: '.$logoutError); 590 } else { 591 $logoutError = null; 592 } 593 594 $assocId = 'saml:'.$spEntityId; 595 596 $idp->handleLogoutResponse($assocId, $relayState, $logoutError); 597 } elseif ($message instanceof \SAML2\LogoutRequest) { 598 SimpleSAML\Logger::info('Received SAML 2.0 LogoutRequest from: '.var_export($spEntityId, true)); 599 SimpleSAML_Stats::log('saml:idp:LogoutRequest:recv', array( 600 'spEntityID' => $spEntityId, 601 'idpEntityID' => $idpMetadata->getString('entityid'), 602 )); 603 604 $spStatsId = $spMetadata->getString('core:statistics-id', $spEntityId); 605 SimpleSAML\Logger::stats('saml20-idp-SLO spinit '.$spStatsId.' '.$idpMetadata->getString('entityid')); 606 607 $state = array( 608 'Responder' => array('sspmod_saml_IdP_SAML2', 'sendLogoutResponse'), 609 'saml:SPEntityId' => $spEntityId, 610 'saml:RelayState' => $message->getRelayState(), 611 'saml:RequestId' => $message->getId(), 612 ); 613 614 $assocId = 'saml:'.$spEntityId; 615 $idp->handleLogoutRequest($state, $assocId); 616 } else { 617 throw new SimpleSAML_Error_BadRequest('Unknown message received on logout endpoint: '.get_class($message)); 618 } 619 } 620 621 622 /** 623 * Retrieve a logout URL for a given logout association. 624 * 625 * @param SimpleSAML_IdP $idp The IdP we are sending a logout request from. 626 * @param array $association The association that should be terminated. 627 * @param string|NULL $relayState An id that should be carried across the logout. 628 * 629 * @return string The logout URL. 630 */ 631 public static function getLogoutURL(SimpleSAML_IdP $idp, array $association, $relayState) 632 { 633 assert(is_string($relayState) || $relayState === null); 634 635 SimpleSAML\Logger::info('Sending SAML 2.0 LogoutRequest to: '.var_export($association['saml:entityID'], true)); 636 637 $metadata = SimpleSAML_Metadata_MetaDataStorageHandler::getMetadataHandler(); 638 $idpMetadata = $idp->getConfig(); 639 $spMetadata = $metadata->getMetaDataConfig($association['saml:entityID'], 'saml20-sp-remote'); 640 641 $bindings = array( 642 \SAML2\Constants::BINDING_HTTP_REDIRECT, 643 \SAML2\Constants::BINDING_HTTP_POST 644 ); 645 $dst = $spMetadata->getEndpointPrioritizedByBinding('SingleLogoutService', $bindings); 646 647 if ($dst['Binding'] === \SAML2\Constants::BINDING_HTTP_POST) { 648 $params = array('association' => $association['id'], 'idp' => $idp->getId()); 649 if ($relayState !== null) { 650 $params['RelayState'] = $relayState; 651 } 652 return SimpleSAML\Module::getModuleURL('core/idp/logout-iframe-post.php', $params); 653 } 654 655 $lr = self::buildLogoutRequest($idpMetadata, $spMetadata, $association, $relayState); 656 $lr->setDestination($dst['Location']); 657 658 $binding = new \SAML2\HTTPRedirect(); 659 return $binding->getRedirectURL($lr); 660 } 661 662 663 /** 664 * Retrieve the metadata for the given SP association. 665 * 666 * @param SimpleSAML_IdP $idp The IdP the association belongs to. 667 * @param array $association The SP association. 668 * 669 * @return SimpleSAML_Configuration Configuration object for the SP metadata. 670 */ 671 public static function getAssociationConfig(SimpleSAML_IdP $idp, array $association) 672 { 673 $metadata = SimpleSAML_Metadata_MetaDataStorageHandler::getMetadataHandler(); 674 try { 675 return $metadata->getMetaDataConfig($association['saml:entityID'], 'saml20-sp-remote'); 676 } catch (Exception $e) { 677 return SimpleSAML_Configuration::loadFromArray(array(), 'Unknown SAML 2 entity.'); 678 } 679 } 680 681 682 /** 683 * Calculate the NameID value that should be used. 684 * 685 * @param SimpleSAML_Configuration $idpMetadata The metadata of the IdP. 686 * @param SimpleSAML_Configuration $spMetadata The metadata of the SP. 687 * @param array &$state The authentication state of the user. 688 * 689 * @return string The NameID value. 690 */ 691 private static function generateNameIdValue( 692 SimpleSAML_Configuration $idpMetadata, 693 SimpleSAML_Configuration $spMetadata, 694 array &$state 695 ) { 696 697 $attribute = $spMetadata->getString('simplesaml.nameidattribute', null); 698 if ($attribute === null) { 699 $attribute = $idpMetadata->getString('simplesaml.nameidattribute', null); 700 if ($attribute === null) { 701 if (!isset($state['UserID'])) { 702 SimpleSAML\Logger::error('Unable to generate NameID. Check the userid.attribute option.'); 703 return null; 704 } 705 $attributeValue = $state['UserID']; 706 $idpEntityId = $idpMetadata->getString('entityid'); 707 $spEntityId = $spMetadata->getString('entityid'); 708 709 $secretSalt = SimpleSAML\Utils\Config::getSecretSalt(); 710 711 $uidData = 'uidhashbase'.$secretSalt; 712 $uidData .= strlen($idpEntityId).':'.$idpEntityId; 713 $uidData .= strlen($spEntityId).':'.$spEntityId; 714 $uidData .= strlen($attributeValue).':'.$attributeValue; 715 $uidData .= $secretSalt; 716 717 return hash('sha1', $uidData); 718 } 719 } 720 721 $attributes = $state['Attributes']; 722 if (!array_key_exists($attribute, $attributes)) { 723 SimpleSAML\Logger::error('Unable to add NameID: Missing '.var_export($attribute, true). 724 ' in the attributes of the user.'); 725 return null; 726 } 727 728 return $attributes[$attribute][0]; 729 } 730 731 732 /** 733 * Helper function for encoding attributes. 734 * 735 * @param SimpleSAML_Configuration $idpMetadata The metadata of the IdP. 736 * @param SimpleSAML_Configuration $spMetadata The metadata of the SP. 737 * @param array $attributes The attributes of the user. 738 * 739 * @return array The encoded attributes. 740 * 741 * @throws SimpleSAML_Error_Exception In case an unsupported encoding is specified by configuration. 742 */ 743 private static function encodeAttributes( 744 SimpleSAML_Configuration $idpMetadata, 745 SimpleSAML_Configuration $spMetadata, 746 array $attributes 747 ) { 748 749 $base64Attributes = $spMetadata->getBoolean('base64attributes', null); 750 if ($base64Attributes === null) { 751 $base64Attributes = $idpMetadata->getBoolean('base64attributes', false); 752 } 753 754 if ($base64Attributes) { 755 $defaultEncoding = 'base64'; 756 } else { 757 $defaultEncoding = 'string'; 758 } 759 760 $srcEncodings = $idpMetadata->getArray('attributeencodings', array()); 761 $dstEncodings = $spMetadata->getArray('attributeencodings', array()); 762 763 /* 764 * Merge the two encoding arrays. Encodings specified in the target metadata 765 * takes precedence over the source metadata. 766 */ 767 $encodings = array_merge($srcEncodings, $dstEncodings); 768 769 $ret = array(); 770 foreach ($attributes as $name => $values) { 771 $ret[$name] = array(); 772 if (array_key_exists($name, $encodings)) { 773 $encoding = $encodings[$name]; 774 } else { 775 $encoding = $defaultEncoding; 776 } 777 778 foreach ($values as $value) { 779 // allow null values 780 if ($value === null) { 781 $ret[$name][] = $value; 782 continue; 783 } 784 785 $attrval = $value; 786 if ($value instanceof DOMNodeList) { 787 $attrval = new \SAML2\XML\saml\AttributeValue($value->item(0)->parentNode); 788 } 789 790 switch ($encoding) { 791 case 'string': 792 $value = (string) $attrval; 793 break; 794 case 'base64': 795 $value = base64_encode((string) $attrval); 796 break; 797 case 'raw': 798 if (is_string($value)) { 799 $doc = \SAML2\DOMDocumentFactory::fromString('<root>'.$value.'</root>'); 800 $value = $doc->firstChild->childNodes; 801 } 802 assert($value instanceof DOMNodeList || $value instanceof \SAML2\XML\saml\NameID); 803 break; 804 default: 805 throw new SimpleSAML_Error_Exception('Invalid encoding for attribute '. 806 var_export($name, true).': '.var_export($encoding, true)); 807 } 808 $ret[$name][] = $value; 809 } 810 } 811 812 return $ret; 813 } 814 815 816 /** 817 * Determine which NameFormat we should use for attributes. 818 * 819 * @param SimpleSAML_Configuration $idpMetadata The metadata of the IdP. 820 * @param SimpleSAML_Configuration $spMetadata The metadata of the SP. 821 * 822 * @return string The NameFormat. 823 */ 824 private static function getAttributeNameFormat( 825 SimpleSAML_Configuration $idpMetadata, 826 SimpleSAML_Configuration $spMetadata 827 ) { 828 829 // try SP metadata first 830 $attributeNameFormat = $spMetadata->getString('attributes.NameFormat', null); 831 if ($attributeNameFormat !== null) { 832 return $attributeNameFormat; 833 } 834 $attributeNameFormat = $spMetadata->getString('AttributeNameFormat', null); 835 if ($attributeNameFormat !== null) { 836 return $attributeNameFormat; 837 } 838 839 // look in IdP metadata 840 $attributeNameFormat = $idpMetadata->getString('attributes.NameFormat', null); 841 if ($attributeNameFormat !== null) { 842 return $attributeNameFormat; 843 } 844 $attributeNameFormat = $idpMetadata->getString('AttributeNameFormat', null); 845 if ($attributeNameFormat !== null) { 846 return $attributeNameFormat; 847 } 848 849 // default 850 return 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic'; 851 } 852 853 854 /** 855 * Build an assertion based on information in the metadata. 856 * 857 * @param SimpleSAML_Configuration $idpMetadata The metadata of the IdP. 858 * @param SimpleSAML_Configuration $spMetadata The metadata of the SP. 859 * @param array &$state The state array with information about the request. 860 * 861 * @return \SAML2\Assertion The assertion. 862 * 863 * @throws SimpleSAML_Error_Exception In case an error occurs when creating a holder-of-key assertion. 864 */ 865 private static function buildAssertion( 866 SimpleSAML_Configuration $idpMetadata, 867 SimpleSAML_Configuration $spMetadata, 868 array &$state 869 ) { 870 assert(isset($state['Attributes'])); 871 assert(isset($state['saml:ConsumerURL'])); 872 873 $now = time(); 874 875 $signAssertion = $spMetadata->getBoolean('saml20.sign.assertion', null); 876 if ($signAssertion === null) { 877 $signAssertion = $idpMetadata->getBoolean('saml20.sign.assertion', true); 878 } 879 880 $config = SimpleSAML_Configuration::getInstance(); 881 882 $a = new \SAML2\Assertion(); 883 if ($signAssertion) { 884 sspmod_saml_Message::addSign($idpMetadata, $spMetadata, $a); 885 } 886 887 $a->setIssuer($idpMetadata->getString('entityid')); 888 $a->setValidAudiences(array($spMetadata->getString('entityid'))); 889 890 $a->setNotBefore($now - 30); 891 892 $assertionLifetime = $spMetadata->getInteger('assertion.lifetime', null); 893 if ($assertionLifetime === null) { 894 $assertionLifetime = $idpMetadata->getInteger('assertion.lifetime', 300); 895 } 896 $a->setNotOnOrAfter($now + $assertionLifetime); 897 898 if (isset($state['saml:AuthnContextClassRef'])) { 899 $a->setAuthnContextClassRef($state['saml:AuthnContextClassRef']); 900 } elseif (\SimpleSAML\Utils\HTTP::isHTTPS()) { 901 $a->setAuthnContextClassRef(\SAML2\Constants::AC_PASSWORD_PROTECTED_TRANSPORT); 902 } else { 903 $a->setAuthnContext(\SAML2\Constants::AC_PASSWORD); 904 } 905 906 $sessionStart = $now; 907 if (isset($state['AuthnInstant'])) { 908 $a->setAuthnInstant($state['AuthnInstant']); 909 $sessionStart = $state['AuthnInstant']; 910 } 911 912 $sessionLifetime = $config->getInteger('session.duration', 8 * 60 * 60); 913 $a->setSessionNotOnOrAfter($sessionStart + $sessionLifetime); 914 915 $a->setSessionIndex(SimpleSAML\Utils\Random::generateID()); 916 917 $sc = new \SAML2\XML\saml\SubjectConfirmation(); 918 $sc->SubjectConfirmationData = new \SAML2\XML\saml\SubjectConfirmationData(); 919 $sc->SubjectConfirmationData->NotOnOrAfter = $now + $assertionLifetime; 920 $sc->SubjectConfirmationData->Recipient = $state['saml:ConsumerURL']; 921 $sc->SubjectConfirmationData->InResponseTo = $state['saml:RequestId']; 922 923 // ProtcolBinding of SP's <AuthnRequest> overwrites IdP hosted metadata configuration 924 $hokAssertion = null; 925 if ($state['saml:Binding'] === \SAML2\Constants::BINDING_HOK_SSO) { 926 $hokAssertion = true; 927 } 928 if ($hokAssertion === null) { 929 $hokAssertion = $idpMetadata->getBoolean('saml20.hok.assertion', false); 930 } 931 932 if ($hokAssertion) { 933 // Holder-of-Key 934 $sc->Method = \SAML2\Constants::CM_HOK; 935 if (\SimpleSAML\Utils\HTTP::isHTTPS()) { 936 if (isset($_SERVER['SSL_CLIENT_CERT']) && !empty($_SERVER['SSL_CLIENT_CERT'])) { 937 // extract certificate data (if this is a certificate) 938 $clientCert = $_SERVER['SSL_CLIENT_CERT']; 939 $pattern = '/^-----BEGIN CERTIFICATE-----([^-]*)^-----END CERTIFICATE-----/m'; 940 if (preg_match($pattern, $clientCert, $matches)) { 941 // we have a client certificate from the browser which we add to the HoK assertion 942 $x509Certificate = new \SAML2\XML\ds\X509Certificate(); 943 $x509Certificate->certificate = str_replace(array("\r", "\n", " "), '', $matches[1]); 944 945 $x509Data = new \SAML2\XML\ds\X509Data(); 946 $x509Data->data[] = $x509Certificate; 947 948 $keyInfo = new \SAML2\XML\ds\KeyInfo(); 949 $keyInfo->info[] = $x509Data; 950 951 $sc->SubjectConfirmationData->info[] = $keyInfo; 952 } else { 953 throw new SimpleSAML_Error_Exception( 954 'Error creating HoK assertion: No valid client certificate provided during TLS handshake '. 955 'with IdP' 956 ); 957 } 958 } else { 959 throw new SimpleSAML_Error_Exception( 960 'Error creating HoK assertion: No client certificate provided during TLS handshake with IdP' 961 ); 962 } 963 } else { 964 throw new SimpleSAML_Error_Exception( 965 'Error creating HoK assertion: No HTTPS connection to IdP, but required for Holder-of-Key SSO' 966 ); 967 } 968 } else { 969 // Bearer 970 $sc->Method = \SAML2\Constants::CM_BEARER; 971 } 972 $a->setSubjectConfirmation(array($sc)); 973 974 // add attributes 975 if ($spMetadata->getBoolean('simplesaml.attributes', true)) { 976 $attributeNameFormat = self::getAttributeNameFormat($idpMetadata, $spMetadata); 977 $a->setAttributeNameFormat($attributeNameFormat); 978 $attributes = self::encodeAttributes($idpMetadata, $spMetadata, $state['Attributes']); 979 $a->setAttributes($attributes); 980 } 981 982 // generate the NameID for the assertion 983 if (isset($state['saml:NameIDFormat'])) { 984 $nameIdFormat = $state['saml:NameIDFormat']; 985 } else { 986 $nameIdFormat = null; 987 } 988 989 if ($nameIdFormat === null || !isset($state['saml:NameID'][$nameIdFormat])) { 990 // either not set in request, or not set to a format we supply. Fall back to old generation method 991 $nameIdFormat = $spMetadata->getString('NameIDFormat', null); 992 if ($nameIdFormat === null) { 993 $nameIdFormat = $idpMetadata->getString('NameIDFormat', \SAML2\Constants::NAMEID_TRANSIENT); 994 } 995 } 996 997 if (isset($state['saml:NameID'][$nameIdFormat])) { 998 $nameId = $state['saml:NameID'][$nameIdFormat]; 999 $nameId->Format = $nameIdFormat; 1000 } else { 1001 $spNameQualifier = $spMetadata->getString('SPNameQualifier', null); 1002 if ($spNameQualifier === null) { 1003 $spNameQualifier = $spMetadata->getString('entityid'); 1004 } 1005 1006 if ($nameIdFormat === \SAML2\Constants::NAMEID_TRANSIENT) { 1007 // generate a random id 1008 $nameIdValue = SimpleSAML\Utils\Random::generateID(); 1009 } else { 1010 /* this code will end up generating either a fixed assigned id (via nameid.attribute) 1011 or random id if not assigned/configured */ 1012 $nameIdValue = self::generateNameIdValue($idpMetadata, $spMetadata, $state); 1013 if ($nameIdValue === null) { 1014 SimpleSAML\Logger::warning('Falling back to transient NameID.'); 1015 $nameIdFormat = \SAML2\Constants::NAMEID_TRANSIENT; 1016 $nameIdValue = SimpleSAML\Utils\Random::generateID(); 1017 } 1018 } 1019 1020 $nameId = new \SAML2\XML\saml\NameID(); 1021 $nameId->Format = $nameIdFormat; 1022 $nameId->value = $nameIdValue; 1023 $nameId->SPNameQualifier = $spNameQualifier; 1024 } 1025 1026 $state['saml:idp:NameID'] = $nameId; 1027 1028 $a->setNameId($nameId); 1029 1030 $encryptNameId = $spMetadata->getBoolean('nameid.encryption', null); 1031 if ($encryptNameId === null) { 1032 $encryptNameId = $idpMetadata->getBoolean('nameid.encryption', false); 1033 } 1034 if ($encryptNameId) { 1035 $a->encryptNameId(sspmod_saml_Message::getEncryptionKey($spMetadata)); 1036 } 1037 1038 return $a; 1039 } 1040 1041 1042 /** 1043 * Encrypt an assertion. 1044 * 1045 * This function takes in a \SAML2\Assertion and encrypts it if encryption of 1046 * assertions are enabled in the metadata. 1047 * 1048 * @param SimpleSAML_Configuration $idpMetadata The metadata of the IdP. 1049 * @param SimpleSAML_Configuration $spMetadata The metadata of the SP. 1050 * @param \SAML2\Assertion $assertion The assertion we are encrypting. 1051 * 1052 * @return \SAML2\Assertion|\SAML2\EncryptedAssertion The assertion. 1053 * 1054 * @throws SimpleSAML_Error_Exception In case the encryption key type is not supported. 1055 */ 1056 private static function encryptAssertion( 1057 SimpleSAML_Configuration $idpMetadata, 1058 SimpleSAML_Configuration $spMetadata, 1059 \SAML2\Assertion $assertion 1060 ) { 1061 1062 $encryptAssertion = $spMetadata->getBoolean('assertion.encryption', null); 1063 if ($encryptAssertion === null) { 1064 $encryptAssertion = $idpMetadata->getBoolean('assertion.encryption', false); 1065 } 1066 if (!$encryptAssertion) { 1067 // we are _not_ encrypting this assertion, and are therefore done 1068 return $assertion; 1069 } 1070 1071 1072 $sharedKey = $spMetadata->getString('sharedkey', null); 1073 if ($sharedKey !== null) { 1074 $key = new XMLSecurityKey(XMLSecurityKey::AES128_CBC); 1075 $key->loadKey($sharedKey); 1076 } else { 1077 $keys = $spMetadata->getPublicKeys('encryption', true); 1078 if (!empty($keys)) { 1079 $key = $keys[0]; 1080 switch ($key['type']) { 1081 case 'X509Certificate': 1082 $pemKey = "-----BEGIN CERTIFICATE-----\n". 1083 chunk_split($key['X509Certificate'], 64). 1084 "-----END CERTIFICATE-----\n"; 1085 break; 1086 default: 1087 throw new SimpleSAML_Error_Exception('Unsupported encryption key type: '.$key['type']); 1088 } 1089 1090 // extract the public key from the certificate for encryption 1091 $key = new XMLSecurityKey(XMLSecurityKey::RSA_OAEP_MGF1P, array('type' => 'public')); 1092 $key->loadKey($pemKey); 1093 } else { 1094 throw new SimpleSAML_Error_ConfigurationError( 1095 'Missing encryption key for entity `' . $spMetadata->getString('entityid') . '`', 1096 null, 1097 $spMetadata->getString('metadata-set') . '.php' 1098 ); 1099 } 1100 } 1101 1102 $ea = new \SAML2\EncryptedAssertion(); 1103 $ea->setAssertion($assertion, $key); 1104 return $ea; 1105 } 1106 1107 1108 /** 1109 * Build a logout request based on information in the metadata. 1110 * 1111 * @param SimpleSAML_Configuration $idpMetadata The metadata of the IdP. 1112 * @param SimpleSAML_Configuration $spMetadata The metadata of the SP. 1113 * @param array $association The SP association. 1114 * @param string|null $relayState An id that should be carried across the logout. 1115 * 1116 * @return \SAML2\LogoutResponse The corresponding SAML2 logout response. 1117 */ 1118 private static function buildLogoutRequest( 1119 SimpleSAML_Configuration $idpMetadata, 1120 SimpleSAML_Configuration $spMetadata, 1121 array $association, 1122 $relayState 1123 ) { 1124 1125 $lr = sspmod_saml_Message::buildLogoutRequest($idpMetadata, $spMetadata); 1126 $lr->setRelayState($relayState); 1127 $lr->setSessionIndex($association['saml:SessionIndex']); 1128 $lr->setNameId($association['saml:NameID']); 1129 1130 $assertionLifetime = $spMetadata->getInteger('assertion.lifetime', null); 1131 if ($assertionLifetime === null) { 1132 $assertionLifetime = $idpMetadata->getInteger('assertion.lifetime', 300); 1133 } 1134 $lr->setNotOnOrAfter(time() + $assertionLifetime); 1135 1136 $encryptNameId = $spMetadata->getBoolean('nameid.encryption', null); 1137 if ($encryptNameId === null) { 1138 $encryptNameId = $idpMetadata->getBoolean('nameid.encryption', false); 1139 } 1140 if ($encryptNameId) { 1141 $lr->encryptNameId(sspmod_saml_Message::getEncryptionKey($spMetadata)); 1142 } 1143 1144 return $lr; 1145 } 1146 1147 1148 /** 1149 * Build a authentication response based on information in the metadata. 1150 * 1151 * @param SimpleSAML_Configuration $idpMetadata The metadata of the IdP. 1152 * @param SimpleSAML_Configuration $spMetadata The metadata of the SP. 1153 * @param string $consumerURL The Destination URL of the response. 1154 * 1155 * @return \SAML2\Response The SAML2 response corresponding to the given data. 1156 */ 1157 private static function buildResponse( 1158 SimpleSAML_Configuration $idpMetadata, 1159 SimpleSAML_Configuration $spMetadata, 1160 $consumerURL 1161 ) { 1162 1163 $signResponse = $spMetadata->getBoolean('saml20.sign.response', null); 1164 if ($signResponse === null) { 1165 $signResponse = $idpMetadata->getBoolean('saml20.sign.response', true); 1166 } 1167 1168 $r = new \SAML2\Response(); 1169 1170 $r->setIssuer($idpMetadata->getString('entityid')); 1171 $r->setDestination($consumerURL); 1172 1173 if ($signResponse) { 1174 sspmod_saml_Message::addSign($idpMetadata, $spMetadata, $r); 1175 } 1176 1177 return $r; 1178 } 1179} 1180