2 * Copyright 2001-2005 Internet2
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
18 * shib-handlers.cpp -- profile handlers that plug into SP
30 #include <shib/shib-threads.h>
31 #include <xercesc/util/Base64.hpp>
33 #ifndef HAVE_STRCASECMP
34 # define strcasecmp stricmp
39 using namespace shibboleth;
40 using namespace shibtarget;
41 using namespace shibtarget::logging;
47 CgiParse(const char* data, unsigned int len);
49 const char* get_value(const char* name) const;
51 static char x2c(char *what);
52 static void url_decode(char *url);
53 static string url_encode(const char* s);
55 char * fmakeword(char stop, unsigned int *cl, const char** ppch);
56 char * makeword(char *line, char stop);
57 void plustospace(char *str);
59 map<string,char*> kvp_map;
62 // Helper class for SAML 2.0 Common Domain Cookie operations
63 class CommonDomainCookie
66 CommonDomainCookie(const char* cookie);
67 ~CommonDomainCookie() {}
68 saml::Iterator<std::string> get() {return m_list;}
69 const char* set(const char* providerId);
70 static const char CDCName[];
72 std::string m_encoded;
73 std::vector<std::string> m_list;
76 class SessionInitiator : virtual public IHandler
79 SessionInitiator(const DOMElement* e) {}
80 ~SessionInitiator() {}
81 pair<bool,void*> run(ShibTarget* st, const IPropertySet* handler, bool isHandler=true);
82 pair<bool,void*> ShibAuthnRequest(
84 const IPropertySet* shire,
87 const char* providerId
91 class SAML1Consumer : virtual public IHandler
94 SAML1Consumer(const DOMElement* e) {}
96 pair<bool,void*> run(ShibTarget* st, const IPropertySet* handler, bool isHandler=true);
99 class ShibLogout : virtual public IHandler
102 ShibLogout(const DOMElement* e) {}
104 pair<bool,void*> run(ShibTarget* st, const IPropertySet* handler, bool isHandler=true);
109 IPlugIn* ShibSessionInitiatorFactory(const DOMElement* e)
111 return new SessionInitiator(e);
114 IPlugIn* SAML1POSTFactory(const DOMElement* e)
116 return new SAML1Consumer(e);
119 IPlugIn* SAML1ArtifactFactory(const DOMElement* e)
121 return new SAML1Consumer(e);
124 IPlugIn* ShibLogoutFactory(const DOMElement* e)
126 return new ShibLogout(e);
129 pair<bool,void*> SessionInitiator::run(ShibTarget* st, const IPropertySet* handler, bool isHandler)
132 const char* resource=NULL;
133 const IPropertySet* ACS=NULL;
134 const IApplication* app=st->getApplication();
138 * Binding is CGI query string with:
139 * target the resource to direct back to later
140 * acsIndex optional index of an ACS to use on the way back in
141 * providerId optional direct invocation of a specific IdP
143 string query=st->getArgs();
144 CgiParse parser(query.c_str(),query.length());
146 const char* option=parser.get_value("acsIndex");
148 ACS=app->getAssertionConsumerServiceByIndex(atoi(option));
149 option=parser.get_value("providerId");
151 resource=parser.get_value("target");
152 if (!resource || !*resource) {
153 pair<bool,const char*> home=app->getString("homeURL");
155 resource=home.second;
157 throw FatalProfileException("Session initiator requires a target parameter or a homeURL application property.");
160 dupresource=resource;
161 resource=dupresource.c_str();
165 // Here we actually use metadata to invoke the SSO service directly.
166 // The only currently understood binding is the Shibboleth profile.
167 Metadata m(app->getMetadataProviders());
168 const IEntityDescriptor* entity=m.lookup(option);
170 throw MetadataException("Session initiator unable to locate metadata for provider ($1).", params(1,option));
171 const IIDPSSODescriptor* role=entity->getIDPSSODescriptor(Constants::SHIB_NS);
173 throw MetadataException(
174 "Session initiator unable to locate a Shibboleth-aware identity provider role for provider ($1).", params(1,option)
176 const IEndpointManager* SSO=role->getSingleSignOnServiceManager();
177 const IEndpoint* ep=SSO->getEndpointByBinding(Constants::SHIB_AUTHNREQUEST_PROFILE_URI);
179 throw MetadataException(
180 "Session initiator unable to locate compatible SSO service for provider ($1).", params(1,option)
182 auto_ptr_char dest(ep->getLocation());
183 return ShibAuthnRequest(
184 st,ACS ? ACS : app->getDefaultAssertionConsumerService(),dest.get(),resource,app->getString("providerId").second
189 // We're running as a "virtual handler" from within the filter.
190 // The target resource is the current one and everything else is defaulted.
191 resource=st->getRequestURL();
194 if (!ACS) ACS=app->getDefaultAssertionConsumerService();
196 // For now, we only support external session initiation via a wayfURL
197 pair<bool,const char*> wayfURL=handler->getString("wayfURL");
199 throw ConfigurationException("Session initiator is missing wayfURL property.");
201 pair<bool,const XMLCh*> wayfBinding=handler->getXMLString("wayfBinding");
202 if (!wayfBinding.first || !XMLString::compareString(wayfBinding.second,Constants::SHIB_AUTHNREQUEST_PROFILE_URI))
204 return ShibAuthnRequest(st,ACS,wayfURL.second,resource,app->getString("providerId").second);
205 else if (!XMLString::compareString(wayfBinding.second,Constants::SHIB_LEGACY_AUTHNREQUEST_PROFILE_URI))
207 return ShibAuthnRequest(st,ACS,wayfURL.second,resource,NULL);
208 else if (!strcmp(handler->getString("wayfBinding").second,"urn:mace:shibboleth:1.0:profiles:EAuth")) {
209 // TODO: Finalize E-Auth profile URI
210 pair<bool,bool> localRelayState=st->getConfig()->getPropertySet("Local")->getBool("localRelayState");
211 if (!localRelayState.first || !localRelayState.second)
212 throw ConfigurationException("E-Authn requests cannot include relay state, so localRelayState must be enabled.");
214 // Here we store the state in a cookie.
215 pair<string,const char*> shib_cookie=st->getCookieNameProps("_shibstate_");
216 st->setCookie(shib_cookie.first,CgiParse::url_encode(resource) + shib_cookie.second);
217 return make_pair(true, st->sendRedirect(wayfURL.second));
220 throw UnsupportedProfileException("Unsupported WAYF binding ($1).", params(1,handler->getString("wayfBinding").second));
223 // Handles Shib 1.x AuthnRequest profile.
224 pair<bool,void*> SessionInitiator::ShibAuthnRequest(
226 const IPropertySet* shire,
229 const char* providerId
232 // Compute the ACS URL. We add the ACS location to the handler baseURL.
233 // Legacy configs will not have an ACS specified, so no suffix will be added.
234 string ACSloc=st->getHandlerURL(target);
235 if (shire) ACSloc+=shire->getString("Location").second;
238 sprintf(timebuf,"%lu",time(NULL));
239 string req=string(dest);
240 req += strchr(dest,'?') ? '&' : '?';
241 req = req + "shire=" + CgiParse::url_encode(ACSloc.c_str()) + "&time=" + timebuf;
243 // How should the resource value be preserved?
244 pair<bool,bool> localRelayState=st->getConfig()->getPropertySet("Local")->getBool("localRelayState");
245 if (!localRelayState.first || !localRelayState.second) {
246 // The old way, just send it along.
247 req+="&target=" + CgiParse::url_encode(target);
250 // Here we store the state in a cookie and send a fixed
251 // value to the IdP so we can recognize it on the way back.
252 pair<string,const char*> shib_cookie=st->getCookieNameProps("_shibstate_");
253 st->setCookie(shib_cookie.first,CgiParse::url_encode(target) + shib_cookie.second);
254 req+="&target=cookie";
257 // Only omitted for 1.1 style requests.
259 req+="&providerId=" + CgiParse::url_encode(providerId);
261 return make_pair(true, st->sendRedirect(req));
264 pair<bool,void*> SAML1Consumer::run(ShibTarget* st, const IPropertySet* handler, bool isHandler)
267 string input,cookie,target,providerId;
268 const IApplication* app=st->getApplication();
270 // Supports either version...
271 pair<bool,unsigned int> version=handler->getUnsignedInt("MinorVersion","urn:oasis:names:tc:SAML:1.0:protocol");
275 pair<bool,const XMLCh*> binding=handler->getXMLString("Binding");
276 if (!binding.first || !XMLString::compareString(binding.second,SAMLBrowserProfile::BROWSER_POST)) {
277 if (strcasecmp(st->getRequestMethod(), "POST"))
278 throw FatalProfileException(
279 "SAML 1.x Browser/POST handler does not support HTTP method ($1).", params(1,st->getRequestMethod())
282 if (!st->getContentType() || strcasecmp(st->getContentType(),"application/x-www-form-urlencoded"))
283 throw FatalProfileException(
284 "Blocked invalid content-type ($1) submitted to SAML 1.x Browser/POST handler.", params(1,st->getContentType())
286 input=st->getPostData();
287 profile|=(version.second==1 ? SAML11_POST : SAML10_POST);
289 else if (!XMLString::compareString(binding.second,SAMLBrowserProfile::BROWSER_ARTIFACT)) {
290 if (strcasecmp(st->getRequestMethod(), "GET"))
291 throw FatalProfileException(
292 "SAML 1.x Browser/Artifact handler does not support HTTP method ($1).", params(1,st->getRequestMethod())
295 profile|=(version.second==1 ? SAML11_ARTIFACT : SAML10_ARTIFACT);
299 throw FatalProfileException("SAML 1.x Browser Profile handler received no data from browser.");
301 string hURL=st->getHandlerURL(st->getRequestURL());
302 pair<bool,const char*> loc=handler->getString("Location");
303 string recipient=loc.first ? hURL + loc.second : hURL;
304 st->getConfig()->getListener()->sessionNew(
315 st->log(ShibTarget::LogLevelDebug, string("profile processing succeeded, new session created (") + cookie + ")");
317 if (target=="default") {
318 pair<bool,const char*> homeURL=app->getString("homeURL");
319 target=homeURL.first ? homeURL.second : "/";
321 else if (target=="cookie" || target.empty()) {
322 // Pull the target value from the "relay state" cookie.
323 pair<string,const char*> relay_cookie = st->getCookieNameProps("_shibstate_");
324 const char* relay_state = st->getCookie(relay_cookie.first);
325 if (!relay_state || !*relay_state) {
326 // No apparent relay state value to use, so fall back on the default.
327 pair<bool,const char*> homeURL=app->getString("homeURL");
328 target=homeURL.first ? homeURL.second : "/";
331 char* rscopy=strdup(relay_state);
332 CgiParse::url_decode(rscopy);
338 // We've got a good session, set the session cookie.
339 pair<string,const char*> shib_cookie=st->getCookieNameProps("_shibsession_");
340 st->setCookie(shib_cookie.first, cookie + shib_cookie.second);
342 const IPropertySet* sessionProps=app->getPropertySet("Sessions");
343 pair<bool,bool> idpHistory=sessionProps->getBool("idpHistory");
344 if (!idpHistory.first || idpHistory.second) {
345 // Set an IdP history cookie locally (essentially just a CDC).
346 CommonDomainCookie cdc(st->getCookie(CommonDomainCookie::CDCName));
348 // Either leave in memory or set an expiration.
349 pair<bool,unsigned int> days=sessionProps->getUnsignedInt("idpHistoryDays");
350 if (!days.first || days.second==0)
351 st->setCookie(CommonDomainCookie::CDCName,string(cdc.set(providerId.c_str())) + shib_cookie.second);
353 time_t now=time(NULL) + (days.second * 24 * 60 * 60);
356 struct tm* ptime=gmtime_r(&now,&res);
358 struct tm* ptime=gmtime(&now);
361 strftime(timebuf,64,"%a, %d %b %Y %H:%M:%S GMT",ptime);
363 CommonDomainCookie::CDCName,
364 string(cdc.set(providerId.c_str())) + shib_cookie.second + "; expires=" + timebuf
369 // Now redirect to the target.
370 return make_pair(true, st->sendRedirect(target));
373 pair<bool,void*> ShibLogout::run(ShibTarget* st, const IPropertySet* handler, bool isHandler)
375 // Recover the session key.
376 pair<string,const char*> shib_cookie = st->getCookieNameProps("_shibsession_");
377 const char* session_id = st->getCookie(shib_cookie.first);
379 // Logout is best effort.
380 if (session_id && *session_id) {
382 st->getConfig()->getListener()->sessionEnd(st->getApplication(),session_id);
384 catch (SAMLException& e) {
385 st->log(ShibTarget::LogLevelError, string("logout processing failed with exception: ") + e.what());
389 st->log(ShibTarget::LogLevelError, "logout processing failed with unknown exception");
392 // We send the cookie property alone, which acts as an empty value.
393 st->setCookie(shib_cookie.first,shib_cookie.second);
396 string query=st->getArgs();
397 CgiParse parser(query.c_str(),query.length());
399 const char* ret=parser.get_value("return");
401 ret=handler->getString("ResponseLocation").second;
403 ret=st->getApplication()->getString("homeURL").second;
406 return make_pair(true, st->sendRedirect(ret));
409 /*************************************************************************
410 * CGI Parser implementation
413 CgiParse::CgiParse(const char* data, unsigned int len)
415 const char* pch = data;
416 unsigned int cl = len;
421 value=fmakeword('&',&cl,&pch);
424 name=makeword(value,'=');
430 CgiParse::~CgiParse()
432 for (map<string,char*>::iterator i=kvp_map.begin(); i!=kvp_map.end(); i++)
437 CgiParse::get_value(const char* name) const
439 map<string,char*>::const_iterator i=kvp_map.find(name);
440 if (i==kvp_map.end())
445 /* Parsing routines modified from NCSA source. */
447 CgiParse::makeword(char *line, char stop)
450 char *word = (char *) malloc(sizeof(char) * (strlen(line) + 1));
452 for(x=0;((line[x]) && (line[x] != stop));x++)
461 line[y++] = line[x++];
467 CgiParse::fmakeword(char stop, unsigned int *cl, const char** ppch)
475 word = (char *) malloc(sizeof(char) * (wsize + 1));
479 word[ll] = *((*ppch)++);
484 word = (char *)realloc(word,sizeof(char)*(wsize+1));
487 if((word[ll] == stop) || word[ll] == EOF || (!(*cl)))
499 CgiParse::plustospace(char *str)
504 if(str[x] == '+') str[x] = ' ';
508 CgiParse::x2c(char *what)
512 digit = (what[0] >= 'A' ? ((what[0] & 0xdf) - 'A')+10 : (what[0] - '0'));
514 digit += (what[1] >= 'A' ? ((what[1] & 0xdf) - 'A')+10 : (what[1] - '0'));
519 CgiParse::url_decode(char *url)
523 for(x=0,y=0;url[y];++x,++y)
525 if((url[x] = url[y]) == '%' && isxdigit(url[y+1]) && isxdigit(url[y+2]))
527 url[x] = x2c(&url[y+1]);
534 static inline char hexchar(unsigned short s)
536 return (s<=9) ? ('0' + s) : ('A' + s - 10);
539 string CgiParse::url_encode(const char* s)
541 static char badchars[]="\"\\+<>#%{}|^~[](),'`;/?:@=&";
545 if (strchr(badchars,*s) || *s<=0x20 || *s>=0x7F) {
547 ret+=hexchar((unsigned char)*s >> 4);
548 ret+=hexchar((unsigned char)*s & 0x0F);
556 // CDC implementation
558 const char CommonDomainCookie::CDCName[] = "_saml_idp";
560 CommonDomainCookie::CommonDomainCookie(const char* cookie)
565 Category& log=Category::getInstance(SHIBT_LOGCAT".CommonDomainCookie");
567 // Copy it so we can URL-decode it.
568 char* b64=strdup(cookie);
569 CgiParse::url_decode(b64);
571 // Chop it up and save off elements.
572 vector<string> templist;
575 while (*ptr && isspace(*ptr)) ptr++;
577 while (*end && !isspace(*end)) end++;
578 templist.push_back(string(ptr,end-ptr));
583 // Now Base64 decode the list.
584 for (vector<string>::iterator i=templist.begin(); i!=templist.end(); i++) {
586 XMLByte* decoded=Base64::decode(reinterpret_cast<const XMLByte*>(i->c_str()),&len);
587 if (decoded && *decoded) {
588 m_list.push_back(reinterpret_cast<char*>(decoded));
589 XMLString::release(&decoded);
592 log.warn("cookie element does not appear to be base64-encoded");
596 const char* CommonDomainCookie::set(const char* providerId)
598 // First scan the list for this IdP.
599 for (vector<string>::iterator i=m_list.begin(); i!=m_list.end(); i++) {
600 if (*i == providerId) {
606 // Append it to the end.
607 m_list.push_back(providerId);
609 // Now rebuild the delimited list.
611 for (vector<string>::const_iterator j=m_list.begin(); j!=m_list.end(); j++) {
612 if (!delimited.empty()) delimited += ' ';
615 XMLByte* b64=Base64::encode(reinterpret_cast<const XMLByte*>(j->c_str()),j->length(),&len);
617 for (pos=b64, pos2=b64; *pos2; pos2++)
622 delimited += reinterpret_cast<char*>(b64);
623 XMLString::release(&b64);
626 m_encoded=CgiParse::url_encode(delimited.c_str());
627 return m_encoded.c_str();