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