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 <saml/exceptions.h>
40 # include <saml/SAMLConfig.h>
41 # include <saml/saml2/core/Protocols.h>
42 # include <saml/saml2/binding/SAML2SOAPClient.h>
43 # include <saml/saml2/metadata/EndpointManager.h>
44 # include <saml/saml2/metadata/Metadata.h>
45 # include <saml/saml2/metadata/MetadataCredentialCriteria.h>
46 using namespace opensaml::saml2;
47 using namespace opensaml::saml2p;
48 using namespace opensaml::saml2md;
49 using namespace opensaml;
51 # include "lite/SAMLConstants.h"
54 using namespace shibsp;
55 using namespace xmltooling;
60 #if defined (_MSC_VER)
61 #pragma warning( push )
62 #pragma warning( disable : 4250 )
65 class SHIBSP_DLLLOCAL SAML2LogoutInitiator : public AbstractHandler, public LogoutInitiator
68 SAML2LogoutInitiator(const DOMElement* e, const char* appId);
69 virtual ~SAML2LogoutInitiator() {
71 if (SPConfig::getConfig().isEnabled(SPConfig::OutOfProcess)) {
72 XMLString::release(&m_outgoing);
73 for_each(m_encoders.begin(), m_encoders.end(), cleanup_pair<const XMLCh*,MessageEncoder>());
78 void init(const char* location); // encapsulates actions that need to run either in the c'tor or setParent
80 void setParent(const PropertySet* parent);
81 void receive(DDF& in, ostream& out);
82 pair<bool,long> run(SPRequest& request, bool isHandler=true) const;
84 const XMLCh* getProtocolFamily() const {
85 return samlconstants::SAML20P_NS;
89 pair<bool,long> doRequest(
90 const Application& application, const HTTPRequest& request, HTTPResponse& httpResponse, Session* session
95 LogoutRequest* buildRequest(
96 const Application& application, const Session& session, const RoleDescriptor& role, const MessageEncoder* encoder=nullptr
99 LogoutEvent* newLogoutEvent(
100 const Application& application, const HTTPRequest* request=nullptr, const Session* session=nullptr
102 LogoutEvent* e = LogoutHandler::newLogoutEvent(application, request, session);
104 e->m_protocol = m_protocol.get();
109 vector<const XMLCh*> m_bindings;
110 map<const XMLCh*,MessageEncoder*> m_encoders;
112 auto_ptr_char m_protocol;
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),
130 m_protocol(samlconstants::SAML20P_NS)
132 // If Location isn't set, defer initialization until the setParent call.
133 pair<bool,const char*> loc = getString("Location");
139 void SAML2LogoutInitiator::setParent(const PropertySet* parent)
141 DOMPropertySet::setParent(parent);
142 pair<bool,const char*> loc = getString("Location");
146 void SAML2LogoutInitiator::init(const char* location)
149 string address = m_appId + location + "::run::SAML2LI";
150 setAddress(address.c_str());
153 m_log.warn("no Location property in SAML2 LogoutInitiator (or parent), can't register as remoted handler");
157 if (SPConfig::getConfig().isEnabled(SPConfig::OutOfProcess)) {
158 // Handle outgoing binding setup.
159 pair<bool,const XMLCh*> outgoing = getXMLString("outgoingBindings");
160 if (outgoing.first) {
161 m_outgoing = XMLString::replicate(outgoing.second);
162 XMLString::trim(m_outgoing);
165 // No override, so we'll install a default binding precedence.
166 string prec = string(samlconstants::SAML20_BINDING_HTTP_REDIRECT) + ' ' + samlconstants::SAML20_BINDING_HTTP_POST + ' ' +
167 samlconstants::SAML20_BINDING_HTTP_POST_SIMPLESIGN + ' ' + samlconstants::SAML20_BINDING_HTTP_ARTIFACT;
168 m_outgoing = XMLString::transcode(prec.c_str());
172 XMLCh* start = m_outgoing;
173 while (start && *start) {
174 pos = XMLString::indexOf(start,chSpace);
176 *(start + pos)=chNull;
177 m_bindings.push_back(start);
179 auto_ptr_char b(start);
180 MessageEncoder * encoder =
181 SAMLConfig::getConfig().MessageEncoderManager.newPlugin(b.get(), pair<const DOMElement*,const XMLCh*>(getElement(), nullptr));
182 if (encoder->isUserAgentPresent() && XMLString::equals(getProtocolFamily(), encoder->getProtocolFamily())) {
183 m_encoders[start] = encoder;
184 m_log.debug("supporting outgoing binding (%s)", b.get());
188 m_log.warn("skipping outgoing binding (%s), not a SAML 2.0 front-channel mechanism", b.get());
191 catch (exception& ex) {
192 m_log.error("error building MessageEncoder: %s", ex.what());
195 start = start + pos + 1;
204 pair<bool,long> SAML2LogoutInitiator::run(SPRequest& request, bool isHandler) const
206 // Defer to base class for front-channel loop first.
207 pair<bool,long> ret = LogoutHandler::run(request, isHandler);
211 // At this point we know the front-channel is handled.
212 // We need the session to do any other work.
214 Session* session = nullptr;
216 session = request.getSession(false, true, false); // don't cache it and ignore all checks
218 return make_pair(false,0L);
220 // We only handle SAML 2.0 sessions.
221 if (!XMLString::equals(session->getProtocol(), m_protocol.get())) {
223 return make_pair(false,0L);
226 catch (exception& ex) {
227 m_log.error("error accessing current session: %s", ex.what());
228 return make_pair(false,0L);
231 if (SPConfig::getConfig().isEnabled(SPConfig::OutOfProcess)) {
232 // When out of process, we run natively.
233 return doRequest(request.getApplication(), request, request, session);
236 // When not out of process, we remote the request.
238 vector<string> headers(1,"Cookie");
239 DDF out,in = wrap(request,&headers);
240 DDFJanitor jin(in), jout(out);
241 out=request.getServiceProvider().getListenerService()->send(in);
242 return unwrap(request, out);
246 void SAML2LogoutInitiator::receive(DDF& in, ostream& out)
249 // Defer to base class for notifications
250 if (in["notify"].integer() == 1)
251 return LogoutHandler::receive(in, out);
254 const char* aid=in["application_id"].string();
255 const Application* app=aid ? SPConfig::getConfig().getServiceProvider()->getApplication(aid) : nullptr;
257 // Something's horribly wrong.
258 m_log.error("couldn't find application (%s) for logout", aid ? aid : "(missing)");
259 throw ConfigurationException("Unable to locate application for logout, deleted?");
262 // Unpack the request.
263 auto_ptr<HTTPRequest> req(getRequest(in));
265 // Set up a response shim.
267 DDFJanitor jout(ret);
268 auto_ptr<HTTPResponse> resp(getResponse(ret));
270 Session* session = nullptr;
272 session = app->getServiceProvider().getSessionCache()->find(*app, *req.get(), nullptr, nullptr);
274 catch (exception& ex) {
275 m_log.error("error accessing current session: %s", ex.what());
278 // With no session, we just skip the request and let it fall through to an empty struct return.
280 if (session->getNameID() && session->getEntityID()) {
281 // Since we're remoted, the result should either be a throw, which we pass on,
282 // a false/0 return, which we just return as an empty structure, or a response/redirect,
283 // which we capture in the facade and send back.
284 doRequest(*app, *req.get(), *resp.get(), session);
287 m_log.log(getParent() ? Priority::WARN : Priority::ERROR, "bypassing SAML 2.0 logout, no NameID or issuing entityID found in session");
289 app->getServiceProvider().getSessionCache()->remove(*app, *req.get(), resp.get());
294 throw ConfigurationException("Cannot perform logout using lite version of shibsp library.");
298 pair<bool,long> SAML2LogoutInitiator::doRequest(
299 const Application& application, const HTTPRequest& httpRequest, HTTPResponse& httpResponse, Session* session
303 auto_ptr<LogoutEvent> logout_event(newLogoutEvent(application, &httpRequest, session));
306 // Do back channel notification.
307 vector<string> sessions(1, session->getID());
308 if (!notifyBackChannel(application, httpRequest.getRequestURL(), sessions, false)) {
310 if (logout_event.get()) {
311 logout_event->m_logoutType = LogoutEvent::LOGOUT_EVENT_PARTIAL;
312 application.getServiceProvider().getTransactionLog()->write(*logout_event);
316 application.getServiceProvider().getSessionCache()->remove(application, httpRequest, &httpResponse);
317 return sendLogoutPage(application, httpRequest, httpResponse, "partial");
321 pair<bool,long> ret = make_pair(false,0L);
323 // With a session in hand, we can create a LogoutRequest message, if we can find a compatible endpoint.
324 MetadataProvider* m = application.getMetadataProvider();
325 Locker metadataLocker(m);
326 MetadataProviderCriteria mc(application, session->getEntityID(), &IDPSSODescriptor::ELEMENT_QNAME, samlconstants::SAML20P_NS);
327 pair<const EntityDescriptor*,const RoleDescriptor*> entity = m->getEntityDescriptor(mc);
329 throw MetadataException(
330 "Unable to locate metadata for identity provider ($entityID)", namedparams(1, "entityID", session->getEntityID())
333 else if (!entity.second) {
334 throw MetadataException(
335 "Unable to locate SAML 2.0 IdP role for identity provider ($entityID).", namedparams(1, "entityID", session->getEntityID())
339 const IDPSSODescriptor* role = dynamic_cast<const IDPSSODescriptor*>(entity.second);
340 if (role->getSingleLogoutServices().empty()) {
341 throw MetadataException(
342 "No SingleLogoutService endpoints in metadata for identity provider ($entityID).", namedparams(1, "entityID", session->getEntityID())
346 const EndpointType* ep=nullptr;
347 const MessageEncoder* encoder=nullptr;
348 vector<const XMLCh*>::const_iterator b;
349 for (b = m_bindings.begin(); b!=m_bindings.end(); ++b) {
350 if (ep=EndpointManager<SingleLogoutService>(role->getSingleLogoutServices()).getByBinding(*b)) {
351 map<const XMLCh*,MessageEncoder*>::const_iterator enc = m_encoders.find(*b);
352 if (enc!=m_encoders.end())
353 encoder = enc->second;
357 if (!ep || !encoder) {
358 m_log.debug("no compatible front channel SingleLogoutService, trying back channel...");
359 shibsp::SecurityPolicy policy(application);
360 shibsp::SOAPClient soaper(policy);
361 MetadataCredentialCriteria mcc(*role);
363 LogoutResponse* logoutResponse=nullptr;
364 auto_ptr_XMLCh binding(samlconstants::SAML20_BINDING_SOAP);
365 const vector<SingleLogoutService*>& endpoints=role->getSingleLogoutServices();
366 for (vector<SingleLogoutService*>::const_iterator epit=endpoints.begin(); !logoutResponse && epit!=endpoints.end(); ++epit) {
368 if (!XMLString::equals((*epit)->getBinding(),binding.get()))
370 LogoutRequest* msg = buildRequest(application, *session, *role);
373 if (logout_event.get()) {
374 logout_event->m_logoutType = LogoutEvent::LOGOUT_EVENT_UNKNOWN;
375 logout_event->m_saml2Request = msg;
376 application.getServiceProvider().getTransactionLog()->write(*logout_event);
379 auto_ptr_char dest((*epit)->getLocation());
380 SAML2SOAPClient client(soaper, false);
381 client.sendSAML(msg, application.getId(), mcc, dest.get());
382 StatusResponseType* srt = client.receiveSAML();
383 if (!(logoutResponse = dynamic_cast<LogoutResponse*>(srt))) {
388 catch (exception& ex) {
389 m_log.error("error sending LogoutRequest message: %s", ex.what());
395 if (!logoutResponse) {
396 if (endpoints.empty())
397 m_log.info("IdP doesn't support single logout protocol over a compatible binding");
399 m_log.warn("IdP didn't respond to logout request");
401 // Log the end result.
402 if (logout_event.get()) {
403 logout_event->m_logoutType = LogoutEvent::LOGOUT_EVENT_PARTIAL;
404 application.getServiceProvider().getTransactionLog()->write(*logout_event);
407 ret = sendLogoutPage(application, httpRequest, httpResponse, "partial");
410 // Check the status, looking for non-success or a partial logout code.
411 const StatusCode* sc = logoutResponse->getStatus() ? logoutResponse->getStatus()->getStatusCode() : nullptr;
412 bool partial = (!sc || !XMLString::equals(sc->getValue(), StatusCode::SUCCESS));
413 if (!partial && sc->getStatusCode()) {
414 // Success, but still need to check for partial.
415 partial = XMLString::equals(sc->getStatusCode()->getValue(), StatusCode::PARTIAL_LOGOUT);
418 // Log the end result.
419 if (logout_event.get()) {
420 logout_event->m_logoutType = partial ? LogoutEvent::LOGOUT_EVENT_PARTIAL : LogoutEvent::LOGOUT_EVENT_GLOBAL;
421 logout_event->m_saml2Response = logoutResponse;
422 application.getServiceProvider().getTransactionLog()->write(*logout_event);
425 delete logoutResponse;
427 ret = sendLogoutPage(application, httpRequest, httpResponse, "partial");
429 const char* returnloc = httpRequest.getParameter("return");
431 limitRelayState(m_log, application, httpRequest, returnloc);
432 ret.second = httpResponse.sendRedirect(returnloc);
435 ret = sendLogoutPage(application, httpRequest, httpResponse, "global");
442 application.getServiceProvider().getSessionCache()->remove(application, httpRequest, &httpResponse);
448 // Save off return location as RelayState.
450 const char* returnloc = httpRequest.getParameter("return");
452 limitRelayState(m_log, application, httpRequest, returnloc);
453 relayState = returnloc;
454 preserveRelayState(application, httpResponse, relayState);
457 auto_ptr<LogoutRequest> msg(buildRequest(application, *session, *role, encoder));
458 msg->setDestination(ep->getLocation());
461 if (logout_event.get()) {
462 logout_event->m_logoutType = LogoutEvent::LOGOUT_EVENT_UNKNOWN;
463 logout_event->m_saml2Request = msg.get();
464 application.getServiceProvider().getTransactionLog()->write(*logout_event);
467 auto_ptr_char dest(ep->getLocation());
468 ret.second = sendMessage(*encoder, msg.get(), relayState.c_str(), dest.get(), role, application, httpResponse);
470 msg.release(); // freed by encoder
475 application.getServiceProvider().getSessionCache()->remove(application, httpRequest, &httpResponse);
478 catch (MetadataException& mex) {
479 // Less noise for IdPs that don't support logout (i.e. most)
480 m_log.info("unable to issue SAML 2.0 logout request: %s", mex.what());
482 catch (exception& ex) {
483 m_log.error("error issuing SAML 2.0 logout request: %s", ex.what());
492 throw ConfigurationException("Cannot perform logout using lite version of shibsp library.");
498 LogoutRequest* SAML2LogoutInitiator::buildRequest(
499 const Application& application, const Session& session, const RoleDescriptor& role, const MessageEncoder* encoder
502 const PropertySet* relyingParty = application.getRelyingParty(dynamic_cast<EntityDescriptor*>(role.getParent()));
504 auto_ptr<LogoutRequest> msg(LogoutRequestBuilder::buildLogoutRequest());
505 Issuer* issuer = IssuerBuilder::buildIssuer();
506 msg->setIssuer(issuer);
507 issuer->setName(relyingParty->getXMLString("entityID").second);
508 auto_ptr_XMLCh index(session.getSessionIndex());
509 if (index.get() && *index.get()) {
510 SessionIndex* si = SessionIndexBuilder::buildSessionIndex();
511 msg->getSessionIndexs().push_back(si);
512 si->setSessionIndex(index.get());
515 const NameID* nameid = session.getNameID();
516 pair<bool,const char*> flag = relyingParty->getString("encryption");
518 (!strcmp(flag.second, "true") || (encoder && !strcmp(flag.second, "front")) || (!encoder && !strcmp(flag.second, "back")))) {
519 auto_ptr<EncryptedID> encrypted(EncryptedIDBuilder::buildEncryptedID());
520 MetadataCredentialCriteria mcc(role);
523 *(application.getMetadataProvider()),
525 encoder ? encoder->isCompact() : false,
526 relyingParty->getXMLString("encryptionAlg").second
528 msg->setEncryptedID(encrypted.release());
531 msg->setNameID(nameid->cloneNameID());
534 msg->setID(SAMLConfig::getConfig().generateIdentifier());
535 msg->setIssueInstant(time(nullptr));
537 return msg.release();