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