2 * Licensed to the University Corporation for Advanced Internet
3 * Development, Inc. (UCAID) under one or more contributor license
4 * agreements. See the NOTICE file distributed with this work for
5 * additional information regarding copyright ownership.
7 * UCAID licenses this file to you under the Apache License,
8 * Version 2.0 (the "License"); you may not use this file except
9 * in compliance with the License. You may obtain a copy of the
12 * http://www.apache.org/licenses/LICENSE-2.0
14 * Unless required by applicable law or agreed to in writing,
15 * software distributed under the License is distributed on an
16 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
17 * either express or implied. See the License for the specific
18 * language governing permissions and limitations under the License.
22 * SAML2LogoutInitiator.cpp
24 * Triggers SP-initiated logout for SAML 2.0 sessions.
28 #include "exceptions.h"
29 #include "Application.h"
30 #include "ServiceProvider.h"
31 #include "SessionCache.h"
32 #include "handler/AbstractHandler.h"
33 #include "handler/LogoutInitiator.h"
36 # include "binding/SOAPClient.h"
37 # include "metadata/MetadataProviderCriteria.h"
38 # include "security/SecurityPolicy.h"
39 # include <boost/algorithm/string.hpp>
40 # include <boost/iterator/indirect_iterator.hpp>
41 # include <saml/exceptions.h>
42 # include <saml/SAMLConfig.h>
43 # include <saml/saml2/core/Protocols.h>
44 # include <saml/saml2/binding/SAML2SOAPClient.h>
45 # include <saml/saml2/metadata/EndpointManager.h>
46 # include <saml/saml2/metadata/Metadata.h>
47 # include <saml/saml2/metadata/MetadataCredentialCriteria.h>
48 using namespace opensaml::saml2;
49 using namespace opensaml::saml2p;
50 using namespace opensaml::saml2md;
51 using namespace opensaml;
53 # include "lite/SAMLConstants.h"
56 using namespace shibsp;
57 using namespace xmltooling;
58 using namespace boost;
63 #if defined (_MSC_VER)
64 #pragma warning( push )
65 #pragma warning( disable : 4250 )
68 class SHIBSP_DLLLOCAL SAML2LogoutInitiator : public AbstractHandler, public LogoutInitiator
71 SAML2LogoutInitiator(const DOMElement* e, const char* appId);
72 virtual ~SAML2LogoutInitiator() {}
74 void init(const char* location); // encapsulates actions that need to run either in the c'tor or setParent
76 void setParent(const PropertySet* parent);
77 void receive(DDF& in, ostream& out);
78 pair<bool,long> run(SPRequest& request, bool isHandler=true) const;
80 const XMLCh* getProtocolFamily() const {
81 return samlconstants::SAML20P_NS;
85 pair<bool,long> doRequest(
86 const Application& application, const HTTPRequest& request, HTTPResponse& httpResponse, Session* session
90 auto_ptr_char m_protocol;
92 auto_ptr<LogoutRequest> buildRequest(
93 const Application& application,
94 const Session& session,
95 const RoleDescriptor& role,
96 const XMLCh* endpoint,
97 const MessageEncoder* encoder=nullptr
100 LogoutEvent* newLogoutEvent(
101 const Application& application, const HTTPRequest* request=nullptr, const Session* session=nullptr
103 LogoutEvent* e = LogoutHandler::newLogoutEvent(application, request, session);
105 e->m_protocol = m_protocol.get();
110 vector<string> m_bindings;
111 map< string,boost::shared_ptr<MessageEncoder> > m_encoders;
115 #if defined (_MSC_VER)
116 #pragma warning( pop )
119 Handler* SHIBSP_DLLLOCAL SAML2LogoutInitiatorFactory(const pair<const DOMElement*,const char*>& p)
121 return new SAML2LogoutInitiator(p.first, p.second);
125 SAML2LogoutInitiator::SAML2LogoutInitiator(const DOMElement* e, const char* appId)
126 : AbstractHandler(e, Category::getInstance(SHIBSP_LOGCAT ".LogoutInitiator.SAML2")), m_appId(appId), m_protocol(samlconstants::SAML20P_NS)
131 // If Location isn't set, defer initialization until the setParent call.
132 pair<bool,const char*> loc = getString("Location");
138 void SAML2LogoutInitiator::setParent(const PropertySet* parent)
140 DOMPropertySet::setParent(parent);
141 pair<bool,const char*> loc = getString("Location");
145 void SAML2LogoutInitiator::init(const char* location)
148 string address = m_appId + location + "::run::SAML2LI";
149 setAddress(address.c_str());
152 m_log.warn("no Location property in SAML2 LogoutInitiator (or parent), can't register as remoted handler");
156 if (SPConfig::getConfig().isEnabled(SPConfig::OutOfProcess)) {
157 pair<bool,bool> async = getBool("asynchronous");
158 m_async = !async.first || async.second;
161 pair<bool,const char*> outgoing = getString("outgoingBindings");
162 if (outgoing.first) {
163 dupBindings = outgoing.second;
167 // No override, so we'll install a default binding precedence.
168 dupBindings = string(samlconstants::SAML20_BINDING_HTTP_REDIRECT) + ' ' + samlconstants::SAML20_BINDING_HTTP_POST + ' ' +
169 samlconstants::SAML20_BINDING_HTTP_POST_SIMPLESIGN + ' ' + samlconstants::SAML20_BINDING_HTTP_ARTIFACT;
171 split(m_bindings, dupBindings, is_space(), algorithm::token_compress_on);
172 for (vector<string>::const_iterator b = m_bindings.begin(); b != m_bindings.end(); ++b) {
174 boost::shared_ptr<MessageEncoder> encoder(
175 SAMLConfig::getConfig().MessageEncoderManager.newPlugin(*b, pair<const DOMElement*,const XMLCh*>(getElement(),nullptr))
177 if (encoder->isUserAgentPresent() && XMLString::equals(getProtocolFamily(), encoder->getProtocolFamily())) {
178 m_encoders[*b] = encoder;
179 m_log.debug("supporting outgoing binding (%s)", b->c_str());
182 m_log.warn("skipping outgoing binding (%s), not a SAML 2.0 front-channel mechanism", b->c_str());
185 catch (std::exception& ex) {
186 m_log.error("error building MessageEncoder: %s", ex.what());
194 pair<bool,long> SAML2LogoutInitiator::run(SPRequest& request, bool isHandler) const
196 // Defer to base class for front-channel loop first.
197 pair<bool,long> ret = LogoutHandler::run(request, isHandler);
201 // At this point we know the front-channel is handled.
202 // We need the session to do any other work.
204 Session* session = nullptr;
206 session = request.getSession(false, true, false); // don't cache it and ignore all checks
208 return make_pair(false, 0L);
210 // We only handle SAML 2.0 sessions.
211 if (!XMLString::equals(session->getProtocol(), m_protocol.get())) {
213 return make_pair(false, 0L);
216 catch (std::exception& ex) {
217 m_log.error("error accessing current session: %s", ex.what());
218 return make_pair(false, 0L);
221 if (SPConfig::getConfig().isEnabled(SPConfig::OutOfProcess)) {
222 // When out of process, we run natively.
223 return doRequest(request.getApplication(), request, request, session);
226 // When not out of process, we remote the request.
228 vector<string> headers(1,"Cookie");
229 DDF out,in = wrap(request,&headers);
230 DDFJanitor jin(in), jout(out);
231 out=request.getServiceProvider().getListenerService()->send(in);
232 return unwrap(request, out);
236 void SAML2LogoutInitiator::receive(DDF& in, ostream& out)
239 // Defer to base class for notifications
240 if (in["notify"].integer() == 1)
241 return LogoutHandler::receive(in, out);
244 const char* aid=in["application_id"].string();
245 const Application* app=aid ? SPConfig::getConfig().getServiceProvider()->getApplication(aid) : nullptr;
247 // Something's horribly wrong.
248 m_log.error("couldn't find application (%s) for logout", aid ? aid : "(missing)");
249 throw ConfigurationException("Unable to locate application for logout, deleted?");
252 // Unpack the request.
253 scoped_ptr<HTTPRequest> req(getRequest(in));
255 // Set up a response shim.
257 DDFJanitor jout(ret);
258 scoped_ptr<HTTPResponse> resp(getResponse(ret));
260 Session* session = nullptr;
262 session = app->getServiceProvider().getSessionCache()->find(*app, *req, nullptr, nullptr);
264 catch (std::exception& ex) {
265 m_log.error("error accessing current session: %s", ex.what());
268 // With no session, we just skip the request and let it fall through to an empty struct return.
270 if (session->getNameID() && session->getEntityID()) {
271 // Since we're remoted, the result should either be a throw, which we pass on,
272 // a false/0 return, which we just return as an empty structure, or a response/redirect,
273 // which we capture in the facade and send back.
274 doRequest(*app, *req, *resp, session);
278 m_log.log(getParent() ? Priority::WARN : Priority::ERROR, "bypassing SAML 2.0 logout, no NameID or issuing entityID found in session");
279 app->getServiceProvider().getSessionCache()->remove(*app, *req, resp.get());
284 throw ConfigurationException("Cannot perform logout using lite version of shibsp library.");
288 pair<bool,long> SAML2LogoutInitiator::doRequest(
289 const Application& application, const HTTPRequest& httpRequest, HTTPResponse& httpResponse, Session* session
292 Locker sessionLocker(session, false);
294 scoped_ptr<LogoutEvent> logout_event(newLogoutEvent(application, &httpRequest, session));
297 // Do back channel notification.
298 vector<string> sessions(1, session->getID());
299 if (!notifyBackChannel(application, httpRequest.getRequestURL(), sessions, false)) {
302 logout_event->m_logoutType = LogoutEvent::LOGOUT_EVENT_PARTIAL;
303 application.getServiceProvider().getTransactionLog()->write(*logout_event);
306 sessionLocker.assign();
308 application.getServiceProvider().getSessionCache()->remove(application, httpRequest, &httpResponse);
309 return sendLogoutPage(application, httpRequest, httpResponse, "partial");
313 pair<bool,long> ret = make_pair(false, 0L);
315 // With a session in hand, we can create a LogoutRequest message, if we can find a compatible endpoint.
316 MetadataProvider* m = application.getMetadataProvider();
317 Locker metadataLocker(m);
318 MetadataProviderCriteria mc(application, session->getEntityID(), &IDPSSODescriptor::ELEMENT_QNAME, samlconstants::SAML20P_NS);
319 pair<const EntityDescriptor*,const RoleDescriptor*> entity = m->getEntityDescriptor(mc);
321 throw MetadataException(
322 "Unable to locate metadata for identity provider ($entityID)", namedparams(1, "entityID", session->getEntityID())
325 else if (!entity.second) {
326 throw MetadataException(
327 "Unable to locate SAML 2.0 IdP role for identity provider ($entityID).", namedparams(1, "entityID", session->getEntityID())
331 const IDPSSODescriptor* role = dynamic_cast<const IDPSSODescriptor*>(entity.second);
332 if (role->getSingleLogoutServices().empty()) {
333 throw MetadataException(
334 "No SingleLogoutService endpoints in metadata for identity provider ($entityID).", namedparams(1, "entityID", session->getEntityID())
338 const EndpointType* ep = nullptr;
339 const MessageEncoder* encoder = nullptr;
340 for (vector<string>::const_iterator b = m_bindings.begin(); b != m_bindings.end(); ++b) {
341 auto_ptr_XMLCh wideb(b->c_str());
342 ep = EndpointManager<SingleLogoutService>(role->getSingleLogoutServices()).getByBinding(wideb.get());
344 map< string,boost::shared_ptr<MessageEncoder> >::const_iterator enc = m_encoders.find(*b);
345 if (enc != m_encoders.end())
346 encoder = enc->second.get();
350 if (!ep || !encoder) {
351 m_log.debug("no compatible front channel SingleLogoutService, trying back channel...");
352 shibsp::SecurityPolicy policy(application);
353 shibsp::SOAPClient soaper(policy);
354 MetadataCredentialCriteria mcc(*role);
356 LogoutResponse* logoutResponse = nullptr;
357 scoped_ptr<StatusResponseType> srt;
358 auto_ptr_XMLCh binding(samlconstants::SAML20_BINDING_SOAP);
359 const vector<SingleLogoutService*>& endpoints = role->getSingleLogoutServices();
360 for (indirect_iterator<vector<SingleLogoutService*>::const_iterator> epit = make_indirect_iterator(endpoints.begin());
361 !logoutResponse && epit != make_indirect_iterator(endpoints.end()); ++epit) {
363 if (!XMLString::equals(epit->getBinding(), binding.get()))
365 auto_ptr<LogoutRequest> msg(buildRequest(application, *session, *role, epit->getLocation()));
369 logout_event->m_logoutType = LogoutEvent::LOGOUT_EVENT_UNKNOWN;
370 logout_event->m_saml2Request = msg.get();
371 application.getServiceProvider().getTransactionLog()->write(*logout_event);
372 logout_event->m_saml2Request = nullptr;
375 SAML2SOAPClient client(soaper, false);
376 auto_ptr_char dest(epit->getLocation());
377 client.sendSAML(msg.release(), application.getId(), mcc, dest.get());
378 srt.reset(client.receiveSAML());
379 if (!(logoutResponse = dynamic_cast<LogoutResponse*>(srt.get()))) {
383 catch (std::exception& ex) {
384 m_log.error("error sending LogoutRequest message: %s", ex.what());
390 if (!logoutResponse) {
391 if (endpoints.empty())
392 m_log.info("IdP doesn't support single logout protocol over a compatible binding");
394 m_log.warn("IdP didn't respond to logout request");
396 // Log the end result.
398 logout_event->m_logoutType = LogoutEvent::LOGOUT_EVENT_PARTIAL;
399 application.getServiceProvider().getTransactionLog()->write(*logout_event);
402 ret = sendLogoutPage(application, httpRequest, httpResponse, "partial");
405 // Check the status, looking for non-success or a partial logout code.
406 const StatusCode* sc = logoutResponse->getStatus() ? logoutResponse->getStatus()->getStatusCode() : nullptr;
407 bool partial = (!sc || !XMLString::equals(sc->getValue(), StatusCode::SUCCESS));
408 if (!partial && sc->getStatusCode()) {
409 // Success, but still need to check for partial.
410 partial = XMLString::equals(sc->getStatusCode()->getValue(), StatusCode::PARTIAL_LOGOUT);
413 // Log the end result.
415 logout_event->m_logoutType = partial ? LogoutEvent::LOGOUT_EVENT_PARTIAL : LogoutEvent::LOGOUT_EVENT_GLOBAL;
416 logout_event->m_saml2Response = logoutResponse;
417 application.getServiceProvider().getTransactionLog()->write(*logout_event);
421 ret = sendLogoutPage(application, httpRequest, httpResponse, "partial");
423 const char* returnloc = httpRequest.getParameter("return");
425 // Relative URLs get promoted, absolutes get validated.
426 if (*returnloc == '/') {
427 string loc(returnloc);
428 httpRequest.absolutize(loc);
429 ret.second = httpResponse.sendRedirect(loc.c_str());
432 application.limitRedirect(httpRequest, returnloc);
433 ret.second = httpResponse.sendRedirect(returnloc);
438 ret = sendLogoutPage(application, httpRequest, httpResponse, "global");
444 sessionLocker.assign();
446 application.getServiceProvider().getSessionCache()->remove(application, httpRequest, &httpResponse);
452 // Save off return location as RelayState.
454 const char* returnloc = httpRequest.getParameter("return");
456 application.limitRedirect(httpRequest, returnloc);
457 relayState = returnloc;
458 httpRequest.absolutize(relayState);
459 cleanRelayState(application, httpRequest, httpResponse);
460 preserveRelayState(application, httpResponse, relayState);
463 auto_ptr<LogoutRequest> msg(buildRequest(application, *session, *role, ep->getLocation(), encoder));
464 msg->setDestination(ep->getLocation());
468 logout_event->m_logoutType = LogoutEvent::LOGOUT_EVENT_UNKNOWN;
469 logout_event->m_saml2Request = msg.get();
470 application.getServiceProvider().getTransactionLog()->write(*logout_event);
473 auto_ptr_char dest(ep->getLocation());
474 ret.second = sendMessage(*encoder, msg.get(), relayState.c_str(), dest.get(), role, application, httpResponse, "true");
476 msg.release(); // freed by encoder
479 sessionLocker.assign();
481 application.getServiceProvider().getSessionCache()->remove(application, httpRequest, &httpResponse);
484 catch (MetadataException& mex) {
485 // Less noise for IdPs that don't support logout (i.e. most)
486 m_log.info("unable to issue SAML 2.0 logout request: %s", mex.what());
488 catch (std::exception& ex) {
489 m_log.error("error issuing SAML 2.0 logout request: %s", ex.what());
494 throw ConfigurationException("Cannot perform logout using lite version of shibsp library.");
500 auto_ptr<LogoutRequest> SAML2LogoutInitiator::buildRequest(
501 const Application& application,
502 const Session& session,
503 const RoleDescriptor& role,
504 const XMLCh* endpoint,
505 const MessageEncoder* encoder) const
507 const PropertySet* relyingParty = application.getRelyingParty(dynamic_cast<EntityDescriptor*>(role.getParent()));
509 auto_ptr<LogoutRequest> msg(LogoutRequestBuilder::buildLogoutRequest());
510 Issuer* issuer = IssuerBuilder::buildIssuer();
511 msg->setIssuer(issuer);
512 issuer->setName(relyingParty->getXMLString("entityID").second);
513 auto_ptr_XMLCh index(session.getSessionIndex());
514 if (index.get() && *index.get()) {
515 SessionIndex* si = SessionIndexBuilder::buildSessionIndex();
516 msg->getSessionIndexs().push_back(si);
517 si->setSessionIndex(index.get());
520 const NameID* nameid = session.getNameID();
521 pair<bool,const char*> flag = relyingParty->getString("encryption");
522 auto_ptr_char dest(endpoint);
523 if (SPConfig::shouldSignOrEncrypt(flag.first ? flag.second : "conditional", dest.get(), encoder != nullptr)) {
525 auto_ptr<EncryptedID> encrypted(EncryptedIDBuilder::buildEncryptedID());
526 MetadataCredentialCriteria mcc(role);
529 *(application.getMetadataProvider()),
531 encoder ? encoder->isCompact() : false,
532 relyingParty->getXMLString("encryptionAlg").second
534 msg->setEncryptedID(encrypted.get());
537 catch (std::exception& ex) {
538 // If we're encrypting deliberately, failure should be fatal.
539 if (flag.first && strcmp(flag.second, "conditional")) {
542 // If opportunistically, just log and move on.
543 m_log.info("Conditional encryption of NameID in LogoutRequest failed: %s", ex.what());
544 auto_ptr<NameID> namewrapper(nameid->cloneNameID());
545 msg->setNameID(namewrapper.get());
546 namewrapper.release();
550 auto_ptr<NameID> namewrapper(nameid->cloneNameID());
551 msg->setNameID(namewrapper.get());
552 namewrapper.release();
555 XMLCh* msgid = SAMLConfig::getConfig().generateIdentifier();
557 XMLString::release(&msgid);
558 msg->setIssueInstant(time(nullptr));
560 if (m_async && encoder) {
561 msg->setExtensions(saml2p::ExtensionsBuilder::buildExtensions());
562 msg->getExtensions()->getUnknownXMLObjects().push_back(AsynchronousBuilder::buildAsynchronous());