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