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 "attribute/Attribute.h"
32 #include "util/TemplateParameters.h"
36 #include <saml/saml2/metadata/Metadata.h>
37 #include <saml/util/SAMLConstants.h>
38 #include <xmltooling/XMLToolingConfig.h>
39 #include <xmltooling/util/NDC.h>
40 #include <xmltooling/util/XMLHelper.h>
42 using namespace shibsp;
43 using namespace opensaml::saml2md;
44 using namespace opensaml;
45 using namespace xmltooling;
49 SHIBSP_DLLLOCAL PluginManager<ServiceProvider,const DOMElement*>::Factory XMLServiceProviderFactory;
51 long SHIBSP_DLLLOCAL sendError(
52 SPRequest& request, const Application* app, const char* page, TemplateParameters& tp, const XMLToolingException* ex=NULL
55 request.setContentType("text/html");
56 request.setResponseHeader("Expires","01-Jan-1997 12:00:00 GMT");
57 request.setResponseHeader("Cache-Control","private,no-store,no-cache");
59 const PropertySet* props=app ? app->getPropertySet("Errors") : NULL;
61 pair<bool,const char*> p=props->getString(page);
63 ifstream infile(p.second);
65 tp.setPropertySet(props);
67 XMLToolingConfig::getConfig().getTemplateEngine()->run(infile, str, tp, ex);
68 return request.sendResponse(str);
71 else if (!strcmp(page,"access")) {
72 istringstream msg("Access Denied");
73 return static_cast<opensaml::GenericResponse&>(request).sendResponse(msg, HTTPResponse::SAML_HTTP_STATUS_FORBIDDEN);
77 string errstr = string("sendError could not process error template (") + page + ")";
78 request.log(SPRequest::SPError, errstr);
79 istringstream msg("Internal Server Error. Please contact the site administrator.");
80 return request.sendError(msg);
83 void SHIBSP_DLLLOCAL clearHeaders(SPRequest& request) {
84 // Clear invariant stuff.
85 request.clearHeader("Shib-Identity-Provider");
86 request.clearHeader("Shib-Authentication-Method");
87 request.clearHeader("Shib-AuthnContext-Class");
88 request.clearHeader("Shib-AuthnContext-Decl");
89 request.clearHeader("Shib-Attributes");
90 request.clearHeader("Shib-Application-ID");
92 // Clear out the list of mapped attributes
93 /* TODO: need some kind of master attribute list via the new resolver
94 Iterator<IAAP*> provs=dynamic_cast<const IApplication&>(getApplication()).getAAPProviders();
95 while (provs.hasNext()) {
96 IAAP* aap=provs.next();
97 xmltooling::Locker locker(aap);
98 Iterator<const IAttributeRule*> rules=aap->getAttributeRules();
99 while (rules.hasNext()) {
100 const char* header=rules.next()->getHeader();
102 request.clearHeader(header);
108 static const XMLCh SessionInitiator[] = UNICODE_LITERAL_16(S,e,s,s,i,o,n,I,n,i,t,i,a,t,o,r);
111 void SHIBSP_API shibsp::registerServiceProviders()
113 SPConfig::getConfig().ServiceProviderManager.registerFactory(XML_SERVICE_PROVIDER, XMLServiceProviderFactory);
116 pair<bool,long> ServiceProvider::doAuthentication(SPRequest& request, bool handler) const
119 xmltooling::NDC ndc("doAuthentication");
122 const Application* app=NULL;
123 const char* procState = "Request Processing Error";
124 string targetURL = request.getRequestURL();
127 RequestMapper::Settings settings = request.getRequestSettings();
128 app = &(request.getApplication());
130 // If not SSL, check to see if we should block or redirect it.
131 if (!request.isSecure()) {
132 pair<bool,const char*> redirectToSSL = settings.first->getString("redirectToSSL");
133 if (redirectToSSL.first) {
134 #ifdef HAVE_STRCASECMP
135 if (!strcasecmp("GET",request.getMethod()) || !strcasecmp("HEAD",request.getMethod())) {
137 if (!stricmp("GET",request.getMethod()) || !stricmp("HEAD",request.getMethod())) {
139 // Compute the new target URL
140 string redirectURL = string("https://") + request.getHostname();
141 if (strcmp(redirectToSSL.second,"443")) {
142 redirectURL = redirectURL + ':' + redirectToSSL.second;
144 redirectURL += request.getRequestURI();
145 return make_pair(true, request.sendRedirect(redirectURL.c_str()));
148 TemplateParameters tp;
149 tp.m_map["requestURL"] = targetURL.substr(0,targetURL.find('?'));
150 return make_pair(true,sendError(request, app, "ssl", tp));
155 const char* handlerURL=request.getHandlerURL(targetURL.c_str());
157 throw ConfigurationException("Cannot determine handler from resource URL, check configuration.");
159 // If the request URL contains the handler base URL for this application, either dispatch
160 // directly (mainly Apache 2.0) or just pass back control.
161 if (strstr(targetURL.c_str(),handlerURL)) {
163 return doHandler(request);
165 return make_pair(true, request.returnOK());
168 // Three settings dictate how to proceed.
169 pair<bool,const char*> authType = settings.first->getString("authType");
170 pair<bool,bool> requireSession = settings.first->getBool("requireSession");
171 pair<bool,const char*> requireSessionWith = settings.first->getString("requireSessionWith");
173 // If no session is required AND the AuthType (an Apache-derived concept) isn't shibboleth,
174 // then we ignore this request and consider it unprotected. Apache might lie to us if
175 // ShibBasicHijack is on, but that's up to it.
176 if ((!requireSession.first || !requireSession.second) && !requireSessionWith.first &&
177 #ifdef HAVE_STRCASECMP
178 (!authType.first || strcasecmp(authType.second,"shibboleth")))
180 (!authType.first || _stricmp(authType.second,"shibboleth")))
182 return make_pair(true,request.returnDecline());
184 // Fix for secadv 20050901
185 clearHeaders(request);
187 procState = "Session Processing Error";
189 Session* session = NULL;
191 session = request.getSession();
193 catch (exception& e) {
194 request.log(SPRequest::SPWarn, string("error during session lookup: ") + e.what());
195 // If it's not a retryable session failure, we throw to the outer handler for reporting.
196 if (dynamic_cast<RetryableProfileException*>(&e)==NULL)
201 // No session. Maybe that's acceptable?
202 if ((!requireSession.first || !requireSession.second) && !requireSessionWith.first)
203 return make_pair(true,request.returnOK());
205 // No session, but we require one. Initiate a new session using the indicated method.
206 procState = "Session Initiator Error";
207 const Handler* initiator=NULL;
208 if (requireSessionWith.first) {
209 initiator=app->getSessionInitiatorById(requireSessionWith.second);
211 throw ConfigurationException(
212 "No session initiator found with id ($1), check requireSessionWith command.",
213 params(1,requireSessionWith.second)
217 initiator=app->getDefaultSessionInitiator();
219 throw ConfigurationException("No default session initiator found, check configuration.");
222 return initiator->run(request,false);
225 // We're done. Everything is okay. Nothing to report. Nothing to do..
226 // Let the caller decide how to proceed.
227 request.log(SPRequest::SPDebug, "doAuthentication succeeded");
228 return make_pair(false,0);
230 catch (XMLToolingException& e) {
231 TemplateParameters tp;
232 tp.m_map["errorType"] = procState;
233 tp.m_map["errorText"] = e.what();
234 tp.m_map["requestURL"] = targetURL.substr(0,targetURL.find('?'));
235 return make_pair(true,sendError(request, app, "session", tp, &e));
237 catch (exception& e) {
238 TemplateParameters tp;
239 tp.m_map["errorType"] = procState;
240 tp.m_map["errorText"] = e.what();
241 tp.m_map["requestURL"] = targetURL.substr(0,targetURL.find('?'));
242 return make_pair(true,sendError(request, app, "session", tp));
246 TemplateParameters tp;
247 tp.m_map["errorType"] = procState;
248 tp.m_map["errorText"] = "Caught an unknown exception.";
249 tp.m_map["requestURL"] = targetURL.substr(0,targetURL.find('?'));
250 return make_pair(true,sendError(request, app, "session", tp));
255 pair<bool,long> ServiceProvider::doAuthorization(SPRequest& request) const
258 xmltooling::NDC ndc("doAuthorization");
261 const Application* app=NULL;
262 const char* procState = "Authorization Processing Error";
263 string targetURL = request.getRequestURL();
266 RequestMapper::Settings settings = request.getRequestSettings();
267 app = &(request.getApplication());
269 // Three settings dictate how to proceed.
270 pair<bool,const char*> authType = settings.first->getString("authType");
271 pair<bool,bool> requireSession = settings.first->getBool("requireSession");
272 pair<bool,const char*> requireSessionWith = settings.first->getString("requireSessionWith");
274 // If no session is required AND the AuthType (an Apache-derived concept) isn't shibboleth,
275 // then we ignore this request and consider it unprotected. Apache might lie to us if
276 // ShibBasicHijack is on, but that's up to it.
277 if ((!requireSession.first || !requireSession.second) && !requireSessionWith.first &&
278 #ifdef HAVE_STRCASECMP
279 (!authType.first || strcasecmp(authType.second,"shibboleth")))
281 (!authType.first || _stricmp(authType.second,"shibboleth")))
283 return make_pair(true,request.returnDecline());
285 // Do we have an access control plugin?
286 if (settings.second) {
287 const Session* session = NULL;
289 session = request.getSession();
291 catch (exception& e) {
292 request.log(SPRequest::SPWarn, string("unable to obtain session to pass to access control provider: ") + e.what());
295 Locker acllock(settings.second);
296 if (settings.second->authorized(request,session)) {
297 // Let the caller decide how to proceed.
298 request.log(SPRequest::SPDebug, "access control provider granted access");
299 return make_pair(false,0);
302 request.log(SPRequest::SPWarn, "access control provider denied access");
303 TemplateParameters tp;
304 tp.m_map["requestURL"] = targetURL;
305 return make_pair(true,sendError(request, app, "access", tp));
307 return make_pair(false,0);
310 return make_pair(true,request.returnDecline());
312 catch (XMLToolingException& e) {
313 TemplateParameters tp;
314 tp.m_map["errorType"] = procState;
315 tp.m_map["errorText"] = e.what();
316 tp.m_map["requestURL"] = targetURL.substr(0,targetURL.find('?'));
317 return make_pair(true,sendError(request, app, "session", tp, &e));
319 catch (exception& e) {
320 TemplateParameters tp;
321 tp.m_map["errorType"] = procState;
322 tp.m_map["errorText"] = e.what();
323 tp.m_map["requestURL"] = targetURL.substr(0,targetURL.find('?'));
324 return make_pair(true,sendError(request, app, "access", tp));
328 TemplateParameters tp;
329 tp.m_map["errorType"] = procState;
330 tp.m_map["errorText"] = "Caught an unknown exception.";
331 tp.m_map["requestURL"] = targetURL.substr(0,targetURL.find('?'));
332 return make_pair(true,sendError(request, app, "access", tp));
337 pair<bool,long> ServiceProvider::doExport(SPRequest& request, bool requireSession) const
340 xmltooling::NDC ndc("doExport");
343 const Application* app=NULL;
344 const char* procState = "Attribute Processing Error";
345 string targetURL = request.getRequestURL();
348 RequestMapper::Settings settings = request.getRequestSettings();
349 app = &(request.getApplication());
351 const Session* session = NULL;
353 session = request.getSession();
355 catch (exception& e) {
356 request.log(SPRequest::SPWarn, string("unable to obtain session to export to request: ") + e.what());
357 // If we have to have a session, then this is a fatal error.
365 throw RetryableProfileException("Unable to obtain session to export to request.");
367 return make_pair(false,0); // just bail silently
370 request.setHeader("Shib-Application-ID", app->getId());
372 // Export the IdP name and Authn method/context info.
373 const char* hval = session->getEntityID();
375 request.setHeader("Shib-Identity-Provider", hval);
376 hval = session->getAuthnContextClassRef();
378 request.setHeader("Shib-Authentication-Method", hval);
379 request.setHeader("Shib-AuthnContext-Class", hval);
381 hval = session->getAuthnContextDeclRef();
383 request.setHeader("Shib-AuthnContext-Decl", hval);
385 // Maybe export the assertion keys.
386 pair<bool,bool> exp=settings.first->getBool("exportAssertion");
387 if (exp.first && exp.second) {
388 //setHeader("Shib-Attributes", reinterpret_cast<char*>(serialized));
389 // TODO: export lookup URLs to access assertions by ID
390 const vector<const char*>& tokens = session->getAssertionIDs();
393 // Export the attributes.
394 const map<string,const Attribute*>& attributes = session->getAttributes();
395 for (map<string,const Attribute*>::const_iterator a = attributes.begin(); a!=attributes.end(); ++a) {
396 const vector<string>& vals = a->second->getSerializedValues();
397 if (!strcmp(a->second->getId(), "REMOTE_USER") && !vals.empty())
398 request.setRemoteUser(vals.front().c_str());
400 string header(request.getSecureHeader(a->second->getId()));
401 for (vector<string>::const_iterator v = vals.begin(); v!=vals.end(); ++v) {
404 string::size_type pos = v->find_first_of(';',string::size_type(0));
405 if (pos!=string::npos) {
407 for (; pos != string::npos; pos = value.find_first_of(';',pos)) {
408 value.insert(pos, "\\");
417 request.setHeader(a->second->getId(), header.c_str());
421 return make_pair(false,0);
423 catch (XMLToolingException& e) {
424 TemplateParameters tp;
425 tp.m_map["errorType"] = procState;
426 tp.m_map["errorText"] = e.what();
427 tp.m_map["requestURL"] = targetURL.substr(0,targetURL.find('?'));
428 return make_pair(true,sendError(request, app, "rm", tp, &e));
430 catch (exception& e) {
431 TemplateParameters tp;
432 tp.m_map["errorType"] = procState;
433 tp.m_map["errorText"] = e.what();
434 tp.m_map["requestURL"] = targetURL.substr(0,targetURL.find('?'));
435 return make_pair(true,sendError(request, app, "rm", tp));
439 TemplateParameters tp;
440 tp.m_map["errorType"] = procState;
441 tp.m_map["errorText"] = "Caught an unknown exception.";
442 tp.m_map["requestURL"] = targetURL.substr(0,targetURL.find('?'));
443 return make_pair(true,sendError(request, app, "rm", tp));
448 pair<bool,long> ServiceProvider::doHandler(SPRequest& request) const
451 xmltooling::NDC ndc("doHandler");
454 const Application* app=NULL;
455 const char* procState = "Shibboleth Handler Error";
456 string targetURL = request.getRequestURL();
459 RequestMapper::Settings settings = request.getRequestSettings();
460 app = &(request.getApplication());
462 const char* handlerURL=request.getHandlerURL(targetURL.c_str());
464 throw ConfigurationException("Cannot determine handler from resource URL, check configuration.");
466 // Make sure we only process handler requests.
467 if (!strstr(targetURL.c_str(),handlerURL))
468 return make_pair(true, request.returnDecline());
470 const PropertySet* sessionProps=app->getPropertySet("Sessions");
472 throw ConfigurationException("Unable to map request to application session settings, check configuration.");
474 // Process incoming request.
475 pair<bool,bool> handlerSSL=sessionProps->getBool("handlerSSL");
477 // Make sure this is SSL, if it should be
478 if ((!handlerSSL.first || handlerSSL.second) && !request.isSecure())
479 throw FatalProfileException("Blocked non-SSL access to Shibboleth handler.");
481 // We dispatch based on our path info. We know the request URL begins with or equals the handler URL,
482 // so the path info is the next character (or null).
483 const Handler* handler=app->getHandler(targetURL.c_str() + strlen(handlerURL));
485 throw BindingException("Shibboleth handler invoked at an unconfigured location.");
487 if (XMLHelper::isNodeNamed(handler->getElement(),samlconstants::SAML20MD_NS,AssertionConsumerService::LOCAL_NAME))
488 procState = "Session Creation Error";
489 else if (XMLString::equals(handler->getElement()->getLocalName(),SessionInitiator))
490 procState = "Session Initiator Error";
491 else if (XMLHelper::isNodeNamed(handler->getElement(),samlconstants::SAML20MD_NS,SingleLogoutService::LOCAL_NAME))
492 procState = "Session Termination Error";
494 procState = "Protocol Handler Error";
495 pair<bool,long> hret=handler->run(request);
497 // Did the handler run successfully?
501 throw BindingException("Configured Shibboleth handler failed to process the request.");
503 catch (MetadataException& e) {
504 TemplateParameters tp;
505 tp.m_map["errorText"] = e.what();
506 // See if a metadata error page is installed.
507 const PropertySet* props=app->getPropertySet("Errors");
509 pair<bool,const char*> p=props->getString("metadata");
511 tp.m_map["errorType"] = procState;
512 tp.m_map["requestURL"] = targetURL.substr(0,targetURL.find('?'));
513 return make_pair(true,sendError(request, app, "metadata", tp, &e));
518 catch (XMLToolingException& 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, "session", tp, &e));
525 catch (exception& e) {
526 TemplateParameters tp;
527 tp.m_map["errorType"] = procState;
528 tp.m_map["errorText"] = e.what();
529 tp.m_map["requestURL"] = targetURL.substr(0,targetURL.find('?'));
530 return make_pair(true,sendError(request, app, "session", tp));
534 TemplateParameters tp;
535 tp.m_map["errorType"] = procState;
536 tp.m_map["errorText"] = "Caught an unknown exception.";
537 tp.m_map["requestURL"] = targetURL.substr(0,targetURL.find('?'));
538 return make_pair(true,sendError(request, app, "session", tp));