Move Shib constants to new lib, fixed symbol conflicts.
[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 #include <ctime>
27 #include <saml/SAMLConfig.h>
28 #include <saml/binding/URLEncoder.h>
29 #include <saml/util/CommonDomainCookie.h>
30 #include <shibsp/SPConfig.h>
31 #include <shibsp/SPConstants.h>
32
33 #ifdef HAVE_UNISTD_H
34 # include <unistd.h>
35 #endif
36
37 using namespace shibsp;
38 using namespace shibtarget;
39 using namespace shibboleth;
40 using namespace saml;
41 using namespace log4cpp;
42 using namespace std;
43
44 using opensaml::CommonDomainCookie;
45 using opensaml::URLEncoder;
46
47 namespace {
48   class SessionInitiator : virtual public IHandler
49   {
50   public:
51     SessionInitiator(const DOMElement* e) {}
52     ~SessionInitiator() {}
53     pair<bool,void*> run(ShibTarget* st, bool isHandler=true) const;
54     pair<bool,void*> ShibAuthnRequest(
55         ShibTarget* st,
56         const IHandler* shire,
57         const char* dest,
58         const char* target,
59         const char* providerId
60         ) const;
61   };
62
63   class SAML1Consumer : virtual public IHandler, public virtual Remoted
64   {
65   public:
66     SAML1Consumer(const DOMElement* e);
67     ~SAML1Consumer();
68     pair<bool,void*> run(ShibTarget* st, bool isHandler=true) const;
69     DDF receive(const DDF& in);
70   private:
71     string m_address;
72     static int counter;
73   };
74
75   int SAML1Consumer::counter = 0;
76
77   class ShibLogout : virtual public IHandler
78   {
79   public:
80     ShibLogout(const DOMElement* e) {}
81     ~ShibLogout() {}
82     pair<bool,void*> run(ShibTarget* st, bool isHandler=true) const;
83   };
84 }
85
86
87 IPlugIn* ShibSessionInitiatorFactory(const DOMElement* e)
88 {
89     return new SessionInitiator(e);
90 }
91
92 IPlugIn* SAML1POSTFactory(const DOMElement* e)
93 {
94     return new SAML1Consumer(e);
95 }
96
97 IPlugIn* SAML1ArtifactFactory(const DOMElement* e)
98 {
99     return new SAML1Consumer(e);
100 }
101
102 IPlugIn* ShibLogoutFactory(const DOMElement* e)
103 {
104     return new ShibLogout(e);
105 }
106
107 pair<bool,void*> SessionInitiator::run(ShibTarget* st, bool isHandler) const
108 {
109     string dupresource;
110     const char* resource=NULL;
111     const IHandler* ACS=NULL;
112     const IApplication* app=st->getApplication();
113     
114     if (isHandler) {
115         /* 
116          * Binding is CGI query string with:
117          *  target      the resource to direct back to later
118          *  acsIndex    optional index of an ACS to use on the way back in
119          *  providerId  optional direct invocation of a specific IdP
120          */
121         const char* option=st->getRequestParameter("acsIndex");
122         if (option)
123             ACS=app->getAssertionConsumerServiceByIndex(atoi(option));
124         option=st->getRequestParameter("providerId");
125         
126         resource=st->getRequestParameter("target");
127         if (!resource || !*resource) {
128             pair<bool,const char*> home=app->getString("homeURL");
129             if (home.first)
130                 resource=home.second;
131             else
132                 throw FatalProfileException("Session initiator requires a target parameter or a homeURL application property.");
133         }
134         else if (!option) {
135             dupresource=resource;
136             resource=dupresource.c_str();
137         }
138         
139         if (option) {
140             // Here we actually use metadata to invoke the SSO service directly.
141             // The only currently understood binding is the Shibboleth profile.
142             Metadata m(app->getMetadataProviders());
143             const IEntityDescriptor* entity=m.lookup(option);
144             if (!entity)
145                 throw MetadataException("Session initiator unable to locate metadata for provider ($1).", params(1,option));
146             const IIDPSSODescriptor* role=entity->getIDPSSODescriptor(shibspconstants::SHIB1_PROTOCOL_ENUM);
147             if (!role)
148                 throw MetadataException(
149                     "Session initiator unable to locate a Shibboleth-aware identity provider role for provider ($1).", params(1,option)
150                     );
151             const IEndpointManager* SSO=role->getSingleSignOnServiceManager();
152             const IEndpoint* ep=SSO->getEndpointByBinding(shibspconstants::SHIB1_AUTHNREQUEST_PROFILE_URI);
153             if (!ep)
154                 throw MetadataException(
155                     "Session initiator unable to locate compatible SSO service for provider ($1).", params(1,option)
156                     );
157             auto_ptr_char dest(ep->getLocation());
158             return ShibAuthnRequest(
159                 st,ACS ? ACS : app->getDefaultAssertionConsumerService(),dest.get(),resource,app->getString("providerId").second
160                 );
161         }
162     }
163     else {
164         // We're running as a "virtual handler" from within the filter.
165         // The target resource is the current one and everything else is defaulted.
166         resource=st->getRequestURL();
167     }
168     
169     if (!ACS) ACS=app->getDefaultAssertionConsumerService();
170     
171     // For now, we only support external session initiation via a wayfURL
172     pair<bool,const char*> wayfURL=getProperties()->getString("wayfURL");
173     if (!wayfURL.first)
174         throw ConfigurationException("Session initiator is missing wayfURL property.");
175
176     pair<bool,const XMLCh*> wayfBinding=getProperties()->getXMLString("wayfBinding");
177     if (!wayfBinding.first || !XMLString::compareString(wayfBinding.second,shibspconstants::SHIB1_AUTHNREQUEST_PROFILE_URI))
178         // Standard Shib 1.x
179         return ShibAuthnRequest(st,ACS,wayfURL.second,resource,app->getString("providerId").second);
180     else if (!strcmp(getProperties()->getString("wayfBinding").second,"urn:mace:shibboleth:1.0:profiles:EAuth")) {
181         // TODO: Finalize E-Auth profile URI
182         pair<bool,bool> localRelayState=st->getConfig()->getPropertySet("InProcess")->getBool("localRelayState");
183         if (!localRelayState.first || !localRelayState.second)
184             throw ConfigurationException("E-Authn requests cannot include relay state, so localRelayState must be enabled.");
185
186         // Here we store the state in a cookie.
187         pair<string,const char*> shib_cookie=st->getCookieNameProps("_shibstate_");
188         st->setCookie(shib_cookie.first,opensaml::SAMLConfig::getConfig().getURLEncoder()->encode(resource) + shib_cookie.second);
189         return make_pair(true, st->sendRedirect(wayfURL.second));
190     }
191    
192     throw UnsupportedProfileException("Unsupported WAYF binding ($1).", params(1,getProperties()->getString("wayfBinding").second));
193 }
194
195 // Handles Shib 1.x AuthnRequest profile.
196 pair<bool,void*> SessionInitiator::ShibAuthnRequest(
197     ShibTarget* st,
198     const IHandler* shire,
199     const char* dest,
200     const char* target,
201     const char* providerId
202     ) const
203 {
204     // Compute the ACS URL. We add the ACS location to the base handlerURL.
205     // Legacy configs will not have the Location property specified, so no suffix will be added.
206     string ACSloc=st->getHandlerURL(target);
207     pair<bool,const char*> loc=shire ? shire->getProperties()->getString("Location") : pair<bool,const char*>(false,NULL);
208     if (loc.first) ACSloc+=loc.second;
209     
210     URLEncoder* urlenc = opensaml::SAMLConfig::getConfig().getURLEncoder();
211
212     char timebuf[16];
213     sprintf(timebuf,"%u",time(NULL));
214     string req=string(dest) + "?shire=" + urlenc->encode(ACSloc.c_str()) + "&time=" + timebuf;
215
216     // How should the resource value be preserved?
217     pair<bool,bool> localRelayState=st->getConfig()->getPropertySet("InProcess")->getBool("localRelayState");
218     if (!localRelayState.first || !localRelayState.second) {
219         // The old way, just send it along.
220         req+="&target=" + urlenc->encode(target);
221     }
222     else {
223         // Here we store the state in a cookie and send a fixed
224         // value to the IdP so we can recognize it on the way back.
225         pair<string,const char*> shib_cookie=st->getCookieNameProps("_shibstate_");
226         st->setCookie(shib_cookie.first,urlenc->encode(target) + shib_cookie.second);
227         req+="&target=cookie";
228     }
229     
230     // Only omitted for 1.1 style requests.
231     if (providerId)
232         req+="&providerId=" + urlenc->encode(providerId);
233
234     return make_pair(true, st->sendRedirect(req));
235 }
236
237 SAML1Consumer::SAML1Consumer(const DOMElement* e)
238 {
239     m_address += ('A' + (counter++));
240     m_address += "::SAML1Consumer::run";
241
242     // Register for remoted messages.
243     if (SPConfig::getConfig().isEnabled(SPConfig::OutOfProcess)) {
244         ListenerService* listener=ShibTargetConfig::getConfig().getINI()->getListener();
245         if (listener)
246             listener->regListener(m_address.c_str(),this);
247         else
248             throw ListenerException("Plugin requires a Listener service");
249     }
250 }
251
252 SAML1Consumer::~SAML1Consumer()
253 {
254     ListenerService* listener=ShibTargetConfig::getConfig().getINI()->getListener();
255     if (listener && SPConfig::getConfig().isEnabled(SPConfig::OutOfProcess))
256         listener->unregListener(m_address.c_str(),this);
257     counter--;
258 }
259
260 /*
261  * IPC message definitions:
262  * 
263  *  [A-Z]::SAML1Consumer::run
264  * 
265  *      IN
266  *      application_id
267  *      client_address
268  *      recipient
269  *      SAMLResponse or SAMLart list
270  * 
271  *      OUT
272  *      key
273  *      provider_id
274  */
275 DDF SAML1Consumer::receive(const DDF& in)
276 {
277 #ifdef _DEBUG
278     saml::NDC ndc("receive");
279 #endif
280     Category& log=Category::getInstance(SHIBT_LOGCAT".SAML1Consumer");
281
282     // Find application.
283     const char* aid=in["application_id"].string();
284     const IApplication* app=aid ? ShibTargetConfig::getConfig().getINI()->getApplication(aid) : NULL;
285     if (!app) {
286         // Something's horribly wrong.
287         log.error("couldn't find application (%s) for new session", aid ? aid : "(missing)");
288         throw SAMLException("Unable to locate application for new session, deleted?");
289     }
290
291     // Check required parameters.
292     const char* client_address=in["client_address"].string();
293     const char* recipient=in["recipient"].string();
294     if (!client_address || !recipient)
295         throw SAMLException("Required parameters missing in call to SAML1Consumer::run");
296     
297     log.debug("processing new assertion for %s", client_address);
298     log.debug("recipient: %s", recipient);
299     log.debug("application: %s", app->getId());
300
301     // Access the application config. It's already locked behind us.
302     STConfig& stc=static_cast<STConfig&>(ShibTargetConfig::getConfig());
303     IConfig* conf=stc.getINI();
304
305     auto_ptr_XMLCh wrecipient(recipient);
306
307     pair<bool,bool> checkAddress=pair<bool,bool>(false,true);
308     pair<bool,bool> checkReplay=pair<bool,bool>(false,true);
309     const PropertySet* props=app->getPropertySet("Sessions");
310     if (props) {
311         checkAddress=props->getBool("checkAddress");
312         if (!checkAddress.first)
313             checkAddress.second=true;
314         checkReplay=props->getBool("checkReplay");
315         if (!checkReplay.first)
316             checkReplay.second=true;
317     }
318
319     // Supports either version...
320     pair<bool,unsigned int> version=getProperties()->getUnsignedInt("MinorVersion","urn:oasis:names:tc:SAML:1.0:protocol");
321     if (!version.first)
322         version.second=1;
323
324     const IRoleDescriptor* role=NULL;
325     Metadata m(app->getMetadataProviders());
326     SAMLBrowserProfile::BrowserProfileResponse bpr;
327
328     try {
329         const char* samlResponse=in["SAMLResponse"].string();
330         if (samlResponse) {
331             // POST profile
332             log.debug("executing Browser/POST profile...");
333             bpr=app->getBrowserProfile()->receive(
334                 samlResponse,
335                 wrecipient.get(),
336                 checkReplay.second ? conf->getReplayCache() : NULL,
337                 version.second
338                 );
339         }
340         else {
341             // Artifact profile
342             vector<const char*> SAMLart;
343             DDF arts=in["SAMLart"];
344             DDF art=arts.first();
345             while (art.isstring()) {
346                 SAMLart.push_back(art.string());
347                 art=arts.next();
348             }
349             auto_ptr<SAMLBrowserProfile::ArtifactMapper> artifactMapper(app->getArtifactMapper());
350             log.debug("executing Browser/Artifact profile...");
351             bpr=app->getBrowserProfile()->receive(
352                 SAMLart,
353                 wrecipient.get(),
354                 artifactMapper.get(),
355                 checkReplay.second ? conf->getReplayCache() : NULL,
356                 version.second
357                 );
358
359             // Blow it away to clear any locks that might be held.
360             delete artifactMapper.release();
361         }
362
363         // Try and map to metadata (again).
364         // Once the metadata layer is in the SAML core, the repetition should be fixed.
365         const IEntityDescriptor* provider=m.lookup(bpr.assertion->getIssuer());
366         if (!provider && bpr.authnStatement->getSubject()->getNameIdentifier() &&
367                 bpr.authnStatement->getSubject()->getNameIdentifier()->getNameQualifier())
368             provider=m.lookup(bpr.authnStatement->getSubject()->getNameIdentifier()->getNameQualifier());
369         if (provider) {
370             const IIDPSSODescriptor* IDP=provider->getIDPSSODescriptor(
371                 version.second==1 ? saml::XML::SAML11_PROTOCOL_ENUM : saml::XML::SAML10_PROTOCOL_ENUM
372                 );
373             role=IDP;
374         }
375         
376         // This isn't likely, since the profile must have found a role.
377         if (!role) {
378             MetadataException ex("Unable to locate role-specific metadata for identity provider.");
379             annotateException(&ex,provider); // throws it
380         }
381     
382         // Maybe verify the client address....
383         if (checkAddress.second) {
384             log.debug("verifying client address");
385             // Verify the client address exists
386             const XMLCh* wip = bpr.authnStatement->getSubjectIP();
387             if (wip && *wip) {
388                 // Verify the client address matches authentication
389                 auto_ptr_char this_ip(wip);
390                 if (strcmp(client_address, this_ip.get())) {
391                     FatalProfileException ex(
392                         SESSION_E_ADDRESSMISMATCH,
393                        "Your client's current address ($1) differs from the one used when you authenticated "
394                         "to your identity provider. To correct this problem, you may need to bypass a proxy server. "
395                         "Please contact your local support staff or help desk for assistance.",
396                         params(1,client_address)
397                         );
398                     annotateException(&ex,role); // throws it
399                 }
400             }
401         }
402     }
403     catch (SAMLException&) {
404         bpr.clear();
405         throw;
406     }
407     catch (...) {
408         log.error("caught unknown exception");
409         bpr.clear();
410 #ifdef _DEBUG
411         throw;
412 #else
413         SAMLException e("An unexpected error occurred while creating your session.");
414         annotateException(&e,role);
415 #endif
416     }
417
418     // It passes all our tests -- create a new session.
419     log.info("creating new session");
420
421     DDF out;
422     try {
423         // Insert into cache.
424         auto_ptr_char authContext(bpr.authnStatement->getAuthMethod());
425         string key=conf->getSessionCache()->insert(
426             app,
427             role->getEntityDescriptor(),
428             client_address,
429             bpr.authnStatement->getSubject(),
430             authContext.get(),
431             bpr.response
432             );
433         // objects owned by cache now
434         log.debug("new session id: %s", key.c_str());
435         auto_ptr_char oname(role->getEntityDescriptor()->getId());
436         out=DDF(NULL).structure();
437         out.addmember("key").string(key.c_str());
438         out.addmember("provider_id").string(oname.get());
439     }
440     catch (...) {
441 #ifdef _DEBUG
442         throw;
443 #else
444         SAMLException e("An unexpected error occurred while creating your session.");
445         annotateException(&e,role);
446 #endif
447     }
448
449     return out;
450 }
451
452 pair<bool,void*> SAML1Consumer::run(ShibTarget* st, bool isHandler) const
453 {
454     DDF in,out;
455     DDFJanitor jin(in),jout(out);
456
457     pair<bool,const XMLCh*> binding=getProperties()->getXMLString("Binding");
458     if (!binding.first || !XMLString::compareString(binding.second,SAMLBrowserProfile::BROWSER_POST)) {
459 #ifdef HAVE_STRCASECMP
460         if (strcasecmp(st->getRequestMethod(), "POST")) {
461 #else
462         if (_stricmp(st->getRequestMethod(), "POST")) {
463 #endif
464             st->log(ShibTarget::LogLevelInfo, "SAML 1.x Browser/POST handler ignoring non-POST request");
465             return pair<bool,void*>(false,NULL);
466         }
467 #ifdef HAVE_STRCASECMP
468         if (!st->getContentType() || strcasecmp(st->getContentType(),"application/x-www-form-urlencoded")) {
469 #else
470         if (!st->getContentType() || _stricmp(st->getContentType(),"application/x-www-form-urlencoded")) {
471 #endif
472             st->log(ShibTarget::LogLevelInfo, "SAML 1.x Browser/POST handler ignoring submission with unknown content-type.");
473             return pair<bool,void*>(false,NULL);
474         }
475
476         const char* samlResponse = st->getRequestParameter("SAMLResponse");
477         if (!samlResponse) {
478             st->log(ShibTarget::LogLevelInfo, "SAML 1.x Browser/POST handler ignoring request with no SAMLResponse parameter.");
479             return pair<bool,void*>(false,NULL);
480         }
481
482         in=DDF(m_address.c_str()).structure();
483         in.addmember("SAMLResponse").string(samlResponse);
484     }
485     else if (!XMLString::compareString(binding.second,SAMLBrowserProfile::BROWSER_ARTIFACT)) {
486 #ifdef HAVE_STRCASECMP
487         if (strcasecmp(st->getRequestMethod(), "GET")) {
488 #else
489         if (_stricmp(st->getRequestMethod(), "GET")) {
490 #endif
491             st->log(ShibTarget::LogLevelInfo, "SAML 1.x Browser/Artifact handler ignoring non-GET request");
492             return pair<bool,void*>(false,NULL);
493         }
494
495         const char* SAMLart=st->getRequestParameter("SAMLart");
496         if (!SAMLart) {
497             st->log(ShibTarget::LogLevelInfo, "SAML 1.x Browser/Artifact handler ignoring request with no SAMLart parameter.");
498             return pair<bool,void*>(false,NULL);
499         }
500
501         in=DDF(m_address.c_str()).structure();
502         DDF artlist=in.addmember("SAMLart").list();
503
504         while (SAMLart) {
505             artlist.add(DDF(NULL).string(SAMLart));
506             SAMLart=st->getRequestParameter("SAMLart",artlist.integer());
507         }
508     }
509     
510     // Compute the endpoint location.
511     string hURL=st->getHandlerURL(st->getRequestURL());
512     pair<bool,const char*> loc=getProperties()->getString("Location");
513     string recipient=loc.first ? hURL + loc.second : hURL;
514     in.addmember("recipient").string(recipient.c_str());
515
516     // Add remaining parameters.
517     in.addmember("application_id").string(st->getApplication()->getId());
518     in.addmember("client_address").string(st->getRemoteAddr());
519
520     out=st->getConfig()->getListener()->send(in);
521     if (!out["key"].isstring())
522         throw FatalProfileException("Remote processing of SAML 1.x Browser profile did not return a usable session key.");
523     string key=out["key"].string();
524
525     st->log(ShibTarget::LogLevelDebug, string("profile processing succeeded, new session created (") + key + ")");
526
527     const char* target=st->getRequestParameter("TARGET");
528     if (target && !strcmp(target,"default")) {
529         pair<bool,const char*> homeURL=st->getApplication()->getString("homeURL");
530         target=homeURL.first ? homeURL.second : "/";
531     }
532     else if (!target || !strcmp(target,"cookie")) {
533         // Pull the target value from the "relay state" cookie.
534         pair<string,const char*> relay_cookie = st->getCookieNameProps("_shibstate_");
535         const char* relay_state = st->getCookie(relay_cookie.first);
536         if (!relay_state || !*relay_state) {
537             // No apparent relay state value to use, so fall back on the default.
538             pair<bool,const char*> homeURL=st->getApplication()->getString("homeURL");
539             target=homeURL.first ? homeURL.second : "/";
540         }
541         else {
542             char* rscopy=strdup(relay_state);
543             opensaml::SAMLConfig::getConfig().getURLEncoder()->decode(rscopy);
544             hURL=rscopy;
545             free(rscopy);
546             target=hURL.c_str();
547         }
548         st->setCookie(relay_cookie.first,relay_cookie.second);
549     }
550
551     // We've got a good session, set the session cookie.
552     pair<string,const char*> shib_cookie=st->getCookieNameProps("_shibsession_");
553     st->setCookie(shib_cookie.first, key + shib_cookie.second);
554
555     const char* providerId=out["provider_id"].string();
556     if (providerId) {
557         const PropertySet* sessionProps=st->getApplication()->getPropertySet("Sessions");
558         pair<bool,bool> idpHistory=sessionProps->getBool("idpHistory");
559         if (!idpHistory.first || idpHistory.second) {
560             // Set an IdP history cookie locally (essentially just a CDC).
561             CommonDomainCookie cdc(st->getCookie(CommonDomainCookie::CDCName));
562
563             // Either leave in memory or set an expiration.
564             pair<bool,unsigned int> days=sessionProps->getUnsignedInt("idpHistoryDays");
565                 if (!days.first || days.second==0)
566                     st->setCookie(CommonDomainCookie::CDCName,string(cdc.set(providerId)) + shib_cookie.second);
567                 else {
568                     time_t now=time(NULL) + (days.second * 24 * 60 * 60);
569 #ifdef HAVE_GMTIME_R
570                     struct tm res;
571                     struct tm* ptime=gmtime_r(&now,&res);
572 #else
573                     struct tm* ptime=gmtime(&now);
574 #endif
575                     char timebuf[64];
576                     strftime(timebuf,64,"%a, %d %b %Y %H:%M:%S GMT",ptime);
577                     st->setCookie(
578                         CommonDomainCookie::CDCName,
579                         string(cdc.set(providerId)) + shib_cookie.second + "; expires=" + timebuf
580                         );
581             }
582         }
583     }
584
585     // Now redirect to the target.
586     return make_pair(true, st->sendRedirect(target));
587 }
588
589 pair<bool,void*> ShibLogout::run(ShibTarget* st, bool isHandler) const
590 {
591     // Recover the session key.
592     pair<string,const char*> shib_cookie = st->getCookieNameProps("_shibsession_");
593     const char* session_id = st->getCookie(shib_cookie.first);
594     
595     // Logout is best effort.
596     if (session_id && *session_id) {
597         try {
598             st->getConfig()->getSessionCache()->remove(session_id,st->getApplication(),st->getRemoteAddr());
599         }
600         catch (SAMLException& e) {
601             st->log(ShibTarget::LogLevelError, string("logout processing failed with exception: ") + e.what());
602         }
603 #ifndef _DEBUG
604         catch (...) {
605             st->log(ShibTarget::LogLevelError, "logout processing failed with unknown exception");
606         }
607 #endif
608         // We send the cookie property alone, which acts as an empty value.
609         st->setCookie(shib_cookie.first,shib_cookie.second);
610     }
611     
612     const char* ret=st->getRequestParameter("return");
613     if (!ret)
614         ret=getProperties()->getString("ResponseLocation").second;
615     if (!ret)
616         ret=st->getApplication()->getString("homeURL").second;
617     if (!ret)
618         ret="/";
619     return make_pair(true, st->sendRedirect(ret));
620 }