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