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 * StorageServiceSessionCache.cpp
20 * StorageService-based SessionCache implementation.
22 * Instead of optimizing this plugin with a buffering scheme that keeps objects around
23 * and avoids extra parsing steps, I'm assuming that systems that require such can
24 * layer their own cache plugin on top of this version either by delegating to it
25 * or using the remoting support. So this version will load sessions directly
26 * from the StorageService, instantiate enough to expose the Session API,
27 * and then delete everything when they're unlocked. All data in memory is always
28 * kept in sync with the StorageService (no lazy updates).
32 #include "Application.h"
33 #include "exceptions.h"
34 #include "ServiceProvider.h"
35 #include "SessionCache.h"
36 #include "TransactionLog.h"
37 #include "attribute/Attribute.h"
38 #include "remoting/ListenerService.h"
39 #include "util/SPConstants.h"
41 #include <xmltooling/util/NDC.h>
42 #include <xmltooling/util/XMLHelper.h>
43 #include <xercesc/util/XMLUniDefs.hpp>
46 # include <saml/SAMLConfig.h>
47 # include <xmltooling/util/StorageService.h>
48 using namespace opensaml::saml2md;
51 # include <xmltooling/util/DateTime.h>
54 using namespace shibsp;
55 using namespace opensaml;
56 using namespace xmltooling;
62 class SSCache : public SessionCache
64 ,public virtual Remoted
68 SSCache(const DOMElement* e);
72 void receive(DDF& in, ostream& out);
76 const Application& application,
77 const char* client_addr=NULL,
78 const saml2md::EntityDescriptor* issuer=NULL,
79 const XMLCh* protocol=NULL,
80 const saml2::NameID* nameid=NULL,
81 const XMLCh* authn_instant=NULL,
82 const XMLCh* session_index=NULL,
83 const XMLCh* authncontext_class=NULL,
84 const XMLCh* authncontext_decl=NULL,
85 const vector<const Assertion*>* tokens=NULL,
86 const vector<Attribute*>* attributes=NULL
88 vector<string>::size_type logout(
89 const saml2md::EntityDescriptor* issuer,
90 const saml2::NameID& nameid,
91 const set<string>* indexes,
93 const Application& application,
94 vector<string>& sessions
98 const saml2md::EntityDescriptor* issuer,
99 const saml2::NameID& nameid,
100 const set<string>* indexes,
101 const Application& application
104 Session* find(const char* key, const Application& application, const char* client_addr=NULL, time_t* timeout=NULL);
105 void remove(const char* key, const Application& application);
112 unsigned long m_cacheTimeout;
114 StorageService* m_storage;
119 // maintain back-mappings of NameID/SessionIndex -> session key
120 void insert(const char* key, time_t expires, const char* name, const char* index);
121 bool stronglyMatches(const XMLCh* idp, const XMLCh* sp, const saml2::NameID& n1, const saml2::NameID& n2) const;
124 const DOMElement* m_root; // Only valid during initialization
125 unsigned long m_inprocTimeout;
127 // inproc means we buffer sessions in memory
129 map<string,StoredSession*> m_hashtable;
131 // management of buffered sessions
132 void dormant(const char* key);
133 static void* cleanup_fn(void*);
135 CondWait* shutdown_wait;
136 Thread* cleanup_thread;
139 class StoredSession : public virtual Session
142 StoredSession(SSCache* cache, DDF& obj) : m_obj(obj),
146 m_cache(cache), m_expires(0), m_lastAccess(time(NULL)), m_lock(NULL) {
147 auto_ptr_XMLCh exp(m_obj["expires"].string());
149 DateTime iso(exp.get());
151 m_expires = iso.getEpoch();
155 const char* nameid = obj["nameid"].string();
157 // Parse and bind the document into an XMLObject.
158 istringstream instr(nameid);
159 DOMDocument* doc = XMLToolingConfig::getConfig().getParser().parse(instr);
160 XercesJanitor<DOMDocument> janitor(doc);
161 auto_ptr<saml2::NameID> n(saml2::NameIDBuilder::buildNameID());
162 n->unmarshall(doc->getDocumentElement(), true);
164 m_nameid = n.release();
168 m_lock = Mutex::create();
174 for_each(m_attributes.begin(), m_attributes.end(), xmltooling::cleanup<Attribute>());
177 for_each(m_tokens.begin(), m_tokens.end(), cleanup_pair<string,Assertion>());
193 const char* getID() const {
196 const char* getApplicationID() const {
197 return m_obj["application_id"].string();
199 const char* getClientAddress() const {
200 return m_obj["client_addr"].string();
202 const char* getEntityID() const {
203 return m_obj["entity_id"].string();
205 const char* getProtocol() const {
206 return m_obj["protocol"].string();
208 const char* getAuthnInstant() const {
209 return m_obj["authn_instant"].string();
212 const saml2::NameID* getNameID() const {
216 const char* getSessionIndex() const {
217 return m_obj["session_index"].string();
219 const char* getAuthnContextClassRef() const {
220 return m_obj["authncontext_class"].string();
222 const char* getAuthnContextDeclRef() const {
223 return m_obj["authncontext_decl"].string();
225 const vector<Attribute*>& getAttributes() const {
226 if (m_attributes.empty())
227 unmarshallAttributes();
230 const multimap<string,const Attribute*>& getIndexedAttributes() const {
231 if (m_attributeIndex.empty()) {
232 if (m_attributes.empty())
233 unmarshallAttributes();
234 for (vector<Attribute*>::const_iterator a = m_attributes.begin(); a != m_attributes.end(); ++a) {
235 const vector<string>& aliases = (*a)->getAliases();
236 for (vector<string>::const_iterator alias = aliases.begin(); alias != aliases.end(); ++alias)
237 m_attributeIndex.insert(multimap<string,const Attribute*>::value_type(*alias, *a));
240 return m_attributeIndex;
242 const vector<const char*>& getAssertionIDs() const {
244 DDF ids = m_obj["assertions"];
245 DDF id = ids.first();
246 while (id.isstring()) {
247 m_ids.push_back(id.string());
254 void validate(const Application& application, const char* client_addr, time_t* timeout);
257 void addAttributes(const vector<Attribute*>& attributes);
258 const Assertion* getAssertion(const char* id) const;
259 void addAssertion(Assertion* assertion);
262 time_t getExpiration() const { return m_expires; }
263 time_t getLastAccess() const { return m_lastAccess; }
266 void unmarshallAttributes() const;
270 saml2::NameID* m_nameid;
271 mutable map<string,Assertion*> m_tokens;
273 mutable vector<Attribute*> m_attributes;
274 mutable multimap<string,const Attribute*> m_attributeIndex;
275 mutable vector<const char*> m_ids;
278 time_t m_expires,m_lastAccess;
282 SessionCache* SHIBSP_DLLLOCAL StorageServiceCacheFactory(const DOMElement* const & e)
284 return new SSCache(e);
288 void SHIBSP_API shibsp::registerSessionCaches()
\r
290 SPConfig::getConfig().SessionCacheManager.registerFactory(STORAGESERVICE_SESSION_CACHE, StorageServiceCacheFactory);
\r
293 void StoredSession::unmarshallAttributes() const
295 Attribute* attribute;
296 DDF attrs = m_obj["attributes"];
297 DDF attr = attrs.first();
298 while (!attr.isnull()) {
300 attribute = Attribute::unmarshall(attr);
301 m_attributes.push_back(attribute);
302 if (m_cache->m_log.isDebugEnabled())
303 m_cache->m_log.debug("unmarshalled attribute (ID: %s) with %d value%s",
304 attribute->getId(), attr.first().integer(), attr.first().integer()!=1 ? "s" : "");
306 catch (AttributeException& ex) {
307 const char* id = attr.first().name();
308 m_cache->m_log.error("error unmarshalling attribute (ID: %s): %s", id ? id : "none", ex.what());
314 void StoredSession::validate(const Application& application, const char* client_addr, time_t* timeout)
316 time_t now = time(NULL);
320 if (now > m_expires) {
321 m_cache->m_log.info("session expired (ID: %s)", getID());
322 throw RetryableProfileException("Your session has expired, and you must re-authenticate.");
328 if (m_cache->m_log.isDebugEnabled())
329 m_cache->m_log.debug("comparing client address %s against %s", client_addr, getClientAddress());
330 if (!XMLString::equals(getClientAddress(),client_addr)) {
331 m_cache->m_log.warn("client address mismatch");
332 throw RetryableProfileException(
333 "Your IP address ($1) does not match the address recorded at the time the session was established.",
334 params(1,client_addr)
342 if (!SPConfig::getConfig().isEnabled(SPConfig::OutOfProcess)) {
343 DDF in("touch::"STORAGESERVICE_SESSION_CACHE"::SessionCache"), out;
346 in.addmember("key").string(getID());
347 in.addmember("version").integer(m_obj["version"].integer());
349 // On 64-bit Windows, time_t doesn't fit in a long, so I'm using ISO timestamps.
350 #ifndef HAVE_GMTIME_R
351 struct tm* ptime=gmtime(timeout);
354 struct tm* ptime=gmtime_r(timeout,&res);
357 strftime(timebuf,32,"%Y-%m-%dT%H:%M:%SZ",ptime);
358 in.addmember("timeout").string(timebuf);
362 out=application.getServiceProvider().getListenerService()->send(in);
369 if (out.isstruct()) {
370 // We got an updated record back.
372 for_each(m_attributes.begin(), m_attributes.end(), xmltooling::cleanup<Attribute>());
373 m_attributes.clear();
374 m_attributeIndex.clear();
381 if (!m_cache->m_storage)
382 throw ConfigurationException("Session touch requires a StorageService.");
384 // Do a versioned read.
387 int curver = m_obj["version"].integer();
388 int ver = m_cache->m_storage->readText(getID(), "session", &record, &lastAccess, curver);
390 m_cache->m_log.warn("unsuccessful versioned read of session (ID: %s), cache out of sync?", getID());
391 throw RetryableProfileException("Your session has expired, and you must re-authenticate.");
394 // Adjust for expiration to recover last access time and check timeout.
395 lastAccess -= m_cache->m_cacheTimeout;
396 if (*timeout > 0 && now - lastAccess >= *timeout) {
397 m_cache->m_log.info("session timed out (ID: %s)", getID());
398 throw RetryableProfileException("Your session has expired, and you must re-authenticate.");
401 // Update storage expiration, if possible.
403 m_cache->m_storage->updateContext(getID(), now + m_cache->m_cacheTimeout);
405 catch (exception& ex) {
406 m_cache->m_log.error("failed to update session expiration: %s", ex.what());
410 // We got an updated record back.
412 istringstream in(record);
415 for_each(m_attributes.begin(), m_attributes.end(), xmltooling::cleanup<Attribute>());
416 m_attributes.clear();
417 m_attributeIndex.clear();
422 throw ConfigurationException("Session touch requires a StorageService.");
431 void StoredSession::addAttributes(const vector<Attribute*>& attributes)
434 xmltooling::NDC ndc("addAttributes");
437 if (!m_cache->m_storage)
438 throw ConfigurationException("Session modification requires a StorageService.");
440 m_cache->m_log.debug("adding attributes to session (%s)", getID());
445 DDF attrs = m_obj["attributes"];
447 attrs = m_obj.addmember("attributes").list();
448 for (vector<Attribute*>::const_iterator a=attributes.begin(); a!=attributes.end(); ++a) {
449 attr = (*a)->marshall();
453 // Tentatively increment the version.
454 m_obj["version"].integer(m_obj["version"].integer()+1);
458 string record(str.str());
461 ver = m_cache->m_storage->updateText(getID(), "session", record.c_str(), 0, m_obj["version"].integer()-1);
464 // Roll back modification to record.
465 m_obj["version"].integer(m_obj["version"].integer()-1);
466 vector<Attribute*>::size_type count = attributes.size();
468 attrs.last().destroy();
473 // Roll back modification to record.
474 m_obj["version"].integer(m_obj["version"].integer()-1);
475 vector<Attribute*>::size_type count = attributes.size();
477 attrs.last().destroy();
480 // Fatal problem with update.
481 throw IOException("Unable to update stored session.");
485 m_cache->m_log.warn("storage service indicates the record is out of sync, updating with a fresh copy...");
486 ver = m_cache->m_storage->readText(getID(), "session", &record, NULL);
488 m_cache->m_log.error("readText failed on StorageService for session (%s)", getID());
489 throw IOException("Unable to read back stored session.");
494 istringstream in(record);
498 for_each(m_attributes.begin(), m_attributes.end(), xmltooling::cleanup<Attribute>());
499 m_attributes.clear();
500 m_attributeIndex.clear();
501 newobj["version"].integer(ver);
507 } while (ver < 0); // negative indicates a sync issue so we retry
509 TransactionLog* xlog = SPConfig::getConfig().getServiceProvider()->getTransactionLog();
511 xlog->log.infoStream() <<
512 "Added the following attributes to session (ID: " <<
514 ") for (applicationId: " <<
515 m_obj["application_id"].string() <<
517 for (vector<Attribute*>::const_iterator a=attributes.begin(); a!=attributes.end(); ++a)
518 xlog->log.infoStream() << "\t" << (*a)->getId() << " (" << (*a)->valueCount() << " values)";
521 // We own them now, so clean them up.
522 for_each(attributes.begin(), attributes.end(), xmltooling::cleanup<Attribute>());
525 const Assertion* StoredSession::getAssertion(const char* id) const
527 if (!m_cache->m_storage)
528 throw ConfigurationException("Assertion retrieval requires a StorageService.");
530 map<string,Assertion*>::const_iterator i = m_tokens.find(id);
531 if (i!=m_tokens.end())
535 if (!m_cache->m_storage->readText(getID(), id, &tokenstr, NULL))
536 throw FatalProfileException("Assertion not found in cache.");
538 // Parse and bind the document into an XMLObject.
539 istringstream instr(tokenstr);
540 DOMDocument* doc = XMLToolingConfig::getConfig().getParser().parse(instr);
541 XercesJanitor<DOMDocument> janitor(doc);
542 auto_ptr<XMLObject> xmlObject(XMLObjectBuilder::buildOneFromElement(doc->getDocumentElement(), true));
545 Assertion* token = dynamic_cast<Assertion*>(xmlObject.get());
547 throw FatalProfileException("Request for cached assertion returned an unknown object type.");
549 // Transfer ownership to us.
555 void StoredSession::addAssertion(Assertion* assertion)
558 xmltooling::NDC ndc("addAssertion");
561 if (!m_cache->m_storage)
562 throw ConfigurationException("Session modification requires a StorageService.");
565 throw FatalProfileException("Unknown object type passed to session for storage.");
567 auto_ptr_char id(assertion->getID());
569 m_cache->m_log.debug("adding assertion (%s) to session (%s)", id.get(), getID());
572 if (!m_cache->m_storage->readText(getID(), "session", NULL, &exp))
573 throw IOException("Unable to load expiration time for stored session.");
575 ostringstream tokenstr;
576 tokenstr << *assertion;
577 if (!m_cache->m_storage->createText(getID(), id.get(), tokenstr.str().c_str(), exp))
578 throw IOException("Attempted to insert duplicate assertion ID into session.");
582 DDF token = DDF(NULL).string(id.get());
583 m_obj["assertions"].add(token);
585 // Tentatively increment the version.
586 m_obj["version"].integer(m_obj["version"].integer()+1);
590 string record(str.str());
593 ver = m_cache->m_storage->updateText(getID(), "session", record.c_str(), 0, m_obj["version"].integer()-1);
597 m_obj["version"].integer(m_obj["version"].integer()-1);
598 m_cache->m_storage->deleteText(getID(), id.get());
604 m_obj["version"].integer(m_obj["version"].integer()-1);
607 // Fatal problem with update.
608 m_cache->m_log.error("updateText failed on StorageService for session (%s)", getID());
609 m_cache->m_storage->deleteText(getID(), id.get());
610 throw IOException("Unable to update stored session.");
614 m_cache->m_log.warn("storage service indicates the record is out of sync, updating with a fresh copy...");
615 ver = m_cache->m_storage->readText(getID(), "session", &record, NULL);
617 m_cache->m_log.error("readText failed on StorageService for session (%s)", getID());
618 m_cache->m_storage->deleteText(getID(), id.get());
619 throw IOException("Unable to read back stored session.");
624 istringstream in(record);
628 for_each(m_attributes.begin(), m_attributes.end(), xmltooling::cleanup<Attribute>());
629 m_attributes.clear();
630 m_attributeIndex.clear();
631 newobj["version"].integer(ver);
637 } while (ver < 0); // negative indicates a sync issue so we retry
642 TransactionLog* xlog = SPConfig::getConfig().getServiceProvider()->getTransactionLog();
645 "Added assertion (ID: %s) to session for (applicationId: %s) with (ID: %s)",
646 id.get(), m_obj["application_id"].string(), getID()
652 SSCache::SSCache(const DOMElement* e)
653 : m_log(Category::getInstance(SHIBSP_LOGCAT".SessionCache")), inproc(true), m_cacheTimeout(3600),
657 m_root(e), m_inprocTimeout(900), m_lock(NULL), shutdown(false), shutdown_wait(NULL), cleanup_thread(NULL)
659 static const XMLCh cacheTimeout[] = UNICODE_LITERAL_12(c,a,c,h,e,T,i,m,e,o,u,t);
\r
660 static const XMLCh inprocTimeout[] = UNICODE_LITERAL_13(i,n,p,r,o,c,T,i,m,e,o,u,t);
\r
661 static const XMLCh _StorageService[] = UNICODE_LITERAL_14(S,t,o,r,a,g,e,S,e,r,v,i,c,e);
663 SPConfig& conf = SPConfig::getConfig();
664 inproc = conf.isEnabled(SPConfig::InProcess);
667 const XMLCh* tag=e->getAttributeNS(NULL,cacheTimeout);
\r
669 m_cacheTimeout = XMLString::parseInt(tag);
\r
670 if (!m_cacheTimeout)
\r
671 m_cacheTimeout=3600;
\r
674 const XMLCh* tag=e->getAttributeNS(NULL,inprocTimeout);
\r
676 m_inprocTimeout = XMLString::parseInt(tag);
\r
677 if (!m_inprocTimeout)
\r
678 m_inprocTimeout=900;
\r
684 if (conf.isEnabled(SPConfig::OutOfProcess)) {
685 const XMLCh* tag = e ? e->getAttributeNS(NULL,_StorageService) : NULL;
687 auto_ptr_char ssid(tag);
688 m_storage = conf.getServiceProvider()->getStorageService(ssid.get());
690 m_log.info("bound to StorageService (%s)", ssid.get());
693 throw ConfigurationException("SessionCache unable to locate StorageService, check configuration.");
697 ListenerService* listener=conf.getServiceProvider()->getListenerService(false);
699 if (!conf.isEnabled(SPConfig::OutOfProcess) && !listener)
700 throw ConfigurationException("SessionCache requires a ListenerService, but none available.");
701 m_lock = RWLock::create();
702 shutdown_wait = CondWait::create();
703 cleanup_thread = Thread::create(&cleanup_fn, (void*)this);
707 if (listener && conf.isEnabled(SPConfig::OutOfProcess)) {
708 listener->regListener("find::"STORAGESERVICE_SESSION_CACHE"::SessionCache",this);
709 listener->regListener("remove::"STORAGESERVICE_SESSION_CACHE"::SessionCache",this);
710 listener->regListener("touch::"STORAGESERVICE_SESSION_CACHE"::SessionCache",this);
713 m_log.info("no ListenerService available, cache remoting disabled");
722 // Shut down the cleanup thread and let it know...
724 shutdown_wait->signal();
725 cleanup_thread->join(NULL);
727 for_each(m_hashtable.begin(),m_hashtable.end(),cleanup_pair<string,StoredSession>());
729 delete shutdown_wait;
733 SPConfig& conf = SPConfig::getConfig();
734 ListenerService* listener=conf.getServiceProvider()->getListenerService(false);
735 if (listener && conf.isEnabled(SPConfig::OutOfProcess)) {
736 listener->unregListener("find::"STORAGESERVICE_SESSION_CACHE"::SessionCache",this);
737 listener->unregListener("remove::"STORAGESERVICE_SESSION_CACHE"::SessionCache",this);
738 listener->unregListener("touch::"STORAGESERVICE_SESSION_CACHE"::SessionCache",this);
748 auto_ptr_char temp(SAMLConfig::getConfig().generateIdentifier());
749 m_storage->createString("SessionCacheTest", temp.get(), "Test", time(NULL) + 60);
750 m_storage->deleteString("SessionCacheTest", temp.get());
753 void SSCache::insert(const char* key, time_t expires, const char* name, const char* index)
756 if (strlen(name) > 255) {
757 dup = string(name).substr(0,255);
762 DDFJanitor jobj(obj);
764 // Since we can't guarantee uniqueness, check for an existing record.
767 int ver = m_storage->readText("NameID", name, &record, &recordexp);
769 // Existing record, so we need to unmarshall it.
770 istringstream in(record);
778 if (!index || !*index)
780 DDF sessions = obj.addmember(index);
781 if (!sessions.islist())
783 DDF session = DDF(NULL).string(key);
784 sessions.add(session);
786 // Remarshall the record.
790 // Try and store it back...
792 ver = m_storage->updateText("NameID", name, out.str().c_str(), max(expires, recordexp), ver);
794 // Out of sync, or went missing, so retry.
795 return insert(key, expires, name, index);
798 else if (!m_storage->createText("NameID", name, out.str().c_str(), expires)) {
799 // Hit a dup, so just retry, hopefully hitting the other branch.
800 return insert(key, expires, name, index);
804 string SSCache::insert(
806 const Application& application,
807 const char* client_addr,
808 const saml2md::EntityDescriptor* issuer,
809 const XMLCh* protocol,
810 const saml2::NameID* nameid,
811 const XMLCh* authn_instant,
812 const XMLCh* session_index,
813 const XMLCh* authncontext_class,
814 const XMLCh* authncontext_decl,
815 const vector<const Assertion*>* tokens,
816 const vector<Attribute*>* attributes
820 xmltooling::NDC ndc("insert");
823 throw ConfigurationException("SessionCache insertion requires a StorageService.");
825 m_log.debug("creating new session");
827 time_t now = time(NULL);
828 auto_ptr_char index(session_index);
829 auto_ptr_char entity_id(issuer ? issuer->getEntityID() : NULL);
830 auto_ptr_char name(nameid ? nameid->getName() : NULL);
833 // Check for a pending logout.
834 if (strlen(name.get()) > 255)
835 const_cast<char*>(name.get())[255] = 0;
837 int ver = m_storage->readText("Logout", name.get(), &pending);
840 DDFJanitor jpend(pendobj);
841 istringstream pstr(pending);
843 // IdP.SP.index contains logout expiration, if any.
844 DDF deadmenwalking = pendobj[issuer ? entity_id.get() : "_shibnull"][application.getString("entityID").second];
845 const char* logexpstr = deadmenwalking[session_index ? index.get() : "_shibnull"].string();
846 if (!logexpstr && session_index) // we tried an exact session match, now try for NULL
847 logexpstr = deadmenwalking["_shibnull"].string();
849 auto_ptr_XMLCh dt(logexpstr);
850 DateTime dtobj(dt.get());
851 dtobj.parseDateTime();
852 time_t logexp = dtobj.getEpoch();
853 if (now - XMLToolingConfig::getConfig().clock_skew_secs < logexp)
854 throw FatalProfileException("A logout message from your identity provider has blocked your login attempt.");
859 auto_ptr_char key(SAMLConfig::getConfig().generateIdentifier());
861 // Store session properties in DDF.
862 DDF obj = DDF(key.get()).structure();
863 obj.addmember("version").integer(1);
864 obj.addmember("application_id").string(application.getId());
866 // On 64-bit Windows, time_t doesn't fit in a long, so I'm using ISO timestamps.
867 #ifndef HAVE_GMTIME_R
868 struct tm* ptime=gmtime(&expires);
871 struct tm* ptime=gmtime_r(&expires,&res);
874 strftime(timebuf,32,"%Y-%m-%dT%H:%M:%SZ",ptime);
875 obj.addmember("expires").string(timebuf);
878 obj.addmember("client_addr").string(client_addr);
880 obj.addmember("entity_id").string(entity_id.get());
882 auto_ptr_char prot(protocol);
883 obj.addmember("protocol").string(prot.get());
886 auto_ptr_char instant(authn_instant);
887 obj.addmember("authn_instant").string(instant.get());
890 obj.addmember("session_index").string(index.get());
891 if (authncontext_class) {
892 auto_ptr_char ac(authncontext_class);
893 obj.addmember("authncontext_class").string(ac.get());
895 if (authncontext_decl) {
896 auto_ptr_char ad(authncontext_decl);
897 obj.addmember("authncontext_decl").string(ad.get());
901 ostringstream namestr;
903 obj.addmember("nameid").string(namestr.str().c_str());
907 obj.addmember("assertions").list();
908 for (vector<const Assertion*>::const_iterator t = tokens->begin(); t!=tokens->end(); ++t) {
909 auto_ptr_char tokenid((*t)->getID());
910 DDF tokid = DDF(NULL).string(tokenid.get());
911 obj["assertions"].add(tokid);
917 DDF attrlist = obj.addmember("attributes").list();
918 for (vector<Attribute*>::const_iterator a=attributes->begin(); a!=attributes->end(); ++a) {
919 attr = (*a)->marshall();
924 ostringstream record;
927 m_log.debug("storing new session...");
928 if (!m_storage->createText(key.get(), "session", record.str().c_str(), now + m_cacheTimeout))
929 throw FatalProfileException("Attempted to create a session with a duplicate key.");
931 // Store the reverse mapping for logout.
934 insert(key.get(), expires, name.get(), index.get());
936 catch (exception& ex) {
937 m_log.error("error storing back mapping of NameID for logout: %s", ex.what());
942 for (vector<const Assertion*>::const_iterator t = tokens->begin(); t!=tokens->end(); ++t) {
943 ostringstream tokenstr;
945 auto_ptr_char tokenid((*t)->getID());
946 if (!m_storage->createText(key.get(), tokenid.get(), tokenstr.str().c_str(), now + m_cacheTimeout))
947 throw IOException("duplicate assertion ID ($1)", params(1, tokenid.get()));
950 catch (exception& ex) {
951 m_log.error("error storing assertion along with session: %s", ex.what());
955 const char* pid = obj["entity_id"].string();
956 m_log.info("new session created: SessionID (%s) IdP (%s) Address (%s)", key.get(), pid ? pid : "none", client_addr);
958 // Transaction Logging
959 TransactionLog* xlog = application.getServiceProvider().getTransactionLog();
961 xlog->log.infoStream() <<
962 "New session (ID: " <<
964 ") with (applicationId: " <<
965 application.getId() <<
966 ") for principal from (IdP: " <<
967 (pid ? pid : "none") <<
968 ") at (ClientAddress: " <<
969 (client_addr ? client_addr : "none") <<
970 ") with (NameIdentifier: " <<
971 (nameid ? name.get() : "none") <<
975 xlog->log.infoStream() <<
976 "Cached the following attributes with session (ID: " <<
978 ") for (applicationId: " <<
979 application.getId() <<
981 for (vector<Attribute*>::const_iterator a=attributes->begin(); a!=attributes->end(); ++a)
982 xlog->log.infoStream() << "\t" << (*a)->getId() << " (" << (*a)->valueCount() << " values)";
989 bool SSCache::matches(
991 const saml2md::EntityDescriptor* issuer,
992 const saml2::NameID& nameid,
993 const set<string>* indexes,
994 const Application& application
997 auto_ptr_char entityID(issuer ? issuer->getEntityID() : NULL);
999 Session* session = find(key, application);
1001 Locker locker(session, false);
1002 if (XMLString::equals(session->getEntityID(), entityID.get()) && session->getNameID() &&
1003 stronglyMatches(issuer->getEntityID(), application.getXMLString("entityID").second, nameid, *session->getNameID())) {
1004 return (!indexes || indexes->empty() || (session->getSessionIndex() ? (indexes->count(session->getSessionIndex())>0) : false));
1008 catch (exception& ex) {
1009 m_log.error("error while matching session (%s): %s", key, ex.what());
1014 vector<string>::size_type SSCache::logout(
1015 const saml2md::EntityDescriptor* issuer,
1016 const saml2::NameID& nameid,
1017 const set<string>* indexes,
1019 const Application& application,
1020 vector<string>& sessionsKilled
1024 xmltooling::NDC ndc("logout");
1028 throw ConfigurationException("SessionCache insertion requires a StorageService.");
1030 auto_ptr_char entityID(issuer ? issuer->getEntityID() : NULL);
1031 auto_ptr_char name(nameid.getName());
1033 m_log.info("request to logout sessions from (%s) for (%s)", entityID.get() ? entityID.get() : "unknown", name.get());
1035 if (strlen(name.get()) > 255)
1036 const_cast<char*>(name.get())[255] = 0;
1039 DDFJanitor jobj(obj);
1044 // Record the logout to prevent post-delivered assertions.
1045 // On 64-bit Windows, time_t doesn't fit in a long, so I'm using ISO timestamps.
1046 #ifndef HAVE_GMTIME_R
1047 struct tm* ptime=gmtime(&expires);
1050 struct tm* ptime=gmtime_r(&expires,&res);
1053 strftime(timebuf,32,"%Y-%m-%dT%H:%M:%SZ",ptime);
1056 ver = m_storage->readText("Logout", name.get(), &record, &oldexp);
1058 istringstream lin(record);
1062 obj = DDF(NULL).structure();
1065 // Structure is keyed by the IdP and SP, with a member per session index containing the expiration.
1066 DDF root = obj.addmember(issuer ? entityID.get() : "_shibnull").addmember(application.getString("entityID").second);
1068 for (set<string>::const_iterator x = indexes->begin(); x!=indexes->end(); ++x)
1069 root.addmember(x->c_str()).string(timebuf);
1072 root.addmember("_shibnull").string(timebuf);
1080 ver = m_storage->updateText("Logout", name.get(), lout.str().c_str(), max(expires, oldexp), ver);
1082 // Out of sync, or went missing, so retry.
1083 return logout(issuer, nameid, indexes, expires, application, sessionsKilled);
1086 else if (!m_storage->createText("Logout", name.get(), lout.str().c_str(), expires)) {
1087 // Hit a dup, so just retry, hopefully hitting the other branch.
1088 return logout(issuer, nameid, indexes, expires, application, sessionsKilled);
1095 // Read in potentially matching sessions.
1096 ver = m_storage->readText("NameID", name.get(), &record);
1098 m_log.debug("no active sessions to logout for supplied issuer and subject");
1102 istringstream in(record);
1105 // The record contains child lists for each known session index.
1107 DDF sessions = obj.first();
1108 while (sessions.islist()) {
1109 if (!indexes || indexes->empty() || indexes->count(sessions.name())) {
1110 key = sessions.first();
1111 while (key.isstring()) {
1112 // Fetch the session for comparison.
1113 Session* session = NULL;
1115 session = find(key.string(), application);
1117 catch (exception& ex) {
1118 m_log.error("error locating session (%s): %s", key.string(), ex.what());
1122 Locker locker(session, false);
1124 if (XMLString::equals(session->getEntityID(), entityID.get())) {
1126 if (stronglyMatches(issuer->getEntityID(), application.getXMLString("entityID").second, nameid, *session->getNameID())) {
1127 sessionsKilled.push_back(key.string());
1131 m_log.debug("session (%s) contained a non-matching NameID, leaving it alone", key.string());
1135 m_log.debug("session (%s) established by different IdP, leaving it alone", key.string());
1139 // Session's gone, so...
1140 sessionsKilled.push_back(key.string());
1143 key = sessions.next();
1146 // No sessions left for this index?
1147 if (sessions.first().isnull())
1150 sessions = obj.next();
1153 if (obj.first().isnull())
1156 // If possible, write back the mapping record (this isn't crucial).
1159 m_storage->deleteText("NameID", name.get());
1161 else if (!sessionsKilled.empty()) {
1164 if (m_storage->updateText("NameID", name.get(), out.str().c_str(), 0, ver) <= 0)
1165 m_log.warn("logout mapping record changed behind us, leaving it alone");
1168 catch (exception& ex) {
1169 m_log.error("error updating logout mapping record: %s", ex.what());
1172 return sessionsKilled.size();
1175 bool SSCache::stronglyMatches(const XMLCh* idp, const XMLCh* sp, const saml2::NameID& n1, const saml2::NameID& n2) const
1177 if (!XMLString::equals(n1.getName(), n2.getName()))
1180 const XMLCh* s1 = n1.getFormat();
1181 const XMLCh* s2 = n2.getFormat();
1183 s1 = saml2::NameID::UNSPECIFIED;
1185 s2 = saml2::NameID::UNSPECIFIED;
1186 if (!XMLString::equals(s1,s2))
1189 s1 = n1.getNameQualifier();
1190 s2 = n2.getNameQualifier();
1195 if (!XMLString::equals(s1,s2))
1198 s1 = n1.getSPNameQualifier();
1199 s2 = n2.getSPNameQualifier();
1204 if (!XMLString::equals(s1,s2))
1212 Session* SSCache::find(const char* key, const Application& application, const char* client_addr, time_t* timeout)
1215 xmltooling::NDC ndc("find");
1217 StoredSession* session=NULL;
1220 m_log.debug("searching local cache for session (%s)", key);
1222 map<string,StoredSession*>::const_iterator i=m_hashtable.find(key);
1223 if (i!=m_hashtable.end()) {
1224 // Save off and lock the session.
1225 session = i->second;
1228 m_log.debug("session found locally, validating it for use");
1236 if (!SPConfig::getConfig().isEnabled(SPConfig::OutOfProcess)) {
1237 m_log.debug("session not found locally, remoting the search");
1238 // Remote the request.
1239 DDF in("find::"STORAGESERVICE_SESSION_CACHE"::SessionCache"), out;
1242 in.addmember("key").string(key);
1243 in.addmember("application_id").string(application.getId());
1244 if (timeout && *timeout) {
1245 // On 64-bit Windows, time_t doesn't fit in a long, so I'm using ISO timestamps.
1246 #ifndef HAVE_GMTIME_R
1247 struct tm* ptime=gmtime(timeout);
1250 struct tm* ptime=gmtime_r(timeout,&res);
1253 strftime(timebuf,32,"%Y-%m-%dT%H:%M:%SZ",ptime);
1254 in.addmember("timeout").string(timebuf);
1258 out=application.getServiceProvider().getListenerService()->send(in);
1259 if (!out.isstruct()) {
1261 m_log.debug("session not found in remote cache");
1265 // Wrap the results in a local entry and save it.
1266 session = new StoredSession(this, out);
1267 // The remote end has handled timeout issues, we handle address and expiration checks.
1276 // We're out of process, so we can search the storage service directly.
1279 throw ConfigurationException("SessionCache lookup requires a StorageService.");
1281 m_log.debug("searching for session (%s)", key);
1286 int ver = m_storage->readText(key, "session", &record, &lastAccess);
1290 m_log.debug("reconstituting session and checking validity");
1292 istringstream in(record);
1295 lastAccess -= m_cacheTimeout; // adjusts it back to the last time the record's timestamp was touched
1296 time_t now=time(NULL);
1298 if (timeout && *timeout > 0 && now - lastAccess >= *timeout) {
1299 m_log.info("session timed out (ID: %s)", key);
1300 remove(key, application);
1301 RetryableProfileException ex("Your session has expired, and you must re-authenticate.");
1302 const char* eid = obj["entity_id"].string();
1309 MetadataProvider* m=application.getMetadataProvider();
1311 annotateException(&ex,m->getEntityDescriptor(MetadataProvider::Criteria(eid2.c_str(),NULL,NULL,false)).first); // throws it
1315 // Update storage expiration, if possible.
1317 m_storage->updateContext(key, now + m_cacheTimeout);
1319 catch (exception& ex) {
1320 m_log.error("failed to update session expiration: %s", ex.what());
1324 // Wrap the results in a local entry and save it.
1325 session = new StoredSession(this, obj);
1326 // We handled timeout issues, still need to handle address and expiration checks.
1329 throw ConfigurationException("SessionCache search requires a StorageService.");
1334 // Lock for writing and repeat the search to avoid duplication.
1336 SharedLock shared(m_lock, false);
1337 if (m_hashtable.count(key)) {
1338 // We're using an existing session entry.
1340 session = m_hashtable[key];
1344 m_hashtable[key]=session;
1350 if (!XMLString::equals(session->getApplicationID(), application.getId())) {
1351 m_log.error("an application (%s) tried to access another application's session", application.getId());
1356 // Verify currency and update the timestamp if indicated by caller.
1358 session->validate(application, client_addr, timeout);
1362 remove(key, application);
1369 void SSCache::remove(const char* key, const Application& application)
1372 xmltooling::NDC ndc("remove");
1374 // Take care of local copy.
1378 if (SPConfig::getConfig().isEnabled(SPConfig::OutOfProcess)) {
1379 // Remove the session from storage directly.
1381 m_storage->deleteContext(key);
1382 m_log.info("removed session (%s)", key);
1384 TransactionLog* xlog = application.getServiceProvider().getTransactionLog();
1385 Locker locker(xlog);
1386 xlog->log.info("Destroyed session (applicationId: %s) (ID: %s)", application.getId(), key);
1388 throw ConfigurationException("SessionCache removal requires a StorageService.");
1392 // Remote the request.
1393 DDF in("remove::"STORAGESERVICE_SESSION_CACHE"::SessionCache");
1396 in.addmember("key").string(key);
1397 in.addmember("application_id").string(application.getId());
1399 DDF out = application.getServiceProvider().getListenerService()->send(in);
1404 void SSCache::dormant(const char* key)
1407 xmltooling::NDC ndc("dormant");
1410 m_log.debug("deleting local copy of session (%s)", key);
1412 // lock the cache for writing, which means we know nobody is sitting in find()
1415 // grab the entry from the table
1416 map<string,StoredSession*>::const_iterator i=m_hashtable.find(key);
1417 if (i==m_hashtable.end()) {
1422 // ok, remove the entry and lock it
1423 StoredSession* entry=i->second;
1424 m_hashtable.erase(key);
1430 // we can release the cache entry lock because we know we're not in the cache anymore
1436 void SSCache::cleanup()
1439 xmltooling::NDC ndc("cleanup");
1442 Mutex* mutex = Mutex::create();
1444 // Load our configuration details...
1445 static const XMLCh cleanupInterval[] = UNICODE_LITERAL_15(c,l,e,a,n,u,p,I,n,t,e,r,v,a,l);
1446 const XMLCh* tag=m_root ? m_root->getAttributeNS(NULL,cleanupInterval) : NULL;
1447 int rerun_timer = 900;
1449 rerun_timer = XMLString::parseInt(tag);
1450 if (rerun_timer <= 0)
1455 m_log.info("cleanup thread started...run every %d secs; timeout after %d secs", rerun_timer, m_inprocTimeout);
1458 shutdown_wait->timedwait(mutex,rerun_timer);
1462 // Ok, let's run through the cleanup process and clean out
1463 // really old sessions. This is a two-pass process. The
1464 // first pass is done holding a read-lock while we iterate over
1465 // the cache. The second pass doesn't need a lock because
1466 // the 'deletes' will lock the cache.
1468 // Pass 1: iterate over the map and find all entries that have not been
1469 // used in the allotted timeout.
1470 vector<string> stale_keys;
1471 time_t stale = time(NULL) - m_inprocTimeout;
1473 m_log.debug("cleanup thread running");
1476 for (map<string,StoredSession*>::const_iterator i=m_hashtable.begin(); i!=m_hashtable.end(); ++i) {
1477 // If the last access was BEFORE the stale timeout...
1479 time_t last=i->second->getLastAccess();
1480 i->second->unlock();
1482 stale_keys.push_back(i->first);
1486 if (!stale_keys.empty()) {
1487 m_log.info("purging %d old sessions", stale_keys.size());
1489 // Pass 2: walk through the list of stale entries and remove them from the cache
1490 for (vector<string>::const_iterator j = stale_keys.begin(); j != stale_keys.end(); ++j)
1491 dormant(j->c_str());
1494 m_log.debug("cleanup thread completed");
1497 m_log.info("cleanup thread exiting");
1504 void* SSCache::cleanup_fn(void* cache_p)
1507 // First, let's block all signals
1508 Thread::mask_all_signals();
1511 // Now run the cleanup process.
1512 reinterpret_cast<SSCache*>(cache_p)->cleanup();
1518 void SSCache::receive(DDF& in, ostream& out)
1521 xmltooling::NDC ndc("receive");
1524 if (!strcmp(in.name(),"find::"STORAGESERVICE_SESSION_CACHE"::SessionCache")) {
1525 const char* key=in["key"].string();
1527 throw ListenerException("Required parameters missing for session lookup.");
1529 const Application* app = SPConfig::getConfig().getServiceProvider()->getApplication(in["application_id"].string());
1531 throw ListenerException("Application not found, check configuration?");
1533 // Do an unversioned read.
1536 if (!m_storage->readText(key, "session", &record, &lastAccess)) {
1538 DDFJanitor jan(ret);
1543 // Adjust for expiration to recover last access time and check timeout.
1544 lastAccess -= m_cacheTimeout;
1545 time_t now=time(NULL);
1547 // See if we need to check for a timeout.
1548 if (in["timeout"].string()) {
1550 auto_ptr_XMLCh dt(in["timeout"].string());
1551 DateTime dtobj(dt.get());
1552 dtobj.parseDateTime();
1553 timeout = dtobj.getEpoch();
1555 if (timeout > 0 && now - lastAccess >= timeout) {
1556 m_log.info("session timed out (ID: %s)", key);
1558 throw RetryableProfileException("Your session has expired, and you must re-authenticate.");
1561 // Update storage expiration, if possible.
1563 m_storage->updateContext(key, now + m_cacheTimeout);
1565 catch (exception& ex) {
1566 m_log.error("failed to update session expiration: %s", ex.what());
1570 // Send the record back.
1573 else if (!strcmp(in.name(),"touch::"STORAGESERVICE_SESSION_CACHE"::SessionCache")) {
1574 const char* key=in["key"].string();
1576 throw ListenerException("Required parameters missing for session check.");
1578 // Do a versioned read.
1581 int curver = in["version"].integer();
1582 int ver = m_storage->readText(key, "session", &record, &lastAccess, curver);
1584 m_log.warn("unsuccessful versioned read of session (ID: %s), caches out of sync?", key);
1585 throw RetryableProfileException("Your session has expired, and you must re-authenticate.");
1588 // Adjust for expiration to recover last access time and check timeout.
1589 lastAccess -= m_cacheTimeout;
1590 time_t now=time(NULL);
1592 // See if we need to check for a timeout.
1594 auto_ptr_XMLCh dt(in["timeout"].string());
1596 DateTime dtobj(dt.get());
1597 dtobj.parseDateTime();
1598 timeout = dtobj.getEpoch();
1601 if (timeout > 0 && now - lastAccess >= timeout) {
1602 m_log.info("session timed out (ID: %s)", key);
1603 throw RetryableProfileException("Your session has expired, and you must re-authenticate.");
1606 // Update storage expiration, if possible.
1608 m_storage->updateContext(key, now + m_cacheTimeout);
1610 catch (exception& ex) {
1611 m_log.error("failed to update session expiration: %s", ex.what());
1615 // Send the record back.
1620 DDFJanitor jan(ret);
1624 else if (!strcmp(in.name(),"remove::"STORAGESERVICE_SESSION_CACHE"::SessionCache")) {
1625 const char* key=in["key"].string();
1627 throw ListenerException("Required parameter missing for session removal.");
1629 const Application* app = SPConfig::getConfig().getServiceProvider()->getApplication(in["application_id"].string());
1631 throw ConfigurationException("Application not found, check configuration?");
1635 DDFJanitor jan(ret);