SSPCPP-616 - clean up concatenated string literals
[shibboleth/cpp-sp.git] / shibsp / impl / StorageServiceSessionCache.cpp
index a3987ab..f9fb5de 100644 (file)
@@ -46,7 +46,6 @@
 #include <algorithm>
 #include <boost/bind.hpp>
 #include <boost/shared_ptr.hpp>
-#include <boost/scoped_ptr.hpp>
 #include <xmltooling/io/HTTPRequest.h>
 #include <xmltooling/io/HTTPResponse.h>
 #include <xmltooling/util/DateTime.h>
@@ -63,6 +62,7 @@
 # include <saml/saml2/metadata/Metadata.h>
 # include <xmltooling/XMLToolingConfig.h>
 # include <xmltooling/util/StorageService.h>
+# include <xercesc/util/XMLStringTokenizer.hpp>
 using namespace opensaml::saml2md;
 #else
 # include <ctime>
@@ -75,7 +75,16 @@ using namespace xmltooling;
 using namespace boost;
 using namespace std;
 
-namespace shibsp {
+namespace {
+
+    // Allows the cache to bind sessions to multiple client address
+    // families based on whatever this function returns.
+    static const char* getAddressFamily(const char* addr) {
+        if (strchr(addr, ':'))
+            return "6";
+        else
+            return "4";
+    }
 
     class StoredSession;
     class SSCache : public SessionCacheEx
@@ -133,7 +142,9 @@ namespace shibsp {
             const set<string>* indexes,
             time_t expires,
             vector<string>& sessions
-            );
+            ) {
+            return _logout(app, issuer, nameid, indexes, expires, sessions, 0);
+        }
         bool matches(
             const Application& app,
             const HTTPRequest& request,
@@ -203,11 +214,21 @@ namespace shibsp {
     private:
 #ifndef SHIBSP_LITE
         // maintain back-mappings of NameID/SessionIndex -> session key
-        void insert(const char* key, time_t expires, const char* name, const char* index);
+        void insert(const char* key, time_t expires, const char* name, const char* index, short attempts=0);
+        vector<string>::size_type _logout(
+            const Application& app,
+            const EntityDescriptor* issuer,
+            const saml2::NameID& nameid,
+            const set<string>* indexes,
+            time_t expires,
+            vector<string>& sessions,
+            short attempts
+            );
         bool stronglyMatches(const XMLCh* idp, const XMLCh* sp, const saml2::NameID& n1, const saml2::NameID& n2) const;
         LogoutEvent* newLogoutEvent(const Application& app) const;
 
-        bool m_cacheAssertions;
+        bool m_cacheAssertions,m_reverseIndex;
+        set<xstring> m_excludedNames;
 #endif
         const DOMElement* m_root;         // Only valid during initialization
         unsigned long m_inprocTimeout,m_cacheTimeout,m_cacheAllowance;
@@ -230,6 +251,15 @@ namespace shibsp {
     {
     public:
         StoredSession(SSCache* cache, DDF& obj) : m_obj(obj), m_cache(cache), m_expires(0), m_lastAccess(time(nullptr)) {
+            // Check for old address format.
+            if (m_obj["client_addr"].isstring()) {
+                const char* saddr = m_obj["client_addr"].string();
+                DDF addrobj = m_obj["client_addr"].structure();
+                if (saddr && *saddr) {
+                    addrobj.addmember(getAddressFamily(saddr)).string(saddr);
+                }
+            }
+
             auto_ptr_XMLCh exp(m_obj["expires"].string());
             if (exp.get()) {
                 DateTime iso(exp.get());
@@ -277,8 +307,21 @@ namespace shibsp {
             return m_obj["application_id"].string();
         }
         const char* getClientAddress() const {
-            return m_obj["client_addr"].string();
+            return m_obj["client_addr"].first().string();
+        }
+
+        const char* getClientAddress(const char* family) const {
+            if (family)
+                return m_obj["client_addr"][family].string();
+            return nullptr;
+        }
+        void setClientAddress(const char* client_addr) {
+            DDF obj = m_obj["client_addr"];
+            if (!obj.isstruct())
+                obj = m_obj.addmember("client_addr").structure();
+            obj.addmember(getAddressFamily(client_addr)).string(client_addr);
         }
+
         const char* getEntityID() const {
             return m_obj["entity_id"].string();
         }
@@ -348,7 +391,7 @@ namespace shibsp {
         DDF m_obj;
 #ifndef SHIBSP_LITE
         scoped_ptr<saml2::NameID> m_nameid;
-        mutable map<string,boost::shared_ptr<Assertion>> m_tokens;
+        mutable map< string,boost::shared_ptr<Assertion> > m_tokens;
 #endif
         mutable vector<Attribute*> m_attributes;
         mutable multimap<string,const Attribute*> m_attributeIndex;
@@ -418,32 +461,41 @@ void StoredSession::validate(const Application& app, const char* client_addr, ti
 
     // Address check?
     if (client_addr) {
-        if (!XMLString::equals(getClientAddress(),client_addr)) {
-            m_cache->m_log.warn("client address mismatch, client (%s), session (%s)", client_addr, getClientAddress());
-            throw RetryableProfileException(
-                "Your IP address ($1) does not match the address recorded at the time the session was established.",
-                params(1,client_addr)
-                );
+        const char* saddr = getClientAddress(getAddressFamily(client_addr));
+        if (saddr && *saddr) {
+            if (!XMLString::equals(saddr, client_addr)) {
+                m_cache->m_log.warn("client address mismatch, client (%s), session (%s)", client_addr, saddr);
+                throw RetryableProfileException(
+                    "Your IP address ($1) does not match the address recorded at the time the session was established.",
+                    params(1, client_addr)
+                    );
+            }
+            client_addr = nullptr;  // clear out parameter as signal that session need not be updated below
+        }
+        else {
+            m_cache->m_log.info("session (%s) not yet bound to client address type, binding it to (%s)", getID(), client_addr);
         }
     }
 
-    if (!timeout)
+    if (!timeout && !client_addr)
         return;
 
     if (!SPConfig::getConfig().isEnabled(SPConfig::OutOfProcess)) {
-        DDF in("touch::"STORAGESERVICE_SESSION_CACHE"::SessionCache"), out;
+        DDF in("touch::" STORAGESERVICE_SESSION_CACHE "::SessionCache"), out;
         DDFJanitor jin(in);
         in.structure();
         in.addmember("key").string(getID());
         in.addmember("version").integer(m_obj["version"].integer());
         in.addmember("application_id").string(app.getId());
-        if (*timeout) {
+        if (client_addr)    // signals we need to bind an additional address to the session
+            in.addmember("client_addr").string(client_addr);
+        if (timeout && *timeout) {
             // On 64-bit Windows, time_t doesn't fit in a long, so I'm using ISO timestamps.
 #ifndef HAVE_GMTIME_R
-            struct tm* ptime=gmtime(timeout);
+            struct tm* ptime = gmtime(timeout);
 #else
             struct tm res;
-            struct tm* ptime=gmtime_r(timeout,&res);
+            struct tm* ptime = gmtime_r(timeout,&res);
 #endif
             char timebuf[32];
             strftime(timebuf,32,"%Y-%m-%dT%H:%M:%SZ",ptime);
@@ -460,6 +512,7 @@ void StoredSession::validate(const Application& app, const char* client_addr, ti
 
         if (out.isstruct()) {
             // We got an updated record back.
+            m_cache->m_log.debug("session updated, reconstituting it");
             m_ids.clear();
             for_each(m_attributes.begin(), m_attributes.end(), xmltooling::cleanup<Attribute>());
             m_attributes.clear();
@@ -473,7 +526,7 @@ void StoredSession::validate(const Application& app, const char* client_addr, ti
         if (!m_cache->m_storage)
             throw ConfigurationException("Session touch requires a StorageService.");
 
-        // Do a versioned read.
+        // Versioned read, since we already have the data in hand if it's current.
         string record;
         time_t lastAccess;
         int curver = m_obj["version"].integer();
@@ -483,20 +536,22 @@ void StoredSession::validate(const Application& app, const char* client_addr, ti
             throw RetryableProfileException("Your session has expired, and you must re-authenticate.");
         }
 
-        // Adjust for expiration to recover last access time and check timeout.
-        unsigned long cacheTimeout = m_cache->getCacheTimeout(app);
-        lastAccess -= cacheTimeout;
-        if (*timeout > 0 && now - lastAccess >= *timeout) {
-            m_cache->m_log.info("session timed out (ID: %s)", getID());
-            throw RetryableProfileException("Your session has expired, and you must re-authenticate.");
-        }
+        if (timeout) {
+            // Adjust for expiration to recover last access time and check timeout.
+            unsigned long cacheTimeout = m_cache->getCacheTimeout(app);
+            lastAccess -= cacheTimeout;
+            if (*timeout > 0 && now - lastAccess >= *timeout) {
+                m_cache->m_log.info("session timed out (ID: %s)", getID());
+                throw RetryableProfileException("Your session has expired, and you must re-authenticate.");
+            }
 
-        // Update storage expiration, if possible.
-        try {
-            m_cache->m_storage->updateContext(getID(), now + cacheTimeout);
-        }
-        catch (std::exception& ex) {
-            m_cache->m_log.error("failed to update session expiration: %s", ex.what());
+            // Update storage expiration, if possible.
+            try {
+                m_cache->m_storage->updateContext(getID(), now + cacheTimeout);
+            }
+            catch (std::exception& ex) {
+                m_cache->m_log.error("failed to update session expiration: %s", ex.what());
+            }
         }
 
         if (ver > curver) {
@@ -511,6 +566,82 @@ void StoredSession::validate(const Application& app, const char* client_addr, ti
             m_obj.destroy();
             m_obj = newobj;
         }
+
+        // We may need to write back a new address into the session using a versioned update loop.
+        if (client_addr) {
+            short attempts = 0;
+            do {
+                const char* saddr = getClientAddress(getAddressFamily(client_addr));
+                if (saddr) {
+                    // Something snuck in and bound the session to this address type, so it better match what we have.
+                    if (!XMLString::equals(saddr, client_addr)) {
+                        m_cache->m_log.warn("client address mismatch, client (%s), session (%s)", client_addr, saddr);
+                        throw RetryableProfileException(
+                            "Your IP address ($1) does not match the address recorded at the time the session was established.",
+                            params(1, client_addr)
+                            );
+                    }
+                    break;  // No need to update.
+                }
+                else {
+                    // Bind it into the session.
+                    setClientAddress(client_addr);
+                }
+
+                // Tentatively increment the version.
+                m_obj["version"].integer(m_obj["version"].integer() + 1);
+
+                ostringstream str;
+                str << m_obj;
+                record = str.str();
+
+                try {
+                    ver = m_cache->m_storage->updateText(getID(), "session", record.c_str(), 0, m_obj["version"].integer() - 1);
+                }
+                catch (std::exception&) {
+                    m_obj["version"].integer(m_obj["version"].integer() - 1);
+                    throw;
+                }
+
+                if (ver <= 0) {
+                    m_obj["version"].integer(m_obj["version"].integer() - 1);
+                }
+
+                if (!ver) {
+                    // Fatal problem with update.
+                    m_cache->m_log.error("updateText failed on StorageService for session (%s)", getID());
+                    throw IOException("Unable to update stored session.");
+                }
+                else if (ver < 0) {
+                    // Out of sync.
+                    if (++attempts > 10) {
+                        m_cache->m_log.error("failed to bind client address, update attempts exceeded limit");
+                        throw IOException("Unable to update stored session, exceeded retry limit.");
+                    }
+                    m_cache->m_log.warn("storage service indicates the record is out of sync, updating with a fresh copy...");
+                    ver = m_cache->m_storage->readText(getID(), "session", &record, nullptr);
+                    if (!ver) {
+                        m_cache->m_log.error("readText failed on StorageService for session (%s)", getID());
+                        throw IOException("Unable to read back stored session.");
+                    }
+
+                    // Reset object.
+                    DDF newobj;
+                    istringstream in(record);
+                    in >> newobj;
+
+                    m_ids.clear();
+                    for_each(m_attributes.begin(), m_attributes.end(), xmltooling::cleanup<Attribute>());
+                    m_attributes.clear();
+                    m_attributeIndex.clear();
+                    newobj["version"].integer(ver);
+                    m_obj.destroy();
+                    m_obj = newobj;
+
+                    ver = -1;
+                }
+            } while (ver < 0); // negative indicates a sync issue so we retry
+        }
 #else
         throw ConfigurationException("Session touch requires a StorageService.");
 #endif
@@ -533,6 +664,7 @@ void StoredSession::addAttributes(const vector<Attribute*>& attributes)
     m_cache->m_log.debug("adding attributes to session (%s)", getID());
 
     int ver;
+    short attempts = 0;
     do {
         DDF attr;
         DDF attrs = m_obj["attributes"];
@@ -575,6 +707,10 @@ void StoredSession::addAttributes(const vector<Attribute*>& attributes)
         }
         else if (ver < 0) {
             // Out of sync.
+            if (++attempts > 10) {
+                m_cache->m_log.error("failed to update stored session, update attempts exceeded limit");
+                throw IOException("Unable to update stored session, exceeded retry limit.");
+            }
             m_cache->m_log.warn("storage service indicates the record is out of sync, updating with a fresh copy...");
             ver = m_cache->m_storage->readText(getID(), "session", &record, nullptr);
             if (!ver) {
@@ -660,12 +796,13 @@ void StoredSession::addAssertion(Assertion* assertion)
         throw IOException("Attempted to insert duplicate assertion ID into session.");
 
     int ver;
+    short attempts = 0;
     do {
         DDF token = DDF(nullptr).string(id.get());
         m_obj["assertions"].add(token);
 
         // Tentatively increment the version.
-        m_obj["version"].integer(m_obj["version"].integer()+1);
+        m_obj["version"].integer(m_obj["version"].integer() + 1);
 
         ostringstream str;
         str << m_obj;
@@ -676,7 +813,7 @@ void StoredSession::addAssertion(Assertion* assertion)
         }
         catch (std::exception&) {
             token.destroy();
-            m_obj["version"].integer(m_obj["version"].integer()-1);
+            m_obj["version"].integer(m_obj["version"].integer() - 1);
             m_cache->m_storage->deleteText(getID(), id.get());
             throw;
         }
@@ -693,6 +830,10 @@ void StoredSession::addAssertion(Assertion* assertion)
         }
         else if (ver < 0) {
             // Out of sync.
+            if (++attempts > 10) {
+                m_cache->m_log.error("failed to update stored session, update attempts exceeded limit");
+                throw IOException("Unable to update stored session, exceeded retry limit.");
+            }
             m_cache->m_log.warn("storage service indicates the record is out of sync, updating with a fresh copy...");
             ver = m_cache->m_storage->readText(getID(), "session", &record, nullptr);
             if (!ver) {
@@ -765,25 +906,30 @@ SessionCacheEx::~SessionCacheEx()
 }
 
 SSCache::SSCache(const DOMElement* e)
-    : m_log(Category::getInstance(SHIBSP_LOGCAT".SessionCache")), inproc(true),
+    : m_log(Category::getInstance(SHIBSP_LOGCAT ".SessionCache")), inproc(true),
 #ifndef SHIBSP_LITE
-      m_storage(nullptr), m_storage_lite(nullptr), m_cacheAssertions(true),
+      m_storage(nullptr), m_storage_lite(nullptr), m_cacheAssertions(true), m_reverseIndex(true),
 #endif
       m_root(e), m_inprocTimeout(900), m_cacheTimeout(0), m_cacheAllowance(0), shutdown(false)
 {
     SPConfig& conf = SPConfig::getConfig();
     inproc = conf.isEnabled(SPConfig::InProcess);
 
-    static const XMLCh cacheAllowance[] =   UNICODE_LITERAL_14(c,a,c,h,e,A,l,l,o,w,a,n,c,e);
-    static const XMLCh cacheAssertions[] =  UNICODE_LITERAL_15(c,a,c,h,e,A,s,s,e,r,t,i,o,n,s);
-    static const XMLCh cacheTimeout[] =     UNICODE_LITERAL_12(c,a,c,h,e,T,i,m,e,o,u,t);
-    static const XMLCh inprocTimeout[] =    UNICODE_LITERAL_13(i,n,p,r,o,c,T,i,m,e,o,u,t);
-    static const XMLCh inboundHeader[] =    UNICODE_LITERAL_13(i,n,b,o,u,n,d,H,e,a,d,e,r);
-    static const XMLCh outboundHeader[] =   UNICODE_LITERAL_14(o,u,t,b,o,u,n,d,H,e,a,d,e,r);
-    static const XMLCh _StorageService[] =  UNICODE_LITERAL_14(S,t,o,r,a,g,e,S,e,r,v,i,c,e);
-    static const XMLCh _StorageServiceLite[] = UNICODE_LITERAL_18(S,t,o,r,a,g,e,S,e,r,v,i,c,e,L,i,t,e);
-
-    m_cacheTimeout = XMLHelper::getAttrInt(e, 0, cacheTimeout);
+    static const XMLCh cacheAllowance[] =       UNICODE_LITERAL_14(c,a,c,h,e,A,l,l,o,w,a,n,c,e);
+    static const XMLCh cacheAssertions[] =      UNICODE_LITERAL_15(c,a,c,h,e,A,s,s,e,r,t,i,o,n,s);
+    static const XMLCh cacheTimeout[] =         UNICODE_LITERAL_12(c,a,c,h,e,T,i,m,e,o,u,t);
+    static const XMLCh excludeReverseIndex[] =  UNICODE_LITERAL_19(e,x,c,l,u,d,e,R,e,v,e,r,s,e,I,n,d,e,x);
+    static const XMLCh inprocTimeout[] =        UNICODE_LITERAL_13(i,n,p,r,o,c,T,i,m,e,o,u,t);
+    static const XMLCh inboundHeader[] =        UNICODE_LITERAL_13(i,n,b,o,u,n,d,H,e,a,d,e,r);
+    static const XMLCh maintainReverseIndex[] = UNICODE_LITERAL_20(m,a,i,n,t,a,i,n,R,e,v,e,r,s,e,I,n,d,e,x);
+    static const XMLCh outboundHeader[] =       UNICODE_LITERAL_14(o,u,t,b,o,u,n,d,H,e,a,d,e,r);
+    static const XMLCh _StorageService[] =      UNICODE_LITERAL_14(S,t,o,r,a,g,e,S,e,r,v,i,c,e);
+    static const XMLCh _StorageServiceLite[] =  UNICODE_LITERAL_18(S,t,o,r,a,g,e,S,e,r,v,i,c,e,L,i,t,e);
+
+    if (e && e->hasAttributeNS(nullptr, cacheTimeout)) {
+        m_log.warn("cacheTimeout property is deprecated in favor of cacheAllowance (see documentation)");
+        m_cacheTimeout = XMLHelper::getAttrInt(e, 0, cacheTimeout);
+    }
     m_cacheAllowance = XMLHelper::getAttrInt(e, 0, cacheAllowance);
     if (inproc)
         m_inprocTimeout = XMLHelper::getAttrInt(e, 900, inprocTimeout);
@@ -800,7 +946,7 @@ SSCache::SSCache(const DOMElement* e)
             if (m_storage)
                 m_log.info("bound to StorageService (%s)", ssid.c_str());
             else
-                m_log.warn("specified StorageService (%s) not found", ssid.c_str());
+                throw ConfigurationException("SessionCache unable to locate StorageService ($1), check configuration.", params(1, ssid.c_str()));
         }
         if (!m_storage) {
             m_storage = conf.getServiceProvider()->getStorageService(nullptr);
@@ -816,7 +962,7 @@ SSCache::SSCache(const DOMElement* e)
             if (m_storage_lite)
                 m_log.info("bound to 'lite' StorageService (%s)", ssid.c_str());
             else
-                m_log.warn("specified 'lite' StorageService (%s) not found", ssid.c_str());
+                throw ConfigurationException("SessionCache unable to locate 'lite' StorageService ($1), check configuration.", params(1, ssid.c_str()));
         }
         if (!m_storage_lite) {
             m_log.info("StorageService for 'lite' use not set, using standard StorageService");
@@ -824,6 +970,13 @@ SSCache::SSCache(const DOMElement* e)
         }
 
         m_cacheAssertions = XMLHelper::getAttrBool(e, true, cacheAssertions);
+        m_reverseIndex = XMLHelper::getAttrBool(e, true, maintainReverseIndex);
+        const XMLCh* excludedNames = e ? e->getAttributeNS(nullptr, excludeReverseIndex) : nullptr;
+        if (excludedNames && *excludedNames) {
+            XMLStringTokenizer toks(excludedNames);
+            while (toks.hasMoreTokens())
+                m_excludedNames.insert(toks.nextToken());
+        }
     }
 #endif
 
@@ -838,9 +991,9 @@ SSCache::SSCache(const DOMElement* e)
 #ifndef SHIBSP_LITE
     else {
         if (listener && conf.isEnabled(SPConfig::OutOfProcess)) {
-            listener->regListener("find::"STORAGESERVICE_SESSION_CACHE"::SessionCache",this);
-            listener->regListener("remove::"STORAGESERVICE_SESSION_CACHE"::SessionCache",this);
-            listener->regListener("touch::"STORAGESERVICE_SESSION_CACHE"::SessionCache",this);
+            listener->regListener("find::" STORAGESERVICE_SESSION_CACHE "::SessionCache",this);
+            listener->regListener("remove::" STORAGESERVICE_SESSION_CACHE "::SessionCache",this);
+            listener->regListener("touch::" STORAGESERVICE_SESSION_CACHE "::SessionCache",this);
         }
         else {
             m_log.info("no ListenerService available, cache remoting disabled");
@@ -866,9 +1019,9 @@ SSCache::~SSCache()
         SPConfig& conf = SPConfig::getConfig();
         ListenerService* listener=conf.getServiceProvider()->getListenerService(false);
         if (listener && conf.isEnabled(SPConfig::OutOfProcess)) {
-            listener->unregListener("find::"STORAGESERVICE_SESSION_CACHE"::SessionCache",this);
-            listener->unregListener("remove::"STORAGESERVICE_SESSION_CACHE"::SessionCache",this);
-            listener->unregListener("touch::"STORAGESERVICE_SESSION_CACHE"::SessionCache",this);
+            listener->unregListener("find::" STORAGESERVICE_SESSION_CACHE "::SessionCache",this);
+            listener->unregListener("remove::" STORAGESERVICE_SESSION_CACHE "::SessionCache",this);
+            listener->unregListener("touch::" STORAGESERVICE_SESSION_CACHE "::SessionCache",this);
         }
     }
 #endif
@@ -878,13 +1031,24 @@ SSCache::~SSCache()
 
 void SSCache::test()
 {
-    auto_ptr_char temp(SAMLConfig::getConfig().generateIdentifier());
+    XMLCh* wide = SAMLConfig::getConfig().generateIdentifier();
+    auto_ptr_char temp(wide);
+    XMLString::release(&wide);
     m_storage->createString("SessionCacheTest", temp.get(), "Test", time(nullptr) + 60);
     m_storage->deleteString("SessionCacheTest", temp.get());
 }
 
-void SSCache::insert(const char* key, time_t expires, const char* name, const char* index)
+void SSCache::insert(const char* key, time_t expires, const char* name, const char* index, short attempts)
 {
+    if (attempts > 10) {
+        throw IOException("Exceeded retry limit.");
+    }
+
+    if (!name || !*name) {
+        m_log.warn("NameID value was empty or null, ignoring request to store for logout");
+        return;
+    }
+
     string dup;
     unsigned int storageLimit = m_storage_lite->getCapabilities().getKeySize();
     if (strlen(name) > storageLimit) {
@@ -926,12 +1090,12 @@ void SSCache::insert(const char* key, time_t expires, const char* name, const ch
         ver = m_storage_lite->updateText("NameID", name, out.str().c_str(), max(expires, recordexp), ver);
         if (ver <= 0) {
             // Out of sync, or went missing, so retry.
-            return insert(key, expires, name, index);
+            return insert(key, expires, name, index, attempts + 1);
         }
     }
     else if (!m_storage_lite->createText("NameID", name, out.str().c_str(), expires)) {
         // Hit a dup, so just retry, hopefully hitting the other branch.
-        return insert(key, expires, name, index);
+        return insert(key, expires, name, index, attempts + 1);
     }
 }
 
@@ -1015,7 +1179,12 @@ void SSCache::insert(
     strftime(timebuf,32,"%Y-%m-%dT%H:%M:%SZ",ptime);
     obj.addmember("expires").string(timebuf);
 
-    obj.addmember("client_addr").string(httpRequest.getRemoteAddr().c_str());
+    string caddr(httpRequest.getRemoteAddr());
+    if (!caddr.empty()) {
+        DDF addrobj = obj.addmember("client_addr").structure();
+        addrobj.addmember(getAddressFamily(caddr.c_str())).string(caddr.c_str());
+    }
+
     if (issuer)
         obj.addmember("entity_id").string(entity_id.get());
     if (protocol) {
@@ -1070,12 +1239,14 @@ void SSCache::insert(
         throw FatalProfileException("Attempted to create a session with a duplicate key.");
 
     // Store the reverse mapping for logout.
-    try {
-        if (nameid)
+    if (name.get() && *name.get() && m_reverseIndex
+            && (m_excludedNames.size() == 0 || m_excludedNames.count(nameid->getName()) == 0)) {
+        try {
             insert(key.get(), expires, name.get(), index.get());
-    }
-    catch (std::exception& ex) {
-        m_log.error("error storing back mapping of NameID for logout: %s", ex.what());
+        }
+        catch (std::exception& ex) {
+            m_log.error("error storing back mapping of NameID for logout: %s", ex.what());
+        }
     }
 
     if (tokens && m_cacheAssertions) {
@@ -1149,13 +1320,14 @@ bool SSCache::matches(
     return false;
 }
 
-vector<string>::size_type SSCache::logout(
+vector<string>::size_type SSCache::_logout(
     const Application& app,
     const saml2md::EntityDescriptor* issuer,
     const saml2::NameID& nameid,
     const set<string>* indexes,
     time_t expires,
-    vector<string>& sessionsKilled
+    vector<string>& sessionsKilled,
+    short attempts
     )
 {
 #ifdef _DEBUG
@@ -1164,6 +1336,8 @@ vector<string>::size_type SSCache::logout(
 
     if (!m_storage)
         throw ConfigurationException("SessionCache logout requires a StorageService.");
+    else if (attempts > 10)
+        throw IOException("Exceeded retry limit.");
 
     auto_ptr_char entityID(issuer ? issuer->getEntityID() : nullptr);
     auto_ptr_char name(nameid.getName());
@@ -1219,18 +1393,23 @@ vector<string>::size_type SSCache::logout(
             ver = m_storage_lite->updateText("Logout", name.get(), lout.str().c_str(), max(expires, oldexp), ver);
             if (ver <= 0) {
                 // Out of sync, or went missing, so retry.
-                return logout(app, issuer, nameid, indexes, expires, sessionsKilled);
+                return _logout(app, issuer, nameid, indexes, expires, sessionsKilled, attempts + 1);
             }
         }
         else if (!m_storage_lite->createText("Logout", name.get(), lout.str().c_str(), expires)) {
             // Hit a dup, so just retry, hopefully hitting the other branch.
-            return logout(app, issuer, nameid, indexes, expires, sessionsKilled);
+            return _logout(app, issuer, nameid, indexes, expires, sessionsKilled, attempts + 1);
         }
 
         obj.destroy();
         record.erase();
     }
 
+    if (!m_reverseIndex) {
+        m_log.error("cannot support logout because maintainReverseIndex property is turned off");
+        throw ConfigurationException("Logout is unsupported by the session cache configuration.");
+    }
+
     // Read in potentially matching sessions.
     ver = m_storage_lite->readText("NameID", name.get(), &record);
     if (ver == 0) {
@@ -1275,9 +1454,11 @@ vector<string>::size_type SSCache::logout(
                     }
                 }
                 else {
-                    // Session's gone, so...
-                    sessionsKilled.push_back(key.string());
-                    key.destroy();
+                    // Session may already be gone, or it may be associated with a different application.
+                    // To be conservative, we'll leave it alone. This isn't really increasing our security
+                    // risk, because if we can't lookup the session, it's unlikely the calling logout code
+                    // can either, so there's no chance of removing the session anyway.
+                    m_log.warn("session (%s) not accessible for logout, may be gone, or associated with a different application", key.string());
                 }
                 key = sessions.next();
             }
@@ -1397,7 +1578,7 @@ Session* SSCache::find(const Application& app, const char* key, const char* clie
         if (!SPConfig::getConfig().isEnabled(SPConfig::OutOfProcess)) {
             m_log.debug("session not found locally, remoting the search");
             // Remote the request.
-            DDF in("find::"STORAGESERVICE_SESSION_CACHE"::SessionCache"), out;
+            DDF in("find::" STORAGESERVICE_SESSION_CACHE "::SessionCache"), out;
             DDFJanitor jin(in);
             in.structure();
             in.addmember("key").string(key);
@@ -1513,7 +1694,7 @@ Session* SSCache::find(const Application& app, const char* key, const char* clie
     }
 
     if (!XMLString::equals(session->getApplicationID(), app.getId())) {
-        m_log.error("an application (%s) tried to access another application's session", app.getId());
+        m_log.warn("an application (%s) tried to access another application's session", app.getId());
         session->unlock();
         return nullptr;
     }
@@ -1619,7 +1800,7 @@ void SSCache::remove(const Application& app, const char* key)
     }
     else {
         // Remote the request.
-        DDF in("remove::"STORAGESERVICE_SESSION_CACHE"::SessionCache");
+        DDF in("remove::" STORAGESERVICE_SESSION_CACHE "::SessionCache");
         DDFJanitor jin(in);
         in.structure();
         in.addmember("key").string(key);
@@ -1747,7 +1928,7 @@ void SSCache::receive(DDF& in, ostream& out)
     if (!app)
         throw ListenerException("Application not found, check configuration?");
 
-    if (!strcmp(in.name(),"find::"STORAGESERVICE_SESSION_CACHE"::SessionCache")) {
+    if (!strcmp(in.name(),"find::" STORAGESERVICE_SESSION_CACHE "::SessionCache")) {
         const char* key=in["key"].string();
         if (!key)
             throw ListenerException("Required parameters missing for session lookup.");
@@ -1756,6 +1937,7 @@ void SSCache::receive(DDF& in, ostream& out)
         string record;
         time_t lastAccess;
         if (!m_storage->readText(key, "session", &record, &lastAccess)) {
+            m_log.debug("session not found in cache (%s)", key);
             DDF ret(nullptr);
             DDFJanitor jan(ret);
             out << ret;
@@ -1799,18 +1981,19 @@ void SSCache::receive(DDF& in, ostream& out)
         // Send the record back.
         out << record;
     }
-    else if (!strcmp(in.name(),"touch::"STORAGESERVICE_SESSION_CACHE"::SessionCache")) {
+    else if (!strcmp(in.name(),"touch::" STORAGESERVICE_SESSION_CACHE "::SessionCache")) {
         const char* key=in["key"].string();
         if (!key)
             throw ListenerException("Required parameters missing for session check.");
+        const char* client_addr = in["client_addr"].string();
 
-        // Do a versioned read.
+        // Do a read. May be unversioned if we need to bind a new client address.
         string record;
         time_t lastAccess;
         int curver = in["version"].integer();
-        int ver = m_storage->readText(key, "session", &record, &lastAccess, curver);
+        int ver = m_storage->readText(key, "session", &record, &lastAccess, client_addr ? 0 : curver);
         if (ver == 0) {
-            m_log.warn("unsuccessful versioned read of session (ID: %s), caches out of sync?", key);
+            m_log.warn("unsuccessful read of session (ID: %s), caches out of sync?", key);
             throw RetryableProfileException("Your session has expired, and you must re-authenticate.");
         }
 
@@ -1841,6 +2024,65 @@ void SSCache::receive(DDF& in, ostream& out)
             m_log.error("failed to update session expiration: %s", ex.what());
         }
 
+        // We may need to write back a new address into the session using a versioned update loop.
+        if (client_addr) {
+            short attempts = 0;
+            m_log.info("binding session (%s) to new client address (%s)", key, client_addr);
+            do {
+                // We have to reconstitute the session object ourselves.
+                DDF sessionobj;
+                DDFJanitor sessionjan(sessionobj);
+                istringstream src(record);
+                src >> sessionobj;
+                ver = sessionobj["version"].integer();
+                const char* saddr = sessionobj["client_addr"][getAddressFamily(client_addr)].string();
+                if (saddr) {
+                    // Something snuck in and bound the session to this address type, so it better match what we have.
+                    if (!XMLString::equals(saddr, client_addr)) {
+                        m_log.warn("client address mismatch, client (%s), session (%s)", client_addr, saddr);
+                        throw RetryableProfileException(
+                            "Your IP address ($1) does not match the address recorded at the time the session was established.",
+                            params(1, client_addr)
+                            );
+                    }
+                    break;  // No need to update.
+                }
+                else {
+                    // Bind it into the session.
+                    sessionobj["client_addr"].addmember(getAddressFamily(client_addr)).string(client_addr);
+                }
+
+                // Tentatively increment the version.
+                sessionobj["version"].integer(sessionobj["version"].integer() + 1);
+
+                ostringstream str;
+                str << sessionobj;
+                record = str.str();
+
+                ver = m_storage->updateText(key, "session", record.c_str(), 0, ver);
+                if (!ver) {
+                    // Fatal problem with update.
+                    m_log.error("updateText failed on StorageService for session (%s)", key);
+                    throw IOException("Unable to update stored session.");
+                }
+                if (ver < 0) {
+                    // Out of sync.
+                    if (++attempts > 10) {
+                        m_log.error("failed to bind client address, update attempts exceeded limit");
+                        throw IOException("Unable to update stored session, exceeded retry limit.");
+                    }
+                    m_log.warn("storage service indicates the record is out of sync, updating with a fresh copy...");
+                    sessionobj["version"].integer(sessionobj["version"].integer() - 1);
+                    ver = m_storage->readText(key, "session", &record);
+                    if (!ver) {
+                        m_log.error("readText failed on StorageService for session (%s)", key);
+                        throw IOException("Unable to read back stored session.");
+                    }
+                    ver = -1;
+                }
+            } while (ver < 0); // negative indicates a sync issue so we retry
+        }
+
         if (ver > curver) {
             // Send the record back.
             out << record;
@@ -1851,7 +2093,7 @@ void SSCache::receive(DDF& in, ostream& out)
             out << ret;
         }
     }
-    else if (!strcmp(in.name(),"remove::"STORAGESERVICE_SESSION_CACHE"::SessionCache")) {
+    else if (!strcmp(in.name(),"remove::" STORAGESERVICE_SESSION_CACHE "::SessionCache")) {
         const char* key=in["key"].string();
         if (!key)
             throw ListenerException("Required parameter missing for session removal.");