Factor out LogoutInitiator class, simpler API to get ACS by binding.
[shibboleth/sp.git] / shibsp / handler / impl / SAML2LogoutInitiator.cpp
1 /*
2  *  Copyright 2001-2010 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/LogoutInitiator.h"
30
31 #ifndef SHIBSP_LITE
32 # include "binding/SOAPClient.h"
33 # include "metadata/MetadataProviderCriteria.h"
34 # include "security/SecurityPolicy.h"
35 # include <saml/exceptions.h>
36 # include <saml/SAMLConfig.h>
37 # include <saml/saml2/core/Protocols.h>
38 # include <saml/saml2/binding/SAML2SOAPClient.h>
39 # include <saml/saml2/metadata/EndpointManager.h>
40 # include <saml/saml2/metadata/Metadata.h>
41 # include <saml/saml2/metadata/MetadataCredentialCriteria.h>
42 using namespace opensaml::saml2;
43 using namespace opensaml::saml2p;
44 using namespace opensaml::saml2md;
45 using namespace opensaml;
46 #else
47 # include "lite/SAMLConstants.h"
48 #endif
49
50 using namespace shibsp;
51 using namespace xmltooling;
52 using namespace std;
53
54 namespace shibsp {
55
56 #if defined (_MSC_VER)
57     #pragma warning( push )
58     #pragma warning( disable : 4250 )
59 #endif
60
61     class SHIBSP_DLLLOCAL SAML2LogoutInitiator : public AbstractHandler, public LogoutInitiator
62     {
63     public:
64         SAML2LogoutInitiator(const DOMElement* e, const char* appId);
65         virtual ~SAML2LogoutInitiator() {
66 #ifndef SHIBSP_LITE
67             if (SPConfig::getConfig().isEnabled(SPConfig::OutOfProcess)) {
68                 XMLString::release(&m_outgoing);
69                 for_each(m_encoders.begin(), m_encoders.end(), cleanup_pair<const XMLCh*,MessageEncoder>());
70             }
71 #endif
72         }
73
74         void setParent(const PropertySet* parent);
75         void receive(DDF& in, ostream& out);
76         pair<bool,long> run(SPRequest& request, bool isHandler=true) const;
77
78         const XMLCh* getProtocolFamily() const {
79             return samlconstants::SAML20P_NS;
80         }
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=nullptr
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(nullptr),
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,nullptr));
143                 if (encoder->isUserAgentPresent() && XMLString::equals(getProtocolFamily(), encoder->getProtocolFamily())) {
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 SAML 2.0 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 = nullptr;
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) : nullptr;
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(nullptr);
246     DDFJanitor jout(ret);
247     auto_ptr<HTTPResponse> resp(getResponse(ret));
248
249     Session* session = nullptr;
250     try {
251          session = app->getServiceProvider().getSessionCache()->find(*app, *req.get(), nullptr, nullptr);
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.log(getParent() ? Priority::WARN : Priority::ERROR, "bypassing SAML 2.0 logout, 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, "partial");
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         if (role->getSingleLogoutServices().empty()) {
310             throw MetadataException(
311                 "No SingleLogoutService endpoints in metadata for identity provider ($entityID).", namedparams(1, "entityID", session->getEntityID())
312                 );
313         }
314
315         const EndpointType* ep=nullptr;
316         const MessageEncoder* encoder=nullptr;
317         vector<const XMLCh*>::const_iterator b;
318         for (b = m_bindings.begin(); b!=m_bindings.end(); ++b) {
319             if (ep=EndpointManager<SingleLogoutService>(role->getSingleLogoutServices()).getByBinding(*b)) {
320                 map<const XMLCh*,MessageEncoder*>::const_iterator enc = m_encoders.find(*b);
321                 if (enc!=m_encoders.end())
322                     encoder = enc->second;
323                 break;
324             }
325         }
326         if (!ep || !encoder) {
327             m_log.debug("no compatible front channel SingleLogoutService, trying back channel...");
328             shibsp::SecurityPolicy policy(application);
329             shibsp::SOAPClient soaper(policy);
330             MetadataCredentialCriteria mcc(*role);
331
332             LogoutResponse* logoutResponse=nullptr;
333             auto_ptr_XMLCh binding(samlconstants::SAML20_BINDING_SOAP);
334             const vector<SingleLogoutService*>& endpoints=role->getSingleLogoutServices();
335             for (vector<SingleLogoutService*>::const_iterator epit=endpoints.begin(); !logoutResponse && epit!=endpoints.end(); ++epit) {
336                 try {
337                     if (!XMLString::equals((*epit)->getBinding(),binding.get()))
338                         continue;
339                     LogoutRequest* msg = buildRequest(application, *session, *role);
340                     auto_ptr_char dest((*epit)->getLocation());
341
342                     SAML2SOAPClient client(soaper, false);
343                     client.sendSAML(msg, application.getId(), mcc, dest.get());
344                     StatusResponseType* srt = client.receiveSAML();
345                     if (!(logoutResponse = dynamic_cast<LogoutResponse*>(srt))) {
346                         delete srt;
347                         break;
348                     }
349                 }
350                 catch (exception& ex) {
351                     m_log.error("error sending LogoutRequest message: %s", ex.what());
352                     soaper.reset();
353                 }
354             }
355
356             // No answer at all?
357             if (!logoutResponse) {
358                 if (endpoints.empty())
359                     m_log.info("IdP doesn't support single logout protocol over a compatible binding");
360                 else
361                     m_log.warn("IdP didn't respond to logout request");
362                 ret = sendLogoutPage(application, httpRequest, httpResponse, "partial");
363             }
364             else {
365                 // Check the status, looking for non-success or a partial logout code.
366                 const StatusCode* sc = logoutResponse->getStatus() ? logoutResponse->getStatus()->getStatusCode() : nullptr;
367                 bool partial = (!sc || !XMLString::equals(sc->getValue(), StatusCode::SUCCESS));
368                 if (!partial && sc->getStatusCode()) {
369                     // Success, but still need to check for partial.
370                     partial = XMLString::equals(sc->getStatusCode()->getValue(), StatusCode::PARTIAL_LOGOUT);
371                 }
372                 delete logoutResponse;
373                 if (partial)
374                     ret = sendLogoutPage(application, httpRequest, httpResponse, "partial");
375                 else {
376                     const char* returnloc = httpRequest.getParameter("return");
377                     if (returnloc) {
378                         ret.second = httpResponse.sendRedirect(returnloc);
379                         ret.first = true;
380                     }
381                     ret = sendLogoutPage(application, httpRequest, httpResponse, "global");
382                 }
383             }
384
385             if (session) {
386                 session->unlock();
387                 session = nullptr;
388                 application.getServiceProvider().getSessionCache()->remove(application, httpRequest, &httpResponse);
389             }
390
391             return ret;
392         }
393
394         // Save off return location as RelayState.
395         string relayState;
396         const char* returnloc = httpRequest.getParameter("return");
397         if (returnloc) {
398             relayState = returnloc;
399             preserveRelayState(application, httpResponse, relayState);
400         }
401
402         auto_ptr<LogoutRequest> msg(buildRequest(application, *session, *role, encoder));
403
404         msg->setDestination(ep->getLocation());
405         auto_ptr_char dest(ep->getLocation());
406         ret.second = sendMessage(*encoder, msg.get(), relayState.c_str(), dest.get(), role, application, httpResponse);
407         ret.first = true;
408         msg.release();  // freed by encoder
409     }
410     catch (exception& ex) {
411         m_log.error("error issuing SAML 2.0 logout request: %s", ex.what());
412     }
413
414     if (session) {
415         session->unlock();
416         session = nullptr;
417         application.getServiceProvider().getSessionCache()->remove(application, httpRequest, &httpResponse);
418     }
419
420     return ret;
421 #else
422     session->unlock();
423     application.getServiceProvider().getSessionCache()->remove(application, httpRequest, &httpResponse);
424     throw ConfigurationException("Cannot perform logout using lite version of shibsp library.");
425 #endif
426 }
427
428 #ifndef SHIBSP_LITE
429
430 LogoutRequest* SAML2LogoutInitiator::buildRequest(
431     const Application& application, const Session& session, const RoleDescriptor& role, const MessageEncoder* encoder
432     ) const
433 {
434     const PropertySet* relyingParty = application.getRelyingParty(dynamic_cast<EntityDescriptor*>(role.getParent()));
435
436     auto_ptr<LogoutRequest> msg(LogoutRequestBuilder::buildLogoutRequest());
437     Issuer* issuer = IssuerBuilder::buildIssuer();
438     msg->setIssuer(issuer);
439     issuer->setName(relyingParty->getXMLString("entityID").second);
440     auto_ptr_XMLCh index(session.getSessionIndex());
441     if (index.get() && *index.get()) {
442         SessionIndex* si = SessionIndexBuilder::buildSessionIndex();
443         msg->getSessionIndexs().push_back(si);
444         si->setSessionIndex(index.get());
445     }
446
447     const NameID* nameid = session.getNameID();
448     pair<bool,const char*> flag = relyingParty->getString("encryption");
449     if (flag.first &&
450         (!strcmp(flag.second, "true") || (encoder && !strcmp(flag.second, "front")) || (!encoder && !strcmp(flag.second, "back")))) {
451         auto_ptr<EncryptedID> encrypted(EncryptedIDBuilder::buildEncryptedID());
452         MetadataCredentialCriteria mcc(role);
453         encrypted->encrypt(
454             *nameid,
455             *(application.getMetadataProvider()),
456             mcc,
457             encoder ? encoder->isCompact() : false,
458             relyingParty->getXMLString("encryptionAlg").second
459             );
460         msg->setEncryptedID(encrypted.release());
461     }
462     else {
463         msg->setNameID(nameid->cloneNameID());
464     }
465
466     return msg.release();
467 }
468
469 #endif