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