https://issues.shibboleth.net/jira/browse/SSPCPP-403
[shibboleth/cpp-sp.git] / shibsp / handler / impl / SAML2Logout.cpp
1 /**
2  * Licensed to the University Corporation for Advanced Internet
3  * Development, Inc. (UCAID) under one or more contributor license
4  * agreements. See the NOTICE file distributed with this work for
5  * additional information regarding copyright ownership.
6  *
7  * UCAID licenses this file to you under the Apache License,
8  * Version 2.0 (the "License"); you may not use this file except
9  * in compliance with the License. You may obtain a copy of the
10  * License at
11  *
12  * http://www.apache.org/licenses/LICENSE-2.0
13  *
14  * Unless required by applicable law or agreed to in writing,
15  * software distributed under the License is distributed on an
16  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
17  * either express or implied. See the License for the specific
18  * language governing permissions and limitations under the License.
19  */
20
21 /**
22  * SAML2Logout.cpp
23  *
24  * Handles SAML 2.0 single logout protocol messages.
25  */
26
27 #include "internal.h"
28 #include "exceptions.h"
29 #include "Application.h"
30 #include "ServiceProvider.h"
31 #include "SPRequest.h"
32 #include "handler/AbstractHandler.h"
33 #include "handler/LogoutHandler.h"
34 #include "util/SPConstants.h"
35
36 #ifndef SHIBSP_LITE
37 # include "SessionCacheEx.h"
38 # include "security/SecurityPolicy.h"
39 # include "security/SecurityPolicyProvider.h"
40 # include "metadata/MetadataProviderCriteria.h"
41 # include "util/TemplateParameters.h"
42 # include <fstream>
43 # include <saml/exceptions.h>
44 # include <saml/SAMLConfig.h>
45 # include <saml/saml2/core/Protocols.h>
46 # include <saml/saml2/metadata/EndpointManager.h>
47 # include <saml/saml2/metadata/Metadata.h>
48 # include <saml/saml2/metadata/MetadataCredentialCriteria.h>
49 # include <xmltooling/util/URLEncoder.h>
50 using namespace opensaml::saml2;
51 using namespace opensaml::saml2p;
52 using namespace opensaml::saml2md;
53 using namespace opensaml;
54 #else
55 # include "lite/SAMLConstants.h"
56 #endif
57
58 using namespace shibsp;
59 using namespace xmltooling;
60 using namespace std;
61
62 namespace shibsp {
63
64 #if defined (_MSC_VER)
65     #pragma warning( push )
66     #pragma warning( disable : 4250 )
67 #endif
68
69     class SHIBSP_DLLLOCAL SAML2Logout : public AbstractHandler, public LogoutHandler
70     {
71     public:
72         SAML2Logout(const DOMElement* e, const char* appId);
73         virtual ~SAML2Logout() {
74 #ifndef SHIBSP_LITE
75             if (SPConfig::getConfig().isEnabled(SPConfig::OutOfProcess)) {
76                 delete m_decoder;
77                 XMLString::release(&m_outgoing);
78                 for_each(m_encoders.begin(), m_encoders.end(), cleanup_pair<const XMLCh*,MessageEncoder>());
79             }
80 #endif
81         }
82
83         void receive(DDF& in, ostream& out);
84         pair<bool,long> run(SPRequest& request, bool isHandler=true) const;
85
86 #ifndef SHIBSP_LITE
87         void generateMetadata(SPSSODescriptor& role, const char* handlerURL) const {
88             const char* loc = getString("Location").second;
89             string hurl(handlerURL);
90             if (*loc != '/')
91                 hurl += '/';
92             hurl += loc;
93             auto_ptr_XMLCh widen(hurl.c_str());
94             SingleLogoutService* ep = SingleLogoutServiceBuilder::buildSingleLogoutService();
95             ep->setLocation(widen.get());
96             ep->setBinding(getXMLString("Binding").second);
97             role.getSingleLogoutServices().push_back(ep);
98             role.addSupport(samlconstants::SAML20P_NS);
99         }
100
101         const char* getType() const {
102             return "SingleLogoutService";
103         }
104 #endif
105         const XMLCh* getProtocolFamily() const {
106             return samlconstants::SAML20P_NS;
107         }
108
109     private:
110         pair<bool,long> doRequest(const Application& application, const HTTPRequest& httpRequest, HTTPResponse& httpResponse) const;
111
112 #ifndef SHIBSP_LITE
113         pair<bool,long> sendResponse(
114             LogoutEvent* logoutEvent,
115             const XMLCh* requestID,
116             const XMLCh* code,
117             const XMLCh* subcode,
118             const char* msg,
119             const char* relayState,
120             const RoleDescriptor* role,
121             const Application& application,
122             HTTPResponse& httpResponse,
123             bool front
124             ) const;
125
126         LogoutEvent* newLogoutEvent(
127             const Application& application, const HTTPRequest* request=nullptr, const Session* session=nullptr
128             ) const {
129             LogoutEvent* e = LogoutHandler::newLogoutEvent(application, request, session);
130             if (e)
131                 e->m_protocol = m_protocol.get();
132             return e;
133         }
134
135         xmltooling::QName m_role;
136         MessageDecoder* m_decoder;
137         XMLCh* m_outgoing;
138         vector<const XMLCh*> m_bindings;
139         map<const XMLCh*,MessageEncoder*> m_encoders;
140         auto_ptr_char m_protocol;
141 #endif
142     };
143
144 #if defined (_MSC_VER)
145     #pragma warning( pop )
146 #endif
147
148     Handler* SHIBSP_DLLLOCAL SAML2LogoutFactory(const pair<const DOMElement*,const char*>& p)
149     {
150         return new SAML2Logout(p.first, p.second);
151     }
152 };
153
154 SAML2Logout::SAML2Logout(const DOMElement* e, const char* appId)
155     : AbstractHandler(e, Category::getInstance(SHIBSP_LOGCAT".Logout.SAML2"))
156 #ifndef SHIBSP_LITE
157         ,m_role(samlconstants::SAML20MD_NS, IDPSSODescriptor::LOCAL_NAME), m_decoder(nullptr), m_outgoing(nullptr), m_protocol(samlconstants::SAML20P_NS)
158 #endif
159 {
160     m_initiator = false;
161 #ifndef SHIBSP_LITE
162     m_preserve.push_back("ID");
163     m_preserve.push_back("entityID");
164     m_preserve.push_back("RelayState");
165
166     if (SPConfig::getConfig().isEnabled(SPConfig::OutOfProcess)) {
167         SAMLConfig& conf = SAMLConfig::getConfig();
168
169         // Handle incoming binding.
170         m_decoder = conf.MessageDecoderManager.newPlugin(
171             getString("Binding").second, pair<const DOMElement*,const XMLCh*>(e,shibspconstants::SHIB2SPCONFIG_NS)
172             );
173         m_decoder->setArtifactResolver(SPConfig::getConfig().getArtifactResolver());
174
175         if (m_decoder->isUserAgentPresent()) {
176             // Handle front-channel binding setup.
177             pair<bool,const XMLCh*> outgoing = getXMLString("outgoingBindings", m_configNS.get());
178             if (outgoing.first) {
179                 m_outgoing = XMLString::replicate(outgoing.second);
180                 XMLString::trim(m_outgoing);
181             }
182             else {
183                 // No override, so we'll install a default binding precedence.
184                 string prec = string(samlconstants::SAML20_BINDING_HTTP_REDIRECT) + ' ' + samlconstants::SAML20_BINDING_HTTP_POST + ' ' +
185                     samlconstants::SAML20_BINDING_HTTP_POST_SIMPLESIGN + ' ' + samlconstants::SAML20_BINDING_HTTP_ARTIFACT;
186                 m_outgoing = XMLString::transcode(prec.c_str());
187             }
188
189             int pos;
190             XMLCh* start = m_outgoing;
191             while (start && *start) {
192                 pos = XMLString::indexOf(start,chSpace);
193                 if (pos != -1)
194                     *(start + pos)=chNull;
195                 m_bindings.push_back(start);
196                 try {
197                     auto_ptr_char b(start);
198                     MessageEncoder * encoder = conf.MessageEncoderManager.newPlugin(
199                         b.get(), pair<const DOMElement*,const XMLCh*>(e,shibspconstants::SHIB2SPCONFIG_NS)
200                         );
201                     if (encoder->isUserAgentPresent() && XMLString::equals(getProtocolFamily(), encoder->getProtocolFamily())) {
202                         m_encoders[start] = encoder;
203                         m_log.debug("supporting outgoing binding (%s)", b.get());
204                     }
205                     else {
206                         delete encoder;
207                         m_log.warn("skipping outgoing binding (%s), not a SAML 2.0 front-channel mechanism", b.get());
208                     }
209                 }
210                 catch (exception& ex) {
211                     m_log.error("error building MessageEncoder: %s", ex.what());
212                 }
213                 if (pos != -1)
214                     start = start + pos + 1;
215                 else
216                     break;
217             }
218         }
219         else {
220             MessageEncoder* encoder = conf.MessageEncoderManager.newPlugin(
221                 getString("Binding").second, pair<const DOMElement*,const XMLCh*>(e,shibspconstants::SHIB2SPCONFIG_NS)
222                 );
223             m_encoders.insert(pair<const XMLCh*,MessageEncoder*>(nullptr, encoder));
224         }
225     }
226 #endif
227
228     string address(appId);
229     address += getString("Location").second;
230     setAddress(address.c_str());
231 }
232
233 pair<bool,long> SAML2Logout::run(SPRequest& request, bool isHandler) const
234 {
235     // Defer to base class for front-channel loop first.
236     // This won't initiate the loop, only continue/end it.
237     pair<bool,long> ret = LogoutHandler::run(request, isHandler);
238     if (ret.first)
239         return ret;
240
241     SPConfig& conf = SPConfig::getConfig();
242     if (conf.isEnabled(SPConfig::OutOfProcess)) {
243         // When out of process, we run natively and directly process the message.
244         return doRequest(request.getApplication(), request, request);
245     }
246     else {
247         // When not out of process, we remote all the message processing.
248         vector<string> headers(1,"Cookie");
249         headers.push_back("User-Agent");
250         DDF out,in = wrap(request, &headers, true);
251         DDFJanitor jin(in), jout(out);
252         out=request.getServiceProvider().getListenerService()->send(in);
253         return unwrap(request, out);
254     }
255 }
256
257 void SAML2Logout::receive(DDF& in, ostream& out)
258 {
259     // Find application.
260     const char* aid=in["application_id"].string();
261     const Application* app=aid ? SPConfig::getConfig().getServiceProvider()->getApplication(aid) : nullptr;
262     if (!app) {
263         // Something's horribly wrong.
264         m_log.error("couldn't find application (%s) for logout", aid ? aid : "(missing)");
265         throw ConfigurationException("Unable to locate application for logout, deleted?");
266     }
267
268     // Unpack the request.
269     auto_ptr<HTTPRequest> req(getRequest(in));
270
271     // Wrap a response shim.
272     DDF ret(nullptr);
273     DDFJanitor jout(ret);
274     auto_ptr<HTTPResponse> resp(getResponse(ret));
275
276     // Since we're remoted, the result should either be a throw, which we pass on,
277     // a false/0 return, which we just return as an empty structure, or a response/redirect,
278     // which we capture in the facade and send back.
279     doRequest(*app, *req.get(), *resp.get());
280     out << ret;
281 }
282
283 pair<bool,long> SAML2Logout::doRequest(const Application& application, const HTTPRequest& request, HTTPResponse& response) const
284 {
285 #ifndef SHIBSP_LITE
286     // First capture the active session ID.
287     SessionCache* cache = application.getServiceProvider().getSessionCache();
288     SessionCacheEx* cacheex = dynamic_cast<SessionCacheEx*>(cache);
289     string session_id = cache->active(application, request);
290
291     auto_ptr<LogoutEvent> logout_event(newLogoutEvent(application, &request));
292     if (logout_event.get() && !session_id.empty())
293         logout_event->m_sessions.push_back(session_id);
294
295     if (!strcmp(request.getMethod(),"GET") && request.getParameter("notifying")) {
296         // This is returning from a front-channel notification, so we have to do the back-channel and then
297         // respond. To do that, we need state from the original request.
298         if (!request.getParameter("entityID")) {
299             cache->remove(application, request, &response);
300             throw FatalProfileException("Application notification loop did not return entityID for LogoutResponse.");
301         }
302
303         // Best effort on back channel and to remove the user agent's session.
304         bool worked1 = false,worked2 = false;
305         if (!session_id.empty()) {
306             vector<string> sessions(1,session_id);
307             worked1 = notifyBackChannel(application, request.getRequestURL(), sessions, false);
308             try {
309                 cache->remove(application, request, &response);
310                 worked2 = true;
311             }
312             catch (exception& ex) {
313                 m_log.error("error removing session (%s): %s", session_id.c_str(), ex.what());
314             }
315         }
316         else {
317             worked1 = worked2 = true;
318         }
319
320         // We need metadata to issue a response.
321         MetadataProvider* m = application.getMetadataProvider();
322         Locker metadataLocker(m);
323         MetadataProviderCriteria mc(
324             application, request.getParameter("entityID"), &IDPSSODescriptor::ELEMENT_QNAME, samlconstants::SAML20P_NS
325             );
326         pair<const EntityDescriptor*,const RoleDescriptor*> entity = m->getEntityDescriptor(mc);
327         if (!entity.first) {
328             throw MetadataException(
329                 "Unable to locate metadata for identity provider ($entityID)",
330                 namedparams(1, "entityID", request.getParameter("entityID"))
331                 );
332         }
333         else if (!entity.second) {
334             throw MetadataException(
335                 "Unable to locate SAML 2.0 IdP role for identity provider ($entityID).",
336                 namedparams(1, "entityID", request.getParameter("entityID"))
337                 );
338         }
339
340         auto_ptr_XMLCh reqid(request.getParameter("ID"));
341         if (worked1 && worked2) {
342             // Successful LogoutResponse. Has to be front-channel or we couldn't be here.
343             if (logout_event.get())
344                 logout_event->m_logoutType = LogoutEvent::LOGOUT_EVENT_GLOBAL;
345             return sendResponse(
346                 logout_event.get(),
347                 reqid.get(),
348                 StatusCode::SUCCESS, nullptr, nullptr,
349                 request.getParameter("RelayState"),
350                 entity.second,
351                 application,
352                 response,
353                 true
354                 );
355         }
356
357         return sendResponse(
358             logout_event.get(),
359             reqid.get(),
360             StatusCode::RESPONDER, nullptr, "Unable to fully destroy principal's session.",
361             request.getParameter("RelayState"),
362             entity.second,
363             application,
364             response,
365             true
366             );
367     }
368
369     // If we get here, it's an external protocol message to decode.
370
371     // Locate policy key.
372     pair<bool,const char*> policyId = getString("policyId", m_configNS.get());  // namespace-qualified if inside handler element
373     if (!policyId.first)
374         policyId = application.getString("policyId");   // unqualified in Application(s) element
375
376     // Lock metadata for use by policy.
377     Locker metadataLocker(application.getMetadataProvider());
378
379     // Create the policy.
380     auto_ptr<SecurityPolicy> policy(
381         application.getServiceProvider().getSecurityPolicyProvider()->createSecurityPolicy(application, &m_role, policyId.second)
382         );
383
384     // Decode the message.
385     string relayState;
386     auto_ptr<XMLObject> msg(m_decoder->decode(relayState, request, *policy.get()));
387     const LogoutRequest* logoutRequest = dynamic_cast<LogoutRequest*>(msg.get());
388     if (logoutRequest) {
389         if (!policy->isAuthenticated())
390             throw SecurityPolicyException("Security of LogoutRequest not established.");
391
392         if (logout_event.get()) {
393             logout_event->m_saml2Request = logoutRequest;
394             if (policy->getIssuerMetadata())
395                 logout_event->m_peer = dynamic_cast<const EntityDescriptor*>(policy->getIssuerMetadata()->getParent());
396             application.getServiceProvider().getTransactionLog()->write(*logout_event);
397             logout_event->m_saml2Request = nullptr;
398         }
399
400         // Message from IdP to logout one or more sessions.
401
402         // If this is front-channel, we have to have a session_id to use already.
403         if (m_decoder->isUserAgentPresent() && session_id.empty()) {
404             m_log.error("no active session");
405             return sendResponse(
406                 logout_event.get(),
407                 logoutRequest->getID(),
408                 StatusCode::REQUESTER, StatusCode::UNKNOWN_PRINCIPAL, "No active session found in request.",
409                 relayState.c_str(),
410                 policy->getIssuerMetadata(),
411                 application,
412                 response,
413                 true
414                 );
415         }
416
417         bool ownedName = false;
418         NameID* nameid = logoutRequest->getNameID();
419         if (!nameid) {
420             // Check for EncryptedID.
421             EncryptedID* encname = logoutRequest->getEncryptedID();
422             if (encname) {
423                 CredentialResolver* cr=application.getCredentialResolver();
424                 if (!cr)
425                     m_log.warn("found encrypted NameID, but no decryption credential was available");
426                 else {
427                     Locker credlocker(cr);
428                     auto_ptr<MetadataCredentialCriteria> mcc(
429                         policy->getIssuerMetadata() ? new MetadataCredentialCriteria(*policy->getIssuerMetadata()) : nullptr
430                         );
431                     try {
432                         auto_ptr<XMLObject> decryptedID(
433                             encname->decrypt(
434                                 *cr,
435                                 application.getRelyingParty(
436                                     policy->getIssuerMetadata() ?
437                                         dynamic_cast<EntityDescriptor*>(policy->getIssuerMetadata()->getParent()) :
438                                             nullptr)->getXMLString("entityID").second,
439                                 mcc.get()
440                                 )
441                             );
442                         nameid = dynamic_cast<NameID*>(decryptedID.get());
443                         if (nameid) {
444                             ownedName = true;
445                             decryptedID.release();
446                         }
447                     }
448                     catch (exception& ex) {
449                         m_log.error(ex.what());
450                     }
451                 }
452             }
453         }
454         if (!nameid) {
455             // No NameID, so must respond with an error.
456             m_log.error("NameID not found in request");
457             return sendResponse(
458                 logout_event.get(),
459                 logoutRequest->getID(),
460                 StatusCode::REQUESTER, StatusCode::UNKNOWN_PRINCIPAL, "NameID not found in request.",
461                 relayState.c_str(),
462                 policy->getIssuerMetadata(),
463                 application,
464                 response,
465                 m_decoder->isUserAgentPresent()
466                 );
467         }
468
469         auto_ptr<NameID> namewrapper(ownedName ? nameid : nullptr);
470
471         // Suck indexes out of the request for next steps.
472         set<string> indexes;
473         EntityDescriptor* entity = policy->getIssuerMetadata() ? dynamic_cast<EntityDescriptor*>(policy->getIssuerMetadata()->getParent()) : nullptr;
474         const vector<SessionIndex*> sindexes = logoutRequest->getSessionIndexs();
475         for (vector<SessionIndex*>::const_iterator i = sindexes.begin(); i != sindexes.end(); ++i) {
476             auto_ptr_char sindex((*i)->getSessionIndex());
477             indexes.insert(sindex.get());
478         }
479
480         // For a front-channel LogoutRequest, we have to match the information in the request
481         // against the current session.
482         if (!session_id.empty()) {
483             if (!cache->matches(application, request, entity, *nameid, &indexes)) {
484                 return sendResponse(
485                     logout_event.get(),
486                     logoutRequest->getID(),
487                     StatusCode::REQUESTER, StatusCode::REQUEST_DENIED, "Active session did not match logout request.",
488                     relayState.c_str(),
489                     policy->getIssuerMetadata(),
490                     application,
491                     response,
492                     true
493                     );
494             }
495
496         }
497
498         // Now we perform "logout" by finding the matching sessions.
499         vector<string> sessions;
500         try {
501             if (cacheex) {
502                 time_t expires = logoutRequest->getNotOnOrAfter() ? logoutRequest->getNotOnOrAfterEpoch() : 0;
503                 cacheex->logout(application, entity, *nameid, &indexes, expires, sessions);
504                 m_log.debug("session cache returned %d sessions bound to NameID in logout request", sessions.size());
505
506                 // Now we actually terminate everything except for the active session,
507                 // if this is front-channel, for notification purposes.
508                 for (vector<string>::const_iterator sit = sessions.begin(); sit != sessions.end(); ++sit)
509                     if (*sit != session_id)
510                         cacheex->remove(application, sit->c_str());   // using the ID-based removal operation
511             }
512             else {
513                 m_log.warn("session cache does not support extended API, can't implement indirect logout of sessions");
514                 if (!session_id.empty())
515                     sessions.push_back(session_id);
516             }
517         }
518         catch (exception& ex) {
519             m_log.error("error while logging out matching sessions: %s", ex.what());
520             if (logout_event.get()) {
521                 logout_event->m_nameID = nameid;
522                 logout_event->m_sessions = sessions;
523                 logout_event->m_logoutType = LogoutEvent::LOGOUT_EVENT_PARTIAL;
524             }
525             return sendResponse(
526                 logout_event.get(),
527                 logoutRequest->getID(),
528                 StatusCode::RESPONDER, nullptr, ex.what(),
529                 relayState.c_str(),
530                 policy->getIssuerMetadata(),
531                 application,
532                 response,
533                 m_decoder->isUserAgentPresent()
534                 );
535         }
536
537         if (m_decoder->isUserAgentPresent()) {
538             // Pass control to the first front channel notification point, if any.
539             map<string,string> parammap;
540             if (!relayState.empty())
541                 parammap["RelayState"] = relayState;
542             auto_ptr_char entityID(entity ? entity->getEntityID() : nullptr);
543             if (entityID.get())
544                 parammap["entityID"] = entityID.get();
545             auto_ptr_char reqID(logoutRequest->getID());
546             if (reqID.get())
547                 parammap["ID"] = reqID.get();
548             pair<bool,long> result = notifyFrontChannel(application, request, response, &parammap);
549             if (result.first)
550                 return result;
551         }
552
553         // For back-channel requests, or if no front-channel notification is needed...
554         bool worked1 = notifyBackChannel(application, request.getRequestURL(), sessions, false);
555         bool worked2 = true;
556         if (!session_id.empty()) {
557             // One last session to yoink...
558             try {
559                 cache->remove(application, request, &response);
560             }
561             catch (exception& ex) {
562                 worked2 = false;
563                 m_log.error("error removing active session (%s): %s", session_id.c_str(), ex.what());
564             }
565         }
566
567         if (logout_event.get()) {
568             logout_event->m_nameID = nameid;
569             logout_event->m_sessions = sessions;
570             logout_event->m_logoutType = (worked1 && worked2) ? LogoutEvent::LOGOUT_EVENT_PARTIAL : LogoutEvent::LOGOUT_EVENT_GLOBAL;
571         }
572         return sendResponse(
573             logout_event.get(),
574             logoutRequest->getID(),
575             (worked1 && worked2) ? StatusCode::SUCCESS : StatusCode::RESPONDER,
576             (worked1 && worked2) ? nullptr : StatusCode::PARTIAL_LOGOUT,
577             nullptr,
578             relayState.c_str(),
579             policy->getIssuerMetadata(),
580             application,
581             response,
582             m_decoder->isUserAgentPresent()
583             );
584     }
585
586     // A LogoutResponse completes an SP-initiated logout sequence.
587     const LogoutResponse* logoutResponse = dynamic_cast<LogoutResponse*>(msg.get());
588     if (logoutResponse) {
589         if (!policy->isAuthenticated()) {
590             SecurityPolicyException ex("Security of LogoutResponse not established.");
591             if (policy->getIssuerMetadata())
592                 annotateException(&ex, policy->getIssuerMetadata()); // throws it
593             ex.raise();
594         }
595  
596         if (logout_event.get()) {
597             logout_event->m_logoutType = LogoutEvent::LOGOUT_EVENT_PARTIAL;
598             logout_event->m_saml2Response = logoutResponse;
599             if (policy->getIssuerMetadata())
600                 logout_event->m_peer = dynamic_cast<const EntityDescriptor*>(policy->getIssuerMetadata()->getParent());
601         }
602
603         try {
604             checkError(logoutResponse, policy->getIssuerMetadata()); // throws if Status doesn't look good...
605         }
606         catch (exception& ex) {
607             if (logout_event.get()) {
608                 logout_event->m_exception = &ex;
609                 application.getServiceProvider().getTransactionLog()->write(*logout_event);
610             }
611             throw;
612         }
613
614         // If relay state is set, recover the original return URL.
615         if (!relayState.empty())
616             recoverRelayState(application, request, response, relayState);
617
618         // Check for partial logout.
619         const StatusCode* sc = logoutResponse->getStatus() ? logoutResponse->getStatus()->getStatusCode() : nullptr;
620         sc = sc ? sc->getStatusCode() : nullptr;
621         if (sc && XMLString::equals(sc->getValue(), StatusCode::PARTIAL_LOGOUT)) {
622             if (logout_event.get())
623                 application.getServiceProvider().getTransactionLog()->write(*logout_event);
624             return sendLogoutPage(application, request, response, "partial");
625         }
626
627         if (logout_event.get()) {
628             logout_event->m_logoutType = LogoutEvent::LOGOUT_EVENT_GLOBAL;
629             application.getServiceProvider().getTransactionLog()->write(*logout_event);
630         }
631
632         if (!relayState.empty()) {
633             limitRelayState(m_log, application, request, relayState.c_str());
634             return make_pair(true, response.sendRedirect(relayState.c_str()));
635         }
636
637         // Return template for completion of logout.
638         return sendLogoutPage(application, request, response, "global");
639     }
640
641     FatalProfileException ex("Incoming message was not a samlp:LogoutRequest or samlp:LogoutResponse.");
642     if (policy->getIssuerMetadata())
643         annotateException(&ex, policy->getIssuerMetadata()); // throws it
644     ex.raise();
645     return make_pair(false,0L);  // never happen, satisfies compiler
646 #else
647     throw ConfigurationException("Cannot process logout message using lite version of shibsp library.");
648 #endif
649 }
650
651 #ifndef SHIBSP_LITE
652
653 pair<bool,long> SAML2Logout::sendResponse(
654     LogoutEvent* logoutEvent,
655     const XMLCh* requestID,
656     const XMLCh* code,
657     const XMLCh* subcode,
658     const char* msg,
659     const char* relayState,
660     const RoleDescriptor* role,
661     const Application& application,
662     HTTPResponse& httpResponse,
663     bool front
664     ) const
665 {
666     // Get endpoint and encoder to use.
667     const EndpointType* ep = nullptr;
668     const MessageEncoder* encoder = nullptr;
669     if (front) {
670         const IDPSSODescriptor* idp = dynamic_cast<const IDPSSODescriptor*>(role);
671         for (vector<const XMLCh*>::const_iterator b = m_bindings.begin(); idp && b!=m_bindings.end(); ++b) {
672             if (ep=EndpointManager<SingleLogoutService>(idp->getSingleLogoutServices()).getByBinding(*b)) {
673                 map<const XMLCh*,MessageEncoder*>::const_iterator enc = m_encoders.find(*b);
674                 if (enc!=m_encoders.end())
675                     encoder = enc->second;
676                 break;
677             }
678         }
679         if (!ep || !encoder) {
680             auto_ptr_char id(dynamic_cast<EntityDescriptor*>(role->getParent())->getEntityID());
681             m_log.error("unable to locate compatible SLO service for provider (%s)", id.get());
682             MetadataException ex("Unable to locate endpoint at IdP ($entityID) to send LogoutResponse.");
683             annotateException(&ex, role);   // throws it
684         }
685     }
686     else {
687         encoder = m_encoders.begin()->second;
688     }
689
690     // Prepare response.
691     auto_ptr<LogoutResponse> logout(LogoutResponseBuilder::buildLogoutResponse());
692     logout->setInResponseTo(requestID);
693     if (ep) {
694         const XMLCh* loc = ep->getResponseLocation();
695         if (!loc || !*loc)
696             loc = ep->getLocation();
697         logout->setDestination(loc);
698     }
699     Issuer* issuer = IssuerBuilder::buildIssuer();
700     logout->setIssuer(issuer);
701     issuer->setName(application.getRelyingParty(dynamic_cast<EntityDescriptor*>(role->getParent()))->getXMLString("entityID").second);
702     fillStatus(*logout.get(), code, subcode, msg);
703     logout->setID(SAMLConfig::getConfig().generateIdentifier());
704     logout->setIssueInstant(time(nullptr));
705
706     if (logoutEvent) {
707         logoutEvent->m_peer = dynamic_cast<EntityDescriptor*>(role->getParent());
708         logoutEvent->m_saml2Response = logout.get();
709         application.getServiceProvider().getTransactionLog()->write(*logoutEvent);
710     }
711
712     auto_ptr_char dest(logout->getDestination());
713     long ret = sendMessage(*encoder, logout.get(), relayState, dest.get(), role, application, httpResponse, front);
714     logout.release();  // freed by encoder
715     return make_pair(true,ret);
716 }
717
718 #endif