2 * Copyright 2001-2009 Internet2
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
18 * SAML2LogoutInitiator.cpp
20 * Triggers SP-initiated logout for SAML 2.0 sessions.
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"
32 # include "binding/SOAPClient.h"
33 # include "metadata/MetadataProviderCriteria.h"
34 # include <saml/SAMLConfig.h>
35 # include <saml/saml2/core/Protocols.h>
36 # include <saml/saml2/binding/SAML2SOAPClient.h>
37 # include <saml/saml2/metadata/EndpointManager.h>
38 # include <saml/saml2/metadata/MetadataCredentialCriteria.h>
39 # include <xmltooling/security/Credential.h>
40 using namespace opensaml::saml2;
41 using namespace opensaml::saml2p;
42 using namespace opensaml::saml2md;
43 using namespace opensaml;
45 # include "lite/SAMLConstants.h"
48 using namespace shibsp;
49 using namespace xmltooling;
54 #if defined (_MSC_VER)
55 #pragma warning( push )
56 #pragma warning( disable : 4250 )
59 class SHIBSP_DLLLOCAL SAML2LogoutInitiator : public AbstractHandler, public LogoutHandler
62 SAML2LogoutInitiator(const DOMElement* e, const char* appId);
63 virtual ~SAML2LogoutInitiator() {
65 if (SPConfig::getConfig().isEnabled(SPConfig::OutOfProcess)) {
66 XMLString::release(&m_outgoing);
67 for_each(m_encoders.begin(), m_encoders.end(), cleanup_pair<const XMLCh*,MessageEncoder>());
72 void setParent(const PropertySet* parent);
73 void receive(DDF& in, ostream& out);
74 pair<bool,long> run(SPRequest& request, bool isHandler=true) const;
77 const char* getType() const {
78 return "LogoutInitiator";
83 pair<bool,long> doRequest(
84 const Application& application, const HTTPRequest& request, HTTPResponse& httpResponse, Session* session
89 LogoutRequest* buildRequest(
90 const Application& application, const Session& session, const RoleDescriptor& role, const MessageEncoder* encoder=NULL
94 vector<const XMLCh*> m_bindings;
95 map<const XMLCh*,MessageEncoder*> m_encoders;
97 auto_ptr_char m_protocol;
100 #if defined (_MSC_VER)
101 #pragma warning( pop )
104 Handler* SHIBSP_DLLLOCAL SAML2LogoutInitiatorFactory(const pair<const DOMElement*,const char*>& p)
106 return new SAML2LogoutInitiator(p.first, p.second);
110 SAML2LogoutInitiator::SAML2LogoutInitiator(const DOMElement* e, const char* appId)
111 : AbstractHandler(e, Category::getInstance(SHIBSP_LOGCAT".LogoutInitiator.SAML2")), m_appId(appId),
115 m_protocol(samlconstants::SAML20P_NS)
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);
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());
133 XMLCh* start = m_outgoing;
134 while (start && *start) {
135 pos = XMLString::indexOf(start,chSpace);
137 *(start + pos)=chNull;
138 m_bindings.push_back(start);
140 auto_ptr_char b(start);
141 MessageEncoder * encoder =
142 SAMLConfig::getConfig().MessageEncoderManager.newPlugin(b.get(),pair<const DOMElement*,const XMLCh*>(e,NULL));
143 if (encoder->isUserAgentPresent()) {
144 m_encoders[start] = encoder;
145 m_log.debug("supporting outgoing binding (%s)", b.get());
149 m_log.warn("skipping outgoing binding (%s), not a front-channel mechanism", b.get());
152 catch (exception& ex) {
153 m_log.error("error building MessageEncoder: %s", ex.what());
156 start = start + pos + 1;
163 pair<bool,const char*> loc = getString("Location");
165 string address = m_appId + loc.second + "::run::SAML2LI";
166 setAddress(address.c_str());
170 void SAML2LogoutInitiator::setParent(const PropertySet* parent)
172 DOMPropertySet::setParent(parent);
173 pair<bool,const char*> loc = getString("Location");
175 string address = m_appId + loc.second + "::run::SAML2LI";
176 setAddress(address.c_str());
179 m_log.warn("no Location property in SAML2 LogoutInitiator (or parent), can't register as remoted handler");
183 pair<bool,long> SAML2LogoutInitiator::run(SPRequest& request, bool isHandler) const
185 // Defer to base class for front-channel loop first.
186 pair<bool,long> ret = LogoutHandler::run(request, isHandler);
190 // At this point we know the front-channel is handled.
191 // We need the session to do any other work.
193 Session* session = NULL;
195 session = request.getSession(false, true, false); // don't cache it and ignore all checks
197 return make_pair(false,0L);
199 // We only handle SAML 2.0 sessions.
200 if (!XMLString::equals(session->getProtocol(), m_protocol.get())) {
202 return make_pair(false,0L);
205 catch (exception& ex) {
206 m_log.error("error accessing current session: %s", ex.what());
207 return make_pair(false,0L);
210 if (SPConfig::getConfig().isEnabled(SPConfig::OutOfProcess)) {
211 // When out of process, we run natively.
212 return doRequest(request.getApplication(), request, request, session);
215 // When not out of process, we remote the request.
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);
225 void SAML2LogoutInitiator::receive(DDF& in, ostream& out)
228 // Defer to base class for notifications
229 if (in["notify"].integer() == 1)
230 return LogoutHandler::receive(in, out);
233 const char* aid=in["application_id"].string();
234 const Application* app=aid ? SPConfig::getConfig().getServiceProvider()->getApplication(aid) : NULL;
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?");
241 // Unpack the request.
242 auto_ptr<HTTPRequest> req(getRequest(in));
244 // Set up a response shim.
246 DDFJanitor jout(ret);
247 auto_ptr<HTTPResponse> resp(getResponse(ret));
249 Session* session = NULL;
251 session = app->getServiceProvider().getSessionCache()->find(*app, *req.get(), NULL, NULL);
253 catch (exception& ex) {
254 m_log.error("error accessing current session: %s", ex.what());
257 // With no session, we just skip the request and let it fall through to an empty struct return.
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);
266 m_log.error("no NameID or issuing entityID found in session");
268 app->getServiceProvider().getSessionCache()->remove(*app, *req.get(), resp.get());
273 throw ConfigurationException("Cannot perform logout using lite version of shibsp library.");
277 pair<bool,long> SAML2LogoutInitiator::doRequest(
278 const Application& application, const HTTPRequest& httpRequest, HTTPResponse& httpResponse, Session* session
281 // Do back channel notification.
282 vector<string> sessions(1, session->getID());
283 if (!notifyBackChannel(application, httpRequest.getRequestURL(), sessions, false)) {
285 application.getServiceProvider().getSessionCache()->remove(application, httpRequest, &httpResponse);
286 return sendLogoutPage(application, httpRequest, httpResponse, true, "Partial logout failure.");
290 pair<bool,long> ret = make_pair(false,0L);
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);
298 throw MetadataException(
299 "Unable to locate metadata for identity provider ($entityID)", namedparams(1, "entityID", session->getEntityID())
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())
308 const IDPSSODescriptor* role = dynamic_cast<const IDPSSODescriptor*>(entity.second);
309 const EndpointType* ep=NULL;
310 const MessageEncoder* encoder=NULL;
311 vector<const XMLCh*>::const_iterator b;
312 for (b = m_bindings.begin(); b!=m_bindings.end(); ++b) {
313 if (ep=EndpointManager<SingleLogoutService>(role->getSingleLogoutServices()).getByBinding(*b)) {
314 map<const XMLCh*,MessageEncoder*>::const_iterator enc = m_encoders.find(*b);
315 if (enc!=m_encoders.end())
316 encoder = enc->second;
320 if (!ep || !encoder) {
321 m_log.warn("no compatible front channel SingleLogoutService, trying back channel...");
322 shibsp::SecurityPolicy policy(application);
323 shibsp::SOAPClient soaper(policy);
324 MetadataCredentialCriteria mcc(*role);
326 LogoutResponse* logoutResponse=NULL;
327 auto_ptr_XMLCh binding(samlconstants::SAML20_BINDING_SOAP);
328 const vector<SingleLogoutService*>& endpoints=role->getSingleLogoutServices();
329 for (vector<SingleLogoutService*>::const_iterator epit=endpoints.begin(); !logoutResponse && epit!=endpoints.end(); ++epit) {
331 if (!XMLString::equals((*epit)->getBinding(),binding.get()))
333 LogoutRequest* msg = buildRequest(application, *session, *role);
334 auto_ptr_char dest((*epit)->getLocation());
336 SAML2SOAPClient client(soaper, false);
337 client.sendSAML(msg, application.getId(), mcc, dest.get());
338 StatusResponseType* srt = client.receiveSAML();
339 if (!(logoutResponse = dynamic_cast<LogoutResponse*>(srt))) {
344 catch (exception& ex) {
345 m_log.error("error sending LogoutRequest message: %s", ex.what());
350 if (!logoutResponse) {
351 ret = sendLogoutPage(
352 application, httpRequest, httpResponse, false,
354 "Identity provider does not support SAML 2 Single Logout protocol." :
355 "Identity provider did not respond to logout request."
358 else if (!logoutResponse->getStatus() || !logoutResponse->getStatus()->getStatusCode() ||
359 !XMLString::equals(logoutResponse->getStatus()->getStatusCode()->getValue(), saml2p::StatusCode::SUCCESS)) {
360 delete logoutResponse;
361 ret = sendLogoutPage(application, httpRequest, httpResponse, false, "Identity provider returned a SAML error in response to logout request.");
364 delete logoutResponse;
365 const char* returnloc = httpRequest.getParameter("return");
367 ret.second = httpResponse.sendRedirect(returnloc);
370 ret = sendLogoutPage(application, httpRequest, httpResponse, false, "Logout completed successfully.");
376 application.getServiceProvider().getSessionCache()->remove(application, httpRequest, &httpResponse);
381 // Save off return location as RelayState.
383 const char* returnloc = httpRequest.getParameter("return");
385 relayState = returnloc;
386 preserveRelayState(application, httpResponse, relayState);
389 auto_ptr<LogoutRequest> msg(buildRequest(application, *session, *role, encoder));
391 msg->setDestination(ep->getLocation());
392 auto_ptr_char dest(ep->getLocation());
393 ret.second = sendMessage(*encoder, msg.get(), relayState.c_str(), dest.get(), role, application, httpResponse);
395 msg.release(); // freed by encoder
397 catch (exception& ex) {
398 m_log.error("error issuing SAML 2.0 logout request: %s", ex.what());
404 application.getServiceProvider().getSessionCache()->remove(application, httpRequest, &httpResponse);
410 application.getServiceProvider().getSessionCache()->remove(application, httpRequest, &httpResponse);
411 throw ConfigurationException("Cannot perform logout using lite version of shibsp library.");
417 LogoutRequest* SAML2LogoutInitiator::buildRequest(
418 const Application& application, const Session& session, const RoleDescriptor& role, const MessageEncoder* encoder
421 const PropertySet* relyingParty = application.getRelyingParty(dynamic_cast<EntityDescriptor*>(role.getParent()));
423 auto_ptr<LogoutRequest> msg(LogoutRequestBuilder::buildLogoutRequest());
424 Issuer* issuer = IssuerBuilder::buildIssuer();
425 msg->setIssuer(issuer);
426 issuer->setName(relyingParty->getXMLString("entityID").second);
427 auto_ptr_XMLCh index(session.getSessionIndex());
428 if (index.get() && *index.get()) {
429 SessionIndex* si = SessionIndexBuilder::buildSessionIndex();
430 msg->getSessionIndexs().push_back(si);
431 si->setSessionIndex(index.get());
434 const NameID* nameid = session.getNameID();
435 pair<bool,const char*> flag = relyingParty->getString("encryption");
437 (!strcmp(flag.second, "true") || (encoder && !strcmp(flag.second, "front")) || (!encoder && !strcmp(flag.second, "back")))) {
438 auto_ptr<EncryptedID> encrypted(EncryptedIDBuilder::buildEncryptedID());
439 MetadataCredentialCriteria mcc(role);
442 *(application.getMetadataProvider()),
444 encoder ? encoder->isCompact() : false,
445 relyingParty->getXMLString("encryptionAlg").second
447 msg->setEncryptedID(encrypted.release());
450 msg->setNameID(nameid->cloneNameID());
454 // No encoder being used, so sign for SOAP client manually.
455 flag = relyingParty->getString("signing");
456 if (flag.first && (!strcmp(flag.second, "true") || !strcmp(flag.second, "back"))) {
457 CredentialResolver* credResolver=application.getCredentialResolver();
459 Locker credLocker(credResolver);
460 // Fill in criteria to use.
461 MetadataCredentialCriteria mcc(role);
462 mcc.setUsage(Credential::SIGNING_CREDENTIAL);
463 pair<bool,const char*> keyName = relyingParty->getString("keyName");
465 mcc.getKeyNames().insert(keyName.second);
466 pair<bool,const XMLCh*> sigalg = relyingParty->getXMLString("signingAlg");
468 mcc.setXMLAlgorithm(sigalg.second);
469 const Credential* cred = credResolver->resolve(&mcc);
471 xmlsignature::Signature* sig = xmlsignature::SignatureBuilder::buildSignature();
472 msg->setSignature(sig);
474 sig->setSignatureAlgorithm(sigalg.second);
475 sigalg = relyingParty->getXMLString("digestAlg");
477 ContentReference* cr = dynamic_cast<ContentReference*>(sig->getContentReference());
479 cr->setDigestAlgorithm(sigalg.second);
482 // Sign response while marshalling.
483 vector<xmlsignature::Signature*> sigs(1,sig);
484 msg->marshall((DOMDocument*)NULL,&sigs,cred);
487 m_log.warn("no signing credential resolved, leaving message unsigned");
491 m_log.warn("no credential resolver installed, leaving message unsigned");
496 return msg.release();