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