2 * Copyright 2001-2005 Internet2
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
18 * shib-handlers.cpp -- profile handlers that plug into SP
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>
38 using namespace shibsp;
39 using namespace shibtarget;
40 using namespace shibboleth;
42 using namespace opensaml::saml2md;
43 using namespace log4cpp;
46 using opensaml::CommonDomainCookie;
47 using opensaml::URLEncoder;
50 class SessionInitiator : virtual public IHandler
53 SessionInitiator(const DOMElement* e) {}
54 ~SessionInitiator() {}
55 pair<bool,long> run(ShibTarget* st, bool isHandler=true) const;
56 pair<bool,long> ShibAuthnRequest(
58 const IHandler* shire,
61 const char* providerId
65 class SAML1Consumer : virtual public IHandler, public virtual Remoted
68 SAML1Consumer(const DOMElement* e);
70 pair<bool,long> run(ShibTarget* st, bool isHandler=true) const;
71 DDF receive(const DDF& in);
77 int SAML1Consumer::counter = 0;
79 class ShibLogout : virtual public IHandler
82 ShibLogout(const DOMElement* e) {}
84 pair<bool,long> run(ShibTarget* st, bool isHandler=true) const;
89 IPlugIn* ShibSessionInitiatorFactory(const DOMElement* e)
91 return new SessionInitiator(e);
94 IPlugIn* SAML1POSTFactory(const DOMElement* e)
96 return new SAML1Consumer(e);
99 IPlugIn* SAML1ArtifactFactory(const DOMElement* e)
101 return new SAML1Consumer(e);
104 IPlugIn* ShibLogoutFactory(const DOMElement* e)
106 return new ShibLogout(e);
109 pair<bool,long> SessionInitiator::run(ShibTarget* st, bool isHandler) const
112 const char* resource=NULL;
113 const IHandler* ACS=NULL;
114 const IApplication& app=dynamic_cast<const IApplication&>(st->getApplication());
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
123 const char* option=st->getParameter("acsIndex");
125 ACS=app.getAssertionConsumerServiceByIndex(atoi(option));
126 option=st->getParameter("providerId");
128 resource=st->getParameter("target");
129 if (!resource || !*resource) {
130 pair<bool,const char*> home=app.getString("homeURL");
132 resource=home.second;
134 throw opensaml::FatalProfileException("Session initiator requires a target parameter or a homeURL application property.");
137 dupresource=resource;
138 resource=dupresource.c_str();
142 // Here we actually use metadata to invoke the SSO service directly.
143 // The only currently understood binding is the Shibboleth profile.
145 MetadataProvider* m=app.getMetadataProvider();
146 xmltooling::Locker locker(m);
147 const EntityDescriptor* entity=m->getEntityDescriptor(option);
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);
152 throw MetadataException(
153 "Session initiator unable to locate a Shibboleth-aware identity provider role for provider ($1).",
154 xmltooling::params(1,option)
156 const EndpointType* ep=EndpointManager<SingleSignOnService>(role->getSingleSignOnServices()).getByBinding(
157 shibspconstants::SHIB1_AUTHNREQUEST_PROFILE_URI
160 throw MetadataException(
161 "Session initiator unable to locate compatible SSO service for provider ($1).", xmltooling::params(1,option)
163 auto_ptr_char dest(ep->getLocation());
164 return ShibAuthnRequest(
165 st,ACS ? ACS : app.getDefaultAssertionConsumerService(),dest.get(),resource,app.getString("providerId").second
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();
175 if (!ACS) ACS=app.getDefaultAssertionConsumerService();
177 // For now, we only support external session initiation via a wayfURL
178 pair<bool,const char*> wayfURL=getProperties()->getString("wayfURL");
180 throw ConfigurationException("Session initiator is missing wayfURL property.");
182 pair<bool,const XMLCh*> wayfBinding=getProperties()->getXMLString("wayfBinding");
183 if (!wayfBinding.first || !XMLString::compareString(wayfBinding.second,shibspconstants::SHIB1_AUTHNREQUEST_PROFILE_URI))
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->getServiceProvider().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.");
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));
199 throw opensaml::BindingException("Unsupported WAYF binding ($1).", xmltooling::params(1,getProperties()->getString("wayfBinding").second));
202 // Handles Shib 1.x AuthnRequest profile.
203 pair<bool,long> SessionInitiator::ShibAuthnRequest(
205 const IHandler* shire,
208 const char* providerId
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;
217 URLEncoder* urlenc = opensaml::SAMLConfig::getConfig().getURLEncoder();
220 sprintf(timebuf,"%u",time(NULL));
221 string req=string(dest) + "?shire=" + urlenc->encode(ACSloc.c_str()) + "&time=" + timebuf;
223 // How should the resource value be preserved?
224 pair<bool,bool> localRelayState=st->getServiceProvider().getPropertySet("InProcess")->getBool("localRelayState");
225 if (!localRelayState.first || !localRelayState.second) {
226 // The old way, just send it along.
227 req+="&target=" + urlenc->encode(target);
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";
238 // Only omitted for 1.1 style requests.
240 req+="&providerId=" + urlenc->encode(providerId);
242 return make_pair(true, st->sendRedirect(req.c_str()));
245 SAML1Consumer::SAML1Consumer(const DOMElement* e)
247 m_address += ('A' + (counter++));
248 m_address += "::SAML1Consumer::run";
250 // Register for remoted messages.
251 if (SPConfig::getConfig().isEnabled(SPConfig::OutOfProcess))
252 SPConfig::getConfig().getServiceProvider()->getListenerService()->regListener(m_address.c_str(),this);
255 SAML1Consumer::~SAML1Consumer()
257 ListenerService* listener=SPConfig::getConfig().getServiceProvider()->getListenerService(false);
258 if (listener && SPConfig::getConfig().isEnabled(SPConfig::OutOfProcess))
259 listener->unregListener(m_address.c_str(),this);
264 * IPC message definitions:
266 * [A-Z]::SAML1Consumer::run
272 * SAMLResponse or SAMLart list
278 DDF SAML1Consumer::receive(const DDF& in)
281 xmltooling::NDC ndc("receive");
283 Category& log=Category::getInstance(SHIBT_LOGCAT".SAML1Consumer");
286 const char* aid=in["application_id"].string();
287 const IApplication* app=aid ? dynamic_cast<const IApplication*>(SPConfig::getConfig().getServiceProvider()->getApplication(aid)) : NULL;
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?");
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");
300 log.debug("processing new assertion for %s", client_address);
301 log.debug("recipient: %s", recipient);
302 log.debug("application: %s", app->getId());
304 // Access the application config.
305 IConfig* conf=dynamic_cast<IConfig*>(SPConfig::getConfig().getServiceProvider());
306 xmltooling::Locker confLocker(conf);
308 auto_ptr_XMLCh wrecipient(recipient);
310 pair<bool,bool> checkAddress=pair<bool,bool>(false,true);
311 pair<bool,bool> checkReplay=pair<bool,bool>(false,true);
312 const PropertySet* props=app->getPropertySet("Sessions");
314 checkAddress=props->getBool("checkAddress");
315 if (!checkAddress.first)
316 checkAddress.second=true;
317 checkReplay=props->getBool("checkReplay");
318 if (!checkReplay.first)
319 checkReplay.second=true;
322 // Supports either version...
323 pair<bool,unsigned int> version=getProperties()->getUnsignedInt("MinorVersion","urn:oasis:names:tc:SAML:1.0:protocol");
327 const EntityDescriptor* provider=NULL;
328 const RoleDescriptor* role=NULL;
329 MetadataProvider* m=app->getMetadataProvider();
330 xmltooling::Locker locker(m);
331 SAMLBrowserProfile::BrowserProfileResponse bpr;
334 const char* samlResponse=in["SAMLResponse"].string();
337 log.debug("executing Browser/POST profile...");
338 bpr=app->getBrowserProfile()->receive(
341 checkReplay.second ? conf->getReplayCache() : NULL,
347 vector<const char*> SAMLart;
348 DDF arts=in["SAMLart"];
349 DDF art=arts.first();
350 while (art.isstring()) {
351 SAMLart.push_back(art.string());
354 auto_ptr<SAMLBrowserProfile::ArtifactMapper> artifactMapper(app->getArtifactMapper());
355 log.debug("executing Browser/Artifact profile...");
356 bpr=app->getBrowserProfile()->receive(
359 artifactMapper.get(),
360 checkReplay.second ? conf->getReplayCache() : NULL,
364 // Blow it away to clear any locks that might be held.
365 delete artifactMapper.release();
368 // Try and map to metadata (again).
369 // Once the metadata layer is in the SAML core, the repetition will be fixed.
370 provider=m->getEntityDescriptor(bpr.assertion->getIssuer());
371 if (!provider && bpr.authnStatement->getSubject()->getNameIdentifier() &&
372 bpr.authnStatement->getSubject()->getNameIdentifier()->getNameQualifier())
373 provider=m->getEntityDescriptor(bpr.authnStatement->getSubject()->getNameIdentifier()->getNameQualifier());
375 role=provider->getIDPSSODescriptor(
376 version.second==1 ? samlconstants::SAML11_PROTOCOL_ENUM : samlconstants::SAML10_PROTOCOL_ENUM
380 // This isn't likely, since the profile must have found a role.
382 MetadataException ex("Unable to locate role-specific metadata for identity provider.");
383 annotateException(&ex,provider); // throws it
386 // Maybe verify the client address....
387 if (checkAddress.second) {
388 log.debug("verifying client address");
389 // Verify the client address exists
390 const XMLCh* wip = bpr.authnStatement->getSubjectIP();
392 // Verify the client address matches authentication
393 auto_ptr_char this_ip(wip);
394 if (strcmp(client_address, this_ip.get())) {
395 opensaml::FatalProfileException ex(
396 "Your client's current address ($1) differs from the one used when you authenticated "
397 "to your identity provider. To correct this problem, you may need to bypass a proxy server. "
398 "Please contact your local support staff or help desk for assistance.",
399 xmltooling::params(1,client_address)
401 annotateException(&ex,role); // throws it
411 log.error("caught unknown exception");
416 SAMLException e("An unexpected error occurred while creating your session.");
417 shibboleth::annotateException(&e,role);
421 // It passes all our tests -- create a new session.
422 log.info("creating new session");
424 // Insert into cache.
425 auto_ptr_char authContext(bpr.authnStatement->getAuthMethod());
426 string key=conf->getSessionCache()->insert(
430 bpr.authnStatement->getSubject(),
434 // objects owned by cache now
435 log.debug("new session id: %s", key.c_str());
436 auto_ptr_char oname(provider->getEntityID());
437 DDF out=DDF(NULL).structure();
438 out.addmember("key").string(key.c_str());
439 out.addmember("provider_id").string(oname.get());
444 pair<bool,long> SAML1Consumer::run(ShibTarget* st, bool isHandler) const
447 DDFJanitor jin(in),jout(out);
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->getMethod(), "POST")) {
454 if (_stricmp(st->getMethod(), "POST")) {
456 st->log(SPRequest::SPInfo, "SAML 1.x Browser/POST handler ignoring non-POST request");
457 return pair<bool,long>(false,NULL);
459 #ifdef HAVE_STRCASECMP
460 if (strcasecmp(st->getContentType().c_str(),"application/x-www-form-urlencoded")) {
462 if (_stricmp(st->getContentType().c_str(),"application/x-www-form-urlencoded")) {
464 st->log(SPRequest::SPInfo, "SAML 1.x Browser/POST handler ignoring submission with unknown content-type.");
465 return pair<bool,long>(false,0);
468 const char* samlResponse = st->getParameter("SAMLResponse");
470 st->log(SPRequest::SPInfo, "SAML 1.x Browser/POST handler ignoring request with no SAMLResponse parameter.");
471 return pair<bool,long>(false,0);
474 in=DDF(m_address.c_str()).structure();
475 in.addmember("SAMLResponse").string(samlResponse);
477 else if (!XMLString::compareString(binding.second,SAMLBrowserProfile::BROWSER_ARTIFACT)) {
478 #ifdef HAVE_STRCASECMP
479 if (strcasecmp(st->getMethod(), "GET")) {
481 if (_stricmp(st->getMethod(), "GET")) {
483 st->log(SPRequest::SPInfo, "SAML 1.x Browser/Artifact handler ignoring non-GET request");
484 return pair<bool,long>(false,0);
487 vector<const char*> arts;
488 if (st->getParameters("SAMLart",arts)==0) {
489 st->log(SPRequest::SPInfo, "SAML 1.x Browser/Artifact handler ignoring request with no SAMLart parameter.");
490 return pair<bool,long>(false,0);
493 in=DDF(m_address.c_str()).structure();
494 DDF artlist=in.addmember("SAMLart").list();
496 for (vector<const char*>::const_iterator a=arts.begin(); a!=arts.end(); ++a)
497 artlist.add(DDF(NULL).string(*a));
500 // Compute the endpoint location.
501 string hURL=st->getHandlerURL(st->getRequestURL());
502 pair<bool,const char*> loc=getProperties()->getString("Location");
503 string recipient=loc.first ? hURL + loc.second : hURL;
504 in.addmember("recipient").string(recipient.c_str());
506 // Add remaining parameters.
507 in.addmember("application_id").string(st->getApplication().getId());
508 in.addmember("client_address").string(st->getRemoteAddr().c_str());
510 out=st->getServiceProvider().getListenerService()->send(in);
511 if (!out["key"].isstring())
512 throw opensaml::FatalProfileException("Remote processing of SAML 1.x Browser profile did not return a usable session key.");
513 string key=out["key"].string();
515 st->log(SPRequest::SPDebug, string("profile processing succeeded, new session created (") + key + ")");
517 const char* target=st->getParameter("TARGET");
518 if (target && !strcmp(target,"default")) {
519 pair<bool,const char*> homeURL=st->getApplication().getString("homeURL");
520 target=homeURL.first ? homeURL.second : "/";
522 else if (!target || !strcmp(target,"cookie")) {
523 // Pull the target value from the "relay state" cookie.
524 pair<string,const char*> relay_cookie = st->getApplication().getCookieNameProps("_shibstate_");
525 const char* relay_state = st->getCookie(relay_cookie.first.c_str());
526 if (!relay_state || !*relay_state) {
527 // No apparent relay state value to use, so fall back on the default.
528 pair<bool,const char*> homeURL=st->getApplication().getString("homeURL");
529 target=homeURL.first ? homeURL.second : "/";
532 char* rscopy=strdup(relay_state);
533 opensaml::SAMLConfig::getConfig().getURLEncoder()->decode(rscopy);
538 st->setCookie(relay_cookie.first.c_str(),relay_cookie.second);
541 // We've got a good session, set the session cookie.
542 pair<string,const char*> shib_cookie=st->getApplication().getCookieNameProps("_shibsession_");
543 key += shib_cookie.second;
544 st->setCookie(shib_cookie.first.c_str(), key.c_str());
546 const char* providerId=out["provider_id"].string();
548 const PropertySet* sessionProps=st->getApplication().getPropertySet("Sessions");
549 pair<bool,bool> idpHistory=sessionProps->getBool("idpHistory");
550 if (!idpHistory.first || idpHistory.second) {
551 // Set an IdP history cookie locally (essentially just a CDC).
552 CommonDomainCookie cdc(st->getCookie(CommonDomainCookie::CDCName));
554 // Either leave in memory or set an expiration.
555 pair<bool,unsigned int> days=sessionProps->getUnsignedInt("idpHistoryDays");
556 if (!days.first || days.second==0) {
557 key = string(cdc.set(providerId)) + shib_cookie.second;
558 st->setCookie(CommonDomainCookie::CDCName, key.c_str());
561 time_t now=time(NULL) + (days.second * 24 * 60 * 60);
564 struct tm* ptime=gmtime_r(&now,&res);
566 struct tm* ptime=gmtime(&now);
569 strftime(timebuf,64,"%a, %d %b %Y %H:%M:%S GMT",ptime);
570 key = string(cdc.set(providerId)) + shib_cookie.second + "; expires=" + timebuf;
571 st->setCookie(CommonDomainCookie::CDCName, key.c_str());
576 // Now redirect to the target.
577 return make_pair(true, st->sendRedirect(target));
580 pair<bool,long> ShibLogout::run(ShibTarget* st, bool isHandler) const
582 // Recover the session key.
583 pair<string,const char*> shib_cookie = st->getApplication().getCookieNameProps("_shibsession_");
584 const char* session_id = st->getCookie(shib_cookie.first.c_str());
586 // Logout is best effort.
587 if (session_id && *session_id) {
589 // TODO: port to new cache API
590 //st->getServiceProvider().getSessionCache()->remove(session_id,st->getApplication(),st->getRemoteAddr().c_str());
592 catch (exception& e) {
593 st->log(SPRequest::SPError, string("logout processing failed with exception: ") + e.what());
597 st->log(SPRequest::SPError, "logout processing failed with unknown exception");
600 // We send the cookie property alone, which acts as an empty value.
601 st->setCookie(shib_cookie.first.c_str(),shib_cookie.second);
604 const char* ret=st->getParameter("return");
606 ret=getProperties()->getString("ResponseLocation").second;
608 ret=st->getApplication().getString("homeURL").second;
611 return make_pair(true, st->sendRedirect(ret));