Local logout handler, makefile changes, bug fix to SAML initiator.
[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 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, 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 = SAMLConfig::getConfig().MessageEncoderManager.newPlugin(b.get(),e);
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 first.
170     pair<bool,long> ret = LogoutHandler::run(request, isHandler);
171     if (ret.first)
172         return ret;
173
174     Session* session = NULL;
175     try {
176         session = request.getSession(false, true, false);  // don't cache it and ignore all checks
177         if (!session)
178             return make_pair(false,0);
179
180         // We only handle SAML 2.0 sessions.
181         if (!XMLString::equals(session->getProtocol(), m_protocol.get()))
182             return make_pair(false,0);
183     }
184     catch (exception& ex) {
185         m_log.error("error accessing current session: %s", ex.what());
186         return make_pair(false,0);
187     }
188
189     // At this point, notification is completed, and we're ready to issue a LogoutRequest.
190
191     if (SPConfig::getConfig().isEnabled(SPConfig::OutOfProcess)) {
192         // When out of process, we run natively.
193         return doRequest(request.getApplication(), session, request);
194     }
195     else {
196         // When not out of process, we remote the request.
197         Locker locker(session);
198         DDF out,in(m_address.c_str());
199         DDFJanitor jin(in), jout(out);
200         in.addmember("application_id").string(request.getApplication().getId());
201         in.addmember("session_id").string(session->getID());
202         out=request.getServiceProvider().getListenerService()->send(in);
203         return unwrap(request, out);
204     }
205 }
206
207 void SAML2LogoutInitiator::receive(DDF& in, ostream& out)
208 {
209 #ifndef SHIBSP_LITE
210     // Defer to base class for notifications
211     if (in["notify"].integer() == 1)
212         return LogoutHandler::receive(in, out);
213
214     // Find application.
215     const char* aid=in["application_id"].string();
216     const Application* app=aid ? SPConfig::getConfig().getServiceProvider()->getApplication(aid) : NULL;
217     if (!app) {
218         // Something's horribly wrong.
219         m_log.error("couldn't find application (%s) for logout", aid ? aid : "(missing)");
220         throw ConfigurationException("Unable to locate application for logout, deleted?");
221     }
222     
223     // Set up a response shim.
224     DDF ret(NULL);
225     DDFJanitor jout(ret);
226     auto_ptr<HTTPResponse> resp(getResponse(ret));
227     
228     Session* session = NULL;
229     try {
230          session = app->getServiceProvider().getSessionCache()->find(in["session_id"].string(), *app, NULL, NULL);
231     }
232     catch (exception& ex) {
233         m_log.error("error accessing current session: %s", ex.what());
234     }
235
236     // With no session, we just skip the request and let it fall through to an empty struct return.
237     if (session) {
238         if (session->getNameID() && session->getEntityID()) {
239             // Since we're remoted, the result should either be a throw, which we pass on,
240             // a false/0 return, which we just return as an empty structure, or a response/redirect,
241             // which we capture in the facade and send back.
242             doRequest(*app, session, *resp.get());
243         }
244         else {
245              m_log.error("no NameID or issuing entityID found in session");
246              session->unlock();
247              session = NULL;
248              app->getServiceProvider().getSessionCache()->remove(in["session_id"].string(), *app);
249          }
250     }
251     out << ret;
252 #else
253     throw ConfigurationException("Cannot perform logout using lite version of shibsp library.");
254 #endif
255 }
256
257 pair<bool,long> SAML2LogoutInitiator::doRequest(const Application& application, Session* session, HTTPResponse& response) const
258 {
259 #ifndef SHIBSP_LITE
260     pair<bool,long> ret = make_pair(false,0);
261     try {
262         // With a session in hand, we can create a LogoutRequest message, if we can find a compatible endpoint.
263         Locker metadataLocker(application.getMetadataProvider());
264         const EntityDescriptor* entity = application.getMetadataProvider()->getEntityDescriptor(session->getEntityID());
265         if (!entity) {
266             throw MetadataException(
267                 "Unable to locate metadata for identity provider ($entityID)",
268                 namedparams(1, "entityID", session->getEntityID())
269                 );
270         }
271         const IDPSSODescriptor* role = entity->getIDPSSODescriptor(samlconstants::SAML20P_NS);
272         if (!role) {
273             throw MetadataException(
274                 "Unable to locate SAML 2.0 IdP role for identity provider ($entityID).",
275                 namedparams(1, "entityID", session->getEntityID())
276                 );
277         }
278
279         const EndpointType* ep=NULL;
280         const MessageEncoder* encoder=NULL;
281         vector<const XMLCh*>::const_iterator b;
282         for (b = m_bindings.begin(); b!=m_bindings.end(); ++b) {
283             if (ep=EndpointManager<SingleLogoutService>(role->getSingleLogoutServices()).getByBinding(*b)) {
284                 map<const XMLCh*,MessageEncoder*>::const_iterator enc = m_encoders.find(*b);
285                 if (enc!=m_encoders.end())
286                     encoder = enc->second;
287                 break;
288             }
289         }
290         if (!ep || !encoder) {
291             m_log.warn("no compatible front channel SingleLogoutService, trying back channel...");
292             shibsp::SecurityPolicy policy(application);
293             shibsp::SOAPClient soaper(policy);
294             MetadataCredentialCriteria mcc(*role);
295
296             LogoutResponse* logoutResponse=NULL;
297             auto_ptr_XMLCh binding(samlconstants::SAML20_BINDING_SOAP);
298             const vector<SingleLogoutService*>& endpoints=role->getSingleLogoutServices();
299             for (vector<SingleLogoutService*>::const_iterator epit=endpoints.begin(); !logoutResponse && epit!=endpoints.end(); ++epit) {
300                 try {
301                     if (!XMLString::equals((*epit)->getBinding(),binding.get()))
302                         continue;
303                     LogoutRequest* msg = buildRequest(application, *session, *role);
304                     auto_ptr_char dest((*epit)->getLocation());
305
306                     SAML2SOAPClient client(soaper, false);
307                     client.sendSAML(msg, mcc, dest.get());
308                     StatusResponseType* srt = client.receiveSAML();
309                     if (!(logoutResponse = dynamic_cast<LogoutResponse*>(srt))) {
310                         delete srt;
311                         break;
312                     }
313                 }
314                 catch (exception& ex) {
315                     m_log.error("error sending LogoutRequest message: %s", ex.what());
316                     soaper.reset();
317                 }
318             }
319
320             if (!logoutResponse)
321                 return sendLogoutPage(application, response, false, "Identity provider did not respond to logout request.");
322             if (!logoutResponse->getStatus() || !logoutResponse->getStatus()->getStatusCode() ||
323                    !XMLString::equals(logoutResponse->getStatus()->getStatusCode()->getValue(), saml2p::StatusCode::SUCCESS)) {
324                 delete logoutResponse;
325                 return sendLogoutPage(application, response, false, "Identity provider returned a SAML error in response to logout request.");
326             }
327             delete logoutResponse;
328             return sendLogoutPage(application, response, false, "Logout completed successfully.");
329         }
330
331         auto_ptr<LogoutRequest> msg(buildRequest(application, *session, *role, encoder));
332
333         msg->setDestination(ep->getLocation());
334         auto_ptr_char dest(ep->getLocation());
335         ret.second = sendMessage(*encoder, msg.get(), NULL, dest.get(), role, application, response, "signRequests");
336         ret.first = true;
337         msg.release();  // freed by encoder
338     }
339     catch (exception& ex) {
340         m_log.error("error issuing SAML 2.0 logout request: %s", ex.what());
341     }
342
343     if (session) {
344         string session_id = session->getID();
345         session->unlock();
346         session = NULL;
347         application.getServiceProvider().getSessionCache()->remove(session_id.c_str(), application);
348     }
349
350     return ret;
351 #else
352     string session_id = session->getID();
353     session->unlock();
354     application.getServiceProvider().getSessionCache()->remove(session_id.c_str(), application);
355     throw ConfigurationException("Cannot perform logout using lite version of shibsp library.");
356 #endif
357 }
358
359 #ifndef SHIBSP_LITE
360
361 LogoutRequest* SAML2LogoutInitiator::buildRequest(
362     const Application& application, const Session& session, const IDPSSODescriptor& role, const MessageEncoder* encoder
363     ) const
364 {
365     auto_ptr<LogoutRequest> msg(LogoutRequestBuilder::buildLogoutRequest());
366     Issuer* issuer = IssuerBuilder::buildIssuer();
367     msg->setIssuer(issuer);
368     issuer->setName(application.getXMLString("entityID").second);
369     auto_ptr_XMLCh index(session.getSessionIndex());
370     if (index.get() && *index.get()) {
371         SessionIndex* si = SessionIndexBuilder::buildSessionIndex();
372         msg->getSessionIndexs().push_back(si);
373         si->setSessionIndex(index.get());
374     }
375
376     const NameID* nameid = session.getNameID();
377     const PropertySet* relyingParty = application.getRelyingParty(dynamic_cast<EntityDescriptor*>(role.getParent()));
378     pair<bool,const char*> flag = relyingParty->getString("encryptRequests");
379     if (flag.first &&
380         (!strcmp(flag.second, "true") || (encoder && !strcmp(flag.second, "front")) || (!encoder && !strcmp(flag.second, "back")))) {
381         auto_ptr<EncryptedID> encrypted(EncryptedIDBuilder::buildEncryptedID());
382         MetadataCredentialCriteria mcc(role);
383         encrypted->encrypt(
384             *nameid,
385             *(application.getMetadataProvider()),
386             mcc,
387             encoder ? encoder->isCompact() : false,
388             relyingParty->getXMLString("encryptionAlg").second
389             );
390         msg->setEncryptedID(encrypted.release());
391     }
392
393     if (!encoder) {
394         // No encoder being used, so sign for SOAP client manually.
395         flag = relyingParty->getString("signRequests");
396         if (flag.first && (!strcmp(flag.second, "true") || !strcmp(flag.second, "back"))) {
397             CredentialResolver* credResolver=application.getCredentialResolver();
398             if (credResolver) {
399                 Locker credLocker(credResolver);
400                 // Fill in criteria to use.
401                 MetadataCredentialCriteria mcc(role);
402                 mcc.setUsage(CredentialCriteria::SIGNING_CREDENTIAL);
403                 pair<bool,const char*> keyName = relyingParty->getString("keyName");
404                 if (keyName.first)
405                     mcc.getKeyNames().insert(keyName.second);
406                 pair<bool,const XMLCh*> sigalg = relyingParty->getXMLString("signatureAlg");
407                 if (sigalg.first)
408                     mcc.setXMLAlgorithm(sigalg.second);
409                 const Credential* cred = credResolver->resolve(&mcc);
410                 if (cred) {
411                     xmlsignature::Signature* sig = xmlsignature::SignatureBuilder::buildSignature();\r
412                     msg->setSignature(sig);\r
413                     pair<bool, const XMLCh*> alg = relyingParty->getXMLString("signatureAlg");\r
414                     if (alg.first)\r
415                         sig->setSignatureAlgorithm(alg.second);\r
416                     alg = relyingParty->getXMLString("digestAlg");\r
417                     if (alg.first) {\r
418                         ContentReference* cr = dynamic_cast<ContentReference*>(sig->getContentReference());\r
419                         if (cr)\r
420                             cr->setDigestAlgorithm(alg.second);\r
421                     }\r
422             \r
423                     // Sign response while marshalling.\r
424                     vector<xmlsignature::Signature*> sigs(1,sig);\r
425                     msg->marshall((DOMDocument*)NULL,&sigs,cred);\r
426                 }
427                 else {
428                     m_log.warn("no signing credential resolved, leaving message unsigned");
429                 }
430             }
431             else {
432                 m_log.warn("no credential resolver installed, leaving message unsigned");
433             }
434         }
435     }
436
437     return msg.release();
438 }
439
440 #endif