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