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 <log4cpp/Category.hh>
65 #include <xercesc/util/Base64.hpp>
66 #include <xercesc/util/regx/RegularExpression.hpp>
68 #ifndef HAVE_STRCASECMP
69 # define strcasecmp stricmp
74 using namespace shibboleth;
75 using namespace shibtarget;
76 using namespace log4cpp;
82 CgiParse(const char* data, unsigned int len);
84 const char* get_value(const char* name) const;
86 static char x2c(char *what);
87 static void url_decode(char *url);
88 static string url_encode(const char* s);
90 char * fmakeword(char stop, unsigned int *cl, const char** ppch);
91 char * makeword(char *line, char stop);
92 void plustospace(char *str);
94 map<string,char*> kvp_map;
97 class SessionInitiator : virtual public IHandler
100 SessionInitiator(const DOMElement* e) {}
101 ~SessionInitiator() {}
102 pair<bool,void*> run(ShibTarget* st, const IPropertySet* handler, bool isHandler=true);
103 pair<bool,void*> ShibAuthnRequest(
105 const IPropertySet* shire,
108 const char* providerId
112 class SAML1Consumer : virtual public IHandler
115 SAML1Consumer(const DOMElement* e) {}
117 pair<bool,void*> run(ShibTarget* st, const IPropertySet* handler, bool isHandler=true);
120 class ShibLogout : virtual public IHandler
123 ShibLogout(const DOMElement* e) {}
125 pair<bool,void*> run(ShibTarget* st, const IPropertySet* handler, bool isHandler=true);
130 IPlugIn* ShibSessionInitiatorFactory(const DOMElement* e)
132 return new SessionInitiator(e);
135 IPlugIn* SAML1POSTFactory(const DOMElement* e)
137 return new SAML1Consumer(e);
140 IPlugIn* SAML1ArtifactFactory(const DOMElement* e)
142 return new SAML1Consumer(e);
145 IPlugIn* ShibLogoutFactory(const DOMElement* e)
147 return new ShibLogout(e);
150 pair<bool,void*> SessionInitiator::run(ShibTarget* st, const IPropertySet* handler, bool isHandler)
153 const char* resource=NULL;
154 const IPropertySet* ACS=NULL;
155 const IApplication* app=st->getApplication();
159 * Binding is CGI query string with:
160 * target the resource to direct back to later
161 * acsIndex optional index of an ACS to use on the way back in
162 * providerId optional direct invocation of a specific IdP
164 string query=st->getArgs();
165 CgiParse parser(query.c_str(),query.length());
167 const char* option=parser.get_value("acsIndex");
169 ACS=app->getAssertionConsumerServiceByIndex(atoi(option));
170 option=parser.get_value("providerId");
172 resource=parser.get_value("target");
173 if (!resource || !*resource) {
174 pair<bool,const char*> home=app->getString("homeURL");
176 resource=home.second;
178 throw FatalProfileException("Session initiator requires a target parameter or a homeURL application property.");
181 dupresource=resource;
182 resource=dupresource.c_str();
186 // Here we actually use metadata to invoke the SSO service directly.
187 // The only currently understood binding is the Shibboleth profile.
188 Metadata m(app->getMetadataProviders());
189 const IEntityDescriptor* entity=m.lookup(option);
191 throw MetadataException("Session initiator unable to locate metadata for provider ($1).", params(1,option));
192 const IIDPSSODescriptor* role=entity->getIDPSSODescriptor(saml::XML::SAML11_PROTOCOL_ENUM);
194 throw MetadataException(
195 "Session initiator unable to locate SAML identity provider role for provider ($1).", params(1,option)
197 const IEndpointManager* SSO=role->getSingleSignOnServiceManager();
198 const IEndpoint* ep=SSO->getEndpointByBinding(Constants::SHIB_AUTHNREQUEST_PROFILE_URI);
200 throw MetadataException(
201 "Session initiator unable to locate compatible SSO service for provider ($1).", params(1,option)
203 auto_ptr_char dest(ep->getLocation());
204 return ShibAuthnRequest(
205 st,ACS ? ACS : app->getDefaultAssertionConsumerService(),dest.get(),resource,app->getString("providerId").second
210 // We're running as a "virtual handler" from within the filter.
211 // The target resource is the current one and everything else is defaulted.
212 resource=st->getRequestURL();
215 if (!ACS) ACS=app->getDefaultAssertionConsumerService();
217 // For now, we only support external session initiation via a wayfURL
218 pair<bool,const char*> wayfURL=handler->getString("wayfURL");
220 throw ConfigurationException("Session initiator is missing wayfURL property.");
222 pair<bool,const XMLCh*> wayfBinding=handler->getXMLString("wayfBinding");
223 if (!wayfBinding.first || !XMLString::compareString(wayfBinding.second,Constants::SHIB_AUTHNREQUEST_PROFILE_URI))
225 return ShibAuthnRequest(st,ACS,wayfURL.second,resource,app->getString("providerId").second);
226 else if (!XMLString::compareString(wayfBinding.second,Constants::SHIB_LEGACY_AUTHNREQUEST_PROFILE_URI))
228 return ShibAuthnRequest(st,ACS,wayfURL.second,resource,NULL);
229 else if (!strcmp(handler->getString("wayfBinding").second,"urn:mace:shibboleth:1.0:profiles:EAuth")) {
230 // TODO: Finalize E-Auth profile URI
231 pair<bool,bool> localRelayState=st->getConfig()->getPropertySet("Local")->getBool("localRelayState");
232 if (!localRelayState.first || !localRelayState.second)
233 throw ConfigurationException("E-Authn requests cannot include relay state, so localRelayState must be enabled.");
235 // Here we store the state in a cookie.
236 pair<string,const char*> shib_cookie=st->getCookieNameProps("_shibstate_");
237 st->setCookie(shib_cookie.first,CgiParse::url_encode(resource) + shib_cookie.second);
238 return make_pair(true, st->sendRedirect(wayfURL.second));
241 throw UnsupportedProfileException("Unsupported WAYF binding ($1).", params(1,handler->getString("wayfBinding").second));
244 // Handles Shib 1.x AuthnRequest profile.
245 pair<bool,void*> SessionInitiator::ShibAuthnRequest(
247 const IPropertySet* shire,
250 const char* providerId
253 // Compute the ACS URL. We add the ACS location to the handler baseURL.
254 // Legacy configs will not have an ACS specified, so no suffix will be added.
255 string ACSloc=st->getHandlerURL(target);
256 if (shire) ACSloc+=shire->getString("Location").second;
259 sprintf(timebuf,"%u",time(NULL));
260 string req=string(dest) + "?shire=" + CgiParse::url_encode(ACSloc.c_str()) + "&time=" + timebuf;
262 // How should the resource value be preserved?
263 pair<bool,bool> localRelayState=st->getConfig()->getPropertySet("Local")->getBool("localRelayState");
264 if (!localRelayState.first || !localRelayState.second) {
265 // The old way, just send it along.
266 req+="&target=" + CgiParse::url_encode(target);
269 // Here we store the state in a cookie and send a fixed
270 // value to the IdP so we can recognize it on the way back.
271 pair<string,const char*> shib_cookie=st->getCookieNameProps("_shibstate_");
272 st->setCookie(shib_cookie.first,CgiParse::url_encode(target) + shib_cookie.second);
273 req+="&target=cookie";
276 // Only omitted for 1.1 style requests.
278 req+="&providerId=" + CgiParse::url_encode(providerId);
280 return make_pair(true, st->sendRedirect(req));
283 pair<bool,void*> SAML1Consumer::run(ShibTarget* st, const IPropertySet* handler, bool isHandler)
286 string input,cookie,target,providerId;
287 const IApplication* app=st->getApplication();
289 // Right now, this only handles SAML 1.1.
290 pair<bool,const XMLCh*> binding=handler->getXMLString("Binding");
291 if (!binding.first || !XMLString::compareString(binding.second,SAMLBrowserProfile::BROWSER_POST)) {
292 if (strcasecmp(st->getRequestMethod(), "POST"))
293 throw FatalProfileException(
294 "SAML 1.1 Browser/POST handler does not support HTTP method ($1).", params(1,st->getRequestMethod())
297 if (!st->getContentType() || strcasecmp(st->getContentType(),"application/x-www-form-urlencoded"))
298 throw FatalProfileException(
299 "Blocked invalid content-type ($1) submitted to SAML 1.1 Browser/POST handler.", params(1,st->getContentType())
301 input=st->getPostData();
302 profile|=SAML11_POST;
304 else if (!XMLString::compareString(binding.second,SAMLBrowserProfile::BROWSER_ARTIFACT)) {
305 if (strcasecmp(st->getRequestMethod(), "GET"))
306 throw FatalProfileException(
307 "SAML 1.1 Browser/Artifact handler does not support HTTP method ($1).", params(1,st->getRequestMethod())
310 profile|=SAML11_ARTIFACT;
314 throw FatalProfileException("SAML 1.1 Browser Profile handler received no data from browser.");
316 string hURL=st->getHandlerURL(st->getRequestURL());
317 pair<bool,const char*> loc=handler->getString("Location");
318 string recipient=loc.first ? hURL + loc.second : hURL;
319 st->getConfig()->getListener()->sessionNew(
330 st->log(ShibTarget::LogLevelDebug, string("profile processing succeeded, new session created (") + cookie + ")");
332 if (target=="default") {
333 pair<bool,const char*> homeURL=app->getString("homeURL");
334 target=homeURL.first ? homeURL.second : "/";
336 else if (target=="cookie") {
337 // Pull the target value from the "relay state" cookie.
338 pair<string,const char*> relay_cookie = st->getCookieNameProps("_shibstate_");
339 const char* relay_state = st->getCookie(relay_cookie.first);
340 if (!relay_state || !*relay_state) {
341 // No apparent relay state value to use, so fall back on the default.
342 pair<bool,const char*> homeURL=app->getString("homeURL");
343 target=homeURL.first ? homeURL.second : "/";
346 char* rscopy=strdup(relay_state);
347 CgiParse::url_decode(rscopy);
353 // We've got a good session, set the session cookie.
354 pair<string,const char*> shib_cookie=st->getCookieNameProps("_shibsession_");
355 st->setCookie(shib_cookie.first, cookie + shib_cookie.second);
357 const IPropertySet* sessionProps=app->getPropertySet("Sessions");
358 pair<bool,bool> idpHistory=sessionProps->getBool("idpHistory");
359 if (!idpHistory.first || idpHistory.second) {
360 // Set an IdP history cookie locally (essentially just a CDC).
361 CommonDomainCookie cdc(st->getCookie(CommonDomainCookie::CDCName));
363 // Either leave in memory or set an expiration.
364 pair<bool,unsigned int> days=sessionProps->getUnsignedInt("idpHistoryDays");
365 if (!days.first || days.second==0)
366 st->setCookie(CommonDomainCookie::CDCName,string(cdc.set(providerId.c_str())) + shib_cookie.second);
368 time_t now=time(NULL) + (days.second * 24 * 60 * 60);
371 struct tm* ptime=gmtime_r(&now,&res);
373 struct tm* ptime=gmtime(&now);
376 strftime(timebuf,64,"%a, %d %b %Y %H:%M:%S GMT",ptime);
378 CommonDomainCookie::CDCName,
379 string(cdc.set(providerId.c_str())) + shib_cookie.second + "; expires=" + timebuf
384 // Now redirect to the target.
385 return make_pair(true, st->sendRedirect(target));
388 pair<bool,void*> ShibLogout::run(ShibTarget* st, const IPropertySet* handler, bool isHandler)
390 // Recover the session key.
391 pair<string,const char*> shib_cookie = st->getCookieNameProps("_shibsession_");
392 const char* session_id = st->getCookie(shib_cookie.first);
394 // Logout is best effort.
395 if (session_id && *session_id) {
397 st->getConfig()->getListener()->sessionEnd(st->getApplication(),session_id);
399 catch (SAMLException& e) {
400 st->log(ShibTarget::LogLevelError, string("logout processing failed with exception: ") + e.what());
404 st->log(ShibTarget::LogLevelError, "logout processing failed with unknown exception");
407 st->setCookie(shib_cookie.first,"");
410 string query=st->getArgs();
411 CgiParse parser(query.c_str(),query.length());
413 const char* ret=parser.get_value("return");
415 ret=handler->getString("ResponseLocation").second;
417 ret=st->getApplication()->getString("homeURL").second;
420 return make_pair(true, st->sendRedirect(ret));
423 /*************************************************************************
424 * CGI Parser implementation
427 CgiParse::CgiParse(const char* data, unsigned int len)
429 const char* pch = data;
430 unsigned int cl = len;
435 value=fmakeword('&',&cl,&pch);
438 name=makeword(value,'=');
444 CgiParse::~CgiParse()
446 for (map<string,char*>::iterator i=kvp_map.begin(); i!=kvp_map.end(); i++)
451 CgiParse::get_value(const char* name) const
453 map<string,char*>::const_iterator i=kvp_map.find(name);
454 if (i==kvp_map.end())
459 /* Parsing routines modified from NCSA source. */
461 CgiParse::makeword(char *line, char stop)
464 char *word = (char *) malloc(sizeof(char) * (strlen(line) + 1));
466 for(x=0;((line[x]) && (line[x] != stop));x++)
475 line[y++] = line[x++];
481 CgiParse::fmakeword(char stop, unsigned int *cl, const char** ppch)
489 word = (char *) malloc(sizeof(char) * (wsize + 1));
493 word[ll] = *((*ppch)++);
498 word = (char *)realloc(word,sizeof(char)*(wsize+1));
501 if((word[ll] == stop) || word[ll] == EOF || (!(*cl)))
513 CgiParse::plustospace(char *str)
518 if(str[x] == '+') str[x] = ' ';
522 CgiParse::x2c(char *what)
526 digit = (what[0] >= 'A' ? ((what[0] & 0xdf) - 'A')+10 : (what[0] - '0'));
528 digit += (what[1] >= 'A' ? ((what[1] & 0xdf) - 'A')+10 : (what[1] - '0'));
533 CgiParse::url_decode(char *url)
537 for(x=0,y=0;url[y];++x,++y)
539 if((url[x] = url[y]) == '%')
541 url[x] = x2c(&url[y+1]);
548 static inline char hexchar(unsigned short s)
550 return (s<=9) ? ('0' + s) : ('A' + s - 10);
553 string CgiParse::url_encode(const char* s)
555 static char badchars[]="\"\\+<>#%{}|^~[]`;/?:@=&";
559 if (strchr(badchars,*s) || *s<=0x1F || *s>=0x7F) {
561 ret+=hexchar(*s >> 4);
562 ret+=hexchar(*s & 0x0F);
570 // CDC implementation
572 const char CommonDomainCookie::CDCName[] = "_saml_idp";
574 CommonDomainCookie::CommonDomainCookie(const char* cookie)
579 Category& log=Category::getInstance(SHIBT_LOGCAT".CommonDomainCookie");
581 // Copy it so we can URL-decode it.
582 char* b64=strdup(cookie);
583 CgiParse::url_decode(b64);
585 // Chop it up and save off elements.
586 vector<string> templist;
589 while (*ptr && isspace(*ptr)) ptr++;
591 while (*end && !isspace(*end)) end++;
592 templist.push_back(string(ptr,end-ptr));
597 // Now Base64 decode the list.
598 for (vector<string>::iterator i=templist.begin(); i!=templist.end(); i++) {
600 XMLByte* decoded=Base64::decode(reinterpret_cast<const XMLByte*>(i->c_str()),&len);
601 if (decoded && *decoded) {
602 m_list.push_back(reinterpret_cast<char*>(decoded));
603 XMLString::release(&decoded);
606 log.warn("cookie element does not appear to be base64-encoded");
610 const char* CommonDomainCookie::set(const char* providerId)
612 // First scan the list for this IdP.
613 for (vector<string>::iterator i=m_list.begin(); i!=m_list.end(); i++) {
614 if (*i == providerId) {
620 // Append it to the end.
621 m_list.push_back(providerId);
623 // Now rebuild the delimited list.
625 for (vector<string>::const_iterator j=m_list.begin(); j!=m_list.end(); j++) {
626 if (!delimited.empty()) delimited += ' ';
629 XMLByte* b64=Base64::encode(reinterpret_cast<const XMLByte*>(j->c_str()),j->length(),&len);
631 for (pos=b64, pos2=b64; *pos2; pos2++)
636 delimited += reinterpret_cast<char*>(b64);
637 XMLString::release(&b64);
640 m_encoded=CgiParse::url_encode(delimited.c_str());
641 return m_encoded.c_str();