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