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