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 * handlers.cpp -- ADFS-aware profile handlers that plug into SP
26 #ifndef HAVE_STRCASECMP
27 # define strcasecmp stricmp
32 using namespace shibboleth;
33 using namespace shibtarget;
35 using namespace adfs::logging;
39 // TODO: Refactor/extend API so I don't have to cut/paste this code out of libshib-target
40 class SessionInitiator : virtual public IHandler
43 SessionInitiator(const DOMElement* e) {}
44 ~SessionInitiator() {}
45 pair<bool,void*> run(ShibTarget* st, const IPropertySet* handler, bool isHandler=true);
48 const IPropertySet* getCompatibleACS(const IApplication* app, const vector<ShibProfile>& profiles);
49 pair<bool,void*> ShibAuthnRequest(
51 const IPropertySet* shire,
54 const char* providerId
56 pair<bool,void*> ADFSAuthnRequest(
58 const IPropertySet* shire,
61 const char* providerId
65 class ADFSHandler : virtual public IHandler
68 ADFSHandler(const DOMElement* e) {}
70 pair<bool,void*> run(ShibTarget* st, const IPropertySet* handler, bool isHandler=true);
75 IPlugIn* ADFSSessionInitiatorFactory(const DOMElement* e)
77 return new SessionInitiator(e);
80 IPlugIn* ADFSHandlerFactory(const DOMElement* e)
82 return new ADFSHandler(e);
85 pair<bool,void*> SessionInitiator::run(ShibTarget* st, const IPropertySet* handler, bool isHandler)
88 const char* resource=NULL;
89 const IPropertySet* ACS=NULL;
90 const IApplication* app=st->getApplication();
94 * Binding is CGI query string with:
95 * target the resource to direct back to later
96 * acsIndex optional index of an ACS to use on the way back in
97 * providerId optional direct invocation of a specific IdP
99 string query=st->getArgs();
100 CgiParse parser(query.c_str(),query.length());
102 const char* option=parser.get_value("acsIndex");
104 ACS=app->getAssertionConsumerServiceByIndex(atoi(option));
105 option=parser.get_value("providerId");
107 resource=parser.get_value("target");
108 if (!resource || !*resource) {
109 pair<bool,const char*> home=app->getString("homeURL");
111 resource=home.second;
113 throw FatalProfileException("Session initiator requires a target parameter or a homeURL application property.");
116 dupresource=resource;
117 resource=dupresource.c_str();
121 // Here we actually use metadata to invoke the SSO service directly.
122 Metadata m(app->getMetadataProviders());
123 const IEntityDescriptor* entity=m.lookup(option);
125 throw MetadataException("Session initiator unable to locate metadata for provider ($1).", params(1,option));
127 // Look for an IdP role with Shib support.
128 const IIDPSSODescriptor* role=entity->getIDPSSODescriptor(Constants::SHIB_NS);
130 // Look for a SSO endpoint with Shib support.
131 const IEndpointManager* SSO=role->getSingleSignOnServiceManager();
132 const IEndpoint* ep=SSO->getEndpointByBinding(Constants::SHIB_AUTHNREQUEST_PROFILE_URI);
135 // Look for an ACS with SAML support.
136 vector<ShibProfile> v;
137 v.push_back(SAML11_POST);
138 v.push_back(SAML11_ARTIFACT);
139 v.push_back(SAML10_ARTIFACT);
140 v.push_back(SAML10_POST);
141 ACS=getCompatibleACS(app,v);
143 auto_ptr_char dest(ep->getLocation());
144 return ShibAuthnRequest(
145 st,ACS ? ACS : app->getDefaultAssertionConsumerService(),dest.get(),resource,app->getString("providerId").second
149 // Look for an IdP role with ADFS support.
150 role=entity->getIDPSSODescriptor(adfs::XML::WSFED_NS);
152 // Finally, look for a SSO endpoint with ADFS support.
153 const IEndpointManager* SSO=role->getSingleSignOnServiceManager();
154 const IEndpoint* ep=SSO->getEndpointByBinding(adfs::XML::WSFED_NS);
157 // Look for an ACS with ADFS support.
158 vector<ShibProfile> v;
159 v.push_back(ADFS_SSO);
160 ACS=getCompatibleACS(app,v);
162 auto_ptr_char dest(ep->getLocation());
163 return ADFSAuthnRequest(
164 st,ACS ? ACS : app->getDefaultAssertionConsumerService(),dest.get(),resource,app->getString("providerId").second
169 throw MetadataException(
170 "Session initiator unable to locate a compatible identity provider SSO endpoint for provider ($1).",
176 // We're running as a "virtual handler" from within the filter.
177 // The target resource is the current one and everything else is defaulted.
178 resource=st->getRequestURL();
181 // For now, we only support external session initiation via a wayfURL
182 pair<bool,const char*> wayfURL=handler->getString("wayfURL");
184 throw ConfigurationException("Session initiator is missing wayfURL property.");
186 pair<bool,const XMLCh*> wayfBinding=handler->getXMLString("wayfBinding");
187 if (!wayfBinding.first || !XMLString::compareString(wayfBinding.second,Constants::SHIB_AUTHNREQUEST_PROFILE_URI))
189 return ShibAuthnRequest(st,ACS,wayfURL.second,resource,app->getString("providerId").second);
190 else if (!XMLString::compareString(wayfBinding.second,Constants::SHIB_LEGACY_AUTHNREQUEST_PROFILE_URI))
192 return ShibAuthnRequest(st,ACS,wayfURL.second,resource,NULL);
193 else if (!strcmp(handler->getString("wayfBinding").second,"urn:mace:shibboleth:1.0:profiles:EAuth")) {
194 pair<bool,bool> localRelayState=st->getConfig()->getPropertySet("Local")->getBool("localRelayState");
195 if (!localRelayState.first || !localRelayState.second)
196 throw ConfigurationException("E-Authn requests cannot include relay state, so localRelayState must be enabled.");
198 // Here we store the state in a cookie.
199 pair<string,const char*> shib_cookie=st->getCookieNameProps("_shibstate_");
200 st->setCookie(shib_cookie.first,CgiParse::url_encode(resource) + shib_cookie.second);
201 return make_pair(true, st->sendRedirect(wayfURL.second));
203 else if (!XMLString::compareString(wayfBinding.second,adfs::XML::WSFED_NS))
204 return ADFSAuthnRequest(st,ACS,wayfURL.second,resource,app->getString("providerId").second);
206 throw UnsupportedProfileException("Unsupported WAYF binding ($1).", params(1,handler->getString("wayfBinding").second));
209 // Get an ACS that can handle one of the desired profiles
210 const IPropertySet* SessionInitiator::getCompatibleACS(const IApplication* app, const vector<ShibProfile>& profiles)
212 // This isn't going to be very efficient until I can revise the IApplication API to
213 // support ACS lookup by profile.
216 for (vector<ShibProfile>::const_iterator p=profiles.begin(); p!=profiles.end(); p++)
219 // See if the default is acceptable.
220 const IPropertySet* ACS=app->getDefaultAssertionConsumerService();
221 pair<bool,const XMLCh*> binding=ACS ? ACS->getXMLString("Binding") : pair<bool,const XMLCh*>(false,NULL);
222 if (!ACS || !binding.first || !XMLString::compareString(binding.second,SAMLBrowserProfile::BROWSER_POST)) {
223 pair<bool,unsigned int> version =
224 ACS ? ACS->getUnsignedInt("MinorVersion","urn:oasis:names:tc:SAML:1.0:protocol") : pair<bool,unsigned int>(false,1);
227 if (mask & (version.second==1 ? SAML11_POST : SAML10_POST))
230 else if (!XMLString::compareString(binding.second,SAMLBrowserProfile::BROWSER_ARTIFACT)) {
231 pair<bool,unsigned int> version=ACS->getUnsignedInt("MinorVersion","urn:oasis:names:tc:SAML:1.0:protocol");
234 if (mask & (version.second==1 ? SAML11_ARTIFACT : SAML10_ARTIFACT))
237 else if (!XMLString::compareString(binding.second,adfs::XML::WSFED_NS)) {
242 // If not, iterate by profile.
243 for (vector<ShibProfile>::const_iterator i=profiles.begin(); i!=profiles.end(); i++) {
244 for (unsigned int j=0; j<=65535; j++) {
245 ACS=app->getAssertionConsumerServiceByIndex(j);
247 break; // we're past 0 and didn't get a hit, so we'll bail
249 binding=ACS->getXMLString("Binding");
250 pair<bool,unsigned int> version=ACS->getUnsignedInt("MinorVersion","urn:oasis:names:tc:SAML:1.0:protocol");
255 if (version.second==1 && (!binding.first || !XMLString::compareString(binding.second,SAMLBrowserProfile::BROWSER_POST)))
258 case SAML11_ARTIFACT:
259 if (version.second==1 && !XMLString::compareString(binding.second,SAMLBrowserProfile::BROWSER_ARTIFACT))
263 if (!XMLString::compareString(binding.second,adfs::XML::WSFED_NS))
267 if (version.second==0 && (!binding.first || !XMLString::compareString(binding.second,SAMLBrowserProfile::BROWSER_POST)))
270 case SAML10_ARTIFACT:
271 if (version.second==0 && !XMLString::compareString(binding.second,SAMLBrowserProfile::BROWSER_ARTIFACT))
284 // Handles Shib 1.x AuthnRequest profile.
285 pair<bool,void*> SessionInitiator::ShibAuthnRequest(
287 const IPropertySet* shire,
290 const char* providerId
294 // Look for an ACS with SAML support.
295 vector<ShibProfile> v;
296 v.push_back(SAML11_POST);
297 v.push_back(SAML11_ARTIFACT);
298 v.push_back(SAML10_ARTIFACT);
299 v.push_back(SAML10_POST);
300 shire=getCompatibleACS(st->getApplication(),v);
303 shire=st->getApplication()->getDefaultAssertionConsumerService();
305 // Compute the ACS URL. We add the ACS location to the handler baseURL.
306 // Legacy configs will not have an ACS specified, so no suffix will be added.
307 string ACSloc=st->getHandlerURL(target);
308 if (shire) ACSloc+=shire->getString("Location").second;
311 sprintf(timebuf,"%lu",time(NULL));
312 string req=string(dest) + "?shire=" + CgiParse::url_encode(ACSloc.c_str()) + "&time=" + timebuf;
314 // How should the resource value be preserved?
315 pair<bool,bool> localRelayState=st->getConfig()->getPropertySet("Local")->getBool("localRelayState");
316 if (!localRelayState.first || !localRelayState.second) {
317 // The old way, just send it along.
318 req+="&target=" + CgiParse::url_encode(target);
321 // Here we store the state in a cookie and send a fixed
322 // value to the IdP so we can recognize it on the way back.
323 pair<string,const char*> shib_cookie=st->getCookieNameProps("_shibstate_");
324 st->setCookie(shib_cookie.first,CgiParse::url_encode(target) + shib_cookie.second);
325 req+="&target=cookie";
328 // Only omitted for 1.1 style requests.
330 req+="&providerId=" + CgiParse::url_encode(providerId);
332 return make_pair(true, st->sendRedirect(req));
335 // Handles ADFS token request profile.
336 pair<bool,void*> SessionInitiator::ADFSAuthnRequest(
338 const IPropertySet* shire,
341 const char* providerId
345 // Look for an ACS with ADFS support.
346 vector<ShibProfile> v;
347 v.push_back(ADFS_SSO);
348 shire=getCompatibleACS(st->getApplication(),v);
351 shire=st->getApplication()->getDefaultAssertionConsumerService();
353 // Compute the ACS URL. We add the ACS location to the handler baseURL.
354 // Legacy configs will not have an ACS specified, so no suffix will be added.
355 string ACSloc=st->getHandlerURL(target);
356 if (shire) ACSloc+=shire->getString("Location").second;
359 time_t epoch=time(NULL);
360 #ifndef HAVE_GMTIME_R
361 struct tm* ptime=gmtime(&epoch);
364 struct tm* ptime=gmtime_r(&epoch,&res);
367 strftime(timebuf,32,"%Y-%m-%dT%H:%M:%SZ",ptime);
369 string req=string(dest) + "?wa=wsignin1.0&wreply=" + CgiParse::url_encode(ACSloc.c_str()) + "&wct=" + CgiParse::url_encode(timebuf);
371 // How should the resource value be preserved?
372 pair<bool,bool> localRelayState=st->getConfig()->getPropertySet("Local")->getBool("localRelayState");
373 if (!localRelayState.first || !localRelayState.second) {
374 // The old way, just send it along.
375 req+="&wctx=" + CgiParse::url_encode(target);
378 // Here we store the state in a cookie and send a fixed
379 // value to the IdP so we can recognize it on the way back.
380 pair<string,const char*> shib_cookie=st->getCookieNameProps("_shibstate_");
381 st->setCookie(shib_cookie.first,CgiParse::url_encode(target) + shib_cookie.second);
385 req+="&wtrealm=" + CgiParse::url_encode(providerId);
387 return make_pair(true, st->sendRedirect(req));
390 pair<bool,void*> ADFSHandler::run(ShibTarget* st, const IPropertySet* handler, bool isHandler)
392 const IApplication* app=st->getApplication();
394 // Check for logout/GET first.
395 if (!strcasecmp(st->getRequestMethod(), "GET")) {
397 * Only legal GET is a signoutcleanup request...
398 * wa=wsignoutcleanup1.0
400 string query=st->getArgs();
401 CgiParse parser(query.c_str(),query.length());
402 const char* wa=parser.get_value("wa");
403 if (!wa || (strcmp(wa,"wsignout1.0") && strcmp(wa,"wsignoutcleanup1.0")))
404 throw FatalProfileException("ADFS protocol handler received invalid action request ($1)", params(1,wa ? wa : "none"));
406 // Recover the session key.
407 pair<string,const char*> shib_cookie = st->getCookieNameProps("_shibsession_");
408 const char* session_id = st->getCookie(shib_cookie.first);
410 // Logout is best effort.
411 if (session_id && *session_id) {
413 st->getConfig()->getListener()->sessionEnd(st->getApplication(),session_id);
415 catch (SAMLException& e) {
416 st->log(ShibTarget::LogLevelError, string("logout processing failed with exception: ") + e.what());
420 st->log(ShibTarget::LogLevelError, "logout processing failed with unknown exception");
423 // We send the cookie property alone, which acts as an empty value.
424 st->setCookie(shib_cookie.first,shib_cookie.second);
427 const char* ret=parser.get_value("wreply");
429 ret=handler->getString("ResponseLocation").second;
431 ret=st->getApplication()->getString("homeURL").second;
434 return make_pair(true, st->sendRedirect(ret));
437 if (strcasecmp(st->getRequestMethod(), "POST"))
438 throw FatalProfileException(
439 "ADFS protocol handler does not support HTTP method ($1)", params(1,st->getRequestMethod())
442 if (!st->getContentType() || strcasecmp(st->getContentType(),"application/x-www-form-urlencoded"))
443 throw FatalProfileException(
444 "Blocked invalid content-type ($1) submitted to ADFS protocol handler", params(1,st->getContentType())
447 string input=st->getPostData();
449 throw FatalProfileException("ADFS protocol handler received no data from browser");
451 ShibProfile profile=ADFS_SSO;
452 string cookie,target,providerId;
454 string hURL=st->getHandlerURL(st->getRequestURL());
455 pair<bool,const char*> loc=handler->getString("Location");
456 string recipient=loc.first ? hURL + loc.second : hURL;
457 st->getConfig()->getListener()->sessionNew(
468 st->log(ShibTarget::LogLevelDebug, string("profile processing succeeded, new session created (") + cookie + ")");
470 if (target=="default") {
471 pair<bool,const char*> homeURL=app->getString("homeURL");
472 target=homeURL.first ? homeURL.second : "/";
474 else if (target=="cookie" || target.empty()) {
475 // Pull the target value from the "relay state" cookie.
476 pair<string,const char*> relay_cookie = st->getCookieNameProps("_shibstate_");
477 const char* relay_state = st->getCookie(relay_cookie.first);
478 if (!relay_state || !*relay_state) {
479 // No apparent relay state value to use, so fall back on the default.
480 pair<bool,const char*> homeURL=app->getString("homeURL");
481 target=homeURL.first ? homeURL.second : "/";
484 char* rscopy=strdup(relay_state);
485 CgiParse::url_decode(rscopy);
491 // We've got a good session, set the session cookie.
492 pair<string,const char*> shib_cookie=st->getCookieNameProps("_shibsession_");
493 st->setCookie(shib_cookie.first, cookie + shib_cookie.second);
495 const IPropertySet* sessionProps=app->getPropertySet("Sessions");
496 pair<bool,bool> idpHistory=sessionProps->getBool("idpHistory");
497 if (!idpHistory.first || idpHistory.second) {
498 // Set an IdP history cookie locally (essentially just a CDC).
499 CommonDomainCookie cdc(st->getCookie(CommonDomainCookie::CDCName));
501 // Either leave in memory or set an expiration.
502 pair<bool,unsigned int> days=sessionProps->getUnsignedInt("idpHistoryDays");
503 if (!days.first || days.second==0)
504 st->setCookie(CommonDomainCookie::CDCName,string(cdc.set(providerId.c_str())) + shib_cookie.second);
506 time_t now=time(NULL) + (days.second * 24 * 60 * 60);
509 struct tm* ptime=gmtime_r(&now,&res);
511 struct tm* ptime=gmtime(&now);
514 strftime(timebuf,64,"%a, %d %b %Y %H:%M:%S GMT",ptime);
516 CommonDomainCookie::CDCName,
517 string(cdc.set(providerId.c_str())) + shib_cookie.second + "; expires=" + timebuf
522 // Now redirect to the target.
523 return make_pair(true, st->sendRedirect(target));