https://issues.shibboleth.net/jira/browse/SSPCPP-239
[shibboleth/cpp-sp.git] / shib-target / shib-handlers.cpp
1 /*
2  *  Copyright 2001-2005 Internet2
3  * 
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
7  *
8  *     http://www.apache.org/licenses/LICENSE-2.0
9  *
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.
15  */
16
17 /*
18  * shib-handlers.cpp -- profile handlers that plug into SP
19  *
20  * Scott Cantor
21  * 5/17/2005
22  */
23
24 #include "internal.h"
25
26 #ifdef HAVE_UNISTD_H
27 # include <unistd.h>
28 #endif
29
30 #include <shib/shib-threads.h>
31 #include <xercesc/util/Base64.hpp>
32
33 #ifndef HAVE_STRCASECMP
34 # define strcasecmp stricmp
35 #endif
36
37 using namespace std;
38 using namespace saml;
39 using namespace shibboleth;
40 using namespace shibtarget;
41 using namespace shibtarget::logging;
42
43 namespace {
44   class CgiParse
45   {
46   public:
47     CgiParse(const char* data, unsigned int len);
48     ~CgiParse();
49     const char* get_value(const char* name) const;
50     
51     static char x2c(char *what);
52     static void url_decode(char *url);
53     static string url_encode(const char* s);
54   private:
55     char * fmakeword(char stop, unsigned int *cl, const char** ppch);
56     char * makeword(char *line, char stop);
57     void plustospace(char *str);
58
59     map<string,char*> kvp_map;
60   };
61
62     // Helper class for SAML 2.0 Common Domain Cookie operations
63     class CommonDomainCookie
64     {
65     public:
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[];
71     private:
72         std::string m_encoded;
73         std::vector<std::string> m_list;
74     };
75
76   class SessionInitiator : virtual public IHandler
77   {
78   public:
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(
83         ShibTarget* st,
84         const IPropertySet* shire,
85         const char* dest,
86         const char* target,
87         const char* providerId
88         );
89   };
90
91   class SAML1Consumer : virtual public IHandler
92   {
93   public:
94     SAML1Consumer(const DOMElement* e) {}
95     ~SAML1Consumer() {}
96     pair<bool,void*> run(ShibTarget* st, const IPropertySet* handler, bool isHandler=true);
97   };
98
99   class ShibLogout : virtual public IHandler
100   {
101   public:
102     ShibLogout(const DOMElement* e) {}
103     ~ShibLogout() {}
104     pair<bool,void*> run(ShibTarget* st, const IPropertySet* handler, bool isHandler=true);
105   };
106 }
107
108
109 IPlugIn* ShibSessionInitiatorFactory(const DOMElement* e)
110 {
111     return new SessionInitiator(e);
112 }
113
114 IPlugIn* SAML1POSTFactory(const DOMElement* e)
115 {
116     return new SAML1Consumer(e);
117 }
118
119 IPlugIn* SAML1ArtifactFactory(const DOMElement* e)
120 {
121     return new SAML1Consumer(e);
122 }
123
124 IPlugIn* ShibLogoutFactory(const DOMElement* e)
125 {
126     return new ShibLogout(e);
127 }
128
129 pair<bool,void*> SessionInitiator::run(ShibTarget* st, const IPropertySet* handler, bool isHandler)
130 {
131     string dupresource;
132     const char* resource=NULL;
133     const IPropertySet* ACS=NULL;
134     const IApplication* app=st->getApplication();
135     
136     if (isHandler) {
137         /* 
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
142          */
143         string query=st->getArgs();
144         CgiParse parser(query.c_str(),query.length());
145
146         const char* option=parser.get_value("acsIndex");
147         if (option)
148             ACS=app->getAssertionConsumerServiceByIndex(atoi(option));
149         option=parser.get_value("providerId");
150         
151         resource=parser.get_value("target");
152         if (!resource || !*resource) {
153             pair<bool,const char*> home=app->getString("homeURL");
154             if (home.first)
155                 resource=home.second;
156             else
157                 throw FatalProfileException("Session initiator requires a target parameter or a homeURL application property.");
158         }
159         else if (!option) {
160             dupresource=resource;
161             resource=dupresource.c_str();
162         }
163         
164         if (option) {
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);
169             if (!entity)
170                 throw MetadataException("Session initiator unable to locate metadata for provider ($1).", params(1,option));
171             const IIDPSSODescriptor* role=entity->getIDPSSODescriptor(Constants::SHIB_NS);
172             if (!role)
173                 throw MetadataException(
174                     "Session initiator unable to locate a Shibboleth-aware identity provider role for provider ($1).", params(1,option)
175                     );
176             const IEndpointManager* SSO=role->getSingleSignOnServiceManager();
177             const IEndpoint* ep=SSO->getEndpointByBinding(Constants::SHIB_AUTHNREQUEST_PROFILE_URI);
178             if (!ep)
179                 throw MetadataException(
180                     "Session initiator unable to locate compatible SSO service for provider ($1).", params(1,option)
181                     );
182             auto_ptr_char dest(ep->getLocation());
183             return ShibAuthnRequest(
184                 st,ACS ? ACS : app->getDefaultAssertionConsumerService(),dest.get(),resource,app->getString("providerId").second
185                 );
186         }
187     }
188     else {
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();
192     }
193     
194     if (!ACS) ACS=app->getDefaultAssertionConsumerService();
195     
196     // For now, we only support external session initiation via a wayfURL
197     pair<bool,const char*> wayfURL=handler->getString("wayfURL");
198     if (!wayfURL.first)
199         throw ConfigurationException("Session initiator is missing wayfURL property.");
200
201     pair<bool,const XMLCh*> wayfBinding=handler->getXMLString("wayfBinding");
202     if (!wayfBinding.first || !XMLString::compareString(wayfBinding.second,Constants::SHIB_AUTHNREQUEST_PROFILE_URI))
203         // Standard Shib 1.x
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))
206         // Shib pre-1.2
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.");
213
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));
218     }
219    
220     throw UnsupportedProfileException("Unsupported WAYF binding ($1).", params(1,handler->getString("wayfBinding").second));
221 }
222
223 // Handles Shib 1.x AuthnRequest profile.
224 pair<bool,void*> SessionInitiator::ShibAuthnRequest(
225     ShibTarget* st,
226     const IPropertySet* shire,
227     const char* dest,
228     const char* target,
229     const char* providerId
230     )
231 {
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;
236     
237     char timebuf[16];
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;
242
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);
248     }
249     else {
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";
255     }
256     
257     // Only omitted for 1.1 style requests.
258     if (providerId)
259         req+="&providerId=" + CgiParse::url_encode(providerId);
260
261     return make_pair(true, st->sendRedirect(req));
262 }
263
264 pair<bool,void*> SAML1Consumer::run(ShibTarget* st, const IPropertySet* handler, bool isHandler)
265 {
266     int profile=0;
267     string input,cookie,target,providerId;
268     const IApplication* app=st->getApplication();
269     
270     // Supports either version...
271     pair<bool,unsigned int> version=handler->getUnsignedInt("MinorVersion","urn:oasis:names:tc:SAML:1.0:protocol");
272     if (!version.first)
273         version.second=1;
274
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())
280                 );
281         
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())
285                 );
286         input=st->getPostData();
287         profile|=(version.second==1 ? SAML11_POST : SAML10_POST);
288     }
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())
293                 );
294         input=st->getArgs();
295         profile|=(version.second==1 ? SAML11_ARTIFACT : SAML10_ARTIFACT);
296     }
297     
298     if (input.empty())
299         throw FatalProfileException("SAML 1.x Browser Profile handler received no data from browser.");
300     
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(
305         app,
306         profile,
307         recipient.c_str(),
308         input.c_str(),
309         st->getRemoteAddr(),
310         target,
311         cookie,
312         providerId
313         );
314
315     st->log(ShibTarget::LogLevelDebug, string("profile processing succeeded, new session created (") + cookie + ")");
316
317     if (target=="default") {
318         pair<bool,const char*> homeURL=app->getString("homeURL");
319         target=homeURL.first ? homeURL.second : "/";
320     }
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 : "/";
329         }
330         else {
331             char* rscopy=strdup(relay_state);
332             CgiParse::url_decode(rscopy);
333             target=rscopy;
334             free(rscopy);
335         }
336     }
337
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);
341
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));
347
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);
352             else {
353                 time_t now=time(NULL) + (days.second * 24 * 60 * 60);
354 #ifdef HAVE_GMTIME_R
355                 struct tm res;
356                 struct tm* ptime=gmtime_r(&now,&res);
357 #else
358                 struct tm* ptime=gmtime(&now);
359 #endif
360                 char timebuf[64];
361                 strftime(timebuf,64,"%a, %d %b %Y %H:%M:%S GMT",ptime);
362                 st->setCookie(
363                     CommonDomainCookie::CDCName,
364                     string(cdc.set(providerId.c_str())) + shib_cookie.second + "; expires=" + timebuf
365                     );
366         }
367     }
368
369     // Now redirect to the target.
370     return make_pair(true, st->sendRedirect(target));
371 }
372
373 pair<bool,void*> ShibLogout::run(ShibTarget* st, const IPropertySet* handler, bool isHandler)
374 {
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);
378     
379     // Logout is best effort.
380     if (session_id && *session_id) {
381         try {
382             st->getConfig()->getListener()->sessionEnd(st->getApplication(),session_id);
383         }
384         catch (SAMLException& e) {
385             st->log(ShibTarget::LogLevelError, string("logout processing failed with exception: ") + e.what());
386         }
387 #ifndef _DEBUG
388         catch (...) {
389             st->log(ShibTarget::LogLevelError, "logout processing failed with unknown exception");
390         }
391 #endif
392         // We send the cookie property alone, which acts as an empty value.
393         st->setCookie(shib_cookie.first,shib_cookie.second);
394     }
395     
396     string query=st->getArgs();
397     CgiParse parser(query.c_str(),query.length());
398
399     const char* ret=parser.get_value("return");
400     if (!ret)
401         ret=handler->getString("ResponseLocation").second;
402     if (!ret)
403         ret=st->getApplication()->getString("homeURL").second;
404     if (!ret)
405         ret="/";
406     return make_pair(true, st->sendRedirect(ret));
407 }
408
409 /*************************************************************************
410  * CGI Parser implementation
411  */
412
413 CgiParse::CgiParse(const char* data, unsigned int len)
414 {
415     const char* pch = data;
416     unsigned int cl = len;
417         
418     while (cl && pch) {
419         char *name;
420         char *value;
421         value=fmakeword('&',&cl,&pch);
422         plustospace(value);
423         url_decode(value);
424         name=makeword(value,'=');
425         kvp_map[name]=value;
426         free(name);
427     }
428 }
429
430 CgiParse::~CgiParse()
431 {
432     for (map<string,char*>::iterator i=kvp_map.begin(); i!=kvp_map.end(); i++)
433         free(i->second);
434 }
435
436 const char*
437 CgiParse::get_value(const char* name) const
438 {
439     map<string,char*>::const_iterator i=kvp_map.find(name);
440     if (i==kvp_map.end())
441         return NULL;
442     return i->second;
443 }
444
445 /* Parsing routines modified from NCSA source. */
446 char *
447 CgiParse::makeword(char *line, char stop)
448 {
449     int x = 0,y;
450     char *word = (char *) malloc(sizeof(char) * (strlen(line) + 1));
451
452     for(x=0;((line[x]) && (line[x] != stop));x++)
453         word[x] = line[x];
454
455     word[x] = '\0';
456     if(line[x])
457         ++x;
458     y=0;
459
460     while(line[x])
461       line[y++] = line[x++];
462     line[y] = '\0';
463     return word;
464 }
465
466 char *
467 CgiParse::fmakeword(char stop, unsigned int *cl, const char** ppch)
468 {
469     int wsize;
470     char *word;
471     int ll;
472
473     wsize = 1024;
474     ll=0;
475     word = (char *) malloc(sizeof(char) * (wsize + 1));
476
477     while(1)
478     {
479         word[ll] = *((*ppch)++);
480         if(ll==wsize-1)
481         {
482             word[ll+1] = '\0';
483             wsize+=1024;
484             word = (char *)realloc(word,sizeof(char)*(wsize+1));
485         }
486         --(*cl);
487         if((word[ll] == stop) || word[ll] == EOF || (!(*cl)))
488         {
489             if(word[ll] != stop)
490                 ll++;
491             word[ll] = '\0';
492             return word;
493         }
494         ++ll;
495     }
496 }
497
498 void
499 CgiParse::plustospace(char *str)
500 {
501     register int x;
502
503     for(x=0;str[x];x++)
504         if(str[x] == '+') str[x] = ' ';
505 }
506
507 char
508 CgiParse::x2c(char *what)
509 {
510     register char digit;
511
512     digit = (what[0] >= 'A' ? ((what[0] & 0xdf) - 'A')+10 : (what[0] - '0'));
513     digit *= 16;
514     digit += (what[1] >= 'A' ? ((what[1] & 0xdf) - 'A')+10 : (what[1] - '0'));
515     return(digit);
516 }
517
518 void
519 CgiParse::url_decode(char *url)
520 {
521     register int x,y;
522
523     for(x=0,y=0;url[y];++x,++y)
524     {
525         if((url[x] = url[y]) == '%' && isxdigit(url[y+1]) && isxdigit(url[y+2]))
526         {
527             url[x] = x2c(&url[y+1]);
528             y+=2;
529         }
530     }
531     url[x] = '\0';
532 }
533
534 static inline char hexchar(unsigned short s)
535 {
536     return (s<=9) ? ('0' + s) : ('A' + s - 10);
537 }
538
539 string CgiParse::url_encode(const char* s)
540 {
541     static char badchars[]="\"\\+<>#%{}|^~[](),'`;/?:@=&";
542
543     string ret;
544     for (; *s; s++) {
545         if (strchr(badchars,*s) || *s<=0x20 || *s>=0x7F) {
546             ret+='%';
547             ret+=hexchar((unsigned char)*s >> 4);
548             ret+=hexchar((unsigned char)*s & 0x0F);
549         }
550         else
551             ret+=*s;
552     }
553     return ret;
554 }
555
556 // CDC implementation
557
558 const char CommonDomainCookie::CDCName[] = "_saml_idp";
559
560 CommonDomainCookie::CommonDomainCookie(const char* cookie)
561 {
562     if (!cookie)
563         return;
564
565     Category& log=Category::getInstance(SHIBT_LOGCAT".CommonDomainCookie");
566
567     // Copy it so we can URL-decode it.
568     char* b64=strdup(cookie);
569     CgiParse::url_decode(b64);
570
571     // Chop it up and save off elements.
572     vector<string> templist;
573     char* ptr=b64;
574     while (*ptr) {
575         while (*ptr && isspace(*ptr)) ptr++;
576         char* end=ptr;
577         while (*end && !isspace(*end)) end++;
578         templist.push_back(string(ptr,end-ptr));
579         ptr=end;
580     }
581     free(b64);
582
583     // Now Base64 decode the list.
584     for (vector<string>::iterator i=templist.begin(); i!=templist.end(); i++) {
585         unsigned int len;
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);
590         }
591         else
592             log.warn("cookie element does not appear to be base64-encoded");
593     }
594 }
595
596 const char* CommonDomainCookie::set(const char* providerId)
597 {
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) {
601             m_list.erase(i);
602             break;
603         }
604     }
605     
606     // Append it to the end.
607     m_list.push_back(providerId);
608     
609     // Now rebuild the delimited list.
610     string delimited;
611     for (vector<string>::const_iterator j=m_list.begin(); j!=m_list.end(); j++) {
612         if (!delimited.empty()) delimited += ' ';
613         
614         unsigned int len;
615         XMLByte* b64=Base64::encode(reinterpret_cast<const XMLByte*>(j->c_str()),j->length(),&len);
616         XMLByte *pos, *pos2;
617         for (pos=b64, pos2=b64; *pos2; pos2++)
618             if (isgraph(*pos2))
619                 *pos++=*pos2;
620         *pos=0;
621         
622         delimited += reinterpret_cast<char*>(b64);
623         XMLString::release(&b64);
624     }
625     
626     m_encoded=CgiParse::url_encode(delimited.c_str());
627     return m_encoded.c_str();
628 }