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