37c150b52e652803552f4d43024047105ca4201b
[shibboleth/cpp-sp.git] / shibsp / handler / impl / SAML2LogoutInitiator.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  * SAML2LogoutInitiator.cpp
23  *
24  * Triggers SP-initiated logout for SAML 2.0 sessions.
25  */
26
27 #include "internal.h"
28 #include "exceptions.h"
29 #include "Application.h"
30 #include "ServiceProvider.h"
31 #include "SessionCache.h"
32 #include "handler/AbstractHandler.h"
33 #include "handler/LogoutInitiator.h"
34
35 #ifndef SHIBSP_LITE
36 # include "binding/SOAPClient.h"
37 # include "metadata/MetadataProviderCriteria.h"
38 # include "security/SecurityPolicy.h"
39 # include <boost/algorithm/string.hpp>
40 # include <boost/iterator/indirect_iterator.hpp>
41 # include <saml/exceptions.h>
42 # include <saml/SAMLConfig.h>
43 # include <saml/saml2/core/Protocols.h>
44 # include <saml/saml2/binding/SAML2SOAPClient.h>
45 # include <saml/saml2/metadata/EndpointManager.h>
46 # include <saml/saml2/metadata/Metadata.h>
47 # include <saml/saml2/metadata/MetadataCredentialCriteria.h>
48 using namespace opensaml::saml2;
49 using namespace opensaml::saml2p;
50 using namespace opensaml::saml2md;
51 using namespace opensaml;
52 #else
53 # include "lite/SAMLConstants.h"
54 #endif
55
56 using namespace shibsp;
57 using namespace xmltooling;
58 using namespace boost;
59 using namespace std;
60
61 namespace shibsp {
62
63 #if defined (_MSC_VER)
64     #pragma warning( push )
65     #pragma warning( disable : 4250 )
66 #endif
67
68     class SHIBSP_DLLLOCAL SAML2LogoutInitiator : public AbstractHandler, public LogoutInitiator
69     {
70     public:
71         SAML2LogoutInitiator(const DOMElement* e, const char* appId);
72         virtual ~SAML2LogoutInitiator() {}
73
74         void init(const char* location);    // encapsulates actions that need to run either in the c'tor or setParent
75
76         void setParent(const PropertySet* parent);
77         void receive(DDF& in, ostream& out);
78         pair<bool,long> run(SPRequest& request, bool isHandler=true) const;
79
80         const XMLCh* getProtocolFamily() const {
81             return samlconstants::SAML20P_NS;
82         }
83
84     private:
85         pair<bool,long> doRequest(
86             const Application& application, const HTTPRequest& request, HTTPResponse& httpResponse, Session* session
87             ) const;
88
89         string m_appId;
90         auto_ptr_char m_protocol;
91 #ifndef SHIBSP_LITE
92         auto_ptr<LogoutRequest> buildRequest(
93             const Application& application,
94             const Session& session,
95             const RoleDescriptor& role,
96             const XMLCh* endpoint,
97             const MessageEncoder* encoder=nullptr
98             ) const;
99
100         LogoutEvent* newLogoutEvent(
101             const Application& application, const HTTPRequest* request=nullptr, const Session* session=nullptr
102             ) const {
103             LogoutEvent* e = LogoutHandler::newLogoutEvent(application, request, session);
104             if (e)
105                 e->m_protocol = m_protocol.get();
106             return e;
107         }
108
109         bool m_async;
110         vector<string> m_bindings;
111         map< string,boost::shared_ptr<MessageEncoder> > m_encoders;
112 #endif
113     };
114
115 #if defined (_MSC_VER)
116     #pragma warning( pop )
117 #endif
118
119     Handler* SHIBSP_DLLLOCAL SAML2LogoutInitiatorFactory(const pair<const DOMElement*,const char*>& p)
120     {
121         return new SAML2LogoutInitiator(p.first, p.second);
122     }
123 };
124
125 SAML2LogoutInitiator::SAML2LogoutInitiator(const DOMElement* e, const char* appId)
126     : AbstractHandler(e, Category::getInstance(SHIBSP_LOGCAT ".LogoutInitiator.SAML2")), m_appId(appId), m_protocol(samlconstants::SAML20P_NS)
127 #ifndef SHIBSP_LITE
128         ,m_async(true)
129 #endif
130 {
131     // If Location isn't set, defer initialization until the setParent call.
132     pair<bool,const char*> loc = getString("Location");
133     if (loc.first) {
134         init(loc.second);
135     }
136 }
137
138 void SAML2LogoutInitiator::setParent(const PropertySet* parent)
139 {
140     DOMPropertySet::setParent(parent);
141     pair<bool,const char*> loc = getString("Location");
142     init(loc.second);
143 }
144
145 void SAML2LogoutInitiator::init(const char* location)
146 {
147     if (location) {
148         string address = m_appId + location + "::run::SAML2LI";
149         setAddress(address.c_str());
150     }
151     else {
152         m_log.warn("no Location property in SAML2 LogoutInitiator (or parent), can't register as remoted handler");
153     }
154
155 #ifndef SHIBSP_LITE
156     if (SPConfig::getConfig().isEnabled(SPConfig::OutOfProcess)) {
157         pair<bool,bool> async = getBool("asynchronous");
158         m_async = !async.first || async.second;
159
160         string dupBindings;
161         pair<bool,const char*> outgoing = getString("outgoingBindings");
162         if (outgoing.first) {
163             dupBindings = outgoing.second;
164             trim(dupBindings);
165         }
166         else {
167             // No override, so we'll install a default binding precedence.
168             dupBindings = string(samlconstants::SAML20_BINDING_HTTP_REDIRECT) + ' ' + samlconstants::SAML20_BINDING_HTTP_POST + ' ' +
169                 samlconstants::SAML20_BINDING_HTTP_POST_SIMPLESIGN + ' ' + samlconstants::SAML20_BINDING_HTTP_ARTIFACT;
170         }
171         split(m_bindings, dupBindings, is_space(), algorithm::token_compress_on);
172         for (vector<string>::const_iterator b = m_bindings.begin(); b != m_bindings.end(); ++b) {
173             try {
174                 boost::shared_ptr<MessageEncoder> encoder(
175                     SAMLConfig::getConfig().MessageEncoderManager.newPlugin(*b, pair<const DOMElement*,const XMLCh*>(getElement(),nullptr))
176                     );
177                 if (encoder->isUserAgentPresent() && XMLString::equals(getProtocolFamily(), encoder->getProtocolFamily())) {
178                     m_encoders[*b] = encoder;
179                     m_log.debug("supporting outgoing binding (%s)", b->c_str());
180                 }
181                 else {
182                     m_log.warn("skipping outgoing binding (%s), not a SAML 2.0 front-channel mechanism", b->c_str());
183                 }
184             }
185             catch (std::exception& ex) {
186                 m_log.error("error building MessageEncoder: %s", ex.what());
187             }
188         }
189     }
190 #endif
191 }
192
193
194 pair<bool,long> SAML2LogoutInitiator::run(SPRequest& request, bool isHandler) const
195 {
196     // Defer to base class for front-channel loop first.
197     pair<bool,long> ret = LogoutHandler::run(request, isHandler);
198     if (ret.first)
199         return ret;
200
201     // At this point we know the front-channel is handled.
202     // We need the session to do any other work.
203
204     Session* session = nullptr;
205     try {
206         session = request.getSession(false, true, false);  // don't cache it and ignore all checks
207         if (!session)
208             return make_pair(false, 0L);
209
210         // We only handle SAML 2.0 sessions.
211         if (!XMLString::equals(session->getProtocol(), m_protocol.get())) {
212             session->unlock();
213             return make_pair(false, 0L);
214         }
215     }
216     catch (std::exception& ex) {
217         m_log.error("error accessing current session: %s", ex.what());
218         return make_pair(false, 0L);
219     }
220
221     if (SPConfig::getConfig().isEnabled(SPConfig::OutOfProcess)) {
222         // When out of process, we run natively.
223         return doRequest(request.getApplication(), request, request, session);
224     }
225     else {
226         // When not out of process, we remote the request.
227         session->unlock();
228         vector<string> headers(1,"Cookie");
229         DDF out,in = wrap(request,&headers);
230         DDFJanitor jin(in), jout(out);
231         out=request.getServiceProvider().getListenerService()->send(in);
232         return unwrap(request, out);
233     }
234 }
235
236 void SAML2LogoutInitiator::receive(DDF& in, ostream& out)
237 {
238 #ifndef SHIBSP_LITE
239     // Defer to base class for notifications
240     if (in["notify"].integer() == 1)
241         return LogoutHandler::receive(in, out);
242
243     // Find application.
244     const char* aid=in["application_id"].string();
245     const Application* app=aid ? SPConfig::getConfig().getServiceProvider()->getApplication(aid) : nullptr;
246     if (!app) {
247         // Something's horribly wrong.
248         m_log.error("couldn't find application (%s) for logout", aid ? aid : "(missing)");
249         throw ConfigurationException("Unable to locate application for logout, deleted?");
250     }
251
252     // Unpack the request.
253     scoped_ptr<HTTPRequest> req(getRequest(in));
254
255     // Set up a response shim.
256     DDF ret(nullptr);
257     DDFJanitor jout(ret);
258     scoped_ptr<HTTPResponse> resp(getResponse(ret));
259
260     Session* session = nullptr;
261     try {
262          session = app->getServiceProvider().getSessionCache()->find(*app, *req, nullptr, nullptr);
263     }
264     catch (std::exception& ex) {
265         m_log.error("error accessing current session: %s", ex.what());
266     }
267
268     // With no session, we just skip the request and let it fall through to an empty struct return.
269     if (session) {
270         if (session->getNameID() && session->getEntityID()) {
271             // Since we're remoted, the result should either be a throw, which we pass on,
272             // a false/0 return, which we just return as an empty structure, or a response/redirect,
273             // which we capture in the facade and send back.
274             doRequest(*app, *req, *resp, session);
275         }
276         else {
277             session->unlock();
278             m_log.log(getParent() ? Priority::WARN : Priority::ERROR, "bypassing SAML 2.0 logout, no NameID or issuing entityID found in session");
279             app->getServiceProvider().getSessionCache()->remove(*app, *req, resp.get());
280         }
281     }
282     out << ret;
283 #else
284     throw ConfigurationException("Cannot perform logout using lite version of shibsp library.");
285 #endif
286 }
287
288 pair<bool,long> SAML2LogoutInitiator::doRequest(
289     const Application& application, const HTTPRequest& httpRequest, HTTPResponse& httpResponse, Session* session
290     ) const
291 {
292     Locker sessionLocker(session, false);
293 #ifndef SHIBSP_LITE
294     scoped_ptr<LogoutEvent> logout_event(newLogoutEvent(application, &httpRequest, session));
295 #endif
296
297     // Do back channel notification.
298     vector<string> sessions(1, session->getID());
299     if (!notifyBackChannel(application, httpRequest.getRequestURL(), sessions, false)) {
300 #ifndef SHIBSP_LITE
301         if (logout_event) {
302             logout_event->m_logoutType = LogoutEvent::LOGOUT_EVENT_PARTIAL;
303             application.getServiceProvider().getTransactionLog()->write(*logout_event);
304         }
305 #endif
306         sessionLocker.assign();
307         session = nullptr;
308         application.getServiceProvider().getSessionCache()->remove(application, httpRequest, &httpResponse);
309         return sendLogoutPage(application, httpRequest, httpResponse, "partial");
310     }
311
312 #ifndef SHIBSP_LITE
313     pair<bool,long> ret = make_pair(false, 0L);
314     try {
315         // With a session in hand, we can create a LogoutRequest message, if we can find a compatible endpoint.
316         MetadataProvider* m = application.getMetadataProvider();
317         Locker metadataLocker(m);
318         MetadataProviderCriteria mc(application, session->getEntityID(), &IDPSSODescriptor::ELEMENT_QNAME, samlconstants::SAML20P_NS);
319         pair<const EntityDescriptor*,const RoleDescriptor*> entity = m->getEntityDescriptor(mc);
320         if (!entity.first) {
321             throw MetadataException(
322                 "Unable to locate metadata for identity provider ($entityID)", namedparams(1, "entityID", session->getEntityID())
323                 );
324         }
325         else if (!entity.second) {
326             throw MetadataException(
327                 "Unable to locate SAML 2.0 IdP role for identity provider ($entityID).", namedparams(1, "entityID", session->getEntityID())
328                 );
329         }
330
331         const IDPSSODescriptor* role = dynamic_cast<const IDPSSODescriptor*>(entity.second);
332         if (role->getSingleLogoutServices().empty()) {
333             throw MetadataException(
334                 "No SingleLogoutService endpoints in metadata for identity provider ($entityID).", namedparams(1, "entityID", session->getEntityID())
335                 );
336         }
337
338         const EndpointType* ep = nullptr;
339         const MessageEncoder* encoder = nullptr;
340         for (vector<string>::const_iterator b = m_bindings.begin(); b != m_bindings.end(); ++b) {
341             auto_ptr_XMLCh wideb(b->c_str());
342             ep = EndpointManager<SingleLogoutService>(role->getSingleLogoutServices()).getByBinding(wideb.get());
343             if (ep) {
344                 map< string,boost::shared_ptr<MessageEncoder> >::const_iterator enc = m_encoders.find(*b);
345                 if (enc != m_encoders.end())
346                     encoder = enc->second.get();
347                 break;
348             }
349         }
350         if (!ep || !encoder) {
351             m_log.debug("no compatible front channel SingleLogoutService, trying back channel...");
352             shibsp::SecurityPolicy policy(application);
353             shibsp::SOAPClient soaper(policy);
354             MetadataCredentialCriteria mcc(*role);
355
356             LogoutResponse* logoutResponse = nullptr;
357             scoped_ptr<StatusResponseType> srt;
358             auto_ptr_XMLCh binding(samlconstants::SAML20_BINDING_SOAP);
359             const vector<SingleLogoutService*>& endpoints = role->getSingleLogoutServices();
360             for (indirect_iterator<vector<SingleLogoutService*>::const_iterator> epit = make_indirect_iterator(endpoints.begin());
361                     !logoutResponse && epit != make_indirect_iterator(endpoints.end()); ++epit) {
362                 try {
363                     if (!XMLString::equals(epit->getBinding(), binding.get()))
364                         continue;
365                     auto_ptr<LogoutRequest> msg(buildRequest(application, *session, *role, epit->getLocation()));
366
367                     // Log the request.
368                     if (logout_event) {
369                         logout_event->m_logoutType = LogoutEvent::LOGOUT_EVENT_UNKNOWN;
370                         logout_event->m_saml2Request = msg.get();
371                         application.getServiceProvider().getTransactionLog()->write(*logout_event);
372                         logout_event->m_saml2Request = nullptr;
373                     }
374
375                     SAML2SOAPClient client(soaper, false);
376                     auto_ptr_char dest(epit->getLocation());
377                     client.sendSAML(msg.release(), application.getId(), mcc, dest.get());
378                     srt.reset(client.receiveSAML());
379                     if (!(logoutResponse = dynamic_cast<LogoutResponse*>(srt.get()))) {
380                         break;
381                     }
382                 }
383                 catch (std::exception& ex) {
384                     m_log.error("error sending LogoutRequest message: %s", ex.what());
385                     soaper.reset();
386                 }
387             }
388
389             // No answer at all?
390             if (!logoutResponse) {
391                 if (endpoints.empty())
392                     m_log.info("IdP doesn't support single logout protocol over a compatible binding");
393                 else
394                     m_log.warn("IdP didn't respond to logout request");
395
396                 // Log the end result.
397                 if (logout_event) {
398                     logout_event->m_logoutType = LogoutEvent::LOGOUT_EVENT_PARTIAL;
399                     application.getServiceProvider().getTransactionLog()->write(*logout_event);
400                 }
401
402                 ret = sendLogoutPage(application, httpRequest, httpResponse, "partial");
403             }
404             else {
405                 // Check the status, looking for non-success or a partial logout code.
406                 const StatusCode* sc = logoutResponse->getStatus() ? logoutResponse->getStatus()->getStatusCode() : nullptr;
407                 bool partial = (!sc || !XMLString::equals(sc->getValue(), StatusCode::SUCCESS));
408                 if (!partial && sc->getStatusCode()) {
409                     // Success, but still need to check for partial.
410                     partial = XMLString::equals(sc->getStatusCode()->getValue(), StatusCode::PARTIAL_LOGOUT);
411                 }
412
413                 // Log the end result.
414                 if (logout_event) {
415                     logout_event->m_logoutType = partial ? LogoutEvent::LOGOUT_EVENT_PARTIAL : LogoutEvent::LOGOUT_EVENT_GLOBAL;
416                     logout_event->m_saml2Response = logoutResponse;
417                     application.getServiceProvider().getTransactionLog()->write(*logout_event);
418                 }
419
420                 if (partial)
421                     ret = sendLogoutPage(application, httpRequest, httpResponse, "partial");
422                 else {
423                     const char* returnloc = httpRequest.getParameter("return");
424                     if (returnloc) {
425                         // Relative URLs get promoted, absolutes get validated.
426                         if (*returnloc == '/') {
427                             string loc(returnloc);
428                             httpRequest.absolutize(loc);
429                             ret.second = httpResponse.sendRedirect(loc.c_str());
430                         }
431                         else {
432                             application.limitRedirect(httpRequest, returnloc);
433                             ret.second = httpResponse.sendRedirect(returnloc);
434                         }
435                         ret.first = true;
436                     }
437                     else {
438                         ret = sendLogoutPage(application, httpRequest, httpResponse, "global");
439                     }
440                 }
441             }
442
443             if (session) {
444                 sessionLocker.assign();
445                 session = nullptr;
446                 application.getServiceProvider().getSessionCache()->remove(application, httpRequest, &httpResponse);
447             }
448
449             return ret;
450         }
451
452         // Save off return location as RelayState.
453         string relayState;
454         const char* returnloc = httpRequest.getParameter("return");
455         if (returnloc) {
456             application.limitRedirect(httpRequest, returnloc);
457             relayState = returnloc;
458             httpRequest.absolutize(relayState);
459             cleanRelayState(application, httpRequest, httpResponse);
460             preserveRelayState(application, httpResponse, relayState);
461         }
462
463         auto_ptr<LogoutRequest> msg(buildRequest(application, *session, *role, ep->getLocation(), encoder));
464         msg->setDestination(ep->getLocation());
465
466         // Log the request.
467         if (logout_event) {
468             logout_event->m_logoutType = LogoutEvent::LOGOUT_EVENT_UNKNOWN;
469             logout_event->m_saml2Request = msg.get();
470             application.getServiceProvider().getTransactionLog()->write(*logout_event);
471         }
472
473         auto_ptr_char dest(ep->getLocation());
474         ret.second = sendMessage(*encoder, msg.get(), relayState.c_str(), dest.get(), role, application, httpResponse, "true");
475         ret.first = true;
476         msg.release();  // freed by encoder
477
478         if (session) {
479             sessionLocker.assign();
480             session = nullptr;
481             application.getServiceProvider().getSessionCache()->remove(application, httpRequest, &httpResponse);
482         }
483     }
484     catch (MetadataException& mex) {
485         // Less noise for IdPs that don't support logout (i.e. most)
486         m_log.info("unable to issue SAML 2.0 logout request: %s", mex.what());
487     }
488     catch (std::exception& ex) {
489         m_log.error("error issuing SAML 2.0 logout request: %s", ex.what());
490     }
491
492     return ret;
493 #else
494     throw ConfigurationException("Cannot perform logout using lite version of shibsp library.");
495 #endif
496 }
497
498 #ifndef SHIBSP_LITE
499
500 auto_ptr<LogoutRequest> SAML2LogoutInitiator::buildRequest(
501     const Application& application,
502     const Session& session,
503     const RoleDescriptor& role,
504     const XMLCh* endpoint,
505     const MessageEncoder* encoder) const
506 {
507     const PropertySet* relyingParty = application.getRelyingParty(dynamic_cast<EntityDescriptor*>(role.getParent()));
508
509     auto_ptr<LogoutRequest> msg(LogoutRequestBuilder::buildLogoutRequest());
510     Issuer* issuer = IssuerBuilder::buildIssuer();
511     msg->setIssuer(issuer);
512     issuer->setName(relyingParty->getXMLString("entityID").second);
513     auto_ptr_XMLCh index(session.getSessionIndex());
514     if (index.get() && *index.get()) {
515         SessionIndex* si = SessionIndexBuilder::buildSessionIndex();
516         msg->getSessionIndexs().push_back(si);
517         si->setSessionIndex(index.get());
518     }
519
520     const NameID* nameid = session.getNameID();
521     pair<bool,const char*> flag = relyingParty->getString("encryption");
522     auto_ptr_char dest(endpoint);
523     if (SPConfig::shouldSignOrEncrypt(flag.first ? flag.second : "conditional", dest.get(), encoder != nullptr)) {
524         try {
525             auto_ptr<EncryptedID> encrypted(EncryptedIDBuilder::buildEncryptedID());
526             MetadataCredentialCriteria mcc(role);
527             encrypted->encrypt(
528                 *nameid,
529                 *(application.getMetadataProvider()),
530                 mcc,
531                 encoder ? encoder->isCompact() : false,
532                 relyingParty->getXMLString("encryptionAlg").second
533             );
534             msg->setEncryptedID(encrypted.get());
535             encrypted.release();
536         }
537         catch (std::exception& ex) {
538             // If we're encrypting deliberately, failure should be fatal.
539             if (flag.first && strcmp(flag.second, "conditional")) {
540                 throw;
541             }
542             // If opportunistically, just log and move on.
543             m_log.info("Conditional encryption of NameID in LogoutRequest failed: %s", ex.what());
544             auto_ptr<NameID> namewrapper(nameid->cloneNameID());
545             msg->setNameID(namewrapper.get());
546             namewrapper.release();
547         }
548     }
549     else {
550         auto_ptr<NameID> namewrapper(nameid->cloneNameID());
551         msg->setNameID(namewrapper.get());
552         namewrapper.release();
553     }
554
555     XMLCh* msgid = SAMLConfig::getConfig().generateIdentifier();
556     msg->setID(msgid);
557     XMLString::release(&msgid);
558     msg->setIssueInstant(time(nullptr));
559
560     if (m_async && encoder) {
561         msg->setExtensions(saml2p::ExtensionsBuilder::buildExtensions());
562         msg->getExtensions()->getUnknownXMLObjects().push_back(AsynchronousBuilder::buildAsynchronous());
563     }
564
565     return msg;
566 }
567
568 #endif