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/util/CommonDomainCookie.h>
30 #include <shibsp/SPConfig.h>
36 using namespace shibsp;
37 using namespace shibtarget;
38 using namespace shibboleth;
40 using namespace log4cpp;
43 using opensaml::CommonDomainCookie;
44 using opensaml::URLEncoder;
47 class SessionInitiator : virtual public IHandler
50 SessionInitiator(const DOMElement* e) {}
51 ~SessionInitiator() {}
52 pair<bool,void*> run(ShibTarget* st, bool isHandler=true) const;
53 pair<bool,void*> ShibAuthnRequest(
55 const IHandler* shire,
58 const char* providerId
62 class SAML1Consumer : virtual public IHandler, public virtual Remoted
65 SAML1Consumer(const DOMElement* e);
67 pair<bool,void*> run(ShibTarget* st, bool isHandler=true) const;
68 DDF receive(const DDF& in);
74 int SAML1Consumer::counter = 0;
76 class ShibLogout : virtual public IHandler
79 ShibLogout(const DOMElement* e) {}
81 pair<bool,void*> run(ShibTarget* st, bool isHandler=true) const;
86 IPlugIn* ShibSessionInitiatorFactory(const DOMElement* e)
88 return new SessionInitiator(e);
91 IPlugIn* SAML1POSTFactory(const DOMElement* e)
93 return new SAML1Consumer(e);
96 IPlugIn* SAML1ArtifactFactory(const DOMElement* e)
98 return new SAML1Consumer(e);
101 IPlugIn* ShibLogoutFactory(const DOMElement* e)
103 return new ShibLogout(e);
106 pair<bool,void*> SessionInitiator::run(ShibTarget* st, bool isHandler) const
109 const char* resource=NULL;
110 const IHandler* ACS=NULL;
111 const IApplication* app=st->getApplication();
115 * Binding is CGI query string with:
116 * target the resource to direct back to later
117 * acsIndex optional index of an ACS to use on the way back in
118 * providerId optional direct invocation of a specific IdP
120 const char* option=st->getRequestParameter("acsIndex");
122 ACS=app->getAssertionConsumerServiceByIndex(atoi(option));
123 option=st->getRequestParameter("providerId");
125 resource=st->getRequestParameter("target");
126 if (!resource || !*resource) {
127 pair<bool,const char*> home=app->getString("homeURL");
129 resource=home.second;
131 throw FatalProfileException("Session initiator requires a target parameter or a homeURL application property.");
134 dupresource=resource;
135 resource=dupresource.c_str();
139 // Here we actually use metadata to invoke the SSO service directly.
140 // The only currently understood binding is the Shibboleth profile.
141 Metadata m(app->getMetadataProviders());
142 const IEntityDescriptor* entity=m.lookup(option);
144 throw MetadataException("Session initiator unable to locate metadata for provider ($1).", params(1,option));
145 const IIDPSSODescriptor* role=entity->getIDPSSODescriptor(Constants::SHIB_NS);
147 throw MetadataException(
148 "Session initiator unable to locate a Shibboleth-aware identity provider role for provider ($1).", params(1,option)
150 const IEndpointManager* SSO=role->getSingleSignOnServiceManager();
151 const IEndpoint* ep=SSO->getEndpointByBinding(Constants::SHIB_AUTHNREQUEST_PROFILE_URI);
153 throw MetadataException(
154 "Session initiator unable to locate compatible SSO service for provider ($1).", params(1,option)
156 auto_ptr_char dest(ep->getLocation());
157 return ShibAuthnRequest(
158 st,ACS ? ACS : app->getDefaultAssertionConsumerService(),dest.get(),resource,app->getString("providerId").second
163 // We're running as a "virtual handler" from within the filter.
164 // The target resource is the current one and everything else is defaulted.
165 resource=st->getRequestURL();
168 if (!ACS) ACS=app->getDefaultAssertionConsumerService();
170 // For now, we only support external session initiation via a wayfURL
171 pair<bool,const char*> wayfURL=getProperties()->getString("wayfURL");
173 throw ConfigurationException("Session initiator is missing wayfURL property.");
175 pair<bool,const XMLCh*> wayfBinding=getProperties()->getXMLString("wayfBinding");
176 if (!wayfBinding.first || !XMLString::compareString(wayfBinding.second,Constants::SHIB_AUTHNREQUEST_PROFILE_URI))
178 return ShibAuthnRequest(st,ACS,wayfURL.second,resource,app->getString("providerId").second);
179 else if (!XMLString::compareString(wayfBinding.second,Constants::SHIB_LEGACY_AUTHNREQUEST_PROFILE_URI))
181 return ShibAuthnRequest(st,ACS,wayfURL.second,resource,NULL);
182 else if (!strcmp(getProperties()->getString("wayfBinding").second,"urn:mace:shibboleth:1.0:profiles:EAuth")) {
183 // TODO: Finalize E-Auth profile URI
184 pair<bool,bool> localRelayState=st->getConfig()->getPropertySet("InProcess")->getBool("localRelayState");
185 if (!localRelayState.first || !localRelayState.second)
186 throw ConfigurationException("E-Authn requests cannot include relay state, so localRelayState must be enabled.");
188 // Here we store the state in a cookie.
189 pair<string,const char*> shib_cookie=st->getCookieNameProps("_shibstate_");
190 st->setCookie(shib_cookie.first,opensaml::SAMLConfig::getConfig().getURLEncoder()->encode(resource) + shib_cookie.second);
191 return make_pair(true, st->sendRedirect(wayfURL.second));
194 throw UnsupportedProfileException("Unsupported WAYF binding ($1).", params(1,getProperties()->getString("wayfBinding").second));
197 // Handles Shib 1.x AuthnRequest profile.
198 pair<bool,void*> SessionInitiator::ShibAuthnRequest(
200 const IHandler* shire,
203 const char* providerId
206 // Compute the ACS URL. We add the ACS location to the base handlerURL.
207 // Legacy configs will not have the Location property specified, so no suffix will be added.
208 string ACSloc=st->getHandlerURL(target);
209 pair<bool,const char*> loc=shire ? shire->getProperties()->getString("Location") : pair<bool,const char*>(false,NULL);
210 if (loc.first) ACSloc+=loc.second;
212 URLEncoder* urlenc = opensaml::SAMLConfig::getConfig().getURLEncoder();
215 sprintf(timebuf,"%u",time(NULL));
216 string req=string(dest) + "?shire=" + urlenc->encode(ACSloc.c_str()) + "&time=" + timebuf;
218 // How should the resource value be preserved?
219 pair<bool,bool> localRelayState=st->getConfig()->getPropertySet("InProcess")->getBool("localRelayState");
220 if (!localRelayState.first || !localRelayState.second) {
221 // The old way, just send it along.
222 req+="&target=" + urlenc->encode(target);
225 // Here we store the state in a cookie and send a fixed
226 // value to the IdP so we can recognize it on the way back.
227 pair<string,const char*> shib_cookie=st->getCookieNameProps("_shibstate_");
228 st->setCookie(shib_cookie.first,urlenc->encode(target) + shib_cookie.second);
229 req+="&target=cookie";
232 // Only omitted for 1.1 style requests.
234 req+="&providerId=" + urlenc->encode(providerId);
236 return make_pair(true, st->sendRedirect(req));
239 SAML1Consumer::SAML1Consumer(const DOMElement* e)
241 m_address += ('A' + (counter++));
242 m_address += "::SAML1Consumer::run";
244 // Register for remoted messages.
245 if (SPConfig::getConfig().isEnabled(SPConfig::OutOfProcess)) {
246 ListenerService* listener=ShibTargetConfig::getConfig().getINI()->getListener();
248 listener->regListener(m_address.c_str(),this);
250 throw ListenerException("Plugin requires a Listener service");
254 SAML1Consumer::~SAML1Consumer()
256 ListenerService* listener=ShibTargetConfig::getConfig().getINI()->getListener();
257 if (listener && SPConfig::getConfig().isEnabled(SPConfig::OutOfProcess))
258 listener->unregListener(m_address.c_str(),this);
263 * IPC message definitions:
265 * [A-Z]::SAML1Consumer::run
271 * SAMLResponse or SAMLart list
277 DDF SAML1Consumer::receive(const DDF& in)
280 saml::NDC ndc("receive");
282 Category& log=Category::getInstance(SHIBT_LOGCAT".SAML1Consumer");
285 const char* aid=in["application_id"].string();
286 const IApplication* app=aid ? ShibTargetConfig::getConfig().getINI()->getApplication(aid) : NULL;
288 // Something's horribly wrong.
289 log.error("couldn't find application (%s) for new session", aid ? aid : "(missing)");
290 throw SAMLException("Unable to locate application for new session, deleted?");
293 // Check required parameters.
294 const char* client_address=in["client_address"].string();
295 const char* recipient=in["recipient"].string();
296 if (!client_address || !recipient)
297 throw SAMLException("Required parameters missing in call to SAML1Consumer::run");
299 log.debug("processing new assertion for %s", client_address);
300 log.debug("recipient: %s", recipient);
301 log.debug("application: %s", app->getId());
303 // Access the application config. It's already locked behind us.
304 STConfig& stc=static_cast<STConfig&>(ShibTargetConfig::getConfig());
305 IConfig* conf=stc.getINI();
307 auto_ptr_XMLCh wrecipient(recipient);
309 pair<bool,bool> checkAddress=pair<bool,bool>(false,true);
310 pair<bool,bool> checkReplay=pair<bool,bool>(false,true);
311 const PropertySet* props=app->getPropertySet("Sessions");
313 checkAddress=props->getBool("checkAddress");
314 if (!checkAddress.first)
315 checkAddress.second=true;
316 checkReplay=props->getBool("checkReplay");
317 if (!checkReplay.first)
318 checkReplay.second=true;
321 // Supports either version...
322 pair<bool,unsigned int> version=getProperties()->getUnsignedInt("MinorVersion","urn:oasis:names:tc:SAML:1.0:protocol");
326 const IRoleDescriptor* role=NULL;
327 Metadata m(app->getMetadataProviders());
328 SAMLBrowserProfile::BrowserProfileResponse bpr;
331 const char* samlResponse=in["SAMLResponse"].string();
334 log.debug("executing Browser/POST profile...");
335 bpr=app->getBrowserProfile()->receive(
338 checkReplay.second ? conf->getReplayCache() : NULL,
344 vector<const char*> SAMLart;
345 DDF arts=in["SAMLart"];
346 DDF art=arts.first();
347 while (art.isstring()) {
348 SAMLart.push_back(art.string());
351 auto_ptr<SAMLBrowserProfile::ArtifactMapper> artifactMapper(app->getArtifactMapper());
352 log.debug("executing Browser/Artifact profile...");
353 bpr=app->getBrowserProfile()->receive(
356 artifactMapper.get(),
357 checkReplay.second ? conf->getReplayCache() : NULL,
361 // Blow it away to clear any locks that might be held.
362 delete artifactMapper.release();
365 // Try and map to metadata (again).
366 // Once the metadata layer is in the SAML core, the repetition should be fixed.
367 const IEntityDescriptor* provider=m.lookup(bpr.assertion->getIssuer());
368 if (!provider && bpr.authnStatement->getSubject()->getNameIdentifier() &&
369 bpr.authnStatement->getSubject()->getNameIdentifier()->getNameQualifier())
370 provider=m.lookup(bpr.authnStatement->getSubject()->getNameIdentifier()->getNameQualifier());
372 const IIDPSSODescriptor* IDP=provider->getIDPSSODescriptor(
373 version.second==1 ? saml::XML::SAML11_PROTOCOL_ENUM : saml::XML::SAML10_PROTOCOL_ENUM
378 // This isn't likely, since the profile must have found a role.
380 MetadataException ex("Unable to locate role-specific metadata for identity provider.");
381 annotateException(&ex,provider); // throws it
384 // Maybe verify the client address....
385 if (checkAddress.second) {
386 log.debug("verifying client address");
387 // Verify the client address exists
388 const XMLCh* wip = bpr.authnStatement->getSubjectIP();
390 // Verify the client address matches authentication
391 auto_ptr_char this_ip(wip);
392 if (strcmp(client_address, this_ip.get())) {
393 FatalProfileException ex(
394 SESSION_E_ADDRESSMISMATCH,
395 "Your client's current address ($1) differs from the one used when you authenticated "
396 "to your identity provider. To correct this problem, you may need to bypass a proxy server. "
397 "Please contact your local support staff or help desk for assistance.",
398 params(1,client_address)
400 annotateException(&ex,role); // throws it
405 catch (SAMLException&) {
410 log.error("caught unknown exception");
415 SAMLException e("An unexpected error occurred while creating your session.");
416 annotateException(&e,role);
420 // It passes all our tests -- create a new session.
421 log.info("creating new session");
425 // Insert into cache.
426 auto_ptr_char authContext(bpr.authnStatement->getAuthMethod());
427 string key=conf->getSessionCache()->insert(
429 role->getEntityDescriptor(),
431 bpr.authnStatement->getSubject(),
435 // objects owned by cache now
436 log.debug("new session id: %s", key.c_str());
437 auto_ptr_char oname(role->getEntityDescriptor()->getId());
438 out=DDF(NULL).structure();
439 out.addmember("key").string(key.c_str());
440 out.addmember("provider_id").string(oname.get());
446 SAMLException e("An unexpected error occurred while creating your session.");
447 annotateException(&e,role);
454 pair<bool,void*> SAML1Consumer::run(ShibTarget* st, bool isHandler) const
457 DDFJanitor jin(in),jout(out);
459 pair<bool,const XMLCh*> binding=getProperties()->getXMLString("Binding");
460 if (!binding.first || !XMLString::compareString(binding.second,SAMLBrowserProfile::BROWSER_POST)) {
461 #ifdef HAVE_STRCASECMP
462 if (strcasecmp(st->getRequestMethod(), "POST")) {
464 if (_stricmp(st->getRequestMethod(), "POST")) {
466 st->log(ShibTarget::LogLevelInfo, "SAML 1.x Browser/POST handler ignoring non-POST request");
467 return pair<bool,void*>(false,NULL);
469 #ifdef HAVE_STRCASECMP
470 if (!st->getContentType() || strcasecmp(st->getContentType(),"application/x-www-form-urlencoded")) {
472 if (!st->getContentType() || _stricmp(st->getContentType(),"application/x-www-form-urlencoded")) {
474 st->log(ShibTarget::LogLevelInfo, "SAML 1.x Browser/POST handler ignoring submission with unknown content-type.");
475 return pair<bool,void*>(false,NULL);
478 const char* samlResponse = st->getRequestParameter("SAMLResponse");
480 st->log(ShibTarget::LogLevelInfo, "SAML 1.x Browser/POST handler ignoring request with no SAMLResponse parameter.");
481 return pair<bool,void*>(false,NULL);
484 in=DDF(m_address.c_str()).structure();
485 in.addmember("SAMLResponse").string(samlResponse);
487 else if (!XMLString::compareString(binding.second,SAMLBrowserProfile::BROWSER_ARTIFACT)) {
488 #ifdef HAVE_STRCASECMP
489 if (strcasecmp(st->getRequestMethod(), "GET")) {
491 if (_stricmp(st->getRequestMethod(), "GET")) {
493 st->log(ShibTarget::LogLevelInfo, "SAML 1.x Browser/Artifact handler ignoring non-GET request");
494 return pair<bool,void*>(false,NULL);
497 const char* SAMLart=st->getRequestParameter("SAMLart");
499 st->log(ShibTarget::LogLevelInfo, "SAML 1.x Browser/Artifact handler ignoring request with no SAMLart parameter.");
500 return pair<bool,void*>(false,NULL);
503 in=DDF(m_address.c_str()).structure();
504 DDF artlist=in.addmember("SAMLart").list();
507 artlist.add(DDF(NULL).string(SAMLart));
508 SAMLart=st->getRequestParameter("SAMLart",artlist.integer());
512 // Compute the endpoint location.
513 string hURL=st->getHandlerURL(st->getRequestURL());
514 pair<bool,const char*> loc=getProperties()->getString("Location");
515 string recipient=loc.first ? hURL + loc.second : hURL;
516 in.addmember("recipient").string(recipient.c_str());
518 // Add remaining parameters.
519 in.addmember("application_id").string(st->getApplication()->getId());
520 in.addmember("client_address").string(st->getRemoteAddr());
522 out=st->getConfig()->getListener()->send(in);
523 if (!out["key"].isstring())
524 throw FatalProfileException("Remote processing of SAML 1.x Browser profile did not return a usable session key.");
525 string key=out["key"].string();
527 st->log(ShibTarget::LogLevelDebug, string("profile processing succeeded, new session created (") + key + ")");
529 const char* target=st->getRequestParameter("TARGET");
530 if (target && !strcmp(target,"default")) {
531 pair<bool,const char*> homeURL=st->getApplication()->getString("homeURL");
532 target=homeURL.first ? homeURL.second : "/";
534 else if (!target || !strcmp(target,"cookie")) {
535 // Pull the target value from the "relay state" cookie.
536 pair<string,const char*> relay_cookie = st->getCookieNameProps("_shibstate_");
537 const char* relay_state = st->getCookie(relay_cookie.first);
538 if (!relay_state || !*relay_state) {
539 // No apparent relay state value to use, so fall back on the default.
540 pair<bool,const char*> homeURL=st->getApplication()->getString("homeURL");
541 target=homeURL.first ? homeURL.second : "/";
544 char* rscopy=strdup(relay_state);
545 opensaml::SAMLConfig::getConfig().getURLEncoder()->decode(rscopy);
550 st->setCookie(relay_cookie.first,relay_cookie.second);
553 // We've got a good session, set the session cookie.
554 pair<string,const char*> shib_cookie=st->getCookieNameProps("_shibsession_");
555 st->setCookie(shib_cookie.first, key + shib_cookie.second);
557 const char* providerId=out["provider_id"].string();
559 const PropertySet* sessionProps=st->getApplication()->getPropertySet("Sessions");
560 pair<bool,bool> idpHistory=sessionProps->getBool("idpHistory");
561 if (!idpHistory.first || idpHistory.second) {
562 // Set an IdP history cookie locally (essentially just a CDC).
563 CommonDomainCookie cdc(st->getCookie(CommonDomainCookie::CDCName));
565 // Either leave in memory or set an expiration.
566 pair<bool,unsigned int> days=sessionProps->getUnsignedInt("idpHistoryDays");
567 if (!days.first || days.second==0)
568 st->setCookie(CommonDomainCookie::CDCName,string(cdc.set(providerId)) + shib_cookie.second);
570 time_t now=time(NULL) + (days.second * 24 * 60 * 60);
573 struct tm* ptime=gmtime_r(&now,&res);
575 struct tm* ptime=gmtime(&now);
578 strftime(timebuf,64,"%a, %d %b %Y %H:%M:%S GMT",ptime);
580 CommonDomainCookie::CDCName,
581 string(cdc.set(providerId)) + shib_cookie.second + "; expires=" + timebuf
587 // Now redirect to the target.
588 return make_pair(true, st->sendRedirect(target));
591 pair<bool,void*> ShibLogout::run(ShibTarget* st, bool isHandler) const
593 // Recover the session key.
594 pair<string,const char*> shib_cookie = st->getCookieNameProps("_shibsession_");
595 const char* session_id = st->getCookie(shib_cookie.first);
597 // Logout is best effort.
598 if (session_id && *session_id) {
600 st->getConfig()->getSessionCache()->remove(session_id,st->getApplication(),st->getRemoteAddr());
602 catch (SAMLException& e) {
603 st->log(ShibTarget::LogLevelError, string("logout processing failed with exception: ") + e.what());
607 st->log(ShibTarget::LogLevelError, "logout processing failed with unknown exception");
610 // We send the cookie property alone, which acts as an empty value.
611 st->setCookie(shib_cookie.first,shib_cookie.second);
614 const char* ret=st->getRequestParameter("return");
616 ret=getProperties()->getString("ResponseLocation").second;
618 ret=st->getApplication()->getString("homeURL").second;
621 return make_pair(true, st->sendRedirect(ret));