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