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