Factor out LogoutInitiator class, simpler API to get ACS by binding.
[shibboleth/sp.git] / shibsp / handler / impl / SAML2LogoutInitiator.cpp
index c0abf10..cba23f6 100644 (file)
@@ -1,6 +1,6 @@
 /*
- *  Copyright 2001-2007 Internet2
- * 
+ *  Copyright 2001-2010 Internet2
+ *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
@@ -16,7 +16,7 @@
 
 /**
  * SAML2LogoutInitiator.cpp
- * 
+ *
  * Triggers SP-initiated logout for SAML 2.0 sessions.
  */
 
 #include "ServiceProvider.h"
 #include "SessionCache.h"
 #include "handler/AbstractHandler.h"
-#include "handler/LogoutHandler.h"
+#include "handler/LogoutInitiator.h"
 
 #ifndef SHIBSP_LITE
 # include "binding/SOAPClient.h"
+# include "metadata/MetadataProviderCriteria.h"
+# include "security/SecurityPolicy.h"
+# include <saml/exceptions.h>
 # include <saml/SAMLConfig.h>
 # include <saml/saml2/core/Protocols.h>
 # include <saml/saml2/binding/SAML2SOAPClient.h>
 # include <saml/saml2/metadata/EndpointManager.h>
+# include <saml/saml2/metadata/Metadata.h>
 # include <saml/saml2/metadata/MetadataCredentialCriteria.h>
 using namespace opensaml::saml2;
 using namespace opensaml::saml2p;
