1 /**
2  * Licensed to the University Corporation for Advanced Internet
3  * Development, Inc. (UCAID) under one or more contributor license
4  * agreements. See the NOTICE file distributed with this work for
5  * additional information regarding copyright ownership.
6  *
7  * UCAID licenses this file to you under the Apache License,
8  * Version 2.0 (the "License"); you may not use this file except
9  * in compliance with the License. You may obtain a copy of the
10  * License at
11  *
12  * http://www.apache.org/licenses/LICENSE-2.0
13  *
14  * Unless required by applicable law or agreed to in writing,
15  * software distributed under the License is distributed on an
16  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
17  * either express or implied. See the License for the specific
18  * language governing permissions and limitations under the License.
19  */
20 
21 /**
22  * StorageServiceSessionCache.cpp
23  *
24  * StorageService-based SessionCache implementation.
25  *
26  * Instead of optimizing this plugin with a buffering scheme that keeps objects around
27  * and avoids extra parsing steps, I'm assuming that systems that require such can
28  * layer their own cache plugin on top of this version either by delegating to it
29  * or using the remoting support. So this version will load sessions directly
30  * from the StorageService, instantiate enough to expose the Session API,
31  * and then delete everything when they're unlocked. All data in memory is always
32  * kept in sync with the StorageService (no lazy updates).
33  */
34 
35 #include "internal.h"
36 
37 #include "Application.h"
38 #include "exceptions.h"
39 #include "ServiceProvider.h"
40 #include "TransactionLog.h"
41 #include "attribute/Attribute.h"
42 #include "handler/RemotedHandler.h"
43 #include "impl/StoredSession.h"
44 #include "impl/StorageServiceSessionCache.h"
45 #include "util/IPRange.h"
46 #include "util/SPConstants.h"
47 
48 #include <algorithm>
49 #define BOOST_BIND_GLOBAL_PLACEHOLDERS
50 #include <boost/bind.hpp>
51 #include <xmltooling/io/HTTPRequest.h>
52 #include <xmltooling/io/HTTPResponse.h>
53 #include <xmltooling/security/DataSealer.h>
54 #include <xmltooling/util/NDC.h>
55 #include <xmltooling/util/Threads.h>
56 #include <xmltooling/util/URLEncoder.h>
57 #include <xmltooling/util/XMLHelper.h>
58 #include <xercesc/util/XMLStringTokenizer.hpp>
59 #include <xercesc/util/XMLUniDefs.hpp>
60 
61 #ifndef SHIBSP_LITE
62 # include <saml/exceptions.h>
63 # include <saml/SAMLConfig.h>
64 # include <saml/saml2/core/Assertions.h>
65 # include <saml/saml2/metadata/Metadata.h>
66 # include <xmltooling/XMLToolingConfig.h>
67 # include <xmltooling/util/ParserPool.h>
68 # include <xmltooling/util/StorageService.h>
69 using namespace opensaml::saml2md;
70 #else
71 # include <xercesc/util/XMLDateTime.hpp>
72 #endif
73 
74 using namespace shibsp;
75 using namespace opensaml;
76 using namespace xmltooling;
77 using namespace boost;
78 using namespace std;
79 
StorageServiceCacheFactory(const DOMElement * const & e,bool deprecationSupport)80 SessionCache* SHIBSP_DLLLOCAL StorageServiceCacheFactory(const DOMElement* const & e, bool deprecationSupport)
81 {
82     return new SSCache(e, deprecationSupport);
83 }
84 
registerSessionCaches()85 void SHIBSP_API shibsp::registerSessionCaches()
86 {
87     SPConfig::getConfig().SessionCacheManager.registerFactory(STORAGESERVICE_SESSION_CACHE, StorageServiceCacheFactory);
88 }
89 
SessionCache()90 SessionCache::SessionCache()
91 {
92 }
93 
~SessionCache()94 SessionCache::~SessionCache()
95 {
96 }
97 
SSCache(const DOMElement * e,bool deprecationSupport)98 SSCache::SSCache(const DOMElement* e, bool deprecationSupport)
99     :
100 #ifndef SHIBSP_LITE
101       m_storage(nullptr), m_storage_lite(nullptr), m_cacheAssertions(true), m_reverseIndex(true), m_softRevocation(true), m_reverseIndexMaxSize(0),
102 #endif
103       m_root(e), m_inprocTimeout(900), m_cacheTimeout(0), m_cacheAllowance(0),
104       m_log(Category::getInstance(SHIBSP_LOGCAT ".SessionCache")), inproc(true), shutdown(false)
105 {
106     SPConfig& conf = SPConfig::getConfig();
107     inproc = conf.isEnabled(SPConfig::InProcess);
108 
109     static const XMLCh cacheAllowance[] =       UNICODE_LITERAL_14(c,a,c,h,e,A,l,l,o,w,a,n,c,e);
110     static const XMLCh cacheAssertions[] =      UNICODE_LITERAL_15(c,a,c,h,e,A,s,s,e,r,t,i,o,n,s);
111     static const XMLCh cacheTimeout[] =         UNICODE_LITERAL_12(c,a,c,h,e,T,i,m,e,o,u,t);
112     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);
113     static const XMLCh persistedAttributes[] =  UNICODE_LITERAL_19(p,e,r,s,i,s,t,e,d,A,t,t,r,i,b,u,t,e,s);
114     static const XMLCh inprocTimeout[] =        UNICODE_LITERAL_13(i,n,p,r,o,c,T,i,m,e,o,u,t);
115     static const XMLCh inboundHeader[] =        UNICODE_LITERAL_13(i,n,b,o,u,n,d,H,e,a,d,e,r);
116     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);
117     static const XMLCh reverseIndexMaxSize[] =  UNICODE_LITERAL_19(r,e,v,e,r,s,e,I,n,d,e,x,M,a,x,S,i,z,e);
118     static const XMLCh softRevocation[] =       UNICODE_LITERAL_14(s,o,f,t,R,e,v,o,c,a,t,i,o,n);
119     static const XMLCh outboundHeader[] =       UNICODE_LITERAL_14(o,u,t,b,o,u,n,d,H,e,a,d,e,r);
120     static const XMLCh _StorageService[] =      UNICODE_LITERAL_14(S,t,o,r,a,g,e,S,e,r,v,i,c,e);
121     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);
122     static const XMLCh _unreliableNetworks[] =  UNICODE_LITERAL_18(u,n,r,e,l,i,a,b,l,e,N,e,t,w,o,r,k,s);
123 
124     if (e && e->hasAttributeNS(nullptr, cacheTimeout)) {
125         SPConfig::getConfig().deprecation().warn("cacheTimeout property replaced by cacheAllowance (see documentation)");
126         m_cacheTimeout = XMLHelper::getAttrInt(e, 0, cacheTimeout);
127     }
128     m_cacheAllowance = XMLHelper::getAttrInt(e, 0, cacheAllowance);
129     if (inproc)
130         m_inprocTimeout = XMLHelper::getAttrInt(e, 900, inprocTimeout);
131     m_inboundHeader = XMLHelper::getAttrString(e, nullptr, inboundHeader);
132     if (!m_inboundHeader.empty())
133         RemotedHandler::addRemotedHeader(m_inboundHeader.c_str());
134     m_outboundHeader = XMLHelper::getAttrString(e, nullptr, outboundHeader);
135 
136 #ifndef SHIBSP_LITE
137     if (conf.isEnabled(SPConfig::OutOfProcess)) {
138         string ssid(XMLHelper::getAttrString(e, nullptr, _StorageService));
139         if (!ssid.empty()) {
140             m_storage = conf.getServiceProvider()->getStorageService(ssid.c_str());
141             if (m_storage)
142                 m_log.info("bound to StorageService (%s)", ssid.c_str());
143             else
144                 throw ConfigurationException("SessionCache unable to locate StorageService ($1), check configuration.", params(1, ssid.c_str()));
145         }
146         if (!m_storage) {
147             m_storage = conf.getServiceProvider()->getStorageService(nullptr);
148             if (m_storage)
149                 m_log.info("bound to arbitrary StorageService");
150             else
151                 throw ConfigurationException("SessionCache unable to locate StorageService, check configuration.");
152         }
153 
154         ssid = XMLHelper::getAttrString(e, nullptr, _StorageServiceLite);
155         if (!ssid.empty()) {
156             m_storage_lite = conf.getServiceProvider()->getStorageService(ssid.c_str());
157             if (m_storage_lite)
158                 m_log.info("bound to 'lite' StorageService (%s)", ssid.c_str());
159             else
160                 throw ConfigurationException("SessionCache unable to locate 'lite' StorageService ($1), check configuration.", params(1, ssid.c_str()));
161         }
162         if (!m_storage_lite) {
163             m_log.info("StorageService for 'lite' use not set, using standard StorageService");
164             m_storage_lite = m_storage;
165         }
166 
167         m_softRevocation = XMLHelper::getAttrBool(e, true, softRevocation);
168         m_cacheAssertions = XMLHelper::getAttrBool(e, deprecationSupport, cacheAssertions);
169         m_reverseIndex = XMLHelper::getAttrBool(e, true, maintainReverseIndex);
170         m_reverseIndexMaxSize = XMLHelper::getAttrInt(e, 0, reverseIndexMaxSize);
171         const XMLCh* excludedNames = e ? e->getAttributeNS(nullptr, excludeReverseIndex) : nullptr;
172         if (excludedNames && *excludedNames) {
173             XMLStringTokenizer toks(excludedNames);
174             while (toks.hasMoreTokens())
175                 m_excludedNames.insert(toks.nextToken());
176         }
177 
178         const XMLCh* persistedAttributeIds = e ? e->getAttributeNS(nullptr, persistedAttributes) : nullptr;
179         if (persistedAttributeIds && *persistedAttributeIds) {
180             XMLStringTokenizer toks(persistedAttributeIds);
181             while (toks.hasMoreTokens()) {
182                 auto_ptr_char tok(toks.nextToken());
183                 m_persistedAttributeIds.insert(tok.get());
184             }
185         }
186 
187         if (!m_persistedAttributeIds.empty()) {
188 			if (XMLToolingConfig::getConfig().getDataSealer() == nullptr)
189 				throw ConfigurationException("Persisting sessions across nodes requires DataSealer component, check configuration");
190 			XMLToolingConfig::getConfig().getDataSealer()->wrap("testing", time(nullptr)); // should throw if no key is installed
191         }
192     }
193 #endif
194 
195     const XMLCh* unreliableNetworks = e ? e->getAttributeNS(nullptr, _unreliableNetworks) : nullptr;
196     if (unreliableNetworks && *unreliableNetworks) {
197         XMLStringTokenizer toks(unreliableNetworks);
198         while (toks.hasMoreTokens()) {
199             auto_ptr_char tok(toks.nextToken());
200             m_unreliableNetworks.push_back(IPRange::parseCIDRBlock(tok.get()));
201         }
202     }
203 
204     ListenerService* listener=conf.getServiceProvider()->getListenerService(false);
205     if (inproc) {
206         if (!conf.isEnabled(SPConfig::OutOfProcess) && !listener)
207             throw ConfigurationException("SessionCache requires a ListenerService, but none available.");
208         m_lock.reset(RWLock::create());
209         shutdown_wait.reset(CondWait::create());
210         cleanup_thread.reset(Thread::create(&cleanup_fn, this));
211     }
212 #ifndef SHIBSP_LITE
213     else {
214         if (listener && conf.isEnabled(SPConfig::OutOfProcess)) {
215             listener->regListener("find::" STORAGESERVICE_SESSION_CACHE "::SessionCache",this);
216             listener->regListener("recover::" STORAGESERVICE_SESSION_CACHE "::SessionCache", this);
217             listener->regListener("remove::" STORAGESERVICE_SESSION_CACHE "::SessionCache",this);
218             listener->regListener("touch::" STORAGESERVICE_SESSION_CACHE "::SessionCache",this);
219         }
220         else {
221             m_log.info("no ListenerService available, cache remoting disabled");
222         }
223     }
224 #endif
225 }
226 
~SSCache()227 SSCache::~SSCache()
228 {
229     if (inproc) {
230         // Shut down the cleanup thread and let it know...
231         shutdown = true;
232         if (shutdown_wait.get())
233             shutdown_wait->signal();
234         if (cleanup_thread.get())
235             cleanup_thread->join(nullptr);
236 
237         for_each(m_hashtable.begin(),m_hashtable.end(),cleanup_pair<string,StoredSession>());
238     }
239 #ifndef SHIBSP_LITE
240     else {
241         SPConfig& conf = SPConfig::getConfig();
242         ListenerService* listener=conf.getServiceProvider()->getListenerService(false);
243         if (listener && conf.isEnabled(SPConfig::OutOfProcess)) {
244             listener->unregListener("find::" STORAGESERVICE_SESSION_CACHE "::SessionCache",this);
245             listener->unregListener("recover::" STORAGESERVICE_SESSION_CACHE "::SessionCache", this);
246             listener->unregListener("remove::" STORAGESERVICE_SESSION_CACHE "::SessionCache",this);
247             listener->unregListener("touch::" STORAGESERVICE_SESSION_CACHE "::SessionCache",this);
248         }
249     }
250 #endif
251 }
252 
getCacheTimeout(const Application & app) const253 unsigned long SSCache::getCacheTimeout(const Application& app) const
254 {
255     // Computes offset for adjusting expiration of sessions.
256     // This can either be static, or dynamic based on the per-app session timeout or lifetime.
257     if (m_cacheTimeout)
258         return m_cacheTimeout;
259     pair<bool, unsigned int> timeout = pair<bool, unsigned int>(false, 3600);
260     const PropertySet* props = app.getPropertySet("Sessions");
261     if (props) {
262         timeout = props->getUnsignedInt("timeout");
263         if (!timeout.first)
264             timeout.second = 3600;
265     }
266     // As long as one of the two factors is set, add them together.
267     if (timeout.second > 0 || m_cacheAllowance > 0)
268         return timeout.second + m_cacheAllowance;
269 
270     // If timeouts are off, and there's no cache slop set, then use the lifetime.
271     timeout = pair<bool, unsigned int>(false, 28800);
272     if (props) {
273         timeout = props->getUnsignedInt("lifetime");
274         if (!timeout.first || timeout.second == 0)
275             timeout.second = 28800;
276     }
277     return timeout.second;
278 }
279 
active(const Application & app,const HTTPRequest & request)280 string SSCache::active(const Application& app, const HTTPRequest& request)
281 {
282     if (!m_inboundHeader.empty()) {
283         string session_id = request.getHeader(m_inboundHeader.c_str());
284         if (!session_id.empty())
285             return session_id;
286     }
287 
288     const char* session_id = request.getCookie(app.getCookieName("_shibsession_").c_str());
289     return (session_id ? session_id : "");
290 }
291 
compareAddresses(const char * client_addr,const char * session_addr) const292 bool SSCache::compareAddresses(const char* client_addr, const char* session_addr) const
293 {
294     if (XMLString::equals(client_addr, session_addr)) {
295         return true;
296     }
297 
298     for (vector<IPRange>::const_iterator i = m_unreliableNetworks.begin(); i != m_unreliableNetworks.end(); ++i) {
299         if (i->contains(client_addr) && i->contains(session_addr)) {
300             return true;
301         }
302     }
303 
304     return false;
305 }
306 
307 #ifndef SHIBSP_LITE
308 
test()309 void SSCache::test()
310 {
311     XMLCh* wide = SAMLConfig::getConfig().generateIdentifier();
312     auto_ptr_char temp(wide);
313     XMLString::release(&wide);
314     m_storage->createString("SessionCacheTest", temp.get(), "Test", time(nullptr) + 60);
315     m_storage->deleteString("SessionCacheTest", temp.get());
316 }
317 
insert(const char * key,time_t expires,const char * name,const char * index,short attempts)318 void SSCache::insert(const char* key, time_t expires, const char* name, const char* index, short attempts)
319 {
320 #ifdef _DEBUG
321     xmltooling::NDC ndc("insert");
322 #endif
323     if (attempts > 10) {
324         throw IOException("Exceeded retry limit.");
325     }
326 
327     if (!name || !*name) {
328         m_log.warn("NameID value was empty or null, ignoring request to store for logout");
329         return;
330     }
331 
332     string dup;
333     unsigned int storageLimit = m_storage_lite->getCapabilities().getKeySize();
334     if (strlen(name) > storageLimit) {
335         dup = string(name).substr(0, storageLimit);
336         name = dup.c_str();
337     }
338 
339     DDF obj;
340     DDFJanitor jobj(obj);
341 
342     // Since we can't guarantee uniqueness, check for an existing record.
343     string record;
344     time_t recordexp = 0;
345     int ver = m_storage_lite->readText("NameID", name, &record, &recordexp);
346     if (ver > 0) {
347         // Existing record, so we need to unmarshall it.
348         istringstream in(record);
349         in >> obj;
350     }
351     else {
352         // New record.
353         obj = DDF(nullptr).structure();
354     }
355 
356     if (!index || !*index)
357         index = "_shibnull";
358     DDF sessions = obj.addmember(index);
359     if (!sessions.isstruct())
360         sessions.structure();
361     else if (sessions.integer() == m_reverseIndexMaxSize)
362         sessions.first().destroy();
363     sessions.addmember(key);
364 
365     // Remarshall the record.
366     ostringstream out;
367     out << obj;
368 
369     // Try and store it back...
370     if (ver > 0) {
371         ver = m_storage_lite->updateText("NameID", name, out.str().c_str(), max(expires, recordexp), ver);
372         if (ver <= 0) {
373             // Out of sync, or went missing, so retry.
374             return insert(key, expires, name, index, attempts + 1);
375         }
376     }
377     else if (!m_storage_lite->createText("NameID", name, out.str().c_str(), expires)) {
378         // Hit a dup, so just retry, hopefully hitting the other branch.
379         return insert(key, expires, name, index, attempts + 1);
380     }
381 }
382 
insert(string & sessionID,const Application & app,const HTTPRequest & httpRequest,HTTPResponse & httpResponse,time_t expires,const saml2md::EntityDescriptor * issuer,const XMLCh * protocol,const saml2::NameID * nameid,const XMLCh * authn_instant,const XMLCh * session_index,const XMLCh * authncontext_class,const XMLCh * authncontext_decl,const vector<const Assertion * > * tokens,const vector<Attribute * > * attributes)383 void SSCache::insert(
384     string& sessionID,
385     const Application& app,
386     const HTTPRequest& httpRequest,
387     HTTPResponse& httpResponse,
388     time_t expires,
389     const saml2md::EntityDescriptor* issuer,
390     const XMLCh* protocol,
391     const saml2::NameID* nameid,
392     const XMLCh* authn_instant,
393     const XMLCh* session_index,
394     const XMLCh* authncontext_class,
395     const XMLCh* authncontext_decl,
396     const vector<const Assertion*>* tokens,
397     const vector<Attribute*>* attributes
398     )
399 {
400 #ifdef _DEBUG
401     xmltooling::NDC ndc("insert");
402 #endif
403     if (!m_storage)
404         throw ConfigurationException("SessionCache insertion requires a StorageService.");
405 
406     m_log.debug("creating new session");
407 
408     time_t now = time(nullptr);
409     auto_ptr_char index(session_index);
410     auto_ptr_char entity_id(issuer ? issuer->getEntityID() : nullptr);
411     auto_ptr_char name(nameid ? nameid->getName() : nullptr);
412 
413     if (name.get() && *name.get()) {
414         // Check for a pending logout.
415         unsigned int storageLimit = m_storage_lite->getCapabilities().getKeySize();
416         string namebuf = name.get();
417         if (namebuf.length() > storageLimit)
418             namebuf = namebuf.substr(0, storageLimit);
419         string pending;
420         int ver = m_storage_lite->readText("Logout", namebuf.c_str(), &pending);
421         if (ver > 0) {
422             DDF pendobj;
423             DDFJanitor jpend(pendobj);
424             istringstream pstr(pending);
425             pstr >> pendobj;
426             // IdP.SP.index contains logout expiration, if any.
427             DDF deadmenwalking = pendobj[issuer ? entity_id.get() : "_shibnull"][app.getRelyingParty(issuer)->getString("entityID").second];
428             const char* logexpstr = deadmenwalking[session_index ? index.get() : "_shibnull"].string();
429             if (!logexpstr && session_index)    // we tried an exact session match, now try for nullptr
430                 logexpstr = deadmenwalking["_shibnull"].string();
431             if (logexpstr) {
432                 auto_ptr_XMLCh dt(logexpstr);
433                 XMLDateTime dtobj(dt.get());
434                 dtobj.parseDateTime();
435                 time_t logexp = dtobj.getEpoch();
436                 if (now - XMLToolingConfig::getConfig().clock_skew_secs < logexp)
437                     throw FatalProfileException("A logout message from your identity provider has blocked your login attempt.");
438             }
439         }
440     }
441 
442     XMLCh* widekey = SAMLConfig::getConfig().generateIdentifier();
443     auto_ptr_char key(widekey);
444     XMLString::release(&widekey);
445 
446     // Store session properties in DDF.
447     DDF obj = DDF(key.get()).structure();
448     DDFJanitor entryobj(obj);
449     obj.addmember("version").integer(1);
450     obj.addmember("application_id").string(app.getId());
451 
452     // On 64-bit Windows, time_t doesn't fit in a long, so I'm using ISO timestamps.
453 #ifndef HAVE_GMTIME_R
454     struct tm* ptime=gmtime(&expires);
455 #else
456     struct tm res;
457     struct tm* ptime=gmtime_r(&expires,&res);
458 #endif
459     char timebuf[32];
460     strftime(timebuf,32,"%Y-%m-%dT%H:%M:%SZ",ptime);
461     obj.addmember("expires").string(timebuf);
462 
463     string caddr(httpRequest.getRemoteAddr());
464     if (!caddr.empty()) {
465         DDF addrobj = obj.addmember("client_addr").structure();
466         addrobj.addmember(StoredSession::getAddressFamily(caddr.c_str())).string(caddr.c_str());
467     }
468 
469     if (issuer)
470         obj.addmember("entity_id").string(entity_id.get());
471     if (protocol) {
472         auto_ptr_char prot(protocol);
473         obj.addmember("protocol").string(prot.get());
474     }
475     if (authn_instant) {
476         auto_ptr_char instant(authn_instant);
477         obj.addmember("authn_instant").string(instant.get());
478     }
479     if (session_index)
480         obj.addmember("session_index").string(index.get());
481     if (authncontext_class) {
482         auto_ptr_char ac(authncontext_class);
483         obj.addmember("authncontext_class").string(ac.get());
484     }
485     if (authncontext_decl) {
486         auto_ptr_char ad(authncontext_decl);
487         obj.addmember("authncontext_decl").string(ad.get());
488     }
489 
490     if (nameid) {
491         ostringstream namestr;
492         namestr << *nameid;
493         obj.addmember("nameid").string(namestr.str().c_str());
494     }
495 
496     if (tokens && m_cacheAssertions) {
497         obj.addmember("assertions").list();
498         for (vector<const Assertion*>::const_iterator t = tokens->begin(); t!=tokens->end(); ++t) {
499             auto_ptr_char tokenid((*t)->getID());
500             DDF tokid = DDF(nullptr).string(tokenid.get());
501             obj["assertions"].add(tokid);
502         }
503     }
504 
505     if (attributes) {
506         DDF attr;
507         DDF attrlist = obj.addmember("attributes").list();
508         for (vector<Attribute*>::const_iterator a=attributes->begin(); a!=attributes->end(); ++a) {
509             attr = (*a)->marshall();
510             attrlist.add(attr);
511         }
512     }
513 
514     ostringstream record;
515     record << obj;
516 
517     m_log.debug("storing new session...");
518     unsigned long cacheTimeout = getCacheTimeout(app);
519     if (!m_storage->createText(key.get(), "session", record.str().c_str(), now + cacheTimeout))
520         throw FatalProfileException("Attempted to create a session with a duplicate key.");
521 
522     // Store the reverse mapping for logout.
523     if (name.get() && *name.get() && m_reverseIndex
524             && (m_excludedNames.size() == 0 || m_excludedNames.count(nameid->getName()) == 0)) {
525         try {
526             insert(key.get(), expires, name.get(), index.get());
527         }
528         catch (const std::exception& ex) {
529             m_log.error("error storing back mapping of NameID for logout: %s", ex.what());
530         }
531     }
532 
533     if (tokens && m_cacheAssertions) {
534         try {
535             for (vector<const Assertion*>::const_iterator t = tokens->begin(); t!=tokens->end(); ++t) {
536                 ostringstream tokenstr;
537                 tokenstr << *(*t);
538                 auto_ptr_char tokenid((*t)->getID());
539                 if (!tokenid.get() || !*tokenid.get() || strlen(tokenid.get()) > m_storage->getCapabilities().getKeySize())
540                     throw IOException("Assertion ID is missing or exceeds key size of storage service.");
541                 else if (!m_storage->createText(key.get(), tokenid.get(), tokenstr.str().c_str(), now + cacheTimeout))
542                     throw IOException("Duplicate assertion ID ($1)", params(1, tokenid.get()));
543             }
544         }
545         catch (const std::exception& ex) {
546             m_log.error("error storing assertion along with session: %s", ex.what());
547         }
548     }
549 
550     const char* pid = obj["entity_id"].string();
551     const char* prot = obj["protocol"].string();
552     m_log.info("new session created: ID (%s) IdP (%s) Protocol(%s) Address (%s)",
553         key.get(), pid ? pid : "none", prot ? prot : "none", httpRequest.getRemoteAddr().c_str());
554 
555     if (!m_outboundHeader.empty())
556         httpResponse.setResponseHeader(m_outboundHeader.c_str(), key.get());
557 
558     time_t cookieLifetime = 0;
559     string shib_cookie = app.getCookieName("_shibsession_", &cookieLifetime);
560     HTTPResponse::samesite_t sameSitePolicy = getSameSitePolicy(app);
561     httpResponse.setCookie(shib_cookie.c_str(), key.get(), cookieLifetime, sameSitePolicy);
562     sessionID = key.get();
563 
564     // See if we need to persist the session data itself to a cookie for cross-node recovery.
565     if (!m_persistedAttributeIds.empty()) {
566         persist(app, httpResponse, obj, expires, sameSitePolicy);
567     }
568 }
569 
persist(const Application & app,HTTPResponse & httpResponse,DDF & session,time_t expires,HTTPResponse::samesite_t sameSitePolicy) const570 void SSCache::persist(
571     const Application& app,
572     HTTPResponse& httpResponse,
573     DDF& session,
574     time_t expires,
575     HTTPResponse::samesite_t sameSitePolicy
576     ) const
577 {
578 #ifdef _DEBUG
579     xmltooling::NDC ndc("persist");
580 #endif
581 
582     m_log.debug("checking if session (%s) should be persisted to cookie", session.name());
583 
584     // We don't save assertions...
585     session["assertions"].destroy();
586 
587     // Check each attribute.
588     DDF attrs = session["attributes"];
589     DDF attr = attrs.first();
590     while (!attr.isnull()) {
591         const char* aname = attr.first().name();
592         if (m_persistedAttributeIds.count(aname) == 0) {
593             m_log.debug("not persisting attribute for session recovery: %s", aname);
594             attr.destroy();
595         }
596         else {
597             m_log.debug("persisting attribute for session recovery: %s", aname);
598         }
599         attr = attrs.next();
600     }
601 
602     if (attrs.integer() == 0) {
603         m_log.info("session (%s) contained no attributes requiring persistence, will not be recoverable", session.name());
604         return;
605     }
606 
607     ostringstream persisted;
608     persisted << session;
609 
610     try {
611         string sealed = XMLToolingConfig::getConfig().getDataSealer()->wrap(persisted.str().c_str(), expires);
612         sealed = XMLToolingConfig::getConfig().getURLEncoder()->encode(sealed.c_str());
613 
614         time_t cookieLifetime;
615         string shib_cookie = app.getCookieName("_shibsealed_", &cookieLifetime);
616         httpResponse.setCookie(shib_cookie.c_str(), sealed.c_str(), cookieLifetime, sameSitePolicy);
617     }
618     catch (const std::exception& e) {
619         m_log.error("failed to wrap session (%s) with DataSealer: %s", session.name(), e.what());
620     }
621 }
622 
matches(const Application & app,xmltooling::HTTPRequest & request,const saml2md::EntityDescriptor * issuer,const saml2::NameID & nameid,const set<string> * indexes)623 bool SSCache::matches(
624     const Application& app,
625     xmltooling::HTTPRequest& request,
626     const saml2md::EntityDescriptor* issuer,
627     const saml2::NameID& nameid,
628     const set<string>* indexes
629     )
630 {
631     auto_ptr_char entityID(issuer ? issuer->getEntityID() : nullptr);
632     try {
633         Session* session = find(app, request);
634         if (session) {
635             Locker locker(session, false);
636             if (XMLString::equals(session->getEntityID(), entityID.get()) && session->getNameID() &&
637                     stronglyMatches(issuer->getEntityID(), app.getRelyingParty(issuer)->getXMLString("entityID").second, nameid, *session->getNameID())) {
638                 return (!indexes || indexes->empty() || (session->getSessionIndex() ? (indexes->count(session->getSessionIndex())>0) : false));
639             }
640         }
641     }
642     catch (const std::exception& ex) {
643         m_log.error("error while matching session: %s", ex.what());
644     }
645     return false;
646 }
647 
_logout(const Application & app,const saml2md::EntityDescriptor * issuer,const saml2::NameID & nameid,const set<string> * indexes,time_t expires,vector<string> & sessionsKilled,short attempts)648 vector<string>::size_type SSCache::_logout(
649     const Application& app,
650     const saml2md::EntityDescriptor* issuer,
651     const saml2::NameID& nameid,
652     const set<string>* indexes,
653     time_t expires,
654     vector<string>& sessionsKilled,
655     short attempts
656     )
657 {
658 #ifdef _DEBUG
659     xmltooling::NDC ndc("logout");
660 #endif
661 
662     if (!m_storage)
663         throw ConfigurationException("SessionCache logout requires a StorageService.");
664     else if (attempts > 10)
665         throw IOException("Exceeded retry limit.");
666 
667     auto_ptr_char entityID(issuer ? issuer->getEntityID() : nullptr);
668     auto_ptr_char name(nameid.getName());
669 
670     m_log.info("request to logout sessions from (%s) for (%s)", entityID.get() ? entityID.get() : "unknown", name.get());
671 
672     unsigned int storageLimit = m_storage_lite->getCapabilities().getKeySize();
673     if (strlen(name.get()) > storageLimit)
674         const_cast<char*>(name.get())[storageLimit] = 0;
675 
676     DDF obj;
677     DDFJanitor jobj(obj);
678     string record;
679     int ver;
680 
681     if (expires) {
682         // Record the logout to prevent post-delivered assertions.
683         // On 64-bit Windows, time_t doesn't fit in a long, so I'm using ISO timestamps.
684 #ifndef HAVE_GMTIME_R
685         struct tm* ptime=gmtime(&expires);
686 #else
687         struct tm res;
688         struct tm* ptime=gmtime_r(&expires,&res);
689 #endif
690         char timebuf[32];
691         strftime(timebuf,32,"%Y-%m-%dT%H:%M:%SZ",ptime);
692 
693         time_t oldexp = 0;
694         ver = m_storage_lite->readText("Logout", name.get(), &record, &oldexp);
695         if (ver > 0) {
696             istringstream lin(record);
697             lin >> obj;
698         }
699         else {
700             obj = DDF(nullptr).structure();
701         }
702 
703         // Structure is keyed by the IdP and SP, with a member per session index containing the expiration.
704         DDF root = obj.addmember(issuer ? entityID.get() : "_shibnull").addmember(app.getRelyingParty(issuer)->getString("entityID").second);
705         if (indexes) {
706             for (set<string>::const_iterator x = indexes->begin(); x!=indexes->end(); ++x)
707                 root.addmember(x->c_str()).string(timebuf);
708         }
709         else {
710             root.addmember("_shibnull").string(timebuf);
711         }
712 
713         // Write it back.
714         ostringstream lout;
715         lout << obj;
716 
717         if (ver > 0) {
718             ver = m_storage_lite->updateText("Logout", name.get(), lout.str().c_str(), max(expires, oldexp), ver);
719             if (ver <= 0) {
720                 // Out of sync, or went missing, so retry.
721                 return _logout(app, issuer, nameid, indexes, expires, sessionsKilled, attempts + 1);
722             }
723         }
724         else if (!m_storage_lite->createText("Logout", name.get(), lout.str().c_str(), expires)) {
725             // Hit a dup, so just retry, hopefully hitting the other branch.
726             return _logout(app, issuer, nameid, indexes, expires, sessionsKilled, attempts + 1);
727         }
728 
729         obj.destroy();
730         record.erase();
731     }
732 
733     if (!m_reverseIndex) {
734         m_log.error("cannot support logout because maintainReverseIndex property is turned off");
735         throw ConfigurationException("Logout is unsupported by the session cache configuration.");
736     }
737 
738     // Read in potentially matching sessions.
739     ver = m_storage_lite->readText("NameID", name.get(), &record);
740     if (ver == 0) {
741         m_log.debug("no active sessions to logout for supplied issuer and subject");
742         return 0;
743     }
744 
745     istringstream in(record);
746     in >> obj;
747 
748     // The record contains child structs for each known session index.
749     DDF key;
750     DDF sessions = obj.first();
751     while (sessions.isstruct()) {
752         if (!indexes || indexes->empty() || indexes->count(sessions.name())) {
753             key = sessions.first();
754             while (!key.isnull()) {
755                 // Fetch the session for comparison.
756                 Session* session = nullptr;
757                 try {
758                     session = find(app, key.name());
759                 }
760                 catch (const std::exception& ex) {
761                     m_log.error("error locating session (%s): %s", key.name(), ex.what());
762                 }
763 
764                 if (session) {
765                     Locker locker(session, false);
766                     // Same issuer?
767                     if (XMLString::equals(session->getEntityID(), entityID.get())) {
768                         // Same NameID?
769                         if (stronglyMatches(issuer->getEntityID(), app.getRelyingParty(issuer)->getXMLString("entityID").second, nameid, *session->getNameID())) {
770                             sessionsKilled.push_back(key.name());
771                             key.destroy();
772                         }
773                         else {
774                             m_log.debug("session (%s) contained a non-matching NameID, leaving it alone", key.name());
775                         }
776                     }
777                     else {
778                         m_log.debug("session (%s) established by different IdP, leaving it alone", key.name());
779                     }
780                 }
781                 else {
782                     // Session may already be gone, or it may be associated with a different application.
783                     // To be conservative, we'll leave it alone. This isn't really increasing our security
784                     // risk, because if we can't lookup the session, it's unlikely the calling logout code
785                     // can either, so there's no chance of removing the session anyway.
786                     m_log.warn("session (%s) not accessible for logout, may be gone, or associated with a different application", key.name());
787                 }
788                 key = sessions.next();
789             }
790 
791             // No sessions left for this index?
792             if (sessions.first().isnull())
793                 sessions.destroy();
794         }
795         sessions = obj.next();
796     }
797 
798     if (obj.first().isnull())
799         obj.destroy();
800 
801     // If possible, write back the mapping record (this isn't crucial).
802     try {
803         if (obj.isnull()) {
804             m_storage_lite->deleteText("NameID", name.get());
805         }
806         else if (!sessionsKilled.empty()) {
807             ostringstream out;
808             out << obj;
809             if (m_storage_lite->updateText("NameID", name.get(), out.str().c_str(), 0, ver) <= 0)
810                 m_log.warn("logout mapping record changed behind us, leaving it alone");
811         }
812     }
813     catch (const std::exception& ex) {
814         m_log.error("error updating logout mapping record: %s", ex.what());
815     }
816 
817     return sessionsKilled.size();
818 }
819 
stronglyMatches(const XMLCh * idp,const XMLCh * sp,const saml2::NameID & n1,const saml2::NameID & n2) const820 bool SSCache::stronglyMatches(const XMLCh* idp, const XMLCh* sp, const saml2::NameID& n1, const saml2::NameID& n2) const
821 {
822     if (!XMLString::equals(n1.getName(), n2.getName()))
823         return false;
824 
825     const XMLCh* s1 = n1.getFormat();
826     const XMLCh* s2 = n2.getFormat();
827     if (!s1 || !*s1)
828         s1 = saml2::NameID::UNSPECIFIED;
829     if (!s2 || !*s2)
830         s2 = saml2::NameID::UNSPECIFIED;
831     if (!XMLString::equals(s1,s2))
832         return false;
833 
834     s1 = n1.getNameQualifier();
835     s2 = n2.getNameQualifier();
836     if (!s1 || !*s1)
837         s1 = idp;
838     if (!s2 || !*s2)
839         s2 = idp;
840     if (!XMLString::equals(s1,s2))
841         return false;
842 
843     s1 = n1.getSPNameQualifier();
844     s2 = n2.getSPNameQualifier();
845     if (!s1 || !*s1)
846         s1 = sp;
847     if (!s2 || !*s2)
848         s2 = sp;
849     if (!XMLString::equals(s1,s2))
850         return false;
851 
852     return true;
853 }
854 
newLogoutEvent(const Application & app) const855 LogoutEvent* SSCache::newLogoutEvent(const Application& app) const
856 {
857     if (!SPConfig::getConfig().isEnabled(SPConfig::Logging))
858         return nullptr;
859     try {
860         auto_ptr<TransactionLog::Event> event(SPConfig::getConfig().EventManager.newPlugin(LOGOUT_EVENT, nullptr, false));
861         LogoutEvent* logout_event = dynamic_cast<LogoutEvent*>(event.get());
862         if (logout_event) {
863             logout_event->m_app = &app;
864             event.release();
865             return logout_event;
866         }
867         else {
868             m_log.warn("unable to audit event, log event object was of an incorrect type");
869         }
870     }
871     catch (const std::exception& ex) {
872         m_log.warn("exception auditing event: %s", ex.what());
873     }
874     return nullptr;
875 }
876 
877 #endif
878 
getSameSitePolicy(const Application & app) const879 HTTPResponse::samesite_t SSCache::getSameSitePolicy(const Application& app) const
880 {
881     const PropertySet* props = app.getPropertySet("Sessions");
882     if (props) {
883         pair<bool,const char*> sameSiteSession = props->getString("sameSiteSession");
884         if (sameSiteSession.first) {
885             if (!strcmp(sameSiteSession.second, "None")) {
886                 return HTTPResponse::SAMESITE_NONE;
887             }
888             else if (!strcmp(sameSiteSession.second, "Lax")) {
889                 return HTTPResponse::SAMESITE_LAX;
890             }
891             else if (!strcmp(sameSiteSession.second, "Strict")) {
892                 return HTTPResponse::SAMESITE_STRICT;
893             }
894         }
895     }
896     return HTTPResponse::SAMESITE_ABSENT;
897 }
898 
_find(const Application & app,const char * key,const char * recovery,const char * client_addr,time_t * timeout)899 Session* SSCache::_find(const Application& app, const char* key, const char* recovery, const char* client_addr, time_t* timeout)
900 {
901 #ifdef _DEBUG
902     xmltooling::NDC ndc("find");
903 #endif
904     StoredSession* session=nullptr;
905 
906     if (inproc) {
907         m_log.debug("searching local cache for session (%s)", key);
908         m_lock->rdlock();
909         map<string,StoredSession*>::const_iterator i=m_hashtable.find(key);
910         if (i!=m_hashtable.end()) {
911             // Save off and lock the session.
912             session = i->second;
913             session->lock();
914             m_lock->unlock();
915             m_log.debug("session found locally, validating it for use");
916         }
917         else {
918             m_lock->unlock();
919         }
920     }
921 
922     if (!session) {
923         if (!SPConfig::getConfig().isEnabled(SPConfig::OutOfProcess)) {
924             m_log.debug("session not found locally, remoting the search");
925             // Remote the request.
926             DDF in("find::" STORAGESERVICE_SESSION_CACHE "::SessionCache"), out;
927             DDFJanitor jin(in);
928             in.structure();
929             in.addmember("key").string(key);
930             in.addmember("sealed").string(recovery);
931             in.addmember("application_id").string(app.getId());
932             if (timeout && *timeout) {
933                 // On 64-bit Windows, time_t doesn't fit in a long, so I'm using ISO timestamps.
934 #ifndef HAVE_GMTIME_R
935                 struct tm* ptime=gmtime(timeout);
936 #else
937                 struct tm res;
938                 struct tm* ptime=gmtime_r(timeout,&res);
939 #endif
940                 char timebuf[32];
941                 strftime(timebuf,32,"%Y-%m-%dT%H:%M:%SZ",ptime);
942                 in.addmember("timeout").string(timebuf);
943             }
944 
945             try {
946                 out=app.getServiceProvider().getListenerService()->send(in);
947                 if (!out.isstruct()) {
948                     out.destroy();
949                     m_log.debug("session not found in remote cache");
950                     return nullptr;
951                 }
952 
953                 // Wrap the results in a local entry and save it.
954                 session = new StoredSession(this, out);
955                 // The remote end has handled timeout issues, we handle address and expiration checks.
956                 timeout = nullptr;
957             }
958             catch (...) {
959                 out.destroy();
960                 throw;
961             }
962         }
963         else {
964             // We're out of process, so we can search the storage service directly.
965 #ifndef SHIBSP_LITE
966             if (!m_storage)
967                 throw ConfigurationException("SessionCache lookup requires a StorageService.");
968 
969             m_log.debug("searching for session (%s)", key);
970 
971             DDF obj;
972             time_t lastAccess = 0;
973             string record;
974             int ver = m_storage->readText(key, "session", &record, &lastAccess);
975             if (!ver) {
976                 if (recovery && *recovery && recover(app, key, recovery)) {
977                     // Retry the read.
978                     ver = m_storage->readText(key, "session", &record, &lastAccess);
979                     if (!ver)
980                         m_log.warn("recovered session (%s) is missing from storage service", key);
981                 }
982                 if (!ver)
983                     return nullptr;
984             }
985 
986             if (0 == lastAccess) {
987                 m_log.error("session (ID: %s) did not report time of last access", key);
988                 throw RetryableProfileException("Your session's last access time was missing, and you must re-authenticate.");
989             }
990 
991             m_log.debug("reconstituting session and checking validity");
992 
993             istringstream in(record);
994             in >> obj;
995 
996             unsigned long cacheTimeout = getCacheTimeout(app);
997             lastAccess -= cacheTimeout;   // adjusts it back to the last time the record's timestamp was touched
998             time_t now=time(nullptr);
999 
1000             if (timeout && *timeout > 0 && now - lastAccess >= *timeout) {
1001                 m_log.info("session timed out (ID: %s)", key);
1002                 scoped_ptr<LogoutEvent> logout_event(newLogoutEvent(app));
1003                 if (logout_event.get()) {
1004                     logout_event->m_logoutType = LogoutEvent::LOGOUT_EVENT_INVALID;
1005                     logout_event->m_sessions.push_back(key);
1006                     app.getServiceProvider().getTransactionLog()->write(*logout_event);
1007                 }
1008                 remove(app, key);
1009                 const char* eid = obj["entity_id"].string();
1010                 if (!eid) {
1011                     obj.destroy();
1012                     throw RetryableProfileException("Your session has expired, and you must re-authenticate.");
1013                 }
1014                 string eid2(eid);
1015                 obj.destroy();
1016                 throw RetryableProfileException("Your session has timed out due to inactivity, and you must re-authenticate.",
1017                     namedparams(1, "entityID", eid2.c_str()));
1018             }
1019 
1020             if (timeout) {
1021                 // Update storage expiration, if possible.
1022                 try {
1023                     m_storage->updateContext(key, now + cacheTimeout);
1024                 }
1025                 catch (const std::exception& ex) {
1026                     m_log.error("failed to update session expiration: %s", ex.what());
1027                 }
1028             }
1029 
1030             // Wrap the results in a local entry and save it.
1031             session = new StoredSession(this, obj);
1032             // We handled timeout issues, still need to handle address and expiration checks.
1033             timeout = nullptr;
1034 #else
1035             throw ConfigurationException("SessionCache search requires a StorageService.");
1036 #endif
1037         }
1038 
1039         if (inproc) {
1040             // Lock for writing and repeat the search to avoid duplication.
1041             m_lock->wrlock();
1042             SharedLock shared(m_lock, false);
1043             if (m_hashtable.count(key)) {
1044                 // We're using an existing session entry.
1045                 delete session;
1046                 session = m_hashtable[key];
1047                 session->lock();
1048             }
1049             else {
1050                 m_hashtable[key]=session;
1051                 session->lock();
1052             }
1053         }
1054     }
1055 
1056     if (!XMLString::equals(session->getApplicationID(), app.getId())) {
1057         m_log.warn("an application (%s) tried to access another application's session", app.getId());
1058         session->unlock();
1059         return nullptr;
1060     }
1061 
1062     // Verify currency and update the timestamp if indicated by caller.
1063     try {
1064         session->validate(app, client_addr, timeout);
1065     }
1066     catch (...) {
1067 #ifndef SHIBSP_LITE
1068         scoped_ptr<LogoutEvent> logout_event(newLogoutEvent(app));
1069         if (logout_event.get()) {
1070             logout_event->m_logoutType = LogoutEvent::LOGOUT_EVENT_INVALID;
1071             logout_event->m_session = session;
1072             logout_event->m_sessions.push_back(session->getID());
1073             app.getServiceProvider().getTransactionLog()->write(*logout_event);
1074         }
1075 #endif
1076         session->unlock();
1077         remove(app, key);
1078         throw;
1079     }
1080 
1081     return session;
1082 }
1083 
find(const Application & app,HTTPRequest & request,const char * client_addr,time_t * timeout)1084 Session* SSCache::find(const Application& app, HTTPRequest& request, const char* client_addr, time_t* timeout)
1085 {
1086 #ifdef _DEBUG
1087     xmltooling::NDC ndc("find");
1088 #endif
1089     string id = active(app, request);
1090     if (id.empty())
1091         return nullptr;
1092 
1093     HTTPResponse::samesite_t sameSitePolicy = getSameSitePolicy(app);
1094     const char* c = request.getCookie(app.getCookieName("_shibsealed_").c_str());
1095 
1096     try {
1097         Session* session = _find(app, id.c_str(), c, client_addr, timeout);
1098         if (session)
1099             return session;
1100 
1101         HTTPResponse* response = dynamic_cast<HTTPResponse*>(&request);
1102         if (response) {
1103             if (!m_outboundHeader.empty())
1104                 response->setResponseHeader(m_outboundHeader.c_str(), nullptr);
1105             response->setCookie(app.getCookieName("_shibsession_").c_str(), nullptr, 0, sameSitePolicy);
1106             response->setCookie(app.getCookieName("_shibsealed_").c_str(), nullptr, 0, sameSitePolicy);
1107         }
1108     }
1109     catch (const std::exception&) {
1110         HTTPResponse* response = dynamic_cast<HTTPResponse*>(&request);
1111         if (response) {
1112             if (!m_outboundHeader.empty())
1113                 response->setResponseHeader(m_outboundHeader.c_str(), nullptr);
1114             response->setCookie(app.getCookieName("_shibsession_").c_str(), nullptr, 0, sameSitePolicy);
1115             response->setCookie(app.getCookieName("_shibsealed_").c_str(), nullptr, 0, sameSitePolicy);
1116         }
1117         throw;
1118     }
1119     return nullptr;
1120 }
1121 
recover(const Application & app,const char * key,const char * data)1122 bool SSCache::recover(const Application& app, const char* key, const char* data)
1123 {
1124 #ifdef _DEBUG
1125     xmltooling::NDC ndc("recover");
1126 #endif
1127 
1128     if (!SPConfig::getConfig().isEnabled(SPConfig::OutOfProcess)) {
1129         m_log.debug("remoting recovery of session from sealed cookie");
1130         // Remote the request.
1131         DDF in("recover::" STORAGESERVICE_SESSION_CACHE "::SessionCache"), out;
1132         DDFJanitor jin(in);
1133         in.structure();
1134         in.addmember("key").string(key);
1135         in.addmember("application_id").string(app.getId());
1136         in.addmember("sealed").string(data);
1137 
1138         out = app.getServiceProvider().getListenerService()->send(in);
1139         if (!out.isint() || out.integer() != 1) {
1140             out.destroy();
1141             m_log.debug("recovery of session (%s) failed", key);
1142             return false;
1143         }
1144 
1145         out.destroy();
1146         m_log.debug("session (%s) recovered from sealed cookie", key);
1147     }
1148     else {
1149         // We're out of process, so we can recover the session.
1150 #ifndef SHIBSP_LITE
1151         const DataSealer* sealer = XMLToolingConfig::getConfig().getDataSealer();
1152         if (!sealer) {
1153             m_log.warn("can't attempt recovery of session (%s), no DataSealer configured", key);
1154             return false;
1155         }
1156 
1157         m_log.debug("checking for revocation of session (%s)", key);
1158         try {
1159             if (m_storage_lite->readString("Revoked", key) > 0) {
1160                 m_log.warn("blocked recovery of revoked session (%s)", key);
1161                 return false;
1162             }
1163         }
1164         catch (const std::exception& ex) {
1165             if (m_softRevocation)
1166                 m_log.warn("ignoring failed check for revocation of session (%s): %s", ex.what());
1167             else {
1168                 m_log.warn("check for revocation of session (%s) failed, treating as revoked: %s", ex.what());
1169                 return false;
1170             }
1171         }
1172 
1173         m_log.debug("attempting recovery of session (%s)", key);
1174 
1175         DDF obj;
1176         DDFJanitor jobj(obj);
1177         string unwrapped;
1178 
1179         char* dup = nullptr;
1180         try {
1181             dup = strdup(data);
1182             XMLToolingConfig::getConfig().getURLEncoder()->decode(dup);
1183             unwrapped = sealer->unwrap(dup);
1184             free(dup);
1185 
1186             stringstream str(unwrapped);
1187             str >> obj;
1188         }
1189         catch (const std::exception& e) {
1190             if (dup)
1191                 free(dup);
1192             m_log.error("failed to unwrap sealed session data with DataSealer: %s", e.what());
1193             return false;
1194         }
1195 
1196         if (!obj.isstruct() || !obj.name() || strcmp(obj.name(), key)) {
1197             m_log.info("recovered session data was invalid for session (%s)", key);
1198             return false;
1199         }
1200 
1201         scoped_ptr<saml2::NameID> nameidObject;
1202         const char* nameid = obj["nameid"].string();
1203         if (nameid) {
1204             // Parse and bind the document into an XMLObject.
1205             istringstream instr(nameid);
1206             DOMDocument* doc = XMLToolingConfig::getConfig().getParser().parse(instr);
1207             XercesJanitor<DOMDocument> janitor(doc);
1208             nameidObject.reset(saml2::NameIDBuilder::buildNameID());
1209             nameidObject->unmarshall(doc->getDocumentElement(), true);
1210             janitor.release();
1211         }
1212 
1213         m_log.debug("storing recovered session (%s)...", key);
1214         time_t now = time(nullptr);
1215         if (!m_storage->createText(key, "session", unwrapped.c_str(), now + getCacheTimeout(app))) {
1216             m_log.debug("recovered session (%s) matched existing record, likely a race condition");
1217             return true;
1218         }
1219 
1220         // Store the reverse mapping for logout.
1221         auto_ptr_char name(nameidObject ? nameidObject->getName() : nullptr);
1222         if (name.get() && *name.get() && m_reverseIndex
1223             && (m_excludedNames.size() == 0 || m_excludedNames.count(nameidObject->getName()) == 0)) {
1224             try {
1225                 auto_ptr_XMLCh exp(obj["expires"].string());
1226                 if (exp.get()) {
1227                     XMLDateTime iso(exp.get());
1228                     iso.parseDateTime();
1229                     insert(key, iso.getEpoch(), name.get(), obj["session_index"].string());
1230                 }
1231             }
1232             catch (const std::exception& ex) {
1233                 m_log.error("error storing back mapping of NameID for logout: %s", ex.what());
1234             }
1235         }
1236 
1237         const char* pid = obj["entity_id"].string();
1238         const char* prot = obj["protocol"].string();
1239         m_log.info("session recovered: ID (%s) IdP (%s) Protocol(%s)",
1240             key, pid ? pid : "none", prot ? prot : "none");
1241 #else
1242         throw ConfigurationException("SessionCache recovery requires a DataSealer.");
1243 #endif
1244     }
1245 
1246     return true;
1247 }
1248 
remove(const Application & app,const HTTPRequest & request,HTTPResponse * response,time_t revocationExp)1249 void SSCache::remove(const Application& app, const HTTPRequest& request, HTTPResponse* response, time_t revocationExp)
1250 {
1251 #ifdef _DEBUG
1252     xmltooling::NDC ndc("remove");
1253 #endif
1254     string session_id;
1255     string shib_cookie = app.getCookieName("_shibsession_");
1256 
1257     if (!m_inboundHeader.empty())
1258         session_id = request.getHeader(m_inboundHeader.c_str());
1259     if (session_id.empty()) {
1260         const char* c = request.getCookie(shib_cookie.c_str());
1261         if (c && *c)
1262             session_id = c;
1263     }
1264 
1265     if (!session_id.empty()) {
1266         if (response) {
1267             if (!m_outboundHeader.empty())
1268                 response->setResponseHeader(m_outboundHeader.c_str(), nullptr);
1269             HTTPResponse::samesite_t sameSitePolicy = getSameSitePolicy(app);
1270             response->setCookie(shib_cookie.c_str(), nullptr, 0, sameSitePolicy);
1271             response->setCookie(app.getCookieName("_shibsealed_").c_str(), nullptr, 0, sameSitePolicy);
1272         }
1273         remove(app, session_id.c_str(), revocationExp);
1274     }
1275 }
1276 
remove(const Application & app,const char * key,time_t revocationExp)1277 void SSCache::remove(const Application& app, const char* key, time_t revocationExp)
1278 {
1279 #ifdef _DEBUG
1280     xmltooling::NDC ndc("remove");
1281 #endif
1282     // Take care of local copy.
1283     if (inproc)
1284         dormant(key);
1285 
1286     if (SPConfig::getConfig().isEnabled(SPConfig::OutOfProcess)) {
1287         // Remove the session from storage directly.
1288 #ifndef SHIBSP_LITE
1289         m_storage->deleteContext(key);
1290         m_log.info("removed session (%s)", key);
1291 
1292         if (!m_persistedAttributeIds.empty()) {
1293             if (!revocationExp) {
1294                 const PropertySet* props = app.getPropertySet("Sessions");
1295                 if (props)
1296                     revocationExp = props->getUnsignedInt("lifetime").second;
1297                 if (!revocationExp)
1298                     revocationExp = 28800;
1299                 revocationExp += time(nullptr);
1300             }
1301             try {
1302                 if (!m_storage_lite->createString("Revoked", key, "1", revocationExp))
1303                     m_log.warn("duplicate insertion of revocation for session (%s)", key);
1304             }
1305             catch (const std::exception& ex) {
1306                 m_log.warn("error recording revocation of session (%s): %s", key, ex.what());
1307             }
1308         }
1309 #else
1310         throw ConfigurationException("SessionCache removal requires a StorageService.");
1311 #endif
1312     }
1313     else {
1314         // Remote the request.
1315         DDF in("remove::" STORAGESERVICE_SESSION_CACHE "::SessionCache");
1316         DDFJanitor jin(in);
1317         in.structure();
1318         in.addmember("key").string(key);
1319         in.addmember("application_id").string(app.getId());
1320 
1321         DDF out = app.getServiceProvider().getListenerService()->send(in);
1322         out.destroy();
1323     }
1324 }
1325 
dormant(const char * key)1326 void SSCache::dormant(const char* key)
1327 {
1328 #ifdef _DEBUG
1329     xmltooling::NDC ndc("dormant");
1330 #endif
1331 
1332     m_log.debug("deleting local copy of session (%s)", key);
1333 
1334     // lock the cache for writing, which means we know nobody is sitting in find()
1335     m_lock->wrlock();
1336 
1337     // grab the entry from the table
1338     map<string,StoredSession*>::const_iterator i=m_hashtable.find(key);
1339     if (i==m_hashtable.end()) {
1340         m_lock->unlock();
1341         return;
1342     }
1343 
1344     // ok, remove the entry and lock it
1345     StoredSession* entry=i->second;
1346     m_hashtable.erase(key);
1347     entry->lock();
1348 
1349     // unlock the cache
1350     m_lock->unlock();
1351 
1352     // we can release the cache entry lock because we know we're not in the cache anymore
1353     entry->unlock();
1354 
1355     delete entry;
1356 }
1357 
cleanup_fn(void * p)1358 void* SSCache::cleanup_fn(void* p)
1359 {
1360 #ifdef _DEBUG
1361     xmltooling::NDC ndc("cleanup");
1362 #endif
1363 
1364     SSCache* pcache = reinterpret_cast<SSCache*>(p);
1365 
1366 #ifndef WIN32
1367     // First, let's block all signals
1368     Thread::mask_all_signals();
1369 #endif
1370 
1371     scoped_ptr<Mutex> mutex(Mutex::create());
1372 
1373     // Load our configuration details...
1374     static const XMLCh cleanupInterval[] = UNICODE_LITERAL_15(c,l,e,a,n,u,p,I,n,t,e,r,v,a,l);
1375     const XMLCh* tag = pcache->m_root ? pcache->m_root->getAttributeNS(nullptr, cleanupInterval) : nullptr;
1376     int rerun_timer = 900;
1377     if (tag && *tag) {
1378         try {
1379             rerun_timer = XMLString::parseInt(tag);
1380         }
1381         catch (XMLException&) {
1382             pcache->m_log.error("cleanupInterval setting was not a numeric value");
1383             rerun_timer = 0;
1384         }
1385         if (rerun_timer <= 0)
1386             rerun_timer = 900;
1387     }
1388 
1389     mutex->lock();
1390 
1391     pcache->m_log.info("cleanup thread started...run every %d secs; timeout after %d secs", rerun_timer, pcache->m_inprocTimeout);
1392 
1393     while (!pcache->shutdown) {
1394         pcache->shutdown_wait->timedwait(mutex.get(), rerun_timer);
1395         if (pcache->shutdown)
1396             break;
1397 
1398         // Ok, let's run through the cleanup process and clean out
1399         // really old sessions.  This is a two-pass process.  The
1400         // first pass is done holding a read-lock while we iterate over
1401         // the cache.  The second pass doesn't need a lock because
1402         // the 'deletes' will lock the cache.
1403 
1404         // Pass 1: iterate over the map and find all entries that have not been
1405         // used in the allotted timeout.
1406         vector<string> stale_keys;
1407         time_t stale = time(nullptr) - pcache->m_inprocTimeout;
1408 
1409         pcache->m_log.debug("cleanup thread running");
1410 
1411         pcache->m_lock->rdlock();
1412         for (map<string,StoredSession*>::const_iterator i = pcache->m_hashtable.begin(); i != pcache->m_hashtable.end(); ++i) {
1413             // If the last access was BEFORE the stale timeout...
1414             i->second->lock();
1415             time_t last=i->second->getLastAccess();
1416             i->second->unlock();
1417             if (last < stale)
1418                 stale_keys.push_back(i->first);
1419         }
1420         pcache->m_lock->unlock();
1421 
1422         if (!stale_keys.empty()) {
1423             pcache->m_log.info("purging %d old sessions", stale_keys.size());
1424 
1425             // Pass 2: walk through the list of stale entries and remove them from the cache
1426             for (vector<string>::const_iterator i = stale_keys.begin(); i != stale_keys.end(); ++i) {
1427                 pcache->dormant(i->c_str());
1428             }
1429         }
1430 
1431         pcache->m_log.debug("cleanup thread completed");
1432     }
1433 
1434     pcache->m_log.info("cleanup thread exiting");
1435 
1436     mutex->unlock();
1437     return nullptr;
1438 }
1439 
1440 #ifndef SHIBSP_LITE
1441 
receive(DDF & in,ostream & out)1442 void SSCache::receive(DDF& in, ostream& out)
1443 {
1444 #ifdef _DEBUG
1445     xmltooling::NDC ndc("receive");
1446 #endif
1447     const Application* app = SPConfig::getConfig().getServiceProvider()->getApplication(in["application_id"].string());
1448     if (!app)
1449         throw ListenerException("Application not found, check configuration?");
1450 
1451     if (!strcmp(in.name(),"find::" STORAGESERVICE_SESSION_CACHE "::SessionCache")) {
1452         const char* key=in["key"].string();
1453         if (!key)
1454             throw ListenerException("Required parameters missing for session lookup.");
1455 
1456         // Do an unversioned read.
1457         string record;
1458         time_t lastAccess = 0;
1459         int ver = m_storage->readText(key, "session", &record, &lastAccess);
1460         if (!ver) {
1461             const char* recovery = in["sealed"].string();
1462             if (recovery && *recovery && recover(*app, key, recovery)) {
1463                 // Retry the read.
1464                 ver = m_storage->readText(key, "session", &record, &lastAccess);
1465                 if (!ver)
1466                     m_log.warn("recovered session (%s) is missing from storage service", key);
1467             }
1468 
1469             if (!ver) {
1470                 DDF ret(nullptr);
1471                 DDFJanitor jan(ret);
1472                 out << ret;
1473                 return;
1474             }
1475         }
1476 
1477         if (lastAccess == 0) {
1478             m_log.error("session (ID: %s) did not report time of last access", key);
1479             throw RetryableProfileException("Your session's last access time was missing, and you must re-authenticate.");
1480         }
1481 
1482         // Adjust for expiration to recover last access time and check timeout.
1483         unsigned long cacheTimeout = getCacheTimeout(*app);
1484         lastAccess -= cacheTimeout;
1485         time_t now=time(nullptr);
1486 
1487         // See if we need to check for a timeout.
1488         if (in["timeout"].string()) {
1489             time_t timeout = 0;
1490             auto_ptr_XMLCh dt(in["timeout"].string());
1491             XMLDateTime dtobj(dt.get());
1492             dtobj.parseDateTime();
1493             timeout = dtobj.getEpoch();
1494 
1495             if (timeout > 0 && now - lastAccess >= timeout) {
1496                 m_log.info("session timed out (ID: %s)", key);
1497                 scoped_ptr<LogoutEvent> logout_event(newLogoutEvent(*app));
1498                 if (logout_event.get()) {
1499                     logout_event->m_logoutType = LogoutEvent::LOGOUT_EVENT_INVALID;
1500                     logout_event->m_sessions.push_back(key);
1501                     app->getServiceProvider().getTransactionLog()->write(*logout_event);
1502                 }
1503                 remove(*app, key);
1504                 throw RetryableProfileException("Your session has timed out due to inactivity, and you must re-authenticate.");
1505             }
1506 
1507             // Update storage expiration, if possible.
1508             try {
1509                 m_storage->updateContext(key, now + cacheTimeout);
1510             }
1511             catch (const std::exception& ex) {
1512                 m_log.error("failed to update session expiration: %s", ex.what());
1513             }
1514         }
1515 
1516         // Send the record back.
1517         out << record;
1518     }
1519     else if (!strcmp(in.name(),"touch::" STORAGESERVICE_SESSION_CACHE "::SessionCache")) {
1520         const char* key=in["key"].string();
1521         if (!key)
1522             throw ListenerException("Required parameters missing for session check.");
1523         const char* client_addr = in["client_addr"].string();
1524 
1525         // Do a read. May be unversioned if we need to bind a new client address.
1526         string record;
1527         time_t lastAccess = 0;
1528         int curver = in["version"].integer();
1529         int ver = m_storage->readText(key, "session", &record, &lastAccess, client_addr ? 0 : curver);
1530         if (ver == 0) {
1531             m_log.info("session (ID: %s) no longer in storage", key);
1532             throw RetryableProfileException("Your session is not available in the session store, and you must re-authenticate.");
1533         }
1534         else if (lastAccess == 0) {
1535             m_log.error("session (ID: %s) did not report time of last access", key);
1536             throw RetryableProfileException("Your session's last access time was missing, and you must re-authenticate.");
1537         }
1538 
1539         // Adjust for expiration to recover last access time and check timeout.
1540         unsigned long cacheTimeout = getCacheTimeout(*app);
1541         lastAccess -= cacheTimeout;
1542         time_t now=time(nullptr);
1543 
1544         // See if we need to check for a timeout.
1545         time_t timeout = 0;
1546         auto_ptr_XMLCh dt(in["timeout"].string());
1547         if (dt.get()) {
1548             XMLDateTime dtobj(dt.get());
1549             dtobj.parseDateTime();
1550             timeout = dtobj.getEpoch();
1551         }
1552 
1553         if (timeout > 0 && now - lastAccess >= timeout) {
1554             m_log.info("session timed out (ID: %s)", key);
1555             throw RetryableProfileException("Your session has timed out due to inactivity, and you must re-authenticate.");
1556         }
1557 
1558         // Update storage expiration, if possible.
1559         try {
1560             m_storage->updateContext(key, now + cacheTimeout);
1561         }
1562         catch (const std::exception& ex) {
1563             m_log.error("failed to update session expiration: %s", ex.what());
1564         }
1565 
1566         // We may need to write back a new address into the session using a versioned update loop.
1567         if (client_addr) {
1568             short attempts = 0;
1569             m_log.info("binding session (%s) to new client address (%s)", key, client_addr);
1570             do {
1571                 // We have to reconstitute the session object ourselves.
1572                 DDF sessionobj;
1573                 DDFJanitor sessionjan(sessionobj);
1574                 istringstream src(record);
1575                 src >> sessionobj;
1576                 ver = sessionobj["version"].integer();
1577                 const char* saddr = sessionobj["client_addr"][StoredSession::getAddressFamily(client_addr)].string();
1578                 if (saddr) {
1579                     // Something snuck in and bound the session to this address type, so it better match what we have.
1580                     if (!XMLString::equals(saddr, client_addr)) {
1581                         m_log.warn("client address mismatch, client (%s), session (%s)", client_addr, saddr);
1582                         throw RetryableProfileException(
1583                             "Your IP address ($1) does not match the address recorded at the time the session was established.",
1584                             params(1, client_addr)
1585                             );
1586                     }
1587                     break;  // No need to update.
1588                 }
1589                 else {
1590                     // Bind it into the session.
1591                     sessionobj["client_addr"].addmember(StoredSession::getAddressFamily(client_addr)).string(client_addr);
1592                 }
1593 
1594                 // Tentatively increment the version.
1595                 sessionobj["version"].integer(sessionobj["version"].integer() + 1);
1596 
1597                 ostringstream str;
1598                 str << sessionobj;
1599                 record = str.str();
1600 
1601                 ver = m_storage->updateText(key, "session", record.c_str(), 0, ver);
1602                 if (!ver) {
1603                     // Fatal problem with update.
1604                     m_log.error("updateText failed on StorageService for session (%s)", key);
1605                     throw IOException("Unable to update stored session.");
1606                 }
1607                 if (ver < 0) {
1608                     // Out of sync.
1609                     if (++attempts > 10) {
1610                         m_log.error("failed to bind client address, update attempts exceeded limit");
1611                         throw IOException("Unable to update stored session, exceeded retry limit.");
1612                     }
1613                     m_log.warn("storage service indicates the record is out of sync, updating with a fresh copy...");
1614                     sessionobj["version"].integer(sessionobj["version"].integer() - 1);
1615                     ver = m_storage->readText(key, "session", &record);
1616                     if (!ver) {
1617                         m_log.error("readText failed on StorageService for session (%s)", key);
1618                         throw IOException("Unable to read back stored session.");
1619                     }
1620                     ver = -1;
1621                 }
1622             } while (ver < 0); // negative indicates a sync issue so we retry
1623         }
1624 
1625         if (ver > curver) {
1626             // Send the record back.
1627             out << record;
1628         }
1629         else {
1630             DDF ret(nullptr);
1631             DDFJanitor jan(ret);
1632             out << ret;
1633         }
1634     }
1635     else if (!strcmp(in.name(),"remove::" STORAGESERVICE_SESSION_CACHE "::SessionCache")) {
1636         const char* key=in["key"].string();
1637         if (!key)
1638             throw ListenerException("Required parameter missing for session removal.");
1639         time_t revocationExp = 0;
1640         auto_ptr_XMLCh dt(in["revocationExp"].string());
1641         if (dt.get()) {
1642             XMLDateTime dtobj(dt.get());
1643             dtobj.parseDateTime();
1644             revocationExp = dtobj.getEpoch();
1645         }
1646 
1647         remove(*app, key, revocationExp);
1648         DDF ret(nullptr);
1649         DDFJanitor jan(ret);
1650         out << ret;
1651     }
1652     else if (!strcmp(in.name(), "recover::" STORAGESERVICE_SESSION_CACHE "::SessionCache")) {
1653         const char* key = in["key"].string();
1654         const char* cookie = in["sealed"].string();
1655         if (!key || !cookie)
1656             throw ListenerException("Required parameter missing for session recovery.");
1657 
1658         DDF ret(nullptr);
1659         DDFJanitor jan(ret);
1660         if (recover(*app, key, cookie))
1661             ret.integer(1L);
1662         else
1663             ret.integer(0L);
1664         out << ret;
1665     }
1666 }
1667 
1668 #endif
1669