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