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