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