@@ -53,8 +57,8 @@ namespace shibsp {
     #pragma warning( push )
     #pragma warning( disable : 4250 )
 #endif
-    
-    class SHIBSP_DLLLOCAL SAML2LogoutInitiator : public AbstractHandler, public LogoutHandler
+
+    class SHIBSP_DLLLOCAL SAML2LogoutInitiator : public AbstractHandler, public LogoutInitiator
     {
     public:
         SAML2LogoutInitiator(const DOMElement* e, const char* appId);
@@ -66,18 +70,24 @@ namespace shibsp {
             }
 #endif
         }
-        
+
         void setParent(const PropertySet* parent);
         void receive(DDF& in, ostream& out);
         pair<bool,long> run(SPRequest& request, bool isHandler=true) const;
 
+        const XMLCh* getProtocolFamily() const {
+            return samlconstants::SAML20P_NS;
+        }
+
     private:
-        pair<bool,long> doRequest(const Application& application, const char* requestURL, Session* session, HTTPResponse& httpResponse) const;
+        pair<bool,long> doRequest(
+            const Application& application, const HTTPRequest& request, HTTPResponse& httpResponse, Session* session
+            ) const;
 
         string m_appId;
 #ifndef SHIBSP_LITE
         LogoutRequest* buildRequest(
-            const Application& application, const Session& session, const IDPSSODescriptor& role, const MessageEncoder* encoder=NULL
+            const Application& application, const Session& session, const RoleDescriptor& role, const MessageEncoder* encoder=nullptr
             ) const;
 
         XMLCh* m_outgoing;
@@ -100,7 +110,7 @@ namespace shibsp {
 SAML2LogoutInitiator::SAML2LogoutInitiator(const DOMElement* e, const char* appId)
     : AbstractHandler(e, Category::getInstance(SHIBSP_LOGCAT".LogoutInitiator.SAML2")), m_appId(appId),
 #ifndef SHIBSP_LITE
-        m_outgoing(NULL),
+        m_outgoing(nullptr),
 #endif
         m_protocol(samlconstants::SAML20P_NS)
 {
@@ -129,9 +139,15 @@ SAML2LogoutInitiator::SAML2LogoutInitiator(const DOMElement* e, const char* appI
             try {
                 auto_ptr_char b(start);
                 MessageEncoder * encoder =
-                    SAMLConfig::getConfig().MessageEncoderManager.newPlugin(b.get(),pair<const DOMElement*,const XMLCh*>(e,NULL));
-                m_encoders[start] = encoder;
-                m_log.debug("supporting outgoing binding (%s)", b.get());
+                    SAMLConfig::getConfig().MessageEncoderManager.newPlugin(b.get(),pair<const DOMElement*,const XMLCh*>(e,nullptr));
+                if (encoder->isUserAgentPresent() && XMLString::equals(getProtocolFamily(), encoder->getProtocolFamily())) {
+                    m_encoders[start] = encoder;
+                    m_log.debug("supporting outgoing binding (%s)", b.get());
+                }
+                else {
+                    delete encoder;
+                    m_log.warn("skipping outgoing binding (%s), not a SAML 2.0 front-channel mechanism", b.get());
+                }
             }
             catch (exception& ex) {
                 m_log.error("error building MessageEncoder: %s", ex.what());
@@ -174,35 +190,33 @@ pair<bool,long> SAML2LogoutInitiator::run(SPRequest& request, bool isHandler) co
     // At this point we know the front-channel is handled.
     // We need the session to do any other work.
 
-    Session* session = NULL;
+    Session* session = nullptr;
     try {
         session = request.getSession(false, true, false);  // don't cache it and ignore all checks
         if (!session)
-            return make_pair(false,0);
+            return make_pair(false,0L);
 
         // We only handle SAML 2.0 sessions.
         if (!XMLString::equals(session->getProtocol(), m_protocol.get())) {
             session->unlock();
-            return make_pair(false,0);
+            return make_pair(false,0L);
         }
     }
     catch (exception& ex) {
         m_log.error("error accessing current session: %s", ex.what());
-        return make_pair(false,0);
+        return make_pair(false,0L);
     }
 
     if (SPConfig::getConfig().isEnabled(SPConfig::OutOfProcess)) {
         // When out of process, we run natively.
-        return doRequest(request.getApplication(), request.getRequestURL(), session, request);
+        return doRequest(request.getApplication(), request, request, session);
     }
     else {
         // When not out of process, we remote the request.
-        Locker locker(session, false);
-        DDF out,in(m_address.c_str());
+        session->unlock();
+        vector<string> headers(1,"Cookie");
+        DDF out,in = wrap(request,&headers);
         DDFJanitor jin(in), jout(out);
-        in.addmember("application_id").string(request.getApplication().getId());
-        in.addmember("session_id").string(session->getID());
-        in.addmember("url").string(request.getRequestURL());
         out=request.getServiceProvider().getListenerService()->send(in);
         return unwrap(request, out);
     }
@@ -217,21 +231,24 @@ void SAML2LogoutInitiator::receive(DDF& in, ostream& out)
 
     // Find application.
     const char* aid=in["application_id"].string();
-    const Application* app=aid ? SPConfig::getConfig().getServiceProvider()->getApplication(aid) : NULL;
+    const Application* app=aid ? SPConfig::getConfig().getServiceProvider()->getApplication(aid) : nullptr;
     if (!app) {
         // Something's horribly wrong.
         m_log.error("couldn't find application (%s) for logout", aid ? aid : "(missing)");
         throw ConfigurationException("Unable to locate application for logout, deleted?");
     }
-    
+
+    // Unpack the request.
+    auto_ptr<HTTPRequest> req(getRequest(in));
+
     // Set up a response shim.
-    DDF ret(NULL);
+    DDF ret(nullptr);
     DDFJanitor jout(ret);
     auto_ptr<HTTPResponse> resp(getResponse(ret));
-    
-    Session* session = NULL;
+
+    Session* session = nullptr;
     try {
-         session = app->getServiceProvider().getSessionCache()->find(in["session_id"].string(), *app, NULL, NULL);
+         session = app->getServiceProvider().getSessionCache()->find(*app, *req.get(), nullptr, nullptr);
     }
     catch (exception& ex) {
         m_log.error("error accessing current session: %s", ex.what());
@@ -243,16 +260,12 @@ void SAML2LogoutInitiator::receive(DDF& in, ostream& out)
             // Since we're remoted, the result should either be a throw, which we pass on,
             // a false/0 return, which we just return as an empty structure, or a response/redirect,
             // which we capture in the facade and send back.
-            doRequest(*app, in["url"].string(), session, *resp.get());
+            doRequest(*app, *req.get(), *resp.get(), session);
         }
         else {
-             m_log.error("no NameID or issuing entityID found in session");
+             m_log.log(getParent() ? Priority::WARN : Priority::ERROR, "bypassing SAML 2.0 logout, no NameID or issuing entityID found in session");
              session->unlock();
-             app->getServiceProvider().getSessionCache()->remove(in["session_id"].string(), *app);
-
-            // Clear the cookie.
-            pair<string,const char*> shib_cookie=app->getCookieNameProps("_shibsession_");
-            resp->setCookie(shib_cookie.first.c_str(), shib_cookie.second);
+             app->getServiceProvider().getSessionCache()->remove(*app, *req.get(), resp.get());
         }
     }
     out << ret;
@@ -262,43 +275,45 @@ void SAML2LogoutInitiator::receive(DDF& in, ostream& out)
 }
 
 pair<bool,long> SAML2LogoutInitiator::doRequest(
-    const Application& application, const char* requestURL, Session* session, HTTPResponse& response
+    const Application& application, const HTTPRequest& httpRequest, HTTPResponse& httpResponse, Session* session
     ) const
 {
-    // Clear the cookie.
-    pair<string,const char*> shib_cookie=application.getCookieNameProps("_shibsession_");
-    response.setCookie(shib_cookie.first.c_str(), shib_cookie.second);
-
     // Do back channel notification.
     vector<string> sessions(1, session->getID());
-    if (!notifyBackChannel(application, requestURL, sessions, false)) {
+    if (!notifyBackChannel(application, httpRequest.getRequestURL(), sessions, false)) {
         session->unlock();
-        application.getServiceProvider().getSessionCache()->remove(sessions.front().c_str(), application);
-        return sendLogoutPage(application, response, true, "Partial logout failure.");
+        application.getServiceProvider().getSessionCache()->remove(application, httpRequest, &httpResponse);
+        return sendLogoutPage(application, httpRequest, httpResponse, "partial");
     }
 
 #ifndef SHIBSP_LITE
-    pair<bool,long> ret = make_pair(false,0);
+    pair<bool,long> ret = make_pair(false,0L);
     try {
         // With a session in hand, we can create a LogoutRequest message, if we can find a compatible endpoint.
-        Locker metadataLocker(application.getMetadataProvider());
-        const EntityDescriptor* entity = application.getMetadataProvider()->getEntityDescriptor(session->getEntityID());
-        if (!entity) {
+        MetadataProvider* m = application.getMetadataProvider();
+        Locker metadataLocker(m);
+        MetadataProviderCriteria mc(application, session->getEntityID(), &IDPSSODescriptor::ELEMENT_QNAME, samlconstants::SAML20P_NS);
+        pair<const EntityDescriptor*,const RoleDescriptor*> entity = m->getEntityDescriptor(mc);
+        if (!entity.first) {
             throw MetadataException(
-                "Unable to locate metadata for identity provider ($entityID)",
-                namedparams(1, "entityID", session->getEntityID())
+                "Unable to locate metadata for identity provider ($entityID)", namedparams(1, "entityID", session->getEntityID())
                 );
         }
-        const IDPSSODescriptor* role = entity->getIDPSSODescriptor(samlconstants::SAML20P_NS);
-        if (!role) {
+        else if (!entity.second) {
             throw MetadataException(
-                "Unable to locate SAML 2.0 IdP role for identity provider ($entityID).",
-                namedparams(1, "entityID", session->getEntityID())
+                "Unable to locate SAML 2.0 IdP role for identity provider ($entityID).", namedparams(1, "entityID", session->getEntityID())
                 );
         }
 
-        const EndpointType* ep=NULL;
-        const MessageEncoder* encoder=NULL;
+        const IDPSSODescriptor* role = dynamic_cast<const IDPSSODescriptor*>(entity.second);
+        if (role->getSingleLogoutServices().empty()) {
+            throw MetadataException(
+                "No SingleLogoutService endpoints in metadata for identity provider ($entityID).", namedparams(1, "entityID", session->getEntityID())
+                );
+        }
+
+        const EndpointType* ep=nullptr;
+        const MessageEncoder* encoder=nullptr;
         vector<const XMLCh*>::const_iterator b;
         for (b = m_bindings.begin(); b!=m_bindings.end(); ++b) {
             if (ep=EndpointManager<SingleLogoutService>(role->getSingleLogoutServices()).getByBinding(*b)) {
@@ -309,12 +324,12 @@ pair<bool,long> SAML2LogoutInitiator::doRequest(
             }
         }
         if (!ep || !encoder) {
-            m_log.warn("no compatible front channel SingleLogoutService, trying back channel...");
+            m_log.debug("no compatible front channel SingleLogoutService, trying back channel...");
             shibsp::SecurityPolicy policy(application);
             shibsp::SOAPClient soaper(policy);
             MetadataCredentialCriteria mcc(*role);
 
-            LogoutResponse* logoutResponse=NULL;
+            LogoutResponse* logoutResponse=nullptr;
             auto_ptr_XMLCh binding(samlconstants::SAML20_BINDING_SOAP);
             const vector<SingleLogoutService*>& endpoints=role->getSingleLogoutServices();
             for (vector<SingleLogoutService*>::const_iterator epit=endpoints.begin(); !logoutResponse && epit!=endpoints.end(); ++epit) {
@@ -325,7 +340,7 @@ pair<bool,long> SAML2LogoutInitiator::doRequest(
                     auto_ptr_char dest((*epit)->getLocation());
 
                     SAML2SOAPClient client(soaper, false);
-                    client.sendSAML(msg, mcc, dest.get());
+                    client.sendSAML(msg, application.getId(), mcc, dest.get());
                     StatusResponseType* srt = client.receiveSAML();
                     if (!(logoutResponse = dynamic_cast<LogoutResponse*>(srt))) {
                         delete srt;
@@ -338,32 +353,57 @@ pair<bool,long> SAML2LogoutInitiator::doRequest(
                 }
             }
 
-            if (!logoutResponse)
-                ret = sendLogoutPage(application, response, false, "Identity provider did not respond to logout request.");
-            else if (!logoutResponse->getStatus() || !logoutResponse->getStatus()->getStatusCode() ||
-                   !XMLString::equals(logoutResponse->getStatus()->getStatusCode()->getValue(), saml2p::StatusCode::SUCCESS)) {
-                delete logoutResponse;
-                ret = sendLogoutPage(application, response, false, "Identity provider returned a SAML error in response to logout request.");
+            // No answer at all?
+            if (!logoutResponse) {
+                if (endpoints.empty())
+                    m_log.info("IdP doesn't support single logout protocol over a compatible binding");
+                else
+                    m_log.warn("IdP didn't respond to logout request");
+                ret = sendLogoutPage(application, httpRequest, httpResponse, "partial");
             }
             else {
+                // Check the status, looking for non-success or a partial logout code.
+                const StatusCode* sc = logoutResponse->getStatus() ? logoutResponse->getStatus()->getStatusCode() : nullptr;
+                bool partial = (!sc || !XMLString::equals(sc->getValue(), StatusCode::SUCCESS));
+                if (!partial && sc->getStatusCode()) {
+                    // Success, but still need to check for partial.
+                    partial = XMLString::equals(sc->getStatusCode()->getValue(), StatusCode::PARTIAL_LOGOUT);
+                }
                 delete logoutResponse;
-                ret = sendLogoutPage(application, response, false, "Logout completed successfully.");
+                if (partial)
+                    ret = sendLogoutPage(application, httpRequest, httpResponse, "partial");
+                else {
+                    const char* returnloc = httpRequest.getParameter("return");
+                    if (returnloc) {
+                        ret.second = httpResponse.sendRedirect(returnloc);
+                        ret.first = true;
+                    }
+                    ret = sendLogoutPage(application, httpRequest, httpResponse, "global");
+                }
             }
 
             if (session) {
-                string session_id = session->getID();
                 session->unlock();
-                session = NULL;
-                application.getServiceProvider().getSessionCache()->remove(session_id.c_str(), application);
+                session = nullptr;
+                application.getServiceProvider().getSessionCache()->remove(application, httpRequest, &httpResponse);
             }
+
             return ret;
         }
 
+        // Save off return location as RelayState.
+        string relayState;
+        const char* returnloc = httpRequest.getParameter("return");
+        if (returnloc) {
+            relayState = returnloc;
+            preserveRelayState(application, httpResponse, relayState);
+        }
+
         auto_ptr<LogoutRequest> msg(buildRequest(application, *session, *role, encoder));
 
         msg->setDestination(ep->getLocation());
         auto_ptr_char dest(ep->getLocation());
-        ret.second = sendMessage(*encoder, msg.get(), NULL, dest.get(), role, application, response, "signRequests");
+        ret.second = sendMessage(*encoder, msg.get(), relayState.c_str(), dest.get(), role, application, httpResponse);
         ret.first = true;
         msg.release();  // freed by encoder
     }
@@ -372,17 +412,15 @@ pair<bool,long> SAML2LogoutInitiator::doRequest(
     }
 
     if (session) {
-        string session_id = session->getID();
         session->unlock();
-        session = NULL;
-        application.getServiceProvider().getSessionCache()->remove(session_id.c_str(), application);
+        session = nullptr;
+        application.getServiceProvider().getSessionCache()->remove(application, httpRequest, &httpResponse);
     }
 
     return ret;
 #else
-    string session_id = session->getID();
     session->unlock();
-    application.getServiceProvider().getSessionCache()->remove(session_id.c_str(), application);
+    application.getServiceProvider().getSessionCache()->remove(application, httpRequest, &httpResponse);
     throw ConfigurationException("Cannot perform logout using lite version of shibsp library.");
 #endif
 }
@@ -390,13 +428,15 @@ pair<bool,long> SAML2LogoutInitiator::doRequest(
 #ifndef SHIBSP_LITE
 
 LogoutRequest* SAML2LogoutInitiator::buildRequest(
-    const Application& application, const Session& session, const IDPSSODescriptor& role, const MessageEncoder* encoder
+    const Application& application, const Session& session, const RoleDescriptor& role, const MessageEncoder* encoder
     ) const
 {
+    const PropertySet* relyingParty = application.getRelyingParty(dynamic_cast<EntityDescriptor*>(role.getParent()));
+
     auto_ptr<LogoutRequest> msg(LogoutRequestBuilder::buildLogoutRequest());
     Issuer* issuer = IssuerBuilder::buildIssuer();
     msg->setIssuer(issuer);
-    issuer->setName(application.getXMLString("entityID").second);
+    issuer->setName(relyingParty->getXMLString("entityID").second);
     auto_ptr_XMLCh index(session.getSessionIndex());
     if (index.get() && *index.get()) {
         SessionIndex* si = SessionIndexBuilder::buildSessionIndex();
@@ -405,8 +445,7 @@ LogoutRequest* SAML2LogoutInitiator::buildRequest(
     }
 
     const NameID* nameid = session.getNameID();
-    const PropertySet* relyingParty = application.getRelyingParty(dynamic_cast<EntityDescriptor*>(role.getParent()));
-    pair<bool,const char*> flag = relyingParty->getString("encryptRequests");
+    pair<bool,const char*> flag = relyingParty->getString("encryption");
     if (flag.first &&
         (!strcmp(flag.second, "true") || (encoder && !strcmp(flag.second, "front")) || (!encoder && !strcmp(flag.second, "back")))) {
         auto_ptr<EncryptedID> encrypted(EncryptedIDBuilder::buildEncryptedID());
@@ -420,49 +459,8 @@ LogoutRequest* SAML2LogoutInitiator::buildRequest(
             );
         msg->setEncryptedID(encrypted.release());
     }
-
-    if (!encoder) {
-        // No encoder being used, so sign for SOAP client manually.
-        flag = relyingParty->getString("signRequests");
-        if (flag.first && (!strcmp(flag.second, "true") || !strcmp(flag.second, "back"))) {
-            CredentialResolver* credResolver=application.getCredentialResolver();
-            if (credResolver) {
-                Locker credLocker(credResolver);
-                // Fill in criteria to use.
-                MetadataCredentialCriteria mcc(role);
-                mcc.setUsage(CredentialCriteria::SIGNING_CREDENTIAL);
-                pair<bool,const char*> keyName = relyingParty->getString("keyName");
-                if (keyName.first)
-                    mcc.getKeyNames().insert(keyName.second);
-                pair<bool,const XMLCh*> sigalg = relyingParty->getXMLString("signatureAlg");
-                if (sigalg.first)
-                    mcc.setXMLAlgorithm(sigalg.second);
-                const Credential* cred = credResolver->resolve(&mcc);
-                if (cred) {
-                    xmlsignature::Signature* sig = xmlsignature::SignatureBuilder::buildSignature();
-                    msg->setSignature(sig);
-                    pair<bool, const XMLCh*> alg = relyingParty->getXMLString("signatureAlg");
-                    if (alg.first)
-                        sig->setSignatureAlgorithm(alg.second);
-                    alg = relyingParty->getXMLString("digestAlg");
-                    if (alg.first) {
-                        ContentReference* cr = dynamic_cast<ContentReference*>(sig->getContentReference());
-                        if (cr)
-                            cr->setDigestAlgorithm(alg.second);
-                    }
-            
-                    // Sign response while marshalling.
-                    vector<xmlsignature::Signature*> sigs(1,sig);
-                    msg->marshall((DOMDocument*)NULL,&sigs,cred);
-                }
-                else {
-                    m_log.warn("no signing credential resolved, leaving message unsigned");
-                }
-            }
-            else {
-                m_log.warn("no credential resolver installed, leaving message unsigned");
-            }
-        }
+    else {
+        msg->setNameID(nameid->cloneNameID());
     }
 
     return msg.release();