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