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