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.
51 * shib-handlers.cpp -- profile handlers that plug into SP
63 #include <shib/shib-threads.h>
64 #include <xercesc/util/Base64.hpp>
66 #ifndef HAVE_STRCASECMP
67 # define strcasecmp stricmp
72 using namespace shibboleth;
73 using namespace shibtarget;
74 using namespace log4cpp;
80 CgiParse(const char* data, unsigned int len);
82 const char* get_value(const char* name) const;
84 static char x2c(char *what);
85 static void url_decode(char *url);
86 static string url_encode(const char* s);
88 char * fmakeword(char stop, unsigned int *cl, const char** ppch);
89 char * makeword(char *line, char stop);
90 void plustospace(char *str);
92 map<string,char*> kvp_map;
95 // Helper class for SAML 2.0 Common Domain Cookie operations
96 class CommonDomainCookie
99 CommonDomainCookie(const char* cookie);
100 ~CommonDomainCookie() {}
101 saml::Iterator<std::string> get() {return m_list;}
102 const char* set(const char* providerId);
103 static const char CDCName[];
105 std::string m_encoded;
106 std::vector<std::string> m_list;
109 class SessionInitiator : virtual public IHandler
112 SessionInitiator(const DOMElement* e) {}
113 ~SessionInitiator() {}
114 pair<bool,void*> run(ShibTarget* st, const IPropertySet* handler, bool isHandler=true);
115 pair<bool,void*> ShibAuthnRequest(
117 const IPropertySet* shire,
120 const char* providerId
124 class SAML1Consumer : virtual public IHandler
127 SAML1Consumer(const DOMElement* e) {}
129 pair<bool,void*> run(ShibTarget* st, const IPropertySet* handler, bool isHandler=true);
132 class ShibLogout : virtual public IHandler
135 ShibLogout(const DOMElement* e) {}
137 pair<bool,void*> run(ShibTarget* st, const IPropertySet* handler, bool isHandler=true);
142 IPlugIn* ShibSessionInitiatorFactory(const DOMElement* e)
144 return new SessionInitiator(e);
147 IPlugIn* SAML1POSTFactory(const DOMElement* e)
149 return new SAML1Consumer(e);
152 IPlugIn* SAML1ArtifactFactory(const DOMElement* e)
154 return new SAML1Consumer(e);
157 IPlugIn* ShibLogoutFactory(const DOMElement* e)
159 return new ShibLogout(e);
162 pair<bool,void*> SessionInitiator::run(ShibTarget* st, const IPropertySet* handler, bool isHandler)
165 const char* resource=NULL;
166 const IPropertySet* ACS=NULL;
167 const IApplication* app=st->getApplication();
171 * Binding is CGI query string with:
172 * target the resource to direct back to later
173 * acsIndex optional index of an ACS to use on the way back in
174 * providerId optional direct invocation of a specific IdP
176 string query=st->getArgs();
177 CgiParse parser(query.c_str(),query.length());
179 const char* option=parser.get_value("acsIndex");
181 ACS=app->getAssertionConsumerServiceByIndex(atoi(option));
182 option=parser.get_value("providerId");
184 resource=parser.get_value("target");
185 if (!resource || !*resource) {
186 pair<bool,const char*> home=app->getString("homeURL");
188 resource=home.second;
190 throw FatalProfileException("Session initiator requires a target parameter or a homeURL application property.");
193 dupresource=resource;
194 resource=dupresource.c_str();
198 // Here we actually use metadata to invoke the SSO service directly.
199 // The only currently understood binding is the Shibboleth profile.
200 Metadata m(app->getMetadataProviders());
201 const IEntityDescriptor* entity=m.lookup(option);
203 throw MetadataException("Session initiator unable to locate metadata for provider ($1).", params(1,option));
204 const IIDPSSODescriptor* role=entity->getIDPSSODescriptor(Constants::SHIB_NS);
206 throw MetadataException(
207 "Session initiator unable to locate a Shibboleth-aware identity provider role for provider ($1).", params(1,option)
209 const IEndpointManager* SSO=role->getSingleSignOnServiceManager();
210 const IEndpoint* ep=SSO->getEndpointByBinding(Constants::SHIB_AUTHNREQUEST_PROFILE_URI);
212 throw MetadataException(
213 "Session initiator unable to locate compatible SSO service for provider ($1).", params(1,option)
215 auto_ptr_char dest(ep->getLocation());
216 return ShibAuthnRequest(
217 st,ACS ? ACS : app->getDefaultAssertionConsumerService(),dest.get(),resource,app->getString("providerId").second
222 // We're running as a "virtual handler" from within the filter.
223 // The target resource is the current one and everything else is defaulted.
224 resource=st->getRequestURL();
227 if (!ACS) ACS=app->getDefaultAssertionConsumerService();
229 // For now, we only support external session initiation via a wayfURL
230 pair<bool,const char*> wayfURL=handler->getString("wayfURL");
232 throw ConfigurationException("Session initiator is missing wayfURL property.");
234 pair<bool,const XMLCh*> wayfBinding=handler->getXMLString("wayfBinding");
235 if (!wayfBinding.first || !XMLString::compareString(wayfBinding.second,Constants::SHIB_AUTHNREQUEST_PROFILE_URI))
237 return ShibAuthnRequest(st,ACS,wayfURL.second,resource,app->getString("providerId").second);
238 else if (!XMLString::compareString(wayfBinding.second,Constants::SHIB_LEGACY_AUTHNREQUEST_PROFILE_URI))
240 return ShibAuthnRequest(st,ACS,wayfURL.second,resource,NULL);
241 else if (!strcmp(handler->getString("wayfBinding").second,"urn:mace:shibboleth:1.0:profiles:EAuth")) {
242 // TODO: Finalize E-Auth profile URI
243 pair<bool,bool> localRelayState=st->getConfig()->getPropertySet("Local")->getBool("localRelayState");
244 if (!localRelayState.first || !localRelayState.second)
245 throw ConfigurationException("E-Authn requests cannot include relay state, so localRelayState must be enabled.");
247 // Here we store the state in a cookie.
248 pair<string,const char*> shib_cookie=st->getCookieNameProps("_shibstate_");
249 st->setCookie(shib_cookie.first,CgiParse::url_encode(resource) + shib_cookie.second);
250 return make_pair(true, st->sendRedirect(wayfURL.second));
253 throw UnsupportedProfileException("Unsupported WAYF binding ($1).", params(1,handler->getString("wayfBinding").second));
256 // Handles Shib 1.x AuthnRequest profile.
257 pair<bool,void*> SessionInitiator::ShibAuthnRequest(
259 const IPropertySet* shire,
262 const char* providerId
265 // Compute the ACS URL. We add the ACS location to the handler baseURL.
266 // Legacy configs will not have an ACS specified, so no suffix will be added.
267 string ACSloc=st->getHandlerURL(target);
268 if (shire) ACSloc+=shire->getString("Location").second;
271 sprintf(timebuf,"%u",time(NULL));
272 string req=string(dest) + "?shire=" + CgiParse::url_encode(ACSloc.c_str()) + "&time=" + timebuf;
274 // How should the resource value be preserved?
275 pair<bool,bool> localRelayState=st->getConfig()->getPropertySet("Local")->getBool("localRelayState");
276 if (!localRelayState.first || !localRelayState.second) {
277 // The old way, just send it along.
278 req+="&target=" + CgiParse::url_encode(target);
281 // Here we store the state in a cookie and send a fixed
282 // value to the IdP so we can recognize it on the way back.
283 pair<string,const char*> shib_cookie=st->getCookieNameProps("_shibstate_");
284 st->setCookie(shib_cookie.first,CgiParse::url_encode(target) + shib_cookie.second);
285 req+="&target=cookie";
288 // Only omitted for 1.1 style requests.
290 req+="&providerId=" + CgiParse::url_encode(providerId);
292 return make_pair(true, st->sendRedirect(req));
295 pair<bool,void*> SAML1Consumer::run(ShibTarget* st, const IPropertySet* handler, bool isHandler)
298 string input,cookie,target,providerId;
299 const IApplication* app=st->getApplication();
301 // Supports either version...
302 pair<bool,unsigned int> version=handler->getUnsignedInt("MinorVersion","urn:oasis:names:tc:SAML:1.0:protocol");
306 pair<bool,const XMLCh*> binding=handler->getXMLString("Binding");
307 if (!binding.first || !XMLString::compareString(binding.second,SAMLBrowserProfile::BROWSER_POST)) {
308 if (strcasecmp(st->getRequestMethod(), "POST"))
309 throw FatalProfileException(
310 "SAML 1.x Browser/POST handler does not support HTTP method ($1).", params(1,st->getRequestMethod())
313 if (!st->getContentType() || strcasecmp(st->getContentType(),"application/x-www-form-urlencoded"))
314 throw FatalProfileException(
315 "Blocked invalid content-type ($1) submitted to SAML 1.x Browser/POST handler.", params(1,st->getContentType())
317 input=st->getPostData();
318 profile|=(version.second==1 ? SAML11_POST : SAML10_POST);
320 else if (!XMLString::compareString(binding.second,SAMLBrowserProfile::BROWSER_ARTIFACT)) {
321 if (strcasecmp(st->getRequestMethod(), "GET"))
322 throw FatalProfileException(
323 "SAML 1.x Browser/Artifact handler does not support HTTP method ($1).", params(1,st->getRequestMethod())
326 profile|=(version.second==1 ? SAML11_ARTIFACT : SAML10_ARTIFACT);
330 throw FatalProfileException("SAML 1.x Browser Profile handler received no data from browser.");
332 string hURL=st->getHandlerURL(st->getRequestURL());
333 pair<bool,const char*> loc=handler->getString("Location");
334 string recipient=loc.first ? hURL + loc.second : hURL;
335 st->getConfig()->getListener()->sessionNew(
346 st->log(ShibTarget::LogLevelDebug, string("profile processing succeeded, new session created (") + cookie + ")");
348 if (target=="default") {
349 pair<bool,const char*> homeURL=app->getString("homeURL");
350 target=homeURL.first ? homeURL.second : "/";
352 else if (target=="cookie" || target.empty()) {
353 // Pull the target value from the "relay state" cookie.
354 pair<string,const char*> relay_cookie = st->getCookieNameProps("_shibstate_");
355 const char* relay_state = st->getCookie(relay_cookie.first);
356 if (!relay_state || !*relay_state) {
357 // No apparent relay state value to use, so fall back on the default.
358 pair<bool,const char*> homeURL=app->getString("homeURL");
359 target=homeURL.first ? homeURL.second : "/";
362 char* rscopy=strdup(relay_state);
363 CgiParse::url_decode(rscopy);
369 // We've got a good session, set the session cookie.
370 pair<string,const char*> shib_cookie=st->getCookieNameProps("_shibsession_");
371 st->setCookie(shib_cookie.first, cookie + shib_cookie.second);
373 const IPropertySet* sessionProps=app->getPropertySet("Sessions");
374 pair<bool,bool> idpHistory=sessionProps->getBool("idpHistory");
375 if (!idpHistory.first || idpHistory.second) {
376 // Set an IdP history cookie locally (essentially just a CDC).
377 CommonDomainCookie cdc(st->getCookie(CommonDomainCookie::CDCName));
379 // Either leave in memory or set an expiration.
380 pair<bool,unsigned int> days=sessionProps->getUnsignedInt("idpHistoryDays");
381 if (!days.first || days.second==0)
382 st->setCookie(CommonDomainCookie::CDCName,string(cdc.set(providerId.c_str())) + shib_cookie.second);
384 time_t now=time(NULL) + (days.second * 24 * 60 * 60);
387 struct tm* ptime=gmtime_r(&now,&res);
389 struct tm* ptime=gmtime(&now);
392 strftime(timebuf,64,"%a, %d %b %Y %H:%M:%S GMT",ptime);
394 CommonDomainCookie::CDCName,
395 string(cdc.set(providerId.c_str())) + shib_cookie.second + "; expires=" + timebuf
400 // Now redirect to the target.
401 return make_pair(true, st->sendRedirect(target));
404 pair<bool,void*> ShibLogout::run(ShibTarget* st, const IPropertySet* handler, bool isHandler)
406 // Recover the session key.
407 pair<string,const char*> shib_cookie = st->getCookieNameProps("_shibsession_");
408 const char* session_id = st->getCookie(shib_cookie.first);
410 // Logout is best effort.
411 if (session_id && *session_id) {
413 st->getConfig()->getListener()->sessionEnd(st->getApplication(),session_id);
415 catch (SAMLException& e) {
416 st->log(ShibTarget::LogLevelError, string("logout processing failed with exception: ") + e.what());
420 st->log(ShibTarget::LogLevelError, "logout processing failed with unknown exception");
423 st->setCookie(shib_cookie.first,"");
426 string query=st->getArgs();
427 CgiParse parser(query.c_str(),query.length());
429 const char* ret=parser.get_value("return");
431 ret=handler->getString("ResponseLocation").second;
433 ret=st->getApplication()->getString("homeURL").second;
436 return make_pair(true, st->sendRedirect(ret));
439 /*************************************************************************
440 * CGI Parser implementation
443 CgiParse::CgiParse(const char* data, unsigned int len)
445 const char* pch = data;
446 unsigned int cl = len;
451 value=fmakeword('&',&cl,&pch);
454 name=makeword(value,'=');
460 CgiParse::~CgiParse()
462 for (map<string,char*>::iterator i=kvp_map.begin(); i!=kvp_map.end(); i++)
467 CgiParse::get_value(const char* name) const
469 map<string,char*>::const_iterator i=kvp_map.find(name);
470 if (i==kvp_map.end())
475 /* Parsing routines modified from NCSA source. */
477 CgiParse::makeword(char *line, char stop)
480 char *word = (char *) malloc(sizeof(char) * (strlen(line) + 1));
482 for(x=0;((line[x]) && (line[x] != stop));x++)
491 line[y++] = line[x++];
497 CgiParse::fmakeword(char stop, unsigned int *cl, const char** ppch)
505 word = (char *) malloc(sizeof(char) * (wsize + 1));
509 word[ll] = *((*ppch)++);
514 word = (char *)realloc(word,sizeof(char)*(wsize+1));
517 if((word[ll] == stop) || word[ll] == EOF || (!(*cl)))
529 CgiParse::plustospace(char *str)
534 if(str[x] == '+') str[x] = ' ';
538 CgiParse::x2c(char *what)
542 digit = (what[0] >= 'A' ? ((what[0] & 0xdf) - 'A')+10 : (what[0] - '0'));
544 digit += (what[1] >= 'A' ? ((what[1] & 0xdf) - 'A')+10 : (what[1] - '0'));
549 CgiParse::url_decode(char *url)
553 for(x=0,y=0;url[y];++x,++y)
555 if((url[x] = url[y]) == '%')
557 url[x] = x2c(&url[y+1]);
564 static inline char hexchar(unsigned short s)
566 return (s<=9) ? ('0' + s) : ('A' + s - 10);
569 string CgiParse::url_encode(const char* s)
571 static char badchars[]="\"\\+<>#%{}|^~[]`;/?:@=&";
575 if (strchr(badchars,*s) || *s<=0x1F || *s>=0x7F) {
577 ret+=hexchar(*s >> 4);
578 ret+=hexchar(*s & 0x0F);
586 // CDC implementation
588 const char CommonDomainCookie::CDCName[] = "_saml_idp";
590 CommonDomainCookie::CommonDomainCookie(const char* cookie)
595 Category& log=Category::getInstance(SHIBT_LOGCAT".CommonDomainCookie");
597 // Copy it so we can URL-decode it.
598 char* b64=strdup(cookie);
599 CgiParse::url_decode(b64);
601 // Chop it up and save off elements.
602 vector<string> templist;
605 while (*ptr && isspace(*ptr)) ptr++;
607 while (*end && !isspace(*end)) end++;
608 templist.push_back(string(ptr,end-ptr));
613 // Now Base64 decode the list.
614 for (vector<string>::iterator i=templist.begin(); i!=templist.end(); i++) {
616 XMLByte* decoded=Base64::decode(reinterpret_cast<const XMLByte*>(i->c_str()),&len);
617 if (decoded && *decoded) {
618 m_list.push_back(reinterpret_cast<char*>(decoded));
619 XMLString::release(&decoded);
622 log.warn("cookie element does not appear to be base64-encoded");
626 const char* CommonDomainCookie::set(const char* providerId)
628 // First scan the list for this IdP.
629 for (vector<string>::iterator i=m_list.begin(); i!=m_list.end(); i++) {
630 if (*i == providerId) {
636 // Append it to the end.
637 m_list.push_back(providerId);
639 // Now rebuild the delimited list.
641 for (vector<string>::const_iterator j=m_list.begin(); j!=m_list.end(); j++) {
642 if (!delimited.empty()) delimited += ' ';
645 XMLByte* b64=Base64::encode(reinterpret_cast<const XMLByte*>(j->c_str()),j->length(),&len);
647 for (pos=b64, pos2=b64; *pos2; pos2++)
652 delimited += reinterpret_cast<char*>(b64);
653 XMLString::release(&b64);
656 m_encoded=CgiParse::url_encode(delimited.c_str());
657 return m_encoded.c_str();