2 * Copyright 2001-2007 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/AbstractHandler.h>
33 #include <shibsp/SPConfig.h>
34 #include <shibsp/SPRequest.h>
40 using namespace shibsp;
41 using namespace shibtarget;
42 using namespace shibboleth;
44 using namespace opensaml::saml2md;
45 using namespace log4cpp;
48 using opensaml::CommonDomainCookie;
49 using opensaml::URLEncoder;
51 #if defined (_MSC_VER)
52 #pragma warning( push )
53 #pragma warning( disable : 4250 )
57 class SessionInitiator : public AbstractHandler
60 SessionInitiator(const DOMElement* e) : AbstractHandler(e) {}
61 ~SessionInitiator() {}
62 pair<bool,long> run(SPRequest& request, bool isHandler=true) const;
63 pair<bool,long> ShibAuthnRequest(
68 const char* providerId
72 class SAML1Consumer : public AbstractHandler, public virtual Remoted
75 SAML1Consumer(const DOMElement* e);
77 pair<bool,long> run(SPRequest& request, bool isHandler=true) const;
78 void receive(DDF& in, ostream& out);
84 int SAML1Consumer::counter = 0;
86 class ShibLogout : public AbstractHandler
89 ShibLogout(const DOMElement* e) : AbstractHandler(e) {}
91 pair<bool,long> run(SPRequest& request, bool isHandler=true) const;
95 #if defined (_MSC_VER)
96 #pragma warning( pop )
99 Handler* ShibSessionInitiatorFactory(const DOMElement* const & e)
101 return new SessionInitiator(e);
104 Handler* SAML1POSTFactory(const DOMElement* const & e)
106 return new SAML1Consumer(e);
109 Handler* SAML1ArtifactFactory(const DOMElement* const & e)
111 return new SAML1Consumer(e);
114 Handler* ShibLogoutFactory(const DOMElement* const & e)
116 return new ShibLogout(e);
119 pair<bool,long> SessionInitiator::run(SPRequest& request, bool isHandler) const
122 const char* resource=NULL;
123 const Handler* ACS=NULL;
124 const IApplication& app=dynamic_cast<const IApplication&>(request.getApplication());
128 * Binding is CGI query string with:
129 * target the resource to direct back to later
130 * acsIndex optional index of an ACS to use on the way back in
131 * providerId optional direct invocation of a specific IdP
133 const char* option=request.getParameter("acsIndex");
135 ACS=app.getAssertionConsumerServiceByIndex(atoi(option));
136 option=request.getParameter("providerId");
138 resource=request.getParameter("target");
139 if (!resource || !*resource) {
140 pair<bool,const char*> home=app.getString("homeURL");
142 resource=home.second;
144 throw opensaml::FatalProfileException("Session initiator requires a target parameter or a homeURL application property.");
147 dupresource=resource;
148 resource=dupresource.c_str();
152 // Here we actually use metadata to invoke the SSO service directly.
153 // The only currently understood binding is the Shibboleth profile.
155 MetadataProvider* m=app.getMetadataProvider();
156 xmltooling::Locker locker(m);
157 const EntityDescriptor* entity=m->getEntityDescriptor(option);
159 throw MetadataException("Session initiator unable to locate metadata for provider ($1).", xmltooling::params(1,option));
160 const IDPSSODescriptor* role=entity->getIDPSSODescriptor(shibspconstants::SHIB1_PROTOCOL_ENUM);
162 throw MetadataException(
163 "Session initiator unable to locate a Shibboleth-aware identity provider role for provider ($1).",
164 xmltooling::params(1,option)
166 const EndpointType* ep=EndpointManager<SingleSignOnService>(role->getSingleSignOnServices()).getByBinding(
167 shibspconstants::SHIB1_AUTHNREQUEST_PROFILE_URI
170 throw MetadataException(
171 "Session initiator unable to locate compatible SSO service for provider ($1).", xmltooling::params(1,option)
173 auto_ptr_char dest(ep->getLocation());
174 return ShibAuthnRequest(
175 request,ACS ? ACS : app.getDefaultAssertionConsumerService(),dest.get(),resource,app.getString("providerId").second
180 // We're running as a "virtual handler" from within the filter.
181 // The target resource is the current one and everything else is defaulted.
182 resource=request.getRequestURL();
185 if (!ACS) ACS=app.getDefaultAssertionConsumerService();
187 // For now, we only support external session initiation via a wayfURL
188 pair<bool,const char*> wayfURL=getString("wayfURL");
190 throw ConfigurationException("Session initiator is missing wayfURL property.");
192 pair<bool,const XMLCh*> wayfBinding=getXMLString("wayfBinding");
193 if (!wayfBinding.first || !XMLString::compareString(wayfBinding.second,shibspconstants::SHIB1_AUTHNREQUEST_PROFILE_URI))
195 return ShibAuthnRequest(request,ACS,wayfURL.second,resource,app.getString("providerId").second);
196 else if (!strcmp(getString("wayfBinding").second,"urn:mace:shibboleth:1.0:profiles:EAuth")) {
197 // TODO: Finalize E-Auth profile URI
198 pair<bool,bool> localRelayState=request.getServiceProvider().getPropertySet("InProcess")->getBool("localRelayState");
199 if (!localRelayState.first || !localRelayState.second)
200 throw ConfigurationException("E-Authn requests cannot include relay state, so localRelayState must be enabled.");
202 // Here we store the state in a cookie.
203 pair<string,const char*> shib_cookie=app.getCookieNameProps("_shibstate_");
204 string stateval = opensaml::SAMLConfig::getConfig().getURLEncoder()->encode(resource) + shib_cookie.second;
205 request.setCookie(shib_cookie.first.c_str(),stateval.c_str());
206 return make_pair(true, request.sendRedirect(wayfURL.second));
209 throw opensaml::BindingException("Unsupported WAYF binding ($1).", xmltooling::params(1,getString("wayfBinding").second));
212 // Handles Shib 1.x AuthnRequest profile.
213 pair<bool,long> SessionInitiator::ShibAuthnRequest(
215 const Handler* shire,
218 const char* providerId
221 // Compute the ACS URL. We add the ACS location to the base handlerURL.
222 // Legacy configs will not have the Location property specified, so no suffix will be added.
223 string ACSloc=request.getHandlerURL(target);
224 pair<bool,const char*> loc=shire ? shire->getString("Location") : pair<bool,const char*>(false,NULL);
225 if (loc.first) ACSloc+=loc.second;
227 URLEncoder* urlenc = opensaml::SAMLConfig::getConfig().getURLEncoder();
230 sprintf(timebuf,"%u",time(NULL));
231 string req=string(dest) + "?shire=" + urlenc->encode(ACSloc.c_str()) + "&time=" + timebuf;
233 // How should the resource value be preserved?
234 pair<bool,bool> localRelayState=request.getServiceProvider().getPropertySet("InProcess")->getBool("localRelayState");
235 if (!localRelayState.first || !localRelayState.second) {
236 // The old way, just send it along.
237 req+="&target=" + urlenc->encode(target);
240 // Here we store the state in a cookie and send a fixed
241 // value to the IdP so we can recognize it on the way back.
242 pair<string,const char*> shib_cookie=request.getApplication().getCookieNameProps("_shibstate_");
243 string stateval = urlenc->encode(target) + shib_cookie.second;
244 request.setCookie(shib_cookie.first.c_str(),stateval.c_str());
245 req+="&target=cookie";
248 // Only omitted for 1.1 style requests.
250 req+="&providerId=" + urlenc->encode(providerId);
252 return make_pair(true, request.sendRedirect(req.c_str()));
255 SAML1Consumer::SAML1Consumer(const DOMElement* e) : AbstractHandler(e)
257 m_address += ('A' + (counter++));
258 m_address += "::SAML1Consumer::run";
260 // Register for remoted messages.
261 if (SPConfig::getConfig().isEnabled(SPConfig::OutOfProcess))
262 SPConfig::getConfig().getServiceProvider()->getListenerService()->regListener(m_address.c_str(),this);
265 SAML1Consumer::~SAML1Consumer()
267 ListenerService* listener=SPConfig::getConfig().getServiceProvider()->getListenerService(false);
268 if (listener && SPConfig::getConfig().isEnabled(SPConfig::OutOfProcess))
269 listener->unregListener(m_address.c_str(),this);
274 * IPC message definitions:
276 * [A-Z]::SAML1Consumer::run
282 * SAMLResponse or SAMLart list
288 void SAML1Consumer::receive(DDF& in, ostream& sink)
291 xmltooling::NDC ndc("receive");
293 Category& log=Category::getInstance(SHIBT_LOGCAT".SAML1Consumer");
296 const char* aid=in["application_id"].string();
297 const IApplication* app=aid ? dynamic_cast<const IApplication*>(SPConfig::getConfig().getServiceProvider()->getApplication(aid)) : NULL;
299 // Something's horribly wrong.
300 log.error("couldn't find application (%s) for new session", aid ? aid : "(missing)");
301 throw SAMLException("Unable to locate application for new session, deleted?");
304 // Check required parameters.
305 const char* client_address=in["client_address"].string();
306 const char* recipient=in["recipient"].string();
307 if (!client_address || !recipient)
308 throw SAMLException("Required parameters missing in call to SAML1Consumer::run");
310 log.debug("processing new assertion for %s", client_address);
311 log.debug("recipient: %s", recipient);
312 log.debug("application: %s", app->getId());
314 // Access the application config.
315 ServiceProvider* conf=SPConfig::getConfig().getServiceProvider();
316 xmltooling::Locker confLocker(conf);
318 auto_ptr_XMLCh wrecipient(recipient);
320 pair<bool,bool> checkAddress=pair<bool,bool>(false,true);
321 pair<bool,bool> checkReplay=pair<bool,bool>(false,true);
322 const PropertySet* props=app->getPropertySet("Sessions");
324 checkAddress=props->getBool("checkAddress");
325 if (!checkAddress.first)
326 checkAddress.second=true;
327 checkReplay=props->getBool("checkReplay");
328 if (!checkReplay.first)
329 checkReplay.second=true;
332 // Supports either version...
333 pair<bool,unsigned int> version=getUnsignedInt("MinorVersion","urn:oasis:names:tc:SAML:1.0:protocol");
337 const EntityDescriptor* provider=NULL;
338 const RoleDescriptor* role=NULL;
339 MetadataProvider* m=app->getMetadataProvider();
340 xmltooling::Locker locker(m);
341 SAMLBrowserProfile::BrowserProfileResponse bpr;
344 const char* samlResponse=in["SAMLResponse"].string();
347 log.debug("executing Browser/POST profile...");
348 bpr=app->getBrowserProfile()->receive(
357 vector<const char*> SAMLart;
358 DDF arts=in["SAMLart"];
359 DDF art=arts.first();
360 while (art.isstring()) {
361 SAMLart.push_back(art.string());
364 auto_ptr<SAMLBrowserProfile::ArtifactMapper> artifactMapper(app->getArtifactMapper());
365 log.debug("executing Browser/Artifact profile...");
366 bpr=app->getBrowserProfile()->receive(
369 artifactMapper.get(),
374 // Blow it away to clear any locks that might be held.
375 delete artifactMapper.release();
378 // Try and map to metadata (again).
379 // Once the metadata layer is in the SAML core, the repetition will be fixed.
380 provider=m->getEntityDescriptor(bpr.assertion->getIssuer());
381 if (!provider && bpr.authnStatement->getSubject()->getNameIdentifier() &&
382 bpr.authnStatement->getSubject()->getNameIdentifier()->getNameQualifier())
383 provider=m->getEntityDescriptor(bpr.authnStatement->getSubject()->getNameIdentifier()->getNameQualifier());
385 role=provider->getIDPSSODescriptor(
386 version.second==1 ? samlconstants::SAML11_PROTOCOL_ENUM : samlconstants::SAML10_PROTOCOL_ENUM
390 // This isn't likely, since the profile must have found a role.
392 MetadataException ex("Unable to locate role-specific metadata for identity provider.");
393 annotateException(&ex,provider); // throws it
396 // Maybe verify the client address....
397 if (checkAddress.second) {
398 log.debug("verifying client address");
399 // Verify the client address exists
400 const XMLCh* wip = bpr.authnStatement->getSubjectIP();
402 // Verify the client address matches authentication
403 auto_ptr_char this_ip(wip);
404 if (strcmp(client_address, this_ip.get())) {
405 opensaml::FatalProfileException ex(
406 "Your client's current address ($1) differs from the one used when you authenticated "
407 "to your identity provider. To correct this problem, you may need to bypass a proxy server. "
408 "Please contact your local support staff or help desk for assistance.",
409 xmltooling::params(1,client_address)
411 annotateException(&ex,role); // throws it
421 log.error("caught unknown exception");
426 SAMLException e("An unexpected error occurred while creating your session.");
427 shibboleth::annotateException(&e,role);
431 // It passes all our tests -- create a new session.
432 log.info("creating new session");
434 // Insert into cache.
435 auto_ptr_char authContext(bpr.authnStatement->getAuthMethod());
436 string key=dynamic_cast<ISessionCache*>(conf->getSessionCache())->insert(
440 bpr.authnStatement->getSubject(),
444 // objects owned by cache now
445 log.debug("new session id: %s", key.c_str());
446 auto_ptr_char oname(provider->getEntityID());
447 DDF out=DDF(NULL).structure();
448 out.addmember("key").string(key.c_str());
449 out.addmember("provider_id").string(oname.get());
454 pair<bool,long> SAML1Consumer::run(SPRequest& request, bool isHandler) const
457 DDFJanitor jin(in),jout(out);
459 pair<bool,const XMLCh*> binding=getXMLString("Binding");
460 if (!binding.first || !XMLString::compareString(binding.second,SAMLBrowserProfile::BROWSER_POST)) {
461 #ifdef HAVE_STRCASECMP
462 if (strcasecmp(request.getMethod(), "POST")) {
464 if (_stricmp(request.getMethod(), "POST")) {
466 request.log(SPRequest::SPInfo, "SAML 1.x Browser/POST handler ignoring non-POST request");
467 return pair<bool,long>(false,NULL);
469 #ifdef HAVE_STRCASECMP
470 if (strcasecmp(request.getContentType().c_str(),"application/x-www-form-urlencoded")) {
472 if (_stricmp(request.getContentType().c_str(),"application/x-www-form-urlencoded")) {
474 request.log(SPRequest::SPInfo, "SAML 1.x Browser/POST handler ignoring submission with unknown content-type.");
475 return pair<bool,long>(false,0);
478 const char* samlResponse = request.getParameter("SAMLResponse");
480 request.log(SPRequest::SPInfo, "SAML 1.x Browser/POST handler ignoring request with no SAMLResponse parameter.");
481 return pair<bool,long>(false,0);
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(request.getMethod(), "GET")) {
491 if (_stricmp(request.getMethod(), "GET")) {
493 request.log(SPRequest::SPInfo, "SAML 1.x Browser/Artifact handler ignoring non-GET request");
494 return pair<bool,long>(false,0);
497 vector<const char*> arts;
498 if (request.getParameters("SAMLart",arts)==0) {
499 request.log(SPRequest::SPInfo, "SAML 1.x Browser/Artifact handler ignoring request with no SAMLart parameter.");
500 return pair<bool,long>(false,0);
503 in=DDF(m_address.c_str()).structure();
504 DDF artlist=in.addmember("SAMLart").list();
506 for (vector<const char*>::const_iterator a=arts.begin(); a!=arts.end(); ++a)
507 artlist.add(DDF(NULL).string(*a));
510 // Compute the endpoint location.
511 string hURL=request.getHandlerURL(request.getRequestURL());
512 pair<bool,const char*> loc=getString("Location");
513 string recipient=loc.first ? hURL + loc.second : hURL;
514 in.addmember("recipient").string(recipient.c_str());
516 // Add remaining parameters.
517 in.addmember("application_id").string(request.getApplication().getId());
518 in.addmember("client_address").string(request.getRemoteAddr().c_str());
520 out=request.getServiceProvider().getListenerService()->send(in);
521 if (!out["key"].isstring())
522 throw opensaml::FatalProfileException("Remote processing of SAML 1.x Browser profile did not return a usable session key.");
523 string key=out["key"].string();
525 request.log(SPRequest::SPDebug, string("profile processing succeeded, new session created (") + key + ")");
527 const char* target=request.getParameter("TARGET");
528 if (target && !strcmp(target,"default")) {
529 pair<bool,const char*> homeURL=request.getApplication().getString("homeURL");
530 target=homeURL.first ? homeURL.second : "/";
532 else if (!target || !strcmp(target,"cookie")) {
533 // Pull the target value from the "relay state" cookie.
534 pair<string,const char*> relay_cookie = request.getApplication().getCookieNameProps("_shibstate_");
535 const char* relay_state = request.getCookie(relay_cookie.first.c_str());
536 if (!relay_state || !*relay_state) {
537 // No apparent relay state value to use, so fall back on the default.
538 pair<bool,const char*> homeURL=request.getApplication().getString("homeURL");
539 target=homeURL.first ? homeURL.second : "/";
542 char* rscopy=strdup(relay_state);
543 opensaml::SAMLConfig::getConfig().getURLEncoder()->decode(rscopy);
548 request.setCookie(relay_cookie.first.c_str(),relay_cookie.second);
551 // We've got a good session, set the session cookie.
552 pair<string,const char*> shib_cookie=request.getApplication().getCookieNameProps("_shibsession_");
553 key += shib_cookie.second;
554 request.setCookie(shib_cookie.first.c_str(), key.c_str());
556 const char* providerId=out["provider_id"].string();
558 const PropertySet* sessionProps=request.getApplication().getPropertySet("Sessions");
559 pair<bool,bool> idpHistory=sessionProps->getBool("idpHistory");
560 if (!idpHistory.first || idpHistory.second) {
561 // Set an IdP history cookie locally (essentially just a CDC).
562 CommonDomainCookie cdc(request.getCookie(CommonDomainCookie::CDCName));
564 // Either leave in memory or set an expiration.
565 pair<bool,unsigned int> days=sessionProps->getUnsignedInt("idpHistoryDays");
566 if (!days.first || days.second==0) {
567 key = string(cdc.set(providerId)) + shib_cookie.second;
568 request.setCookie(CommonDomainCookie::CDCName, key.c_str());
571 time_t now=time(NULL) + (days.second * 24 * 60 * 60);
574 struct tm* ptime=gmtime_r(&now,&res);
576 struct tm* ptime=gmtime(&now);
579 strftime(timebuf,64,"%a, %d %b %Y %H:%M:%S GMT",ptime);
580 key = string(cdc.set(providerId)) + shib_cookie.second + "; expires=" + timebuf;
581 request.setCookie(CommonDomainCookie::CDCName, key.c_str());
586 // Now redirect to the target.
587 return make_pair(true, request.sendRedirect(target));
590 pair<bool,long> ShibLogout::run(SPRequest& request, bool isHandler) const
592 // Recover the session key.
593 pair<string,const char*> shib_cookie = request.getApplication().getCookieNameProps("_shibsession_");
594 const char* session_id = request.getCookie(shib_cookie.first.c_str());
596 // Logout is best effort.
597 if (session_id && *session_id) {
599 // TODO: port to new cache API
600 //request.getServiceProvider().getSessionCache()->remove(session_id,request.getApplication(),request.getRemoteAddr().c_str());
602 catch (exception& e) {
603 request.log(SPRequest::SPError, string("logout processing failed with exception: ") + e.what());
607 request.log(SPRequest::SPError, "logout processing failed with unknown exception");
610 // We send the cookie property alone, which acts as an empty value.
611 request.setCookie(shib_cookie.first.c_str(),shib_cookie.second);
614 const char* ret=request.getParameter("return");
616 ret=getString("ResponseLocation").second;
618 ret=request.getApplication().getString("homeURL").second;
621 return make_pair(true, request.sendRedirect(ret));