2 * Copyright 2001-2007 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 <saml/SAMLConfig.h>
34 # include <saml/saml2/core/Protocols.h>
35 # include <saml/saml2/binding/SAML2SOAPClient.h>
36 # include <saml/saml2/metadata/EndpointManager.h>
37 # include <saml/saml2/metadata/MetadataCredentialCriteria.h>
38 using namespace opensaml::saml2;
39 using namespace opensaml::saml2p;
40 using namespace opensaml::saml2md;
41 using namespace opensaml;
43 # include "lite/SAMLConstants.h"
46 using namespace shibsp;
47 using namespace xmltooling;
52 #if defined (_MSC_VER)
53 #pragma warning( push )
54 #pragma warning( disable : 4250 )
57 class SHIBSP_DLLLOCAL SAML2LogoutInitiator : public AbstractHandler, public LogoutHandler
60 SAML2LogoutInitiator(const DOMElement* e, const char* appId);
61 virtual ~SAML2LogoutInitiator() {
63 if (SPConfig::getConfig().isEnabled(SPConfig::OutOfProcess)) {
64 XMLString::release(&m_outgoing);
65 for_each(m_encoders.begin(), m_encoders.end(), cleanup_pair<const XMLCh*,MessageEncoder>());
70 void setParent(const PropertySet* parent);
71 void receive(DDF& in, ostream& out);
72 pair<bool,long> run(SPRequest& request, bool isHandler=true) const;
75 pair<bool,long> doRequest(const Application& application, const char* requestURL, Session* session, HTTPResponse& httpResponse) const;
79 LogoutRequest* buildRequest(
80 const Application& application, const Session& session, const IDPSSODescriptor& role, const MessageEncoder* encoder=NULL
84 vector<const XMLCh*> m_bindings;
85 map<const XMLCh*,MessageEncoder*> m_encoders;
87 auto_ptr_char m_protocol;
90 #if defined (_MSC_VER)
91 #pragma warning( pop )
94 Handler* SHIBSP_DLLLOCAL SAML2LogoutInitiatorFactory(const pair<const DOMElement*,const char*>& p)
96 return new SAML2LogoutInitiator(p.first, p.second);
100 SAML2LogoutInitiator::SAML2LogoutInitiator(const DOMElement* e, const char* appId)
101 : AbstractHandler(e, Category::getInstance(SHIBSP_LOGCAT".LogoutInitiator.SAML2")), m_appId(appId),
105 m_protocol(samlconstants::SAML20P_NS)
108 if (SPConfig::getConfig().isEnabled(SPConfig::OutOfProcess)) {
109 // Handle outgoing binding setup.
110 pair<bool,const XMLCh*> outgoing = getXMLString("outgoingBindings");
111 if (outgoing.first) {
112 m_outgoing = XMLString::replicate(outgoing.second);
113 XMLString::trim(m_outgoing);
116 // No override, so we'll install a default binding precedence.
117 string prec = string(samlconstants::SAML20_BINDING_HTTP_REDIRECT) + ' ' + samlconstants::SAML20_BINDING_HTTP_POST + ' ' +
118 samlconstants::SAML20_BINDING_HTTP_POST_SIMPLESIGN + ' ' + samlconstants::SAML20_BINDING_HTTP_ARTIFACT;
119 m_outgoing = XMLString::transcode(prec.c_str());
123 XMLCh* start = m_outgoing;
124 while (start && *start) {
125 pos = XMLString::indexOf(start,chSpace);
127 *(start + pos)=chNull;
128 m_bindings.push_back(start);
130 auto_ptr_char b(start);
131 MessageEncoder * encoder =
132 SAMLConfig::getConfig().MessageEncoderManager.newPlugin(b.get(),pair<const DOMElement*,const XMLCh*>(e,NULL));
133 m_encoders[start] = encoder;
134 m_log.debug("supporting outgoing binding (%s)", b.get());
136 catch (exception& ex) {
137 m_log.error("error building MessageEncoder: %s", ex.what());
140 start = start + pos + 1;
147 pair<bool,const char*> loc = getString("Location");
149 string address = m_appId + loc.second + "::run::SAML2LI";
150 setAddress(address.c_str());
154 void SAML2LogoutInitiator::setParent(const PropertySet* parent)
156 DOMPropertySet::setParent(parent);
157 pair<bool,const char*> loc = getString("Location");
159 string address = m_appId + loc.second + "::run::SAML2LI";
160 setAddress(address.c_str());
163 m_log.warn("no Location property in SAML2 LogoutInitiator (or parent), can't register as remoted handler");
167 pair<bool,long> SAML2LogoutInitiator::run(SPRequest& request, bool isHandler) const
169 // Defer to base class for front-channel loop first.
170 pair<bool,long> ret = LogoutHandler::run(request, isHandler);
174 // At this point we know the front-channel is handled.
175 // We need the session to do any other work.
177 Session* session = NULL;
179 session = request.getSession(false, true, false); // don't cache it and ignore all checks
181 return make_pair(false,0);
183 // We only handle SAML 2.0 sessions.
184 if (!XMLString::equals(session->getProtocol(), m_protocol.get())) {
186 return make_pair(false,0);
189 catch (exception& ex) {
190 m_log.error("error accessing current session: %s", ex.what());
191 return make_pair(false,0);
194 if (SPConfig::getConfig().isEnabled(SPConfig::OutOfProcess)) {
195 // When out of process, we run natively.
196 return doRequest(request.getApplication(), request.getRequestURL(), session, request);
199 // When not out of process, we remote the request.
200 Locker locker(session, false);
201 DDF out,in(m_address.c_str());
202 DDFJanitor jin(in), jout(out);
203 in.addmember("application_id").string(request.getApplication().getId());
204 in.addmember("session_id").string(session->getID());
205 in.addmember("url").string(request.getRequestURL());
206 out=request.getServiceProvider().getListenerService()->send(in);
207 return unwrap(request, out);
211 void SAML2LogoutInitiator::receive(DDF& in, ostream& out)
214 // Defer to base class for notifications
215 if (in["notify"].integer() == 1)
216 return LogoutHandler::receive(in, out);
219 const char* aid=in["application_id"].string();
220 const Application* app=aid ? SPConfig::getConfig().getServiceProvider()->getApplication(aid) : NULL;
222 // Something's horribly wrong.
223 m_log.error("couldn't find application (%s) for logout", aid ? aid : "(missing)");
224 throw ConfigurationException("Unable to locate application for logout, deleted?");
227 // Set up a response shim.
229 DDFJanitor jout(ret);
230 auto_ptr<HTTPResponse> resp(getResponse(ret));
232 Session* session = NULL;
234 session = app->getServiceProvider().getSessionCache()->find(in["session_id"].string(), *app, NULL, NULL);
236 catch (exception& ex) {
237 m_log.error("error accessing current session: %s", ex.what());
240 // With no session, we just skip the request and let it fall through to an empty struct return.
242 if (session->getNameID() && session->getEntityID()) {
243 // Since we're remoted, the result should either be a throw, which we pass on,
244 // a false/0 return, which we just return as an empty structure, or a response/redirect,
245 // which we capture in the facade and send back.
246 doRequest(*app, in["url"].string(), session, *resp.get());
249 m_log.error("no NameID or issuing entityID found in session");
251 app->getServiceProvider().getSessionCache()->remove(in["session_id"].string(), *app);
254 pair<string,const char*> shib_cookie=app->getCookieNameProps("_shibsession_");
255 resp->setCookie(shib_cookie.first.c_str(), shib_cookie.second);
260 throw ConfigurationException("Cannot perform logout using lite version of shibsp library.");
264 pair<bool,long> SAML2LogoutInitiator::doRequest(
265 const Application& application, const char* requestURL, Session* session, HTTPResponse& response
269 pair<string,const char*> shib_cookie=application.getCookieNameProps("_shibsession_");
270 response.setCookie(shib_cookie.first.c_str(), shib_cookie.second);
272 // Do back channel notification.
273 vector<string> sessions(1, session->getID());
274 if (!notifyBackChannel(application, requestURL, sessions, false)) {
276 application.getServiceProvider().getSessionCache()->remove(sessions.front().c_str(), application);
277 return sendLogoutPage(application, response, true, "Partial logout failure.");
281 pair<bool,long> ret = make_pair(false,0);
283 // With a session in hand, we can create a LogoutRequest message, if we can find a compatible endpoint.
284 Locker metadataLocker(application.getMetadataProvider());
285 const EntityDescriptor* entity = application.getMetadataProvider()->getEntityDescriptor(session->getEntityID());
287 throw MetadataException(
288 "Unable to locate metadata for identity provider ($entityID)",
289 namedparams(1, "entityID", session->getEntityID())
292 const IDPSSODescriptor* role = entity->getIDPSSODescriptor(samlconstants::SAML20P_NS);
294 throw MetadataException(
295 "Unable to locate SAML 2.0 IdP role for identity provider ($entityID).",
296 namedparams(1, "entityID", session->getEntityID())
300 const EndpointType* ep=NULL;
301 const MessageEncoder* encoder=NULL;
302 vector<const XMLCh*>::const_iterator b;
303 for (b = m_bindings.begin(); b!=m_bindings.end(); ++b) {
304 if (ep=EndpointManager<SingleLogoutService>(role->getSingleLogoutServices()).getByBinding(*b)) {
305 map<const XMLCh*,MessageEncoder*>::const_iterator enc = m_encoders.find(*b);
306 if (enc!=m_encoders.end())
307 encoder = enc->second;
311 if (!ep || !encoder) {
312 m_log.warn("no compatible front channel SingleLogoutService, trying back channel...");
313 shibsp::SecurityPolicy policy(application);
314 shibsp::SOAPClient soaper(policy);
315 MetadataCredentialCriteria mcc(*role);
317 LogoutResponse* logoutResponse=NULL;
318 auto_ptr_XMLCh binding(samlconstants::SAML20_BINDING_SOAP);
319 const vector<SingleLogoutService*>& endpoints=role->getSingleLogoutServices();
320 for (vector<SingleLogoutService*>::const_iterator epit=endpoints.begin(); !logoutResponse && epit!=endpoints.end(); ++epit) {
322 if (!XMLString::equals((*epit)->getBinding(),binding.get()))
324 LogoutRequest* msg = buildRequest(application, *session, *role);
325 auto_ptr_char dest((*epit)->getLocation());
327 SAML2SOAPClient client(soaper, false);
328 client.sendSAML(msg, mcc, dest.get());
329 StatusResponseType* srt = client.receiveSAML();
330 if (!(logoutResponse = dynamic_cast<LogoutResponse*>(srt))) {
335 catch (exception& ex) {
336 m_log.error("error sending LogoutRequest message: %s", ex.what());
342 ret = sendLogoutPage(application, response, false, "Identity provider did not respond to logout request.");
343 else if (!logoutResponse->getStatus() || !logoutResponse->getStatus()->getStatusCode() ||
344 !XMLString::equals(logoutResponse->getStatus()->getStatusCode()->getValue(), saml2p::StatusCode::SUCCESS)) {
345 delete logoutResponse;
346 ret = sendLogoutPage(application, response, false, "Identity provider returned a SAML error in response to logout request.");
349 delete logoutResponse;
350 ret = sendLogoutPage(application, response, false, "Logout completed successfully.");
354 string session_id = session->getID();
357 application.getServiceProvider().getSessionCache()->remove(session_id.c_str(), application);
362 auto_ptr<LogoutRequest> msg(buildRequest(application, *session, *role, encoder));
364 msg->setDestination(ep->getLocation());
365 auto_ptr_char dest(ep->getLocation());
366 ret.second = sendMessage(*encoder, msg.get(), NULL, dest.get(), role, application, response);
368 msg.release(); // freed by encoder
370 catch (exception& ex) {
371 m_log.error("error issuing SAML 2.0 logout request: %s", ex.what());
375 string session_id = session->getID();
378 application.getServiceProvider().getSessionCache()->remove(session_id.c_str(), application);
383 string session_id = session->getID();
385 application.getServiceProvider().getSessionCache()->remove(session_id.c_str(), application);
386 throw ConfigurationException("Cannot perform logout using lite version of shibsp library.");
392 LogoutRequest* SAML2LogoutInitiator::buildRequest(
393 const Application& application, const Session& session, const IDPSSODescriptor& role, const MessageEncoder* encoder
396 auto_ptr<LogoutRequest> msg(LogoutRequestBuilder::buildLogoutRequest());
397 Issuer* issuer = IssuerBuilder::buildIssuer();
398 msg->setIssuer(issuer);
399 issuer->setName(application.getXMLString("entityID").second);
400 auto_ptr_XMLCh index(session.getSessionIndex());
401 if (index.get() && *index.get()) {
402 SessionIndex* si = SessionIndexBuilder::buildSessionIndex();
403 msg->getSessionIndexs().push_back(si);
404 si->setSessionIndex(index.get());
407 const NameID* nameid = session.getNameID();
408 const PropertySet* relyingParty = application.getRelyingParty(dynamic_cast<EntityDescriptor*>(role.getParent()));
409 pair<bool,const char*> flag = relyingParty->getString("encryption");
411 (!strcmp(flag.second, "true") || (encoder && !strcmp(flag.second, "front")) || (!encoder && !strcmp(flag.second, "back")))) {
412 auto_ptr<EncryptedID> encrypted(EncryptedIDBuilder::buildEncryptedID());
413 MetadataCredentialCriteria mcc(role);
416 *(application.getMetadataProvider()),
418 encoder ? encoder->isCompact() : false,
419 relyingParty->getXMLString("encryptionAlg").second
421 msg->setEncryptedID(encrypted.release());
425 // No encoder being used, so sign for SOAP client manually.
426 flag = relyingParty->getString("signing");
427 if (flag.first && (!strcmp(flag.second, "true") || !strcmp(flag.second, "back"))) {
428 CredentialResolver* credResolver=application.getCredentialResolver();
430 Locker credLocker(credResolver);
431 // Fill in criteria to use.
432 MetadataCredentialCriteria mcc(role);
433 mcc.setUsage(CredentialCriteria::SIGNING_CREDENTIAL);
434 pair<bool,const char*> keyName = relyingParty->getString("keyName");
436 mcc.getKeyNames().insert(keyName.second);
437 pair<bool,const XMLCh*> sigalg = relyingParty->getXMLString("signingAlg");
439 mcc.setXMLAlgorithm(sigalg.second);
440 const Credential* cred = credResolver->resolve(&mcc);
442 xmlsignature::Signature* sig = xmlsignature::SignatureBuilder::buildSignature();
443 msg->setSignature(sig);
445 sig->setSignatureAlgorithm(sigalg.second);
446 sigalg = relyingParty->getXMLString("digestAlg");
448 ContentReference* cr = dynamic_cast<ContentReference*>(sig->getContentReference());
450 cr->setDigestAlgorithm(sigalg.second);
453 // Sign response while marshalling.
454 vector<xmlsignature::Signature*> sigs(1,sig);
455 msg->marshall((DOMDocument*)NULL,&sigs,cred);
458 m_log.warn("no signing credential resolved, leaving message unsigned");
462 m_log.warn("no credential resolver installed, leaving message unsigned");
467 return msg.release();