Fix log4shib/log4cpp link check.
[shibboleth/cpp-sp.git] / adfs / 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  * handlers.cpp -- ADFS-aware profile handlers that plug into SP
19  *
20  * Scott Cantor
21  * 10/10/2005
22  */
23
24 #include "internal.h"
25
26 #ifndef HAVE_STRCASECMP
27 # define strcasecmp stricmp
28 #endif
29
30 using namespace std;
31 using namespace saml;
32 using namespace shibboleth;
33 using namespace shibtarget;
34 using namespace adfs;
35 using namespace adfs::logging;
36
37 namespace {
38   
39   // TODO: Refactor/extend API so I don't have to cut/paste this code out of libshib-target
40   class SessionInitiator : virtual public IHandler
41   {
42   public:
43     SessionInitiator(const DOMElement* e) {}
44     ~SessionInitiator() {}
45     pair<bool,void*> run(ShibTarget* st, const IPropertySet* handler, bool isHandler=true);
46   
47   private:
48     const IPropertySet* getCompatibleACS(const IApplication* app, const vector<ShibProfile>& profiles);
49     pair<bool,void*> ShibAuthnRequest(
50         ShibTarget* st,
51         const IPropertySet* shire,
52         const char* dest,
53         const char* target,
54         const char* providerId
55         );
56     pair<bool,void*> ADFSAuthnRequest(
57         ShibTarget* st,
58         const IPropertySet* shire,
59         const char* dest,
60         const char* target,
61         const char* providerId
62         );
63   };
64
65   class ADFSHandler : virtual public IHandler
66   {
67   public:
68     ADFSHandler(const DOMElement* e) {}
69     ~ADFSHandler() {}
70     pair<bool,void*> run(ShibTarget* st, const IPropertySet* handler, bool isHandler=true);
71   };
72 }
73
74
75 IPlugIn* ADFSSessionInitiatorFactory(const DOMElement* e)
76 {
77     return new SessionInitiator(e);
78 }
79
80 IPlugIn* ADFSHandlerFactory(const DOMElement* e)
81 {
82     return new ADFSHandler(e);
83 }
84
85 pair<bool,void*> SessionInitiator::run(ShibTarget* st, const IPropertySet* handler, bool isHandler)
86 {
87     string dupresource;
88     const char* resource=NULL;
89     const IPropertySet* ACS=NULL;
90     const IApplication* app=st->getApplication();
91     
92     if (isHandler) {
93         /* 
94          * Binding is CGI query string with:
95          *  target      the resource to direct back to later
96          *  acsIndex    optional index of an ACS to use on the way back in
97          *  providerId  optional direct invocation of a specific IdP
98          */
99         string query=st->getArgs();
100         CgiParse parser(query.c_str(),query.length());
101
102         const char* option=parser.get_value("acsIndex");
103         if (option)
104             ACS=app->getAssertionConsumerServiceByIndex(atoi(option));
105         option=parser.get_value("providerId");
106         
107         resource=parser.get_value("target");
108         if (!resource || !*resource) {
109             pair<bool,const char*> home=app->getString("homeURL");
110             if (home.first)
111                 resource=home.second;
112             else
113                 throw FatalProfileException("Session initiator requires a target parameter or a homeURL application property.");
114         }
115         else if (!option) {
116             dupresource=resource;
117             resource=dupresource.c_str();
118         }
119         
120         if (option) {
121             // Here we actually use metadata to invoke the SSO service directly.
122             Metadata m(app->getMetadataProviders());
123             const IEntityDescriptor* entity=m.lookup(option);
124             if (!entity)
125                 throw MetadataException("Session initiator unable to locate metadata for provider ($1).", params(1,option));
126
127             // Look for an IdP role with Shib support.
128             const IIDPSSODescriptor* role=entity->getIDPSSODescriptor(Constants::SHIB_NS);
129             if (role) {
130                 // Look for a SSO endpoint with Shib support.
131                 const IEndpointManager* SSO=role->getSingleSignOnServiceManager();
132                 const IEndpoint* ep=SSO->getEndpointByBinding(Constants::SHIB_AUTHNREQUEST_PROFILE_URI);
133                 if (ep) {
134                     if (!ACS) {
135                         // Look for an ACS with SAML support.
136                         vector<ShibProfile> v;
137                         v.push_back(SAML11_POST);
138                         v.push_back(SAML11_ARTIFACT);
139                         v.push_back(SAML10_ARTIFACT);
140                         v.push_back(SAML10_POST);
141                         ACS=getCompatibleACS(app,v);
142                     }
143                     auto_ptr_char dest(ep->getLocation());
144                     return ShibAuthnRequest(
145                         st,ACS ? ACS : app->getDefaultAssertionConsumerService(),dest.get(),resource,app->getString("providerId").second
146                         );
147                 }
148             }
149             // Look for an IdP role with ADFS support.
150             role=entity->getIDPSSODescriptor(adfs::XML::WSFED_NS);
151             if (role) {
152                 // Finally, look for a SSO endpoint with ADFS support.
153                 const IEndpointManager* SSO=role->getSingleSignOnServiceManager();
154                 const IEndpoint* ep=SSO->getEndpointByBinding(adfs::XML::WSFED_NS);
155                 if (ep) {
156                     if (!ACS) {
157                         // Look for an ACS with ADFS support.
158                         vector<ShibProfile> v;
159                         v.push_back(ADFS_SSO);
160                         ACS=getCompatibleACS(app,v);
161                     }
162                     auto_ptr_char dest(ep->getLocation());
163                     return ADFSAuthnRequest(
164                         st,ACS ? ACS : app->getDefaultAssertionConsumerService(),dest.get(),resource,app->getString("providerId").second
165                         );
166                 }
167             }
168
169             throw MetadataException(
170                 "Session initiator unable to locate a compatible identity provider SSO endpoint for provider ($1).",
171                 params(1,option)
172                 );
173         }
174     }
175     else {
176         // We're running as a "virtual handler" from within the filter.
177         // The target resource is the current one and everything else is defaulted.
178         resource=st->getRequestURL();
179     }
180     
181     // For now, we only support external session initiation via a wayfURL
182     pair<bool,const char*> wayfURL=handler->getString("wayfURL");
183     if (!wayfURL.first)
184         throw ConfigurationException("Session initiator is missing wayfURL property.");
185
186     pair<bool,const XMLCh*> wayfBinding=handler->getXMLString("wayfBinding");
187     if (!wayfBinding.first || !XMLString::compareString(wayfBinding.second,Constants::SHIB_AUTHNREQUEST_PROFILE_URI))
188         // Standard Shib 1.x
189         return ShibAuthnRequest(st,ACS,wayfURL.second,resource,app->getString("providerId").second);
190     else if (!XMLString::compareString(wayfBinding.second,Constants::SHIB_LEGACY_AUTHNREQUEST_PROFILE_URI))
191         // Shib pre-1.2
192         return ShibAuthnRequest(st,ACS,wayfURL.second,resource,NULL);
193     else if (!strcmp(handler->getString("wayfBinding").second,"urn:mace:shibboleth:1.0:profiles:EAuth")) {
194         pair<bool,bool> localRelayState=st->getConfig()->getPropertySet("Local")->getBool("localRelayState");
195         if (!localRelayState.first || !localRelayState.second)
196             throw ConfigurationException("E-Authn requests cannot include relay state, so localRelayState must be enabled.");
197
198         // Here we store the state in a cookie.
199         pair<string,const char*> shib_cookie=st->getCookieNameProps("_shibstate_");
200         st->setCookie(shib_cookie.first,CgiParse::url_encode(resource) + shib_cookie.second);
201         return make_pair(true, st->sendRedirect(wayfURL.second));
202     }
203     else if (!XMLString::compareString(wayfBinding.second,adfs::XML::WSFED_NS))
204         return ADFSAuthnRequest(st,ACS,wayfURL.second,resource,app->getString("providerId").second);
205    
206     throw UnsupportedProfileException("Unsupported WAYF binding ($1).", params(1,handler->getString("wayfBinding").second));
207 }
208
209 // Get an ACS that can handle one of the desired profiles
210 const IPropertySet* SessionInitiator::getCompatibleACS(const IApplication* app, const vector<ShibProfile>& profiles)
211 {
212     // This isn't going to be very efficient until I can revise the IApplication API to
213     // support ACS lookup by profile.
214     
215     int mask=0;
216     for (vector<ShibProfile>::const_iterator p=profiles.begin(); p!=profiles.end(); p++)
217         mask+=*p;
218     
219     // See if the default is acceptable.
220     const IPropertySet* ACS=app->getDefaultAssertionConsumerService();
221     pair<bool,const XMLCh*> binding=ACS ? ACS->getXMLString("Binding") : pair<bool,const XMLCh*>(false,NULL);
222     if (!ACS || !binding.first || !XMLString::compareString(binding.second,SAMLBrowserProfile::BROWSER_POST)) {
223         pair<bool,unsigned int> version =
224             ACS ? ACS->getUnsignedInt("MinorVersion","urn:oasis:names:tc:SAML:1.0:protocol") : pair<bool,unsigned int>(false,1);
225         if (!version.first)
226             version.second=1;
227         if (mask & (version.second==1 ? SAML11_POST : SAML10_POST))
228             return ACS;
229     }
230     else if (!XMLString::compareString(binding.second,SAMLBrowserProfile::BROWSER_ARTIFACT)) {
231         pair<bool,unsigned int> version=ACS->getUnsignedInt("MinorVersion","urn:oasis:names:tc:SAML:1.0:protocol");
232         if (!version.first)
233             version.second=1;
234         if (mask & (version.second==1 ? SAML11_ARTIFACT : SAML10_ARTIFACT))
235             return ACS;
236     }
237     else if (!XMLString::compareString(binding.second,adfs::XML::WSFED_NS)) {
238         if (mask & ADFS_SSO)
239             return ACS;
240     }
241     
242     // If not, iterate by profile.
243     for (vector<ShibProfile>::const_iterator i=profiles.begin(); i!=profiles.end(); i++) {
244         for (unsigned int j=0; j<=65535; j++) {
245             ACS=app->getAssertionConsumerServiceByIndex(j);
246             if (!ACS && j)
247                 break;  // we're past 0 and didn't get a hit, so we'll bail
248             else if (ACS) {
249                 binding=ACS->getXMLString("Binding");
250                 pair<bool,unsigned int> version=ACS->getUnsignedInt("MinorVersion","urn:oasis:names:tc:SAML:1.0:protocol");
251                 if (!version.first)
252                     version.second=1;
253                 switch (*i) {
254                     case SAML11_POST:
255                         if (version.second==1 && (!binding.first || !XMLString::compareString(binding.second,SAMLBrowserProfile::BROWSER_POST)))
256                             return ACS;
257                         break;
258                     case SAML11_ARTIFACT:
259                         if (version.second==1 && !XMLString::compareString(binding.second,SAMLBrowserProfile::BROWSER_ARTIFACT))
260                             return ACS;
261                         break;
262                     case ADFS_SSO:
263                         if (!XMLString::compareString(binding.second,adfs::XML::WSFED_NS))
264                             return ACS;
265                         break;
266                     case SAML10_POST:
267                         if (version.second==0 && (!binding.first || !XMLString::compareString(binding.second,SAMLBrowserProfile::BROWSER_POST)))
268                             return ACS;
269                         break;
270                     case SAML10_ARTIFACT:
271                         if (version.second==0 && !XMLString::compareString(binding.second,SAMLBrowserProfile::BROWSER_ARTIFACT))
272                             return ACS;
273                         break;
274                     default:
275                         break;
276                 }
277             }
278         }
279     }
280     
281     return NULL;
282 }
283
284 // Handles Shib 1.x AuthnRequest profile.
285 pair<bool,void*> SessionInitiator::ShibAuthnRequest(
286     ShibTarget* st,
287     const IPropertySet* shire,
288     const char* dest,
289     const char* target,
290     const char* providerId
291     )
292 {
293     if (!shire) {
294         // Look for an ACS with SAML support.
295         vector<ShibProfile> v;
296         v.push_back(SAML11_POST);
297         v.push_back(SAML11_ARTIFACT);
298         v.push_back(SAML10_ARTIFACT);
299         v.push_back(SAML10_POST);
300         shire=getCompatibleACS(st->getApplication(),v);
301     }
302     if (!shire)
303         shire=st->getApplication()->getDefaultAssertionConsumerService();
304     
305     // Compute the ACS URL. We add the ACS location to the handler baseURL.
306     // Legacy configs will not have an ACS specified, so no suffix will be added.
307     string ACSloc=st->getHandlerURL(target);
308     if (shire) ACSloc+=shire->getString("Location").second;
309     
310     char timebuf[16];
311     sprintf(timebuf,"%lu",time(NULL));
312     string req=string(dest) + "?shire=" + CgiParse::url_encode(ACSloc.c_str()) + "&time=" + timebuf;
313
314     // How should the resource value be preserved?
315     pair<bool,bool> localRelayState=st->getConfig()->getPropertySet("Local")->getBool("localRelayState");
316     if (!localRelayState.first || !localRelayState.second) {
317         // The old way, just send it along.
318         req+="&target=" + CgiParse::url_encode(target);
319     }
320     else {
321         // Here we store the state in a cookie and send a fixed
322         // value to the IdP so we can recognize it on the way back.
323         pair<string,const char*> shib_cookie=st->getCookieNameProps("_shibstate_");
324         st->setCookie(shib_cookie.first,CgiParse::url_encode(target) + shib_cookie.second);
325         req+="&target=cookie";
326     }
327     
328     // Only omitted for 1.1 style requests.
329     if (providerId)
330         req+="&providerId=" + CgiParse::url_encode(providerId);
331
332     return make_pair(true, st->sendRedirect(req));
333 }
334
335 // Handles ADFS token request profile.
336 pair<bool,void*> SessionInitiator::ADFSAuthnRequest(
337     ShibTarget* st,
338     const IPropertySet* shire,
339     const char* dest,
340     const char* target,
341     const char* providerId
342     )
343 {
344     if (!shire) {
345         // Look for an ACS with ADFS support.
346         vector<ShibProfile> v;
347         v.push_back(ADFS_SSO);
348         shire=getCompatibleACS(st->getApplication(),v);
349     }
350     if (!shire)
351         shire=st->getApplication()->getDefaultAssertionConsumerService();
352
353     // Compute the ACS URL. We add the ACS location to the handler baseURL.
354     // Legacy configs will not have an ACS specified, so no suffix will be added.
355     string ACSloc=st->getHandlerURL(target);
356     if (shire) ACSloc+=shire->getString("Location").second;
357     
358     // UTC timestamp
359     time_t epoch=time(NULL);
360 #ifndef HAVE_GMTIME_R
361     struct tm* ptime=gmtime(&epoch);
362 #else
363     struct tm res;
364     struct tm* ptime=gmtime_r(&epoch,&res);
365 #endif
366     char timebuf[32];
367     strftime(timebuf,32,"%Y-%m-%dT%H:%M:%SZ",ptime);
368
369     string req=string(dest) + "?wa=wsignin1.0&wreply=" + CgiParse::url_encode(ACSloc.c_str()) + "&wct=" + CgiParse::url_encode(timebuf);
370
371     // How should the resource value be preserved?
372     pair<bool,bool> localRelayState=st->getConfig()->getPropertySet("Local")->getBool("localRelayState");
373     if (!localRelayState.first || !localRelayState.second) {
374         // The old way, just send it along.
375         req+="&wctx=" + CgiParse::url_encode(target);
376     }
377     else {
378         // Here we store the state in a cookie and send a fixed
379         // value to the IdP so we can recognize it on the way back.
380         pair<string,const char*> shib_cookie=st->getCookieNameProps("_shibstate_");
381         st->setCookie(shib_cookie.first,CgiParse::url_encode(target) + shib_cookie.second);
382         req+="&wctx=cookie";
383     }
384     
385     req+="&wtrealm=" + CgiParse::url_encode(providerId);
386
387     return make_pair(true, st->sendRedirect(req));
388 }
389
390 pair<bool,void*> ADFSHandler::run(ShibTarget* st, const IPropertySet* handler, bool isHandler)
391 {
392     const IApplication* app=st->getApplication();
393     
394     // Check for logout/GET first.
395     if (!strcasecmp(st->getRequestMethod(), "GET")) {
396         /* 
397          * Only legal GET is a signoutcleanup request...
398          *  wa=wsignoutcleanup1.0
399          */
400         string query=st->getArgs();
401         CgiParse parser(query.c_str(),query.length());
402         const char* wa=parser.get_value("wa");
403         if (!wa || (strcmp(wa,"wsignout1.0") && strcmp(wa,"wsignoutcleanup1.0")))
404             throw FatalProfileException("ADFS protocol handler received invalid action request ($1)", params(1,wa ? wa : "none"));
405         
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);
409         
410         // Logout is best effort.
411         if (session_id && *session_id) {
412             try {
413                 st->getConfig()->getListener()->sessionEnd(st->getApplication(),session_id);
414             }
415             catch (SAMLException& e) {
416                 st->log(ShibTarget::LogLevelError, string("logout processing failed with exception: ") + e.what());
417             }
418 #ifndef _DEBUG
419             catch (...) {
420                 st->log(ShibTarget::LogLevelError, "logout processing failed with unknown exception");
421             }
422 #endif
423             // We send the cookie property alone, which acts as an empty value.
424             st->setCookie(shib_cookie.first,shib_cookie.second);
425         }
426         
427         const char* ret=parser.get_value("wreply");
428         if (!ret)
429             ret=handler->getString("ResponseLocation").second;
430         if (!ret)
431             ret=st->getApplication()->getString("homeURL").second;
432         if (!ret)
433             ret="/";
434         return make_pair(true, st->sendRedirect(ret));
435     }
436     
437     if (strcasecmp(st->getRequestMethod(), "POST"))
438         throw FatalProfileException(
439             "ADFS protocol handler does not support HTTP method ($1)", params(1,st->getRequestMethod())
440             );
441     
442     if (!st->getContentType() || strcasecmp(st->getContentType(),"application/x-www-form-urlencoded"))
443         throw FatalProfileException(
444             "Blocked invalid content-type ($1) submitted to ADFS protocol handler", params(1,st->getContentType())
445             );
446
447     string input=st->getPostData();
448     if (input.empty())
449         throw FatalProfileException("ADFS protocol handler received no data from browser");
450
451     ShibProfile profile=ADFS_SSO;
452     string cookie,target,providerId;
453     
454     string hURL=st->getHandlerURL(st->getRequestURL());
455     pair<bool,const char*> loc=handler->getString("Location");
456     string recipient=loc.first ? hURL + loc.second : hURL;
457     st->getConfig()->getListener()->sessionNew(
458         app,
459         profile,
460         recipient.c_str(),
461         input.c_str(),
462         st->getRemoteAddr(),
463         target,
464         cookie,
465         providerId
466         );
467
468     st->log(ShibTarget::LogLevelDebug, string("profile processing succeeded, new session created (") + cookie + ")");
469
470     if (target=="default") {
471         pair<bool,const char*> homeURL=app->getString("homeURL");
472         target=homeURL.first ? homeURL.second : "/";
473     }
474     else if (target=="cookie" || target.empty()) {
475         // Pull the target value from the "relay state" cookie.
476         pair<string,const char*> relay_cookie = st->getCookieNameProps("_shibstate_");
477         const char* relay_state = st->getCookie(relay_cookie.first);
478         if (!relay_state || !*relay_state) {
479             // No apparent relay state value to use, so fall back on the default.
480             pair<bool,const char*> homeURL=app->getString("homeURL");
481             target=homeURL.first ? homeURL.second : "/";
482         }
483         else {
484             char* rscopy=strdup(relay_state);
485             CgiParse::url_decode(rscopy);
486             target=rscopy;
487             free(rscopy);
488         }
489     }
490
491     // We've got a good session, set the session cookie.
492     pair<string,const char*> shib_cookie=st->getCookieNameProps("_shibsession_");
493     st->setCookie(shib_cookie.first, cookie + shib_cookie.second);
494
495     const IPropertySet* sessionProps=app->getPropertySet("Sessions");
496     pair<bool,bool> idpHistory=sessionProps->getBool("idpHistory");
497     if (!idpHistory.first || idpHistory.second) {
498         // Set an IdP history cookie locally (essentially just a CDC).
499         CommonDomainCookie cdc(st->getCookie(CommonDomainCookie::CDCName));
500
501         // Either leave in memory or set an expiration.
502         pair<bool,unsigned int> days=sessionProps->getUnsignedInt("idpHistoryDays");
503             if (!days.first || days.second==0)
504                 st->setCookie(CommonDomainCookie::CDCName,string(cdc.set(providerId.c_str())) + shib_cookie.second);
505             else {
506                 time_t now=time(NULL) + (days.second * 24 * 60 * 60);
507 #ifdef HAVE_GMTIME_R
508                 struct tm res;
509                 struct tm* ptime=gmtime_r(&now,&res);
510 #else
511                 struct tm* ptime=gmtime(&now);
512 #endif
513                 char timebuf[64];
514                 strftime(timebuf,64,"%a, %d %b %Y %H:%M:%S GMT",ptime);
515                 st->setCookie(
516                     CommonDomainCookie::CDCName,
517                     string(cdc.set(providerId.c_str())) + shib_cookie.second + "; expires=" + timebuf
518                     );
519         }
520     }
521
522     // Now redirect to the target.
523     return make_pair(true, st->sendRedirect(target));
524 }