ILIAS  release_5-4 Revision v5.4.26-12-gabc799a52e6
SAML2.php
Go to the documentation of this file.
1<?php
2
4use SAML2\SOAP;
5
12{
13
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'];
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
42
43 $idpMetadata = $idp->getConfig();
44
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
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'];
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
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
162 private static function getAssertionConsumerService(
163 array $supportedBindings,
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) {
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
249 {
250
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 */
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
316 'SAML2.0 - IdP.SSOService: IdP initiated authentication: '.var_export($spEntityId, true)
317 );
318 } else {
320 $request = $binding->receive();
321
322 if (!($request instanceof \SAML2\AuthnRequest)) {
324 'Message received on authentication request endpoint wasn\'t an authentication request.'
325 );
326 }
327
328 $spEntityId = $request->getIssuer();
329 if ($spEntityId === null) {
331 'Received message on authentication request endpoint without issuer.'
332 );
333 }
334 $spMetadata = $metadata->getMetaDataConfig($spEntityId, 'saml20-sp-remote');
335
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
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
384 $supportedBindings,
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) {
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
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
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 );
486 $lr->setDestination($dst['Location']);
487
488 $binding->send($lr);
489 }
490
491
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
507 $idpMetadata = $idp->getConfig();
508 $spMetadata = $metadata->getMetaDataConfig($spEntityId, 'saml20-sp-remote');
509
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 );
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
557 {
558
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
569 $idpMetadata = $idp->getConfig();
570 $spMetadata = $metadata->getMetaDataConfig($spEntityId, 'saml20-sp-remote');
571
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()) {
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
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
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
656 $lr->setDestination($dst['Location']);
657
658 $binding = new \SAML2\HTTPRedirect();
659 return $binding->getRedirectURL($lr);
660 }
661
662
672 {
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
691 private static function generateNameIdValue(
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
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
743 private static function encodeAttributes(
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
824 private static function getAttributeNameFormat(
827 ) {
828
829 // try SP metadata first
830 $attributeNameFormat = $spMetadata->getString('attributes.NameFormat', null);
831 if ($attributeNameFormat !== null) {
833 }
834 $attributeNameFormat = $spMetadata->getString('AttributeNameFormat', null);
835 if ($attributeNameFormat !== null) {
837 }
838
839 // look in IdP metadata
840 $attributeNameFormat = $idpMetadata->getString('attributes.NameFormat', null);
841 if ($attributeNameFormat !== null) {
843 }
844 $attributeNameFormat = $idpMetadata->getString('AttributeNameFormat', null);
845 if ($attributeNameFormat !== null) {
847 }
848
849 // default
850 return 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic';
851 }
852
853
865 private static function buildAssertion(
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
881
882 $a = new \SAML2\Assertion();
883 if ($signAssertion) {
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
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 {
954 'Error creating HoK assertion: No valid client certificate provided during TLS handshake '.
955 'with IdP'
956 );
957 }
958 } else {
960 'Error creating HoK assertion: No client certificate provided during TLS handshake with IdP'
961 );
962 }
963 } else {
965 'Error creating HoK assertion: No HTTPS connection to IdP, but required for Holder-of-Key SSO'
966 );
967 }
968 } else {
969 // Bearer
971 }
972 $a->setSubjectConfirmation(array($sc));
973
974 // add attributes
975 if ($spMetadata->getBoolean('simplesaml.attributes', true)) {
977 $a->setAttributeNameFormat($attributeNameFormat);
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
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 */
1013 if ($nameIdValue === null) {
1014 SimpleSAML\Logger::warning('Falling back to transient NameID.');
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) {
1036 }
1037
1038 return $a;
1039 }
1040
1041
1056 private static function encryptAssertion(
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
1118 private static function buildLogoutRequest(
1121 array $association,
1123 ) {
1124
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) {
1142 }
1143
1144 return $lr;
1145 }
1146
1147
1157 private static function buildResponse(
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) {
1175 }
1176
1177 return $r;
1178 }
1179}
$metadata['__DYNAMIC:1__']
foreach($paths as $path) $request
Definition: asyncclient.php:32
$nameIdValue
$nameIdFormat
$spEntityId
$attributeNameFormat
$sc SubjectConfirmationData
if(!array_key_exists('stateid', $_REQUEST)) $state
Handle linkback() response from LinkedIn.
Definition: linkback.php:10
An exception for terminatinating execution or to throw for unit testing.
static getCurrentBinding()
Guess the current binding.
Definition: Binding.php:62
static getBinding($urn)
Retrieve a binding with the given URN.
Definition: Binding.php:28
const BINDING_HOK_SSO
The URN for the Holder-of-Key Web Browser SSO Profile binding.
Definition: Constants.php:55
const NAMEID_TRANSIENT
Transient NameID format.
Definition: Constants.php:200
const CM_BEARER
Bearer subject confirmation method.
Definition: Constants.php:60
const BINDING_HTTP_ARTIFACT
The URN for the HTTP-ARTIFACT binding.
Definition: Constants.php:40
const CM_HOK
Holder-of-Key subject confirmation method.
Definition: Constants.php:65
const BINDING_PAOS
The URN for the PAOS binding.
Definition: Constants.php:50
static info($string)
Definition: Logger.php:199
static warning($string)
Definition: Logger.php:177
static stats($string)
Definition: Logger.php:222
static error($string)
Definition: Logger.php:166
static debug($string)
Definition: Logger.php:211
static getModuleURL($resource, array $parameters=array())
Get absolute URL to a specified module resource.
Definition: Module.php:220
static getSecretSalt()
Retrieve the secret salt.
Definition: Config.php:49
static checkSessionCookie($retryURL=null)
Check for session cookie, and show missing-cookie page if it is missing.
Definition: HTTP.php:286
static generateID()
Generate a random identifier, ID_LENGTH bytes long.
Definition: Random.php:26
const EXCEPTION_HANDLER_FUNC
The index in the state array which contains the exception handler function.
Definition: State.php:69
const RESTART
The index in the state array which contains the restart URL.
Definition: State.php:57
static getInstance($instancename='simplesaml')
Get a configuration file by its instance name.
static loadFromArray($config, $location='[ARRAY]', $instance=null)
Loads a configuration from the given array.
log($default_level)
Print the exception to the log, by default with log level error.
Definition: Exception.php:235
static getByState(array &$state)
Retrieve the IdP "owning" the state.
Definition: IdP.php:145
static getMetadataHandler()
This function retrieves the current instance of the metadata handler.
static log($event, array $data=array())
Notify about an event.
Definition: Stats.php:71
static fromException(Exception $exception)
Create a SAML2 error from an exception.
Definition: Error.php:103
static sendLogoutResponse(SimpleSAML_IdP $idp, array $state)
Send a logout response.
Definition: SAML2.php:498
static receiveAuthnRequest(SimpleSAML_IdP $idp)
Receive an authentication request.
Definition: SAML2.php:248
static handleAuthError(SimpleSAML_Error_Exception $exception, array $state)
Handle authentication error.
Definition: SAML2.php:96
static buildAssertion(SimpleSAML_Configuration $idpMetadata, SimpleSAML_Configuration $spMetadata, array &$state)
Build an assertion based on information in the metadata.
Definition: SAML2.php:865
static getAssociationConfig(SimpleSAML_IdP $idp, array $association)
Retrieve the metadata for the given SP association.
Definition: SAML2.php:671
static getLogoutURL(SimpleSAML_IdP $idp, array $association, $relayState)
Retrieve a logout URL for a given logout association.
Definition: SAML2.php:631
static receiveLogoutMessage(SimpleSAML_IdP $idp)
Receive a logout message.
Definition: SAML2.php:556
static buildLogoutRequest(SimpleSAML_Configuration $idpMetadata, SimpleSAML_Configuration $spMetadata, array $association, $relayState)
Build a logout request based on information in the metadata.
Definition: SAML2.php:1118
static getAttributeNameFormat(SimpleSAML_Configuration $idpMetadata, SimpleSAML_Configuration $spMetadata)
Determine which NameFormat we should use for attributes.
Definition: SAML2.php:824
static processSOAPAuthnRequest(array &$state)
Definition: SAML2.php:443
static sendLogoutRequest(SimpleSAML_IdP $idp, array $association, $relayState)
Send a logout request to a given association.
Definition: SAML2.php:462
static encodeAttributes(SimpleSAML_Configuration $idpMetadata, SimpleSAML_Configuration $spMetadata, array $attributes)
Helper function for encoding attributes.
Definition: SAML2.php:743
static generateNameIdValue(SimpleSAML_Configuration $idpMetadata, SimpleSAML_Configuration $spMetadata, array &$state)
Calculate the NameID value that should be used.
Definition: SAML2.php:691
static sendResponse(array $state)
Send a response to the SP.
Definition: SAML2.php:19
static encryptAssertion(SimpleSAML_Configuration $idpMetadata, SimpleSAML_Configuration $spMetadata, \SAML2\Assertion $assertion)
Encrypt an assertion.
Definition: SAML2.php:1056
static getAssertionConsumerService(array $supportedBindings, SimpleSAML_Configuration $spMetadata, $AssertionConsumerServiceURL, $ProtocolBinding, $AssertionConsumerServiceIndex)
Find SP AssertionConsumerService based on parameter in AuthnRequest.
Definition: SAML2.php:162
static buildResponse(SimpleSAML_Configuration $idpMetadata, SimpleSAML_Configuration $spMetadata, $consumerURL)
Build a authentication response based on information in the metadata.
Definition: SAML2.php:1157
static getResponseError(\SAML2\StatusResponse $response)
Retrieve the status code of a response as a sspmod_saml_Error.
Definition: Message.php:452
static addSign(SimpleSAML_Configuration $srcMetadata, SimpleSAML_Configuration $dstMetadata, \SAML2\SignedElement $element)
Add signature key and sender certificate to an element (Message or Assertion).
Definition: Message.php:20
static buildLogoutRequest(SimpleSAML_Configuration $srcMetadata, SimpleSAML_Configuration $dstMetadata)
Build a logout request based on information in the metadata.
Definition: Message.php:536
static buildLogoutResponse(SimpleSAML_Configuration $srcMetadata, SimpleSAML_Configuration $dstMetadata)
Build a logout response based on information in the metadata.
Definition: Message.php:556
static getEncryptionKey(SimpleSAML_Configuration $metadata)
Retrieve the encryption key for the given entity.
Definition: Message.php:865
static validateMessage(SimpleSAML_Configuration $srcMetadata, SimpleSAML_Configuration $dstMetadata, \SAML2\Message $message)
Check signature on a SAML2 message if enabled.
Definition: Message.php:236
$key
Definition: croninfo.php:18
$r
Definition: example_031.php:79
if(array_key_exists('yes', $_REQUEST)) $attributes
Definition: getconsent.php:85
$config
Definition: bootstrap.php:15
$nameId
Definition: saml2-acs.php:138
catch(Exception $e) $message
$assertionLifetime
$lr
$binding
$relayState
if(!isset($associations[$assocId])) $association
if(!isset($_REQUEST['association'])) $assocId
$dst
$idpMetadata
$encryptNameId
$spMetadata
$bindings
$keys
hash(StreamInterface $stream, $algo, $rawOutput=false)
Calculate a hash of a Stream.
Definition: functions.php:406
Attribute-related utility methods.
$ret
Definition: parser.php:6
$idpEntityId
Definition: prp.php:12
$idp
Definition: prp.php:13
if((!isset($_SERVER['DOCUMENT_ROOT'])) OR(empty($_SERVER['DOCUMENT_ROOT']))) $_SERVER['DOCUMENT_ROOT']
$values