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.
20 * Interface to a Shibboleth ServiceProvider instance.
24 #include "exceptions.h"
25 #include "AccessControl.h"
26 #include "Application.h"
28 #include "ServiceProvider.h"
29 #include "SessionCache.h"
30 #include "SPRequest.h"
31 #include "util/TemplateParameters.h"
35 #include <saml/saml2/metadata/Metadata.h>
36 #include <saml/util/SAMLConstants.h>
37 #include <xmltooling/XMLToolingConfig.h>
38 #include <xmltooling/util/NDC.h>
\r
39 #include <xmltooling/util/XMLHelper.h>
\r
41 using namespace shibsp;
42 using namespace opensaml::saml2md;
43 using namespace opensaml;
44 using namespace xmltooling;
48 //SHIBSP_DLLLOCAL PluginManager<ServiceProvider,const DOMElement*>::Factory XMLServiceProviderFactory;
50 long SHIBSP_DLLLOCAL sendError(
51 SPRequest& request, const Application* app, const char* page, TemplateParameters& tp, const XMLToolingException* ex=NULL
54 request.setContentType("text/html");
55 request.setResponseHeader("Expires","01-Jan-1997 12:00:00 GMT");
56 request.setResponseHeader("Cache-Control","private,no-store,no-cache");
58 const PropertySet* props=app ? app->getPropertySet("Errors") : NULL;
60 pair<bool,const char*> p=props->getString(page);
62 ifstream infile(p.second);
64 tp.setPropertySet(props);
66 XMLToolingConfig::getConfig().getTemplateEngine()->run(infile, str, tp, ex);
67 return request.sendResponse(str);
70 else if (!strcmp(page,"access")) {
71 istringstream msg("Access Denied");
72 return static_cast<opensaml::GenericResponse&>(request).sendResponse(msg, HTTPResponse::SAML_HTTP_STATUS_FORBIDDEN);
76 string errstr = string("sendError could not process error template (") + page + ")";
77 request.log(SPRequest::SPError, errstr);
78 istringstream msg("Internal Server Error. Please contact the site administrator.");
79 return request.sendError(msg);
82 void SHIBSP_DLLLOCAL clearHeaders(SPRequest& request) {
83 // Clear invariant stuff.
84 request.clearHeader("Shib-Origin-Site");
85 request.clearHeader("Shib-Identity-Provider");
86 request.clearHeader("Shib-Authentication-Method");
87 request.clearHeader("Shib-NameIdentifier-Format");
88 request.clearHeader("Shib-Attributes");
89 request.clearHeader("Shib-Application-ID");
91 // Clear out the list of mapped attributes
93 Iterator<IAAP*> provs=dynamic_cast<const IApplication&>(getApplication()).getAAPProviders();
94 while (provs.hasNext()) {
95 IAAP* aap=provs.next();
96 xmltooling::Locker locker(aap);
97 Iterator<const IAttributeRule*> rules=aap->getAttributeRules();
98 while (rules.hasNext()) {
99 const char* header=rules.next()->getHeader();
101 request.clearHeader(header);
107 static const XMLCh SessionInitiator[] = UNICODE_LITERAL_16(S,e,s,s,i,o,n,I,n,i,t,i,a,t,o,r);
\r
110 void SHIBSP_API shibsp::registerServiceProviders()
112 //SPConfig::getConfig().ServiceProviderManager.registerFactory(XML_SERVICE_PROVIDER, XMLServiceProviderFactory);
115 pair<bool,long> ServiceProvider::doAuthentication(SPRequest& request, bool handler) const
118 xmltooling::NDC ndc("doAuthentication");
\r
121 const Application* app=NULL;
\r
122 const char* procState = "Request Processing Error";
\r
123 string targetURL = request.getRequestURL();
\r
126 RequestMapper::Settings settings = request.getRequestSettings();
\r
127 app = &(request.getApplication());
\r
129 // If not SSL, check to see if we should block or redirect it.
\r
130 if (!request.isSecure()) {
\r
131 pair<bool,const char*> redirectToSSL = settings.first->getString("redirectToSSL");
\r
132 if (redirectToSSL.first) {
\r
133 #ifdef HAVE_STRCASECMP
\r
134 if (!strcasecmp("GET",request.getMethod()) || !strcasecmp("HEAD",request.getMethod())) {
\r
136 if (!stricmp("GET",request.getMethod()) || !stricmp("HEAD",request.getMethod())) {
\r
138 // Compute the new target URL
\r
139 string redirectURL = string("https://") + request.getHostname();
\r
140 if (strcmp(redirectToSSL.second,"443")) {
\r
141 redirectURL = redirectURL + ':' + redirectToSSL.second;
\r
143 redirectURL += request.getRequestURI();
\r
144 return make_pair(true, request.sendRedirect(redirectURL.c_str()));
\r
147 TemplateParameters tp;
\r
148 tp.m_map["requestURL"] = targetURL.substr(0,targetURL.find('?'));
\r
149 return make_pair(true,sendError(request, app, "ssl", tp));
\r
154 const char* handlerURL=request.getHandlerURL(targetURL.c_str());
\r
156 throw ConfigurationException("Cannot determine handler from resource URL, check configuration.");
\r
158 // If the request URL contains the handler base URL for this application, either dispatch
\r
159 // directly (mainly Apache 2.0) or just pass back control.
\r
160 if (strstr(targetURL.c_str(),handlerURL)) {
\r
162 return doHandler(request);
\r
164 return make_pair(true, request.returnOK());
\r
167 // Three settings dictate how to proceed.
\r
168 pair<bool,const char*> authType = settings.first->getString("authType");
\r
169 pair<bool,bool> requireSession = settings.first->getBool("requireSession");
\r
170 pair<bool,const char*> requireSessionWith = settings.first->getString("requireSessionWith");
\r
172 // If no session is required AND the AuthType (an Apache-derived concept) isn't shibboleth,
\r
173 // then we ignore this request and consider it unprotected. Apache might lie to us if
\r
174 // ShibBasicHijack is on, but that's up to it.
\r
175 if ((!requireSession.first || !requireSession.second) && !requireSessionWith.first &&
\r
176 #ifdef HAVE_STRCASECMP
\r
177 (!authType.first || strcasecmp(authType.second,"shibboleth")))
\r
179 (!authType.first || _stricmp(authType.second,"shibboleth")))
\r
181 return make_pair(true,request.returnDecline());
\r
183 // Fix for secadv 20050901
\r
184 clearHeaders(request);
\r
186 pair<string,const char*> shib_cookie = app->getCookieNameProps("_shibsession_");
\r
187 const char* session_id = request.getCookie(shib_cookie.first.c_str());
\r
188 if (!session_id || !*session_id) {
\r
189 // No session. Maybe that's acceptable?
\r
190 if ((!requireSession.first || !requireSession.second) && !requireSessionWith.first)
\r
191 return make_pair(true,request.returnOK());
\r
193 // No cookie, but we require a session. Initiate a new session using the indicated method.
\r
194 procState = "Session Initiator Error";
\r
195 const Handler* initiator=NULL;
\r
196 if (requireSessionWith.first) {
\r
197 initiator=app->getSessionInitiatorById(requireSessionWith.second);
\r
199 throw ConfigurationException(
\r
200 "No session initiator found with id ($1), check requireSessionWith command.",
\r
201 params(1,requireSessionWith.second)
\r
205 initiator=app->getDefaultSessionInitiator();
\r
207 throw ConfigurationException("No default session initiator found, check configuration.");
\r
210 return initiator->run(request,false);
\r
213 procState = "Session Processing Error";
\r
214 const Session* session=NULL;
\r
216 session=request.getSession();
\r
217 // Make a localized exception throw if the session isn't valid.
\r
219 throw RetryableProfileException("Session no longer valid.");
\r
221 catch (exception& e) {
\r
222 request.log(SPRequest::SPWarn, string("session processing failed: ") + e.what());
\r
224 // If no session is required, bail now.
\r
225 if ((!requireSession.first || !requireSession.second) && !requireSessionWith.first)
\r
226 // Has to be OK because DECLINED will just cause Apache
\r
227 // to fail when it can't locate anything to process the
\r
228 // AuthType. No session plus requireSession false means
\r
229 // do not authenticate the user at this time.
\r
230 return make_pair(true, request.returnOK());
\r
232 // Try and cast down.
\r
233 exception* base = &e;
\r
234 RetryableProfileException* trycast=dynamic_cast<RetryableProfileException*>(base);
\r
236 // Session is invalid but we can retry -- initiate a new session.
\r
237 procState = "Session Initiator Error";
\r
238 const Handler* initiator=NULL;
\r
239 if (requireSessionWith.first) {
\r
240 initiator=app->getSessionInitiatorById(requireSessionWith.second);
\r
242 throw ConfigurationException(
\r
243 "No session initiator found with id ($1), check requireSessionWith command.",
\r
244 params(1,requireSessionWith.second)
\r
248 initiator=app->getDefaultSessionInitiator();
\r
250 throw ConfigurationException("No default session initiator found, check configuration.");
\r
252 return initiator->run(request,false);
\r
254 throw; // send it to the outer handler
\r
257 // We're done. Everything is okay. Nothing to report. Nothing to do..
\r
258 // Let the caller decide how to proceed.
\r
259 request.log(SPRequest::SPDebug, "doAuthentication succeeded");
\r
260 return make_pair(false,0);
\r
262 catch (XMLToolingException& e) {
\r
263 TemplateParameters tp;
\r
264 tp.m_map["errorType"] = procState;
\r
265 tp.m_map["errorText"] = e.what();
\r
266 tp.m_map["requestURL"] = targetURL.substr(0,targetURL.find('?'));
\r
267 return make_pair(true,sendError(request, app, "session", tp, &e));
\r
269 catch (exception& e) {
\r
270 TemplateParameters tp;
\r
271 tp.m_map["errorType"] = procState;
\r
272 tp.m_map["errorText"] = e.what();
\r
273 tp.m_map["requestURL"] = targetURL.substr(0,targetURL.find('?'));
\r
274 return make_pair(true,sendError(request, app, "session", tp));
\r
278 TemplateParameters tp;
\r
279 tp.m_map["errorType"] = procState;
\r
280 tp.m_map["errorText"] = "Caught an unknown exception.";
\r
281 tp.m_map["requestURL"] = targetURL.substr(0,targetURL.find('?'));
\r
282 return make_pair(true,sendError(request, app, "session", tp));
\r
287 pair<bool,long> ServiceProvider::doAuthorization(SPRequest& request) const
290 xmltooling::NDC ndc("doAuthorization");
\r
293 const Application* app=NULL;
\r
294 const char* procState = "Authorization Processing Error";
\r
295 string targetURL = request.getRequestURL();
\r
298 RequestMapper::Settings settings = request.getRequestSettings();
\r
299 app = &(request.getApplication());
\r
301 // Three settings dictate how to proceed.
\r
302 pair<bool,const char*> authType = settings.first->getString("authType");
\r
303 pair<bool,bool> requireSession = settings.first->getBool("requireSession");
\r
304 pair<bool,const char*> requireSessionWith = settings.first->getString("requireSessionWith");
\r
306 // If no session is required AND the AuthType (an Apache-derived concept) isn't shibboleth,
\r
307 // then we ignore this request and consider it unprotected. Apache might lie to us if
\r
308 // ShibBasicHijack is on, but that's up to it.
\r
309 if ((!requireSession.first || !requireSession.second) && !requireSessionWith.first &&
\r
310 #ifdef HAVE_STRCASECMP
\r
311 (!authType.first || strcasecmp(authType.second,"shibboleth")))
\r
313 (!authType.first || _stricmp(authType.second,"shibboleth")))
\r
315 return make_pair(true,request.returnDecline());
\r
317 // Do we have an access control plugin?
\r
318 if (settings.second) {
\r
319 const Session* session =NULL;
\r
320 pair<string,const char*> shib_cookie=app->getCookieNameProps("_shibsession_");
\r
321 const char *session_id = request.getCookie(shib_cookie.first.c_str());
\r
323 if (session_id && *session_id) {
\r
324 session = request.getSession();
\r
327 catch (exception&) {
\r
328 request.log(SPRequest::SPWarn, "unable to obtain session information to pass to access control provider");
\r
331 Locker acllock(settings.second);
\r
332 if (settings.second->authorized(request,session)) {
\r
333 // Let the caller decide how to proceed.
\r
334 request.log(SPRequest::SPDebug, "access control provider granted access");
\r
335 return make_pair(false,0);
\r
338 request.log(SPRequest::SPWarn, "access control provider denied access");
\r
339 TemplateParameters tp;
\r
340 tp.m_map["requestURL"] = targetURL;
\r
341 return make_pair(true,sendError(request, app, "access", tp));
\r
343 return make_pair(false,0);
\r
346 return make_pair(true,request.returnDecline());
\r
348 catch (XMLToolingException& e) {
349 TemplateParameters tp;
350 tp.m_map["errorType"] = procState;
351 tp.m_map["errorText"] = e.what();
352 tp.m_map["requestURL"] = targetURL.substr(0,targetURL.find('?'));
353 return make_pair(true,sendError(request, app, "session", tp, &e));
355 catch (exception& e) {
\r
356 TemplateParameters tp;
\r
357 tp.m_map["errorType"] = procState;
\r
358 tp.m_map["errorText"] = e.what();
\r
359 tp.m_map["requestURL"] = targetURL.substr(0,targetURL.find('?'));
\r
360 return make_pair(true,sendError(request, app, "access", tp));
\r
364 TemplateParameters tp;
\r
365 tp.m_map["errorType"] = procState;
\r
366 tp.m_map["errorText"] = "Caught an unknown exception.";
\r
367 tp.m_map["requestURL"] = targetURL.substr(0,targetURL.find('?'));
\r
368 return make_pair(true,sendError(request, app, "access", tp));
\r
373 pair<bool,long> ServiceProvider::doExport(SPRequest& request, bool requireSession) const
376 xmltooling::NDC ndc("doExport");
\r
379 const Application* app=NULL;
\r
380 const char* procState = "Attribute Processing Error";
\r
381 string targetURL = request.getRequestURL();
\r
384 RequestMapper::Settings settings = request.getRequestSettings();
385 app = &(request.getApplication());
387 const Session* session=NULL;
\r
388 pair<string,const char*> shib_cookie=app->getCookieNameProps("_shibsession_");
\r
389 const char *session_id = request.getCookie(shib_cookie.first.c_str());
\r
391 if (session_id && *session_id) {
\r
392 session = request.getSession();
\r
395 catch (exception&) {
\r
396 request.log(SPRequest::SPWarn, "unable to obtain session information to export into request headers");
\r
397 // If we have to have a session, then this is a fatal error.
\r
398 if (requireSession)
\r
404 if (requireSession)
\r
405 throw RetryableProfileException("Unable to obtain session information for request.");
\r
407 return make_pair(false,0); // just bail silently
\r
411 TODO: port to new cache API
\r
412 // Extract data from session.
\r
413 pair<const char*,const SAMLSubject*> sub=m_cacheEntry->getSubject(false,true);
\r
414 pair<const char*,const SAMLResponse*> unfiltered=m_cacheEntry->getTokens(true,false);
\r
415 pair<const char*,const SAMLResponse*> filtered=m_cacheEntry->getTokens(false,true);
\r
417 // Maybe export the tokens.
\r
418 pair<bool,bool> exp=m_settings.first->getBool("exportAssertion");
\r
419 if (exp.first && exp.second && unfiltered.first && *unfiltered.first) {
\r
420 unsigned int outlen;
\r
421 XMLByte* serialized =
\r
422 Base64::encode(reinterpret_cast<XMLByte*>((char*)unfiltered.first), XMLString::stringLen(unfiltered.first), &outlen);
\r
423 XMLByte *pos, *pos2;
\r
424 for (pos=serialized, pos2=serialized; *pos2; pos2++)
\r
425 if (isgraph(*pos2))
\r
428 setHeader("Shib-Attributes", reinterpret_cast<char*>(serialized));
\r
429 XMLString::release(&serialized);
\r
432 // Export the SAML AuthnMethod and the origin site name, and possibly the NameIdentifier.
\r
433 setHeader("Shib-Origin-Site", m_cacheEntry->getProviderId());
\r
434 setHeader("Shib-Identity-Provider", m_cacheEntry->getProviderId());
\r
435 setHeader("Shib-Authentication-Method", m_cacheEntry->getAuthnContext());
\r
437 // Get the AAP providers, which contain the attribute policy info.
\r
438 Iterator<IAAP*> provs=m_app->getAAPProviders();
\r
441 while (provs.hasNext()) {
\r
442 IAAP* aap=provs.next();
\r
443 xmltooling::Locker locker(aap);
\r
444 const XMLCh* format = sub.second->getNameIdentifier()->getFormat();
\r
445 const IAttributeRule* rule=aap->lookup(format ? format : SAMLNameIdentifier::UNSPECIFIED);
\r
446 if (rule && rule->getHeader()) {
\r
447 auto_ptr_char form(format ? format : SAMLNameIdentifier::UNSPECIFIED);
\r
448 auto_ptr_char nameid(sub.second->getNameIdentifier()->getName());
\r
449 setHeader("Shib-NameIdentifier-Format", form.get());
\r
450 if (!strcmp(rule->getHeader(),"REMOTE_USER"))
\r
451 setRemoteUser(nameid.get());
\r
453 setHeader(rule->getHeader(), nameid.get());
\r
457 setHeader("Shib-Application-ID", m_app->getId());
\r
459 // Export the attributes.
\r
460 Iterator<SAMLAssertion*> a_iter(filtered.second ? filtered.second->getAssertions() : EMPTY(SAMLAssertion*));
\r
461 while (a_iter.hasNext()) {
\r
462 SAMLAssertion* assert=a_iter.next();
\r
463 Iterator<SAMLStatement*> statements=assert->getStatements();
\r
464 while (statements.hasNext()) {
\r
465 SAMLAttributeStatement* astate=dynamic_cast<SAMLAttributeStatement*>(statements.next());
\r
468 Iterator<SAMLAttribute*> attrs=astate->getAttributes();
\r
469 while (attrs.hasNext()) {
\r
470 SAMLAttribute* attr=attrs.next();
\r
472 // Are we supposed to export it?
\r
474 while (provs.hasNext()) {
\r
475 IAAP* aap=provs.next();
\r
476 xmltooling::Locker locker(aap);
\r
477 const IAttributeRule* rule=aap->lookup(attr->getName(),attr->getNamespace());
\r
478 if (!rule || !rule->getHeader())
\r
481 Iterator<string> vals=attr->getSingleByteValues();
\r
482 if (!strcmp(rule->getHeader(),"REMOTE_USER") && vals.hasNext())
\r
483 setRemoteUser(vals.next().c_str());
\r
486 string header = getSecureHeader(rule->getHeader());
\r
487 if (!header.empty())
\r
489 for (; vals.hasNext(); it++) {
\r
490 string value = vals.next();
\r
491 for (string::size_type pos = value.find_first_of(";", string::size_type(0));
\r
492 pos != string::npos;
\r
493 pos = value.find_first_of(";", pos)) {
\r
494 value.insert(pos, "\\");
\r
501 setHeader(rule->getHeader(), header.c_str());
\r
509 return make_pair(false,0);
\r
511 catch (XMLToolingException& e) {
\r
512 TemplateParameters tp;
513 tp.m_map["errorType"] = procState;
\r
514 tp.m_map["errorText"] = e.what();
\r
515 tp.m_map["requestURL"] = targetURL.substr(0,targetURL.find('?'));
\r
516 return make_pair(true,sendError(request, app, "rm", tp, &e));
\r
518 catch (exception& e) {
519 TemplateParameters tp;
520 tp.m_map["errorType"] = procState;
521 tp.m_map["errorText"] = e.what();
522 tp.m_map["requestURL"] = targetURL.substr(0,targetURL.find('?'));
523 return make_pair(true,sendError(request, app, "rm", tp));
527 TemplateParameters tp;
528 tp.m_map["errorType"] = procState;
\r
529 tp.m_map["errorText"] = "Caught an unknown exception.";
\r
530 tp.m_map["requestURL"] = targetURL.substr(0,targetURL.find('?'));
\r
531 return make_pair(true,sendError(request, app, "rm", tp));
\r
536 pair<bool,long> ServiceProvider::doHandler(SPRequest& request) const
539 xmltooling::NDC ndc("doHandler");
542 const Application* app=NULL;
543 const char* procState = "Shibboleth Handler Error";
544 string targetURL = request.getRequestURL();
547 RequestMapper::Settings settings = request.getRequestSettings();
548 app = &(request.getApplication());
550 const char* handlerURL=request.getHandlerURL(targetURL.c_str());
552 throw ConfigurationException("Cannot determine handler from resource URL, check configuration.");
554 // Make sure we only process handler requests.
555 if (!strstr(targetURL.c_str(),handlerURL))
556 return make_pair(true, request.returnDecline());
558 const PropertySet* sessionProps=app->getPropertySet("Sessions");
560 throw ConfigurationException("Unable to map request to application session settings, check configuration.");
562 // Process incoming request.
563 pair<bool,bool> handlerSSL=sessionProps->getBool("handlerSSL");
565 // Make sure this is SSL, if it should be
566 if ((!handlerSSL.first || handlerSSL.second) && strcmp(request.getScheme(),"https"))
567 throw FatalProfileException("Blocked non-SSL access to Shibboleth handler.");
569 // We dispatch based on our path info. We know the request URL begins with or equals the handler URL,
570 // so the path info is the next character (or null).
571 const Handler* handler=app->getHandler(targetURL.c_str() + strlen(handlerURL));
573 throw BindingException("Shibboleth handler invoked at an unconfigured location.");
575 if (XMLHelper::isNodeNamed(handler->getElement(),samlconstants::SAML20MD_NS,AssertionConsumerService::LOCAL_NAME))
576 procState = "Session Creation Error";
577 else if (XMLString::equals(handler->getElement()->getLocalName(),SessionInitiator))
578 procState = "Session Initiator Error";
579 else if (XMLHelper::isNodeNamed(handler->getElement(),samlconstants::SAML20MD_NS,SingleLogoutService::LOCAL_NAME))
580 procState = "Session Termination Error";
582 procState = "Protocol Handler Error";
583 pair<bool,long> hret=handler->run(request);
585 // Did the handler run successfully?
589 throw BindingException("Configured Shibboleth handler failed to process the request.");
591 catch (MetadataException& e) {
592 TemplateParameters tp;
593 tp.m_map["errorText"] = e.what();
594 // See if a metadata error page is installed.
595 const PropertySet* props=app->getPropertySet("Errors");
597 pair<bool,const char*> p=props->getString("metadata");
599 tp.m_map["errorType"] = procState;
600 tp.m_map["requestURL"] = targetURL.substr(0,targetURL.find('?'));
601 return make_pair(true,sendError(request, app, "metadata", tp, &e));
606 catch (XMLToolingException& e) {
607 TemplateParameters tp;
608 tp.m_map["errorType"] = procState;
609 tp.m_map["errorText"] = e.what();
610 tp.m_map["requestURL"] = targetURL.substr(0,targetURL.find('?'));
611 return make_pair(true,sendError(request, app, "session", tp, &e));
613 catch (exception& e) {
614 TemplateParameters tp;
615 tp.m_map["errorType"] = procState;
616 tp.m_map["errorText"] = e.what();
617 tp.m_map["requestURL"] = targetURL.substr(0,targetURL.find('?'));
618 return make_pair(true,sendError(request, app, "session", tp));
622 TemplateParameters tp;
623 tp.m_map["errorType"] = procState;
624 tp.m_map["errorText"] = "Caught an unknown exception.";
625 tp.m_map["requestURL"] = targetURL.substr(0,targetURL.find('?'));
626 return make_pair(true,sendError(request, app, "session", tp));