Simplify/rename signing and encryption properties.
[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 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.SAML2")), 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, false);
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              app->getServiceProvider().getSessionCache()->remove(in["session_id"].string(), *app);
252
253             // Clear the cookie.
254             pair<string,const char*> shib_cookie=app->getCookieNameProps("_shibsession_");
255             resp->setCookie(shib_cookie.first.c_str(), shib_cookie.second);
256         }
257     }
258     out << ret;
259 #else
260     throw ConfigurationException("Cannot perform logout using lite version of shibsp library.");
261 #endif
262 }
263
264 pair<bool,long> SAML2LogoutInitiator::doRequest(
265     const Application& application, const char* requestURL, Session* session, HTTPResponse& response
266     ) const
267 {
268     // Clear the cookie.
269     pair<string,const char*> shib_cookie=application.getCookieNameProps("_shibsession_");
270     response.setCookie(shib_cookie.first.c_str(), shib_cookie.second);
271
272     // Do back channel notification.
273     vector<string> sessions(1, session->getID());
274     if (!notifyBackChannel(application, requestURL, sessions, false)) {
275         session->unlock();
276         application.getServiceProvider().getSessionCache()->remove(sessions.front().c_str(), application);
277         return sendLogoutPage(application, response, true, "Partial logout failure.");
278     }
279
280 #ifndef SHIBSP_LITE
281     pair<bool,long> ret = make_pair(false,0);
282     try {
283         // With a session in hand, we can create a LogoutRequest message, if we can find a compatible endpoint.
284         Locker metadataLocker(application.getMetadataProvider());
285         const EntityDescriptor* entity = application.getMetadataProvider()->getEntityDescriptor(session->getEntityID());
286         if (!entity) {
287             throw MetadataException(
288                 "Unable to locate metadata for identity provider ($entityID)",
289                 namedparams(1, "entityID", session->getEntityID())
290                 );
291         }
292         const IDPSSODescriptor* role = entity->getIDPSSODescriptor(samlconstants::SAML20P_NS);
293         if (!role) {
294             throw MetadataException(
295                 "Unable to locate SAML 2.0 IdP role for identity provider ($entityID).",
296                 namedparams(1, "entityID", session->getEntityID())
297                 );
298         }
299
300         const EndpointType* ep=NULL;
301         const MessageEncoder* encoder=NULL;
302         vector<const XMLCh*>::const_iterator b;
303         for (b = m_bindings.begin(); b!=m_bindings.end(); ++b) {
304             if (ep=EndpointManager<SingleLogoutService>(role->getSingleLogoutServices()).getByBinding(*b)) {
305                 map<const XMLCh*,MessageEncoder*>::const_iterator enc = m_encoders.find(*b);
306                 if (enc!=m_encoders.end())
307                     encoder = enc->second;
308                 break;
309             }
310         }
311         if (!ep || !encoder) {
312             m_log.warn("no compatible front channel SingleLogoutService, trying back channel...");
313             shibsp::SecurityPolicy policy(application);
314             shibsp::SOAPClient soaper(policy);
315             MetadataCredentialCriteria mcc(*role);
316
317             LogoutResponse* logoutResponse=NULL;
318             auto_ptr_XMLCh binding(samlconstants::SAML20_BINDING_SOAP);
319             const vector<SingleLogoutService*>& endpoints=role->getSingleLogoutServices();
320             for (vector<SingleLogoutService*>::const_iterator epit=endpoints.begin(); !logoutResponse && epit!=endpoints.end(); ++epit) {
321                 try {
322                     if (!XMLString::equals((*epit)->getBinding(),binding.get()))
323                         continue;
324                     LogoutRequest* msg = buildRequest(application, *session, *role);
325                     auto_ptr_char dest((*epit)->getLocation());
326
327                     SAML2SOAPClient client(soaper, false);
328                     client.sendSAML(msg, mcc, dest.get());
329                     StatusResponseType* srt = client.receiveSAML();
330                     if (!(logoutResponse = dynamic_cast<LogoutResponse*>(srt))) {
331                         delete srt;
332                         break;
333                     }
334                 }
335                 catch (exception& ex) {
336                     m_log.error("error sending LogoutRequest message: %s", ex.what());
337                     soaper.reset();
338                 }
339             }
340
341             if (!logoutResponse)
342                 ret = sendLogoutPage(application, response, false, "Identity provider did not respond to logout request.");
343             else if (!logoutResponse->getStatus() || !logoutResponse->getStatus()->getStatusCode() ||
344                    !XMLString::equals(logoutResponse->getStatus()->getStatusCode()->getValue(), saml2p::StatusCode::SUCCESS)) {
345                 delete logoutResponse;
346                 ret = sendLogoutPage(application, response, false, "Identity provider returned a SAML error in response to logout request.");
347             }
348             else {
349                 delete logoutResponse;
350                 ret = sendLogoutPage(application, response, false, "Logout completed successfully.");
351             }
352
353             if (session) {
354                 string session_id = session->getID();
355                 session->unlock();
356                 session = NULL;
357                 application.getServiceProvider().getSessionCache()->remove(session_id.c_str(), application);
358             }
359             return ret;
360         }
361
362         auto_ptr<LogoutRequest> msg(buildRequest(application, *session, *role, encoder));
363
364         msg->setDestination(ep->getLocation());
365         auto_ptr_char dest(ep->getLocation());
366         ret.second = sendMessage(*encoder, msg.get(), NULL, dest.get(), role, application, response);
367         ret.first = true;
368         msg.release();  // freed by encoder
369     }
370     catch (exception& ex) {
371         m_log.error("error issuing SAML 2.0 logout request: %s", ex.what());
372     }
373
374     if (session) {
375         string session_id = session->getID();
376         session->unlock();
377         session = NULL;
378         application.getServiceProvider().getSessionCache()->remove(session_id.c_str(), application);
379     }
380
381     return ret;
382 #else
383     string session_id = session->getID();
384     session->unlock();
385     application.getServiceProvider().getSessionCache()->remove(session_id.c_str(), application);
386     throw ConfigurationException("Cannot perform logout using lite version of shibsp library.");
387 #endif
388 }
389
390 #ifndef SHIBSP_LITE
391
392 LogoutRequest* SAML2LogoutInitiator::buildRequest(
393     const Application& application, const Session& session, const IDPSSODescriptor& role, const MessageEncoder* encoder
394     ) const
395 {
396     auto_ptr<LogoutRequest> msg(LogoutRequestBuilder::buildLogoutRequest());
397     Issuer* issuer = IssuerBuilder::buildIssuer();
398     msg->setIssuer(issuer);
399     issuer->setName(application.getXMLString("entityID").second);
400     auto_ptr_XMLCh index(session.getSessionIndex());
401     if (index.get() && *index.get()) {
402         SessionIndex* si = SessionIndexBuilder::buildSessionIndex();
403         msg->getSessionIndexs().push_back(si);
404         si->setSessionIndex(index.get());
405     }
406
407     const NameID* nameid = session.getNameID();
408     const PropertySet* relyingParty = application.getRelyingParty(dynamic_cast<EntityDescriptor*>(role.getParent()));
409     pair<bool,const char*> flag = relyingParty->getString("encryption");
410     if (flag.first &&
411         (!strcmp(flag.second, "true") || (encoder && !strcmp(flag.second, "front")) || (!encoder && !strcmp(flag.second, "back")))) {
412         auto_ptr<EncryptedID> encrypted(EncryptedIDBuilder::buildEncryptedID());
413         MetadataCredentialCriteria mcc(role);
414         encrypted->encrypt(
415             *nameid,
416             *(application.getMetadataProvider()),
417             mcc,
418             encoder ? encoder->isCompact() : false,
419             relyingParty->getXMLString("encryptionAlg").second
420             );
421         msg->setEncryptedID(encrypted.release());
422     }
423
424     if (!encoder) {
425         // No encoder being used, so sign for SOAP client manually.
426         flag = relyingParty->getString("signing");
427         if (flag.first && (!strcmp(flag.second, "true") || !strcmp(flag.second, "back"))) {
428             CredentialResolver* credResolver=application.getCredentialResolver();
429             if (credResolver) {
430                 Locker credLocker(credResolver);
431                 // Fill in criteria to use.
432                 MetadataCredentialCriteria mcc(role);
433                 mcc.setUsage(CredentialCriteria::SIGNING_CREDENTIAL);
434                 pair<bool,const char*> keyName = relyingParty->getString("keyName");
435                 if (keyName.first)
436                     mcc.getKeyNames().insert(keyName.second);
437                 pair<bool,const XMLCh*> sigalg = relyingParty->getXMLString("signingAlg");
438                 if (sigalg.first)
439                     mcc.setXMLAlgorithm(sigalg.second);
440                 const Credential* cred = credResolver->resolve(&mcc);
441                 if (cred) {
442                     xmlsignature::Signature* sig = xmlsignature::SignatureBuilder::buildSignature();
443                     msg->setSignature(sig);
444                     if (sigalg.first)
445                         sig->setSignatureAlgorithm(sigalg.second);
446                     sigalg = relyingParty->getXMLString("digestAlg");
447                     if (sigalg.first) {
448                         ContentReference* cr = dynamic_cast<ContentReference*>(sig->getContentReference());
449                         if (cr)
450                             cr->setDigestAlgorithm(sigalg.second);
451                     }
452             
453                     // Sign response while marshalling.
454                     vector<xmlsignature::Signature*> sigs(1,sig);
455                     msg->marshall((DOMDocument*)NULL,&sigs,cred);
456                 }
457                 else {
458                     m_log.warn("no signing credential resolved, leaving message unsigned");
459                 }
460             }
461             else {
462                 m_log.warn("no credential resolver installed, leaving message unsigned");
463             }
464         }
465     }
466
467     return msg.release();
468 }
469
470 #endif