2 * The Shibboleth License, Version 1.
4 * University Corporation for Advanced Internet Development, Inc.
8 * Redistribution and use in source and binary forms, with or without
9 * modification, are permitted provided that the following conditions are met:
11 * Redistributions of source code must retain the above copyright notice, this
12 * list of conditions and the following disclaimer.
14 * Redistributions in binary form must reproduce the above copyright notice,
15 * this list of conditions and the following disclaimer in the documentation
16 * and/or other materials provided with the distribution, if any, must include
17 * the following acknowledgment: "This product includes software developed by
18 * the University Corporation for Advanced Internet Development
19 * <http://www.ucaid.edu>Internet2 Project. Alternately, this acknowledegement
20 * may appear in the software itself, if and wherever such third-party
21 * acknowledgments normally appear.
23 * Neither the name of Shibboleth nor the names of its contributors, nor
24 * Internet2, nor the University Corporation for Advanced Internet Development,
25 * Inc., nor UCAID may be used to endorse or promote products derived from this
26 * software without specific prior written permission. For written permission,
27 * please contact shibboleth@shibboleth.org
29 * Products derived from this software may not be called Shibboleth, Internet2,
30 * UCAID, or the University Corporation for Advanced Internet Development, nor
31 * may Shibboleth appear in their name, without prior written permission of the
32 * University Corporation for Advanced Internet Development.
35 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
36 * AND WITH ALL FAULTS. ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
37 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
38 * PARTICULAR PURPOSE, AND NON-INFRINGEMENT ARE DISCLAIMED AND THE ENTIRE RISK
39 * OF SATISFACTORY QUALITY, PERFORMANCE, ACCURACY, AND EFFORT IS WITH LICENSEE.
40 * IN NO EVENT SHALL THE COPYRIGHT OWNER, CONTRIBUTORS OR THE UNIVERSITY
41 * CORPORATION FOR ADVANCED INTERNET DEVELOPMENT, INC. BE LIABLE FOR ANY DIRECT,
42 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
43 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
44 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
45 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
46 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
47 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
52 * shib-ccache.cpp -- SHAR Credential Cache
54 * Originally from mod_shib
55 * Modified by: Derek Atkins <derek@ihtfp.com>
66 #include <shib/shib-threads.h>
68 #include <log4cpp/Category.hh>
73 #ifdef HAVE_LIBDMALLOCXX
78 using namespace log4cpp;
80 using namespace shibboleth;
81 using namespace shibtarget;
83 static const XMLCh cleanupInterval[] =
84 { chLatin_c, chLatin_l, chLatin_e, chLatin_a, chLatin_n, chLatin_u, chLatin_p,
85 chLatin_I, chLatin_n, chLatin_t, chLatin_e, chLatin_r, chLatin_v, chLatin_a, chLatin_l, chNull
87 static const XMLCh cacheTimeout[] =
88 { chLatin_c, chLatin_a, chLatin_c, chLatin_h, chLatin_e,
89 chLatin_T, chLatin_i, chLatin_m, chLatin_e, chLatin_o, chLatin_u, chLatin_t, chNull
91 static const XMLCh AAConnectTimeout[] =
92 { chLatin_A, chLatin_A, chLatin_C, chLatin_o, chLatin_n, chLatin_n, chLatin_e, chLatin_c, chLatin_t,
93 chLatin_T, chLatin_i, chLatin_m, chLatin_e, chLatin_o, chLatin_u, chLatin_t, chNull
95 static const XMLCh AATimeout[] =
96 { chLatin_A, chLatin_A, chLatin_T, chLatin_i, chLatin_m, chLatin_e, chLatin_o, chLatin_u, chLatin_t, chNull };
98 static const XMLCh defaultLifetime[] =
99 { chLatin_d, chLatin_e, chLatin_f, chLatin_a, chLatin_u, chLatin_l, chLatin_t,
100 chLatin_L, chLatin_i, chLatin_f, chLatin_e, chLatin_t, chLatin_i, chLatin_m, chLatin_e, chNull
102 static const XMLCh retryInterval[] =
103 { chLatin_r, chLatin_e, chLatin_t, chLatin_r, chLatin_y,
104 chLatin_I, chLatin_n, chLatin_t, chLatin_e, chLatin_r, chLatin_v, chLatin_a, chLatin_l, chNull
106 static const XMLCh strictValidity[] =
107 { chLatin_s, chLatin_t, chLatin_r, chLatin_i, chLatin_c, chLatin_t,
108 chLatin_V, chLatin_a, chLatin_l, chLatin_i, chLatin_d, chLatin_i, chLatin_t, chLatin_y, chNull
110 static const XMLCh propagateErrors[] =
111 { chLatin_p, chLatin_r, chLatin_o, chLatin_p, chLatin_a, chLatin_g, chLatin_a, chLatin_t, chLatin_e,
112 chLatin_E, chLatin_r, chLatin_r, chLatin_o, chLatin_r, chLatin_s, chNull
115 class InternalCCache;
116 class InternalCCacheEntry : public ISessionCacheEntry
119 InternalCCacheEntry(const IApplication* application, SAMLAuthenticationStatement* s, const char *client_addr, SAMLResponse* r=NULL);
120 ~InternalCCacheEntry();
122 void lock() { m_lock->lock(); }
123 void unlock() { m_lock->unlock(); }
125 bool isValid(time_t lifetime, time_t timeout) const;
126 const char* getClientAddress() const { return m_clientAddress.c_str(); }
127 const char* getSerializedStatement() const { return m_statement.c_str(); }
128 const SAMLAuthenticationStatement* getStatement() const { return p_auth; }
130 Iterator<SAMLAssertion*> getAssertions();
131 void preFetch(int prefetch_window);
133 void setCache(InternalCCache *cache) { m_cache = cache; }
134 time_t lastAccess() const { return m_lastAccess; }
137 bool responseValid(int slop);
138 void populate(int slop);
139 SAMLResponse* getNewResponse();
141 string m_application_id;
145 string m_clientAddress;
146 time_t m_sessionCreated;
147 time_t m_responseCreated;
148 mutable time_t m_lastAccess;
151 const SAMLNameIdentifier* m_nameid;
152 SAMLAuthenticationStatement* p_auth;
153 SAMLResponse* m_response;
154 InternalCCache *m_cache;
156 log4cpp::Category* log;
161 class InternalCCache : public ISessionCache
164 InternalCCache(const DOMElement* e);
165 virtual ~InternalCCache();
167 void thread_init() {};
168 void thread_end() {};
170 string generateKey() const;
171 ISessionCacheEntry* find(const char* key);
173 const char* key, const IApplication* application, SAMLAuthenticationStatement* s, const char *client_addr, SAMLResponse* r=NULL
175 void remove(const char* key);
177 InternalCCacheEntry* findi(const char* key);
181 const DOMElement* m_root; // Only valid during initialization
183 map<string,InternalCCacheEntry*> m_hashtable;
185 log4cpp::Category* log;
187 static void* cleanup_fcn(void*); // XXX Assumed an InternalCCache
189 CondWait* shutdown_wait;
190 Thread* cleanup_thread;
192 // extracted config settings
193 unsigned int m_AATimeout,m_AAConnectTimeout;
194 unsigned int m_defaultLifetime,m_retryInterval;
195 bool m_strictValidity,m_propagateErrors;
196 friend class InternalCCacheEntry;
199 IPlugIn* MemoryCacheFactory(const DOMElement* e)
201 return new InternalCCache(e);
204 /******************************************************************************/
205 /* InternalCCache: in memory session cache */
206 /******************************************************************************/
208 InternalCCache::InternalCCache(const DOMElement* e)
209 : m_root(e), m_AATimeout(30), m_AAConnectTimeout(15), m_defaultLifetime(1800), m_retryInterval(300),
210 m_strictValidity(true), m_propagateErrors(false), lock(RWLock::create()),
211 log (&Category::getInstance("shibtarget.InternalCCache"))
213 const XMLCh* tag=m_root->getAttributeNS(NULL,AATimeout);
215 m_AATimeout = XMLString::parseInt(tag);
220 tag=m_root->getAttributeNS(NULL,AAConnectTimeout);
222 m_AAConnectTimeout = XMLString::parseInt(tag);
223 if (!m_AAConnectTimeout)
224 m_AAConnectTimeout=15;
227 tag=m_root->getAttributeNS(NULL,defaultLifetime);
229 m_defaultLifetime = XMLString::parseInt(tag);
230 if (!m_defaultLifetime)
231 m_defaultLifetime=1800;
234 tag=m_root->getAttributeNS(NULL,retryInterval);
236 m_retryInterval = XMLString::parseInt(tag);
237 if (!m_retryInterval)
241 tag=m_root->getAttributeNS(NULL,strictValidity);
242 if (tag && (*tag==chDigit_0 || *tag==chLatin_f))
243 m_strictValidity=false;
245 tag=m_root->getAttributeNS(NULL,propagateErrors);
246 if (tag && (*tag==chDigit_1 || *tag==chLatin_t))
247 m_propagateErrors=true;
249 shutdown_wait = CondWait::create();
251 cleanup_thread = Thread::create(&cleanup_fcn, (void*)this);
254 InternalCCache::~InternalCCache()
256 // Shut down the cleanup thread and let it know...
258 shutdown_wait->signal();
259 cleanup_thread->join(NULL);
261 for (map<string,InternalCCacheEntry*>::iterator i=m_hashtable.begin(); i!=m_hashtable.end(); i++)
264 delete shutdown_wait;
267 string InternalCCache::generateKey() const
274 // assumes a lock is held..
275 InternalCCacheEntry* InternalCCache::findi(const char* key)
277 log->debug("findI: \"%s\"", key);
279 map<string,InternalCCacheEntry*>::const_iterator i=m_hashtable.find(key);
280 if (i==m_hashtable.end()) {
281 log->debug("No Match found");
284 log->debug("Match Found.");
289 ISessionCacheEntry* InternalCCache::find(const char* key)
291 log->debug("Find: \"%s\"", key);
292 ReadLock rwlock(lock);
294 InternalCCacheEntry* entry = findi(key);
295 if (!entry) return NULL;
297 // Lock the "database record" for the caller -- they have to unlock the item.
302 void InternalCCache::insert(
303 const char* key, const IApplication* application, SAMLAuthenticationStatement* s, const char* client_addr, SAMLResponse* r
306 log->debug("caching new entry for application %s: \"%s\"", application->getId(), key);
308 InternalCCacheEntry* entry = new InternalCCacheEntry(application, s, client_addr, r);
309 entry->setCache(this);
312 m_hashtable[key]=entry;
316 // remove the entry from the database and then destroy the cacheentry
317 void InternalCCache::remove(const char* key)
319 log->debug("removing cache entry \"key\"", key);
321 // lock the cache for writing, which means we know nobody is sitting in find()
324 // grab the entry from the database.
325 ISessionCacheEntry* entry = findi(key);
332 // ok, remove the entry and lock it
333 m_hashtable.erase(key);
334 dynamic_cast<InternalCCacheEntry*>(entry)->lock();
337 // we can release the entry lock because we know we're not in the cache anymore
340 // Now delete the entry
344 void InternalCCache::cleanup()
346 Mutex* mutex = Mutex::create();
347 saml::NDC ndc("InternalCCache::cleanup()");
350 int timeout_life = 0;
352 // Load our configuration details...
353 const XMLCh* tag=m_root->getAttributeNS(NULL,cleanupInterval);
355 rerun_timer = XMLString::parseInt(tag);
357 tag=m_root->getAttributeNS(NULL,cacheTimeout);
359 timeout_life = XMLString::parseInt(tag);
361 if (rerun_timer <= 0)
362 rerun_timer = 300; // rerun every 5 minutes
364 if (timeout_life <= 0)
365 timeout_life = 28800; // timeout after 8 hours
369 log->debug("Cleanup thread started... Run every %d secs; timeout after %d secs",
370 rerun_timer, timeout_life);
372 while (shutdown == false) {
373 shutdown_wait->timedwait(mutex,rerun_timer);
375 if (shutdown == true)
378 log->info("Cleanup thread running...");
380 // Ok, let's run through the cleanup process and clean out
381 // really old sessions. This is a two-pass process. The
382 // first pass is done holding a read-lock while we iterate over
383 // the database. The second pass doesn't need a lock because
384 // the 'deletes' will lock the database.
386 // Pass 1: iterate over the map and find all entries that have not been
388 vector<string> stale_keys;
389 time_t stale = time(NULL) - timeout_life;
392 for (map<string,InternalCCacheEntry*>::iterator i=m_hashtable.begin();
393 i != m_hashtable.end(); i++)
395 // If the last access was BEFORE the stale timeout...
397 time_t last=i->second->lastAccess();
400 stale_keys.push_back(i->first);
404 log->info("deleting %d old items.", stale_keys.size());
406 // Pass 2: walk through the list of stale entries and remove them from
408 for (vector<string>::iterator j = stale_keys.begin(); j != stale_keys.end(); j++)
412 log->debug("Cleanup thread finished.");
419 void* InternalCCache::cleanup_fcn(void* cache_p)
421 InternalCCache* cache = reinterpret_cast<InternalCCache*>(cache_p);
423 // First, let's block all signals
424 Thread::mask_all_signals();
426 // Now run the cleanup process.
431 /******************************************************************************/
432 /* InternalCCacheEntry: A Credential Cache Entry */
433 /******************************************************************************/
435 InternalCCacheEntry::InternalCCacheEntry(
436 const IApplication* application, SAMLAuthenticationStatement *s, const char* client_addr, SAMLResponse* r
437 ) : m_response(r), m_responseCreated(r ? time(NULL) : 0), m_lastRetry(0),
438 log(&Category::getInstance("shibtarget::InternalCCacheEntry"))
441 log->error("NULL auth statement");
442 throw SAMLException("InternalCCacheEntry() passed an empty SAML Statement");
445 m_application_id=application->getId();
447 m_nameid = s->getSubject()->getNameIdentifier();
448 auto_ptr_char h(m_nameid->getName());
449 auto_ptr_char d(m_nameid->getNameQualifier());
451 m_originSite = d.get();
453 m_clientAddress = client_addr;
454 m_sessionCreated = m_lastAccess = time(NULL);
459 // Save the serialized version of the auth statement
462 m_statement = os.str();
465 // Run pushed data through the AAP. Note that we could end up with an empty response!
466 Metadata m(application->getMetadataProviders());
467 const IProvider* site=m.lookup(m_nameid->getNameQualifier());
469 throw MetadataException("unable to locate origin site's metadata during attribute acceptance processing");
470 Iterator<SAMLAssertion*> assertions=r->getAssertions();
471 for (unsigned long i=0; i < assertions.size();) {
473 AAP::apply(application->getAAPProviders(),site,*(assertions[i]));
476 catch (SAMLException&) {
477 log->info("no statements remain, removing assertion");
478 r->removeAssertion(i);
483 m_lock = Mutex::create();
485 log->info("New Session Created...");
486 log->debug("Handle: \"%s\", Site: \"%s\", Address: %s", h.get(), d.get(), client_addr);
489 InternalCCacheEntry::~InternalCCacheEntry()
491 log->debug("deleting entry for %s@%s", m_handle.c_str(), m_originSite.c_str());
497 bool InternalCCacheEntry::isValid(time_t lifetime, time_t timeout) const
499 saml::NDC ndc("isValid");
500 log->debug("test session %s@%s, (lifetime=%ld, timeout=%ld)",
501 m_handle.c_str(), m_originSite.c_str(), lifetime, timeout);
502 time_t now=time(NULL);
503 if (lifetime > 0 && now > m_sessionCreated+lifetime) {
504 log->debug("session beyond lifetime");
508 if (timeout > 0 && now-m_lastAccess >= timeout) {
509 log->debug("session timed out");
516 Iterator<SAMLAssertion*> InternalCCacheEntry::getAssertions()
518 saml::NDC ndc("getAssertions");
521 return (m_response) ? m_response->getAssertions() : EMPTY(SAMLAssertion*);
524 void InternalCCacheEntry::preFetch(int prefetch_window)
526 saml::NDC ndc("preFetch");
527 populate(prefetch_window);
530 bool InternalCCacheEntry::responseValid(int slop)
532 saml::NDC ndc("responseValid");
534 log->info("checking AA response validity");
536 // This is awful, but the XMLDateTime class is truly horrible.
537 time_t now=time(NULL)+slop;
539 struct tm* ptime=gmtime(&now);
542 struct tm* ptime=gmtime_r(&now,&res);
545 strftime(timebuf,32,"%Y-%m-%dT%H:%M:%SZ",ptime);
546 auto_ptr_XMLCh timeptr(timebuf);
547 XMLDateTime curDateTime(timeptr.get());
548 curDateTime.parseDateTime();
551 Iterator<SAMLAssertion*> iter = m_response->getAssertions();
552 while (iter.hasNext()) {
553 SAMLAssertion* assertion = iter.next();
555 log->debug ("testing assertion...");
557 const XMLDateTime* thistime = assertion->getNotOnOrAfter();
559 // If there is no time, then just continue and ignore this assertion.
564 auto_ptr_char nowptr(curDateTime.getRawData());
565 auto_ptr_char assnptr(thistime->getRawData());
567 log->debug ("comparing now (%s) to %s", nowptr.get(), assnptr.get());
568 int result=XMLDateTime::compareOrder(&curDateTime, thistime);
570 if (result != XMLDateTime::LESS_THAN) {
571 log->debug("nope, not still valid");
576 // If we didn't find any assertions with times, then see if we're
577 // older than the default response lifetime.
579 if ((now - m_responseCreated) > m_cache->m_defaultLifetime) {
580 log->debug("response is beyond default life, so it's invalid");
585 log->debug("yep, response still valid");
589 void InternalCCacheEntry::populate(int slop)
591 saml::NDC ndc("populate");
592 log->debug("populating session cache for application %s", m_application_id.c_str());
594 // Do we have any data cached?
596 // Can we use what we have?
597 if (responseValid(slop))
600 // If we're being strict, dump what we have and reset timestamps.
601 if (m_cache->m_strictValidity) {
602 log->info("strictly enforcing attribute validity, dumping expired data");
610 // Need to try and get a new response.
613 SAMLResponse* new_response=getNewResponse();
616 m_response=new_response;
617 m_responseCreated=time(NULL);
619 log->debug("fetched and stored new response");
622 catch (SAMLException& e) {
623 if (typeid(e)==typeid(InvalidHandleException) || m_cache->m_propagateErrors)
625 log->warn("suppressed SAML exception caught while trying to fetch attributes");
628 if (m_cache->m_propagateErrors)
630 log->warn("suppressed exception caught while trying to fetch attributes");
634 SAMLResponse* InternalCCacheEntry::getNewResponse()
636 saml::NDC ndc("getNewResponse");
638 // The retryInterval determines how often to poll an AA that might be down.
639 if ((time(NULL) - m_lastRetry) < m_cache->m_retryInterval)
642 log->debug("retry interval exceeded, so trying again");
643 m_lastRetry=time(NULL);
645 log->info("trying to request attributes for %s@%s -> %s", m_handle.c_str(), m_originSite.c_str(), m_application_id.c_str());
647 // Lookup application for session to get providerId and attributes to request.
648 IConfig* conf=ShibTargetConfig::getConfig().getINI();
650 const IApplication* application=conf->getApplication(m_application_id.c_str());
652 log->crit("unable to locate application for session, deleted?");
653 throw ShibTargetException(SHIBRPC_INTERNAL_ERROR,"Unable to locate application for session, deleted?");
655 pair<bool,const XMLCh*> providerID=application->getXMLString("providerId");
656 if (!providerID.first) {
657 log->crit("unable to determine ProviderID for application, not set?");
658 throw ShibTargetException(SHIBRPC_INTERNAL_ERROR,"Unable to determine ProviderID for application, not set?");
661 // Get signing policies.
662 bool signRequest=false;
663 bool signedResponse=false;
664 bool signedAssertions=false;
665 const IPropertySet* props=application->getPropertySet("Policy");
667 pair<bool,bool> flag=props->getBool("signRequest");
669 signRequest=flag.second;
670 flag=props->getBool("signedResponse");
672 signedResponse=flag.second;
673 flag=props->getBool("signedAssertions");
675 signedAssertions=flag.second;
678 // Try this request. The binding wrapper class handles most of the details.
679 Metadata m(application->getMetadataProviders());
680 const IProvider* site=m.lookup(m_nameid->getNameQualifier());
682 log->error("unable to locate origin site's metadata during attribute query");
683 throw ShibTargetException(SHIBRPC_INTERNAL_ERROR,"Unable to locate origin site's metadata during attribute query.");
686 // Try to locate an AA role.
687 const IAttributeAuthorityRole* AA=NULL;
688 Iterator<const IProviderRole*> roles=site->getRoles();
689 while (!AA && roles.hasNext()) {
690 const IProviderRole* role=roles.next();
691 if (dynamic_cast<const IAttributeAuthorityRole*>(role)) {
692 // Check for SAML 1.x protocol support.
693 if (role->hasSupport(saml::XML::SAMLP_NS))
694 AA=dynamic_cast<const IAttributeAuthorityRole*>(role);
698 log->error("unable to locate metadata for origin site's Attribute Authority");
699 throw ShibTargetException(SHIBRPC_INTERNAL_ERROR,"Unable to locate metadata for origin site's Attribute Authority.",site);
703 SAMLResponse* response = NULL;
705 // Build a SAML Request....
706 SAMLAttributeQuery* q=new SAMLAttributeQuery(
707 new SAMLSubject(static_cast<SAMLNameIdentifier*>(m_nameid->clone())),
709 application->getAttributeDesignators().clone()
711 auto_ptr<SAMLRequest> req(new SAMLRequest(EMPTY(QName),q));
713 // Sign it? Highly doubtful we'll ever use this, but just for fun...
715 Credentials creds(conf->getCredentialsProviders());
716 const ICredResolver* signingCred=creds.lookup(application->getSigningCred(site));
717 req->sign(SIGNATURE_RSA,signingCred->getKey(),signingCred->getCertificates());
720 log->debug("Trying to query an AA...");
722 SAMLConfig::SAMLBindingConfig bindconf;
723 bindconf.timeout=m_cache->m_AATimeout;
724 bindconf.conn_timeout=m_cache->m_AAConnectTimeout;
725 ShibBinding binding(application->getRevocationProviders(),application->getTrustProviders(),conf->getCredentialsProviders());
726 response=binding.send(*req,AA,application->getTLSCred(site),application->getAudiences(),p_auth->getBindings(),bindconf);
728 catch (SAMLException& e) {
729 log->error("caught SAML exception during query to AA: %s", e.what());
730 if (typeid(e)==typeid(InvalidHandleException))
734 throw ShibTargetException(SHIBRPC_SAML_EXCEPTION, os.str().c_str(), AA);
736 // See if we got a response.
738 log->error("no response obtained");
739 throw ShibTargetException(SHIBRPC_INTERNAL_ERROR,"Unable to obtain attributes from user's origin site.",AA);
741 else if (signedResponse && !response->isSigned()) {
743 log->error("unsigned response obtained, but we were told it must be signed.");
744 throw ShibTargetException(SHIBRPC_INTERNAL_ERROR,"Unable to obtain attributes from user's origin site.",AA);
747 // Run it through the AAP. Note that we could end up with an empty response!
748 Iterator<SAMLAssertion*> a=response->getAssertions();
749 for (unsigned long i=0; i < a.size();) {
751 if (signedAssertions && !(a[i]->isSigned())) {
752 log->warn("removing unsigned assertion from response, in accordance with signedAssertions policy");
753 response->removeAssertion(i);
756 AAP::apply(application->getAAPProviders(),site,*(a[i]));
759 catch (SAMLException&) {
760 log->info("no statements remain, removing assertion");
761 response->removeAssertion(i);