https://issues.shibboleth.net/jira/browse/SSPCPP-270
[shibboleth/cpp-sp.git] / shibsp / handler / impl / SAML2LogoutInitiator.cpp
1 /*
2  *  Copyright 2001-2009 Internet2
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *     http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16
17 /**
18  * SAML2LogoutInitiator.cpp
19  *
20  * Triggers SP-initiated logout for SAML 2.0 sessions.
21  */
22
23 #include "internal.h"
24 #include "exceptions.h"
25 #include "Application.h"
26 #include "ServiceProvider.h"
27 #include "SessionCache.h"
28 #include "SPRequest.h"
29 #include "handler/AbstractHandler.h"
30 #include "handler/LogoutHandler.h"
31
32 #ifndef SHIBSP_LITE
33 # include "binding/SOAPClient.h"
34 # include "metadata/MetadataProviderCriteria.h"
35 # include "security/SecurityPolicy.h"
36 # include <saml/exceptions.h>
37 # include <saml/SAMLConfig.h>
38 # include <saml/saml2/core/Protocols.h>
39 # include <saml/saml2/binding/SAML2SOAPClient.h>
40 # include <saml/saml2/metadata/EndpointManager.h>
41 # include <saml/saml2/metadata/Metadata.h>
42 # include <saml/saml2/metadata/MetadataCredentialCriteria.h>
43 using namespace opensaml::saml2;
44 using namespace opensaml::saml2p;
45 using namespace opensaml::saml2md;
46 using namespace opensaml;
47 #else
48 # include "lite/SAMLConstants.h"
49 #endif
50
51 using namespace shibsp;
52 using namespace xmltooling;
53 using namespace std;
54
55 namespace shibsp {
56
57 #if defined (_MSC_VER)
58     #pragma warning( push )
59     #pragma warning( disable : 4250 )
60 #endif
61
62     class SHIBSP_DLLLOCAL SAML2LogoutInitiator : public AbstractHandler, public LogoutHandler
63     {
64     public:
65         SAML2LogoutInitiator(const DOMElement* e, const char* appId);
66         virtual ~SAML2LogoutInitiator() {
67 #ifndef SHIBSP_LITE
68             if (SPConfig::getConfig().isEnabled(SPConfig::OutOfProcess)) {
69                 XMLString::release(&m_outgoing);
70                 for_each(m_encoders.begin(), m_encoders.end(), cleanup_pair<const XMLCh*,MessageEncoder>());
71             }
72 #endif
73         }
74
75         void setParent(const PropertySet* parent);
76         void receive(DDF& in, ostream& out);
77         pair<bool,long> run(SPRequest& request, bool isHandler=true) const;
78
79 #ifndef SHIBSP_LITE
80         const char* getType() const {
81             return "LogoutInitiator";
82         }
83 #endif
84
85     private:
86         pair<bool,long> doRequest(
87             const Application& application, const HTTPRequest& request, HTTPResponse& httpResponse, Session* session
88             ) const;
89
90         string m_appId;
91 #ifndef SHIBSP_LITE
92         LogoutRequest* buildRequest(
93             const Application& application, const Session& session, const RoleDescriptor& role, const MessageEncoder* encoder=NULL
94             ) const;
95
96         XMLCh* m_outgoing;
97         vector<const XMLCh*> m_bindings;
98         map<const XMLCh*,MessageEncoder*> m_encoders;
99 #endif
100         auto_ptr_char m_protocol;
101     };
102
103 #if defined (_MSC_VER)
104     #pragma warning( pop )
105 #endif
106
107     Handler* SHIBSP_DLLLOCAL SAML2LogoutInitiatorFactory(const pair<const DOMElement*,const char*>& p)
108     {
109         return new SAML2LogoutInitiator(p.first, p.second);
110     }
111 };
112
113 SAML2LogoutInitiator::SAML2LogoutInitiator(const DOMElement* e, const char* appId)
114     : AbstractHandler(e, Category::getInstance(SHIBSP_LOGCAT".LogoutInitiator.SAML2")), m_appId(appId),
115 #ifndef SHIBSP_LITE
116         m_outgoing(NULL),
117 #endif
118         m_protocol(samlconstants::SAML20P_NS)
119 {
120 #ifndef SHIBSP_LITE
121     if (SPConfig::getConfig().isEnabled(SPConfig::OutOfProcess)) {
122         // Handle outgoing binding setup.
123         pair<bool,const XMLCh*> outgoing = getXMLString("outgoingBindings");
124         if (outgoing.first) {
125             m_outgoing = XMLString::replicate(outgoing.second);
126             XMLString::trim(m_outgoing);
127         }
128         else {
129             // No override, so we'll install a default binding precedence.
130             string prec = string(samlconstants::SAML20_BINDING_HTTP_REDIRECT) + ' ' + samlconstants::SAML20_BINDING_HTTP_POST + ' ' +
131                 samlconstants::SAML20_BINDING_HTTP_POST_SIMPLESIGN + ' ' + samlconstants::SAML20_BINDING_HTTP_ARTIFACT;
132             m_outgoing = XMLString::transcode(prec.c_str());
133         }
134
135         int pos;
136         XMLCh* start = m_outgoing;
137         while (start && *start) {
138             pos = XMLString::indexOf(start,chSpace);
139             if (pos != -1)
140                 *(start + pos)=chNull;
141             m_bindings.push_back(start);
142             try {
143                 auto_ptr_char b(start);
144                 MessageEncoder * encoder =
145                     SAMLConfig::getConfig().MessageEncoderManager.newPlugin(b.get(),pair<const DOMElement*,const XMLCh*>(e,NULL));
146                 if (encoder->isUserAgentPresent()) {
147                     m_encoders[start] = encoder;
148                     m_log.debug("supporting outgoing binding (%s)", b.get());
149                 }
150                 else {
151                     delete encoder;
152                     m_log.warn("skipping outgoing binding (%s), not a front-channel mechanism", b.get());
153                 }
154             }
155             catch (exception& ex) {
156                 m_log.error("error building MessageEncoder: %s", ex.what());
157             }
158             if (pos != -1)
159                 start = start + pos + 1;
160             else
161                 break;
162         }
163     }
164 #endif
165
166     pair<bool,const char*> loc = getString("Location");
167     if (loc.first) {
168         string address = m_appId + loc.second + "::run::SAML2LI";
169         setAddress(address.c_str());
170     }
171 }
172
173 void SAML2LogoutInitiator::setParent(const PropertySet* parent)
174 {
175     DOMPropertySet::setParent(parent);
176     pair<bool,const char*> loc = getString("Location");
177     if (loc.first) {
178         string address = m_appId + loc.second + "::run::SAML2LI";
179         setAddress(address.c_str());
180     }
181     else {
182         m_log.warn("no Location property in SAML2 LogoutInitiator (or parent), can't register as remoted handler");
183     }
184 }
185
186 pair<bool,long> SAML2LogoutInitiator::run(SPRequest& request, bool isHandler) const
187 {
188     // Defer to base class for front-channel loop first.
189     pair<bool,long> ret = LogoutHandler::run(request, isHandler);
190     if (ret.first)
191         return ret;
192
193     // At this point we know the front-channel is handled.
194     // We need the session to do any other work.
195
196     Session* session = NULL;
197     try {
198         session = request.getSession(false, true, false);  // don't cache it and ignore all checks
199         if (!session)
200             return make_pair(false,0L);
201
202         // We only handle SAML 2.0 sessions.
203         if (!XMLString::equals(session->getProtocol(), m_protocol.get())) {
204             session->unlock();
205             return make_pair(false,0L);
206         }
207     }
208     catch (exception& ex) {
209         m_log.error("error accessing current session: %s", ex.what());
210         return make_pair(false,0L);
211     }
212
213     if (SPConfig::getConfig().isEnabled(SPConfig::OutOfProcess)) {
214         // When out of process, we run natively.
215         return doRequest(request.getApplication(), request, request, session);
216     }
217     else {
218         // When not out of process, we remote the request.
219         session->unlock();
220         vector<string> headers(1,"Cookie");
221         DDF out,in = wrap(request,&headers);
222         DDFJanitor jin(in), jout(out);
223         out=request.getServiceProvider().getListenerService()->send(in);
224         return unwrap(request, out);
225     }
226 }
227
228 void SAML2LogoutInitiator::receive(DDF& in, ostream& out)
229 {
230 #ifndef SHIBSP_LITE
231     // Defer to base class for notifications
232     if (in["notify"].integer() == 1)
233         return LogoutHandler::receive(in, out);
234
235     // Find application.
236     const char* aid=in["application_id"].string();
237     const Application* app=aid ? SPConfig::getConfig().getServiceProvider()->getApplication(aid) : NULL;
238     if (!app) {
239         // Something's horribly wrong.
240         m_log.error("couldn't find application (%s) for logout", aid ? aid : "(missing)");
241         throw ConfigurationException("Unable to locate application for logout, deleted?");
242     }
243
244     // Unpack the request.
245     auto_ptr<HTTPRequest> req(getRequest(in));
246
247     // Set up a response shim.
248     DDF ret(NULL);
249     DDFJanitor jout(ret);
250     auto_ptr<HTTPResponse> resp(getResponse(ret));
251
252     Session* session = NULL;
253     try {
254          session = app->getServiceProvider().getSessionCache()->find(*app, *req.get(), NULL, NULL);
255     }
256     catch (exception& ex) {
257         m_log.error("error accessing current session: %s", ex.what());
258     }
259
260     // With no session, we just skip the request and let it fall through to an empty struct return.
261     if (session) {
262         if (session->getNameID() && session->getEntityID()) {
263             // Since we're remoted, the result should either be a throw, which we pass on,
264             // a false/0 return, which we just return as an empty structure, or a response/redirect,
265             // which we capture in the facade and send back.
266             doRequest(*app, *req.get(), *resp.get(), session);
267         }
268         else {
269              m_log.error("no NameID or issuing entityID found in session");
270              session->unlock();
271              app->getServiceProvider().getSessionCache()->remove(*app, *req.get(), resp.get());
272         }
273     }
274     out << ret;
275 #else
276     throw ConfigurationException("Cannot perform logout using lite version of shibsp library.");
277 #endif
278 }
279
280 pair<bool,long> SAML2LogoutInitiator::doRequest(
281     const Application& application, const HTTPRequest& httpRequest, HTTPResponse& httpResponse, Session* session
282     ) const
283 {
284     // Do back channel notification.
285     vector<string> sessions(1, session->getID());
286     if (!notifyBackChannel(application, httpRequest.getRequestURL(), sessions, false)) {
287         session->unlock();
288         application.getServiceProvider().getSessionCache()->remove(application, httpRequest, &httpResponse);
289         return sendLogoutPage(application, httpRequest, httpResponse, "partial");
290     }
291
292 #ifndef SHIBSP_LITE
293     pair<bool,long> ret = make_pair(false,0L);
294     try {
295         // With a session in hand, we can create a LogoutRequest message, if we can find a compatible endpoint.
296         MetadataProvider* m = application.getMetadataProvider();
297         Locker metadataLocker(m);
298         MetadataProviderCriteria mc(application, session->getEntityID(), &IDPSSODescriptor::ELEMENT_QNAME, samlconstants::SAML20P_NS);
299         pair<const EntityDescriptor*,const RoleDescriptor*> entity = m->getEntityDescriptor(mc);
300         if (!entity.first) {
301             throw MetadataException(
302                 "Unable to locate metadata for identity provider ($entityID)", namedparams(1, "entityID", session->getEntityID())
303                 );
304         }
305         else if (!entity.second) {
306             throw MetadataException(
307                 "Unable to locate SAML 2.0 IdP role for identity provider ($entityID).", namedparams(1, "entityID", session->getEntityID())
308                 );
309         }
310
311         const IDPSSODescriptor* role = dynamic_cast<const IDPSSODescriptor*>(entity.second);
312         const EndpointType* ep=NULL;
313         const MessageEncoder* encoder=NULL;
314         vector<const XMLCh*>::const_iterator b;
315         for (b = m_bindings.begin(); b!=m_bindings.end(); ++b) {
316             if (ep=EndpointManager<SingleLogoutService>(role->getSingleLogoutServices()).getByBinding(*b)) {
317                 map<const XMLCh*,MessageEncoder*>::const_iterator enc = m_encoders.find(*b);
318                 if (enc!=m_encoders.end())
319                     encoder = enc->second;
320                 break;
321             }
322         }
323         if (!ep || !encoder) {
324             m_log.debug("no compatible front channel SingleLogoutService, trying back channel...");
325             shibsp::SecurityPolicy policy(application);
326             shibsp::SOAPClient soaper(policy);
327             MetadataCredentialCriteria mcc(*role);
328
329             LogoutResponse* logoutResponse=NULL;
330             auto_ptr_XMLCh binding(samlconstants::SAML20_BINDING_SOAP);
331             const vector<SingleLogoutService*>& endpoints=role->getSingleLogoutServices();
332             for (vector<SingleLogoutService*>::const_iterator epit=endpoints.begin(); !logoutResponse && epit!=endpoints.end(); ++epit) {
333                 try {
334                     if (!XMLString::equals((*epit)->getBinding(),binding.get()))
335                         continue;
336                     LogoutRequest* msg = buildRequest(application, *session, *role);
337                     auto_ptr_char dest((*epit)->getLocation());
338
339                     SAML2SOAPClient client(soaper, false);
340                     client.sendSAML(msg, application.getId(), mcc, dest.get());
341                     StatusResponseType* srt = client.receiveSAML();
342                     if (!(logoutResponse = dynamic_cast<LogoutResponse*>(srt))) {
343                         delete srt;
344                         break;
345                     }
346                 }
347                 catch (exception& ex) {
348                     m_log.error("error sending LogoutRequest message: %s", ex.what());
349                     soaper.reset();
350                 }
351             }
352
353             // No answer at all?
354             if (!logoutResponse) {
355                 if (endpoints.empty())
356                     m_log.info("IdP doesn't support single logout protocol over a compatible binding");
357                 else
358                     m_log.warn("IdP didn't respond to logout request");
359                 ret = sendLogoutPage(application, httpRequest, httpResponse, "partial");
360             }
361             else {
362                 // Check the status, looking for non-success or a partial logout code.
363                 const StatusCode* sc = logoutResponse->getStatus() ? logoutResponse->getStatus()->getStatusCode() : NULL;
364                 bool partial = (!sc || !XMLString::equals(sc->getValue(), StatusCode::SUCCESS));
365                 if (!partial && sc->getStatusCode()) {
366                     // Success, but still need to check for partial.
367                     partial = XMLString::equals(sc->getStatusCode()->getValue(), StatusCode::PARTIAL_LOGOUT);
368                 }
369                 delete logoutResponse;
370                 if (partial)
371                     ret = sendLogoutPage(application, httpRequest, httpResponse, "partial");
372                 else {
373                     const char* returnloc = httpRequest.getParameter("return");
374                     if (returnloc) {
375                         ret.second = httpResponse.sendRedirect(returnloc);
376                         ret.first = true;
377                     }
378                     ret = sendLogoutPage(application, httpRequest, httpResponse, "global");
379                 }
380             }
381
382             if (session) {
383                 session->unlock();
384                 session = NULL;
385                 application.getServiceProvider().getSessionCache()->remove(application, httpRequest, &httpResponse);
386             }
387
388             return ret;
389         }
390
391         // Save off return location as RelayState.
392         string relayState;
393         const char* returnloc = httpRequest.getParameter("return");
394         if (returnloc) {
395             relayState = returnloc;
396             preserveRelayState(application, httpResponse, relayState);
397         }
398
399         auto_ptr<LogoutRequest> msg(buildRequest(application, *session, *role, encoder));
400
401         msg->setDestination(ep->getLocation());
402         auto_ptr_char dest(ep->getLocation());
403         ret.second = sendMessage(*encoder, msg.get(), relayState.c_str(), dest.get(), role, application, httpResponse);
404         ret.first = true;
405         msg.release();  // freed by encoder
406     }
407     catch (exception& ex) {
408         m_log.error("error issuing SAML 2.0 logout request: %s", ex.what());
409     }
410
411     if (session) {
412         session->unlock();
413         session = NULL;
414         application.getServiceProvider().getSessionCache()->remove(application, httpRequest, &httpResponse);
415     }
416
417     return ret;
418 #else
419     session->unlock();
420     application.getServiceProvider().getSessionCache()->remove(application, httpRequest, &httpResponse);
421     throw ConfigurationException("Cannot perform logout using lite version of shibsp library.");
422 #endif
423 }
424
425 #ifndef SHIBSP_LITE
426
427 LogoutRequest* SAML2LogoutInitiator::buildRequest(
428     const Application& application, const Session& session, const RoleDescriptor& role, const MessageEncoder* encoder
429     ) const
430 {
431     const PropertySet* relyingParty = application.getRelyingParty(dynamic_cast<EntityDescriptor*>(role.getParent()));
432
433     auto_ptr<LogoutRequest> msg(LogoutRequestBuilder::buildLogoutRequest());
434     Issuer* issuer = IssuerBuilder::buildIssuer();
435     msg->setIssuer(issuer);
436     issuer->setName(relyingParty->getXMLString("entityID").second);
437     auto_ptr_XMLCh index(session.getSessionIndex());
438     if (index.get() && *index.get()) {
439         SessionIndex* si = SessionIndexBuilder::buildSessionIndex();
440         msg->getSessionIndexs().push_back(si);
441         si->setSessionIndex(index.get());
442     }
443
444     const NameID* nameid = session.getNameID();
445     pair<bool,const char*> flag = relyingParty->getString("encryption");
446     if (flag.first &&
447         (!strcmp(flag.second, "true") || (encoder && !strcmp(flag.second, "front")) || (!encoder && !strcmp(flag.second, "back")))) {
448         auto_ptr<EncryptedID> encrypted(EncryptedIDBuilder::buildEncryptedID());
449         MetadataCredentialCriteria mcc(role);
450         encrypted->encrypt(
451             *nameid,
452             *(application.getMetadataProvider()),
453             mcc,
454             encoder ? encoder->isCompact() : false,
455             relyingParty->getXMLString("encryptionAlg").second
456             );
457         msg->setEncryptedID(encrypted.release());
458     }
459     else {
460         msg->setNameID(nameid->cloneNameID());
461     }
462
463     return msg.release();
464 }
465
466 #endif