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