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