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"
27 #include "ServiceProvider.h"
28 #include "SessionCache.h"
29 #include "SPRequest.h"
30 #include "attribute/Attribute.h"
31 #include "handler/SessionInitiator.h"
32 #include "util/TemplateParameters.h"
36 #include <xmltooling/XMLToolingConfig.h>
37 #include <xmltooling/util/NDC.h>
38 #include <xmltooling/util/XMLHelper.h>
40 using namespace shibsp;
41 using namespace xmltooling;
45 SHIBSP_DLLLOCAL PluginManager<ServiceProvider,string,const DOMElement*>::Factory XMLServiceProviderFactory;
47 long SHIBSP_DLLLOCAL sendError(SPRequest& request, const Application* app, const char* page, TemplateParameters& tp, bool mayRedirect=false)
50 // Check for redirection on errors instead of template.
51 const PropertySet* sessions=app ? app->getPropertySet("Sessions") : NULL;
52 pair<bool,const char*> redirectErrors = sessions ? sessions->getString("redirectErrors") : pair<bool,const char*>(false,NULL);
53 if (redirectErrors.first) {
54 string loc(redirectErrors.second);
55 loc = loc + '?' + tp.toQueryString();
56 return request.sendRedirect(loc.c_str());
60 request.setContentType("text/html");
61 request.setResponseHeader("Expires","01-Jan-1997 12:00:00 GMT");
62 request.setResponseHeader("Cache-Control","private,no-store,no-cache");
64 const PropertySet* props=app ? app->getPropertySet("Errors") : NULL;
66 pair<bool,const char*> p=props->getString(page);
68 ifstream infile(p.second);
70 tp.setPropertySet(props);
72 XMLToolingConfig::getConfig().getTemplateEngine()->run(infile, str, tp, tp.getRichException());
73 return request.sendResponse(str);
76 else if (!strcmp(page,"access")) {
77 istringstream msg("Access Denied");
78 return request.sendResponse(msg, HTTPResponse::XMLTOOLING_HTTP_STATUS_FORBIDDEN);
82 string errstr = string("sendError could not process error template (") + page + ")";
83 request.log(SPRequest::SPError, errstr);
84 istringstream msg("Internal Server Error. Please contact the site administrator.");
85 return request.sendError(msg);
88 void SHIBSP_DLLLOCAL clearHeaders(SPRequest& request) {
89 // Clear invariant stuff.
90 request.clearHeader("Shib-Identity-Provider");
91 request.clearHeader("Shib-Authentication-Method");
92 request.clearHeader("Shib-AuthnContext-Class");
93 request.clearHeader("Shib-AuthnContext-Decl");
94 request.clearHeader("Shib-Attributes");
95 request.clearHeader("Shib-Application-ID");
97 // TODO: Figure out a way to clear attribute headers...
99 AttributeExtractor* extractor = request.getApplication().getAttributeExtractor();
101 Locker locker(extractor);
102 extractor->clearHeaders(request);
104 AttributeResolver* resolver = request.getApplication().getAttributeResolver();
106 Locker locker(resolver);
107 resolver->clearHeaders(request);
113 void SHIBSP_API shibsp::registerServiceProviders()
115 SPConfig::getConfig().ServiceProviderManager.registerFactory(XML_SERVICE_PROVIDER, XMLServiceProviderFactory);
118 pair<bool,long> ServiceProvider::doAuthentication(SPRequest& request, bool handler) const
121 xmltooling::NDC ndc("doAuthentication");
124 const Application* app=NULL;
125 string targetURL = request.getRequestURL();
128 RequestMapper::Settings settings = request.getRequestSettings();
129 app = &(request.getApplication());
131 // If not SSL, check to see if we should block or redirect it.
132 if (!request.isSecure()) {
133 pair<bool,const char*> redirectToSSL = settings.first->getString("redirectToSSL");
134 if (redirectToSSL.first) {
135 #ifdef HAVE_STRCASECMP
136 if (!strcasecmp("GET",request.getMethod()) || !strcasecmp("HEAD",request.getMethod())) {
138 if (!stricmp("GET",request.getMethod()) || !stricmp("HEAD",request.getMethod())) {
140 // Compute the new target URL
141 string redirectURL = string("https://") + request.getHostname();
142 if (strcmp(redirectToSSL.second,"443")) {
143 redirectURL = redirectURL + ':' + redirectToSSL.second;
145 redirectURL += request.getRequestURI();
146 return make_pair(true, request.sendRedirect(redirectURL.c_str()));
149 TemplateParameters tp;
150 tp.m_map["requestURL"] = targetURL.substr(0,targetURL.find('?'));
151 return make_pair(true,sendError(request, app, "ssl", tp));
156 const char* handlerURL=request.getHandlerURL(targetURL.c_str());
158 throw ConfigurationException("Cannot determine handler from resource URL, check configuration.");
160 // If the request URL contains the handler base URL for this application, either dispatch
161 // directly (mainly Apache 2.0) or just pass back control.
162 if (strstr(targetURL.c_str(),handlerURL)) {
164 return doHandler(request);
166 return make_pair(true, request.returnOK());
169 // Three settings dictate how to proceed.
170 pair<bool,const char*> authType = settings.first->getString("authType");
171 pair<bool,bool> requireSession = settings.first->getBool("requireSession");
172 pair<bool,const char*> requireSessionWith = settings.first->getString("requireSessionWith");
174 // If no session is required AND the AuthType (an Apache-derived concept) isn't shibboleth,
175 // then we ignore this request and consider it unprotected. Apache might lie to us if
176 // ShibBasicHijack is on, but that's up to it.
177 if ((!requireSession.first || !requireSession.second) && !requireSessionWith.first &&
178 #ifdef HAVE_STRCASECMP
179 (!authType.first || strcasecmp(authType.second,"shibboleth")))
181 (!authType.first || _stricmp(authType.second,"shibboleth")))
183 return make_pair(true,request.returnDecline());
185 // Fix for secadv 20050901
186 clearHeaders(request);
188 Session* session = NULL;
190 session = request.getSession();
192 catch (exception& e) {
193 request.log(SPRequest::SPWarn, string("error during session lookup: ") + e.what());
194 // If it's not a retryable session failure, we throw to the outer handler for reporting.
195 if (dynamic_cast<opensaml::RetryableProfileException*>(&e)==NULL)
200 // No session. Maybe that's acceptable?
201 if ((!requireSession.first || !requireSession.second) && !requireSessionWith.first)
202 return make_pair(true,request.returnOK());
204 // No session, but we require one. Initiate a new session using the indicated method.
205 const Handler* initiator=NULL;
206 if (requireSessionWith.first) {
207 initiator=app->getSessionInitiatorById(requireSessionWith.second);
209 throw ConfigurationException(
210 "No session initiator found with id ($1), check requireSessionWith command.",
211 params(1,requireSessionWith.second)
215 initiator=app->getDefaultSessionInitiator();
217 throw ConfigurationException("No default session initiator found, check configuration.");
220 return initiator->run(request,false);
223 // We're done. Everything is okay. Nothing to report. Nothing to do..
224 // Let the caller decide how to proceed.
225 request.log(SPRequest::SPDebug, "doAuthentication succeeded");
226 return make_pair(false,0);
228 catch (exception& e) {
229 TemplateParameters tp(&e);
230 tp.m_map["requestURL"] = targetURL.substr(0,targetURL.find('?'));
231 return make_pair(true,sendError(request, app, "session", tp));
235 TemplateParameters tp;
236 tp.m_map["errorType"] = "Unexpected Error";
237 tp.m_map["errorText"] = "Caught an unknown exception.";
238 tp.m_map["requestURL"] = targetURL.substr(0,targetURL.find('?'));
239 return make_pair(true,sendError(request, app, "session", tp));
244 pair<bool,long> ServiceProvider::doAuthorization(SPRequest& request) const
247 xmltooling::NDC ndc("doAuthorization");
250 const Application* app=NULL;
251 string targetURL = request.getRequestURL();
254 RequestMapper::Settings settings = request.getRequestSettings();
255 app = &(request.getApplication());
257 // Three settings dictate how to proceed.
258 pair<bool,const char*> authType = settings.first->getString("authType");
259 pair<bool,bool> requireSession = settings.first->getBool("requireSession");
260 pair<bool,const char*> requireSessionWith = settings.first->getString("requireSessionWith");
262 // If no session is required AND the AuthType (an Apache-derived concept) isn't shibboleth,
263 // then we ignore this request and consider it unprotected. Apache might lie to us if
264 // ShibBasicHijack is on, but that's up to it.
265 if ((!requireSession.first || !requireSession.second) && !requireSessionWith.first &&
266 #ifdef HAVE_STRCASECMP
267 (!authType.first || strcasecmp(authType.second,"shibboleth")))
269 (!authType.first || _stricmp(authType.second,"shibboleth")))
271 return make_pair(true,request.returnDecline());
273 // Do we have an access control plugin?
274 if (settings.second) {
275 const Session* session = NULL;
277 session = request.getSession();
279 catch (exception& e) {
280 request.log(SPRequest::SPWarn, string("unable to obtain session to pass to access control provider: ") + e.what());
283 Locker acllock(settings.second);
284 if (settings.second->authorized(request,session)) {
285 // Let the caller decide how to proceed.
286 request.log(SPRequest::SPDebug, "access control provider granted access");
287 return make_pair(false,0);
290 request.log(SPRequest::SPWarn, "access control provider denied access");
291 TemplateParameters tp;
292 tp.m_map["requestURL"] = targetURL;
293 return make_pair(true,sendError(request, app, "access", tp));
295 return make_pair(false,0);
298 return make_pair(true,request.returnDecline());
300 catch (exception& e) {
301 TemplateParameters tp(&e);
302 tp.m_map["requestURL"] = targetURL.substr(0,targetURL.find('?'));
303 return make_pair(true,sendError(request, app, "access", tp));
307 TemplateParameters tp;
308 tp.m_map["errorType"] = "Unexpected Error";
309 tp.m_map["errorText"] = "Caught an unknown exception.";
310 tp.m_map["requestURL"] = targetURL.substr(0,targetURL.find('?'));
311 return make_pair(true,sendError(request, app, "access", tp));
316 pair<bool,long> ServiceProvider::doExport(SPRequest& request, bool requireSession) const
319 xmltooling::NDC ndc("doExport");
322 const Application* app=NULL;
323 string targetURL = request.getRequestURL();
326 RequestMapper::Settings settings = request.getRequestSettings();
327 app = &(request.getApplication());
329 const Session* session = NULL;
331 session = request.getSession();
333 catch (exception& e) {
334 request.log(SPRequest::SPWarn, string("unable to obtain session to export to request: ") + e.what());
335 // If we have to have a session, then this is a fatal error.
343 throw opensaml::RetryableProfileException("Unable to obtain session to export to request.");
345 return make_pair(false,0); // just bail silently
348 request.setHeader("Shib-Application-ID", app->getId());
350 // Export the IdP name and Authn method/context info.
351 const char* hval = session->getEntityID();
353 request.setHeader("Shib-Identity-Provider", hval);
354 hval = session->getAuthnContextClassRef();
356 request.setHeader("Shib-Authentication-Method", hval);
357 request.setHeader("Shib-AuthnContext-Class", hval);
359 hval = session->getAuthnContextDeclRef();
361 request.setHeader("Shib-AuthnContext-Decl", hval);
363 // Maybe export the assertion keys.
364 pair<bool,bool> exp=settings.first->getBool("exportAssertion");
365 if (exp.first && exp.second) {
366 //setHeader("Shib-Attributes", reinterpret_cast<char*>(serialized));
367 // TODO: export lookup URLs to access assertions by ID
368 const vector<const char*>& tokens = session->getAssertionIDs();
371 // Export the attributes.
372 bool remoteUserSet = false;
373 const multimap<string,Attribute*>& attributes = session->getAttributes();
374 for (multimap<string,Attribute*>::const_iterator a = attributes.begin(); a!=attributes.end(); ++a) {
375 const vector<string>& vals = a->second->getSerializedValues();
377 // See if this needs to be set as the REMOTE_USER value.
378 if (!remoteUserSet && !vals.empty() && app->getRemoteUserAttributeIds().count(a->first)) {
379 request.setRemoteUser(vals.front().c_str());
380 remoteUserSet = true;
383 // Handle the normal export case.
384 string header(request.getSecureHeader(a->first.c_str()));
385 for (vector<string>::const_iterator v = vals.begin(); v!=vals.end(); ++v) {
388 string::size_type pos = v->find_first_of(';',string::size_type(0));
389 if (pos!=string::npos) {
391 for (; pos != string::npos; pos = value.find_first_of(';',pos)) {
392 value.insert(pos, "\\");
401 request.setHeader(a->first.c_str(), header.c_str());
404 return make_pair(false,0);
406 catch (exception& e) {
407 TemplateParameters tp(&e);
408 tp.m_map["requestURL"] = targetURL.substr(0,targetURL.find('?'));
409 return make_pair(true,sendError(request, app, "rm", tp));
413 TemplateParameters tp;
414 tp.m_map["errorType"] = "Unexpected Error";
415 tp.m_map["errorText"] = "Caught an unknown exception.";
416 tp.m_map["requestURL"] = targetURL.substr(0,targetURL.find('?'));
417 return make_pair(true,sendError(request, app, "rm", tp));
422 pair<bool,long> ServiceProvider::doHandler(SPRequest& request) const
425 xmltooling::NDC ndc("doHandler");
428 const Application* app=NULL;
429 string targetURL = request.getRequestURL();
432 RequestMapper::Settings settings = request.getRequestSettings();
433 app = &(request.getApplication());
435 const char* handlerURL=request.getHandlerURL(targetURL.c_str());
437 throw ConfigurationException("Cannot determine handler from resource URL, check configuration.");
439 // Make sure we only process handler requests.
440 if (!strstr(targetURL.c_str(),handlerURL))
441 return make_pair(true, request.returnDecline());
443 const PropertySet* sessionProps=app->getPropertySet("Sessions");
445 throw ConfigurationException("Unable to map request to application session settings, check configuration.");
447 // Process incoming request.
448 pair<bool,bool> handlerSSL=sessionProps->getBool("handlerSSL");
450 // Make sure this is SSL, if it should be
451 if ((!handlerSSL.first || handlerSSL.second) && !request.isSecure())
452 throw opensaml::FatalProfileException("Blocked non-SSL access to Shibboleth handler.");
454 // We dispatch based on our path info. We know the request URL begins with or equals the handler URL,
455 // so the path info is the next character (or null).
456 const Handler* handler=app->getHandler(targetURL.c_str() + strlen(handlerURL));
458 throw ConfigurationException("Shibboleth handler invoked at an unconfigured location.");
460 pair<bool,long> hret=handler->run(request);
462 // Did the handler run successfully?
466 throw ConfigurationException("Configured Shibboleth handler failed to process the request.");
468 catch (opensaml::saml2md::MetadataException& e) {
469 TemplateParameters tp(&e);
470 tp.m_map["requestURL"] = targetURL.substr(0,targetURL.find('?'));
471 // See if a metadata error page is installed.
472 const PropertySet* props=app ? app->getPropertySet("Errors") : NULL;
474 pair<bool,const char*> p=props->getString("metadata");
476 return make_pair(true,sendError(request, app, "metadata", tp, true));
478 return make_pair(true,sendError(request, app, "session", tp, true));
480 catch (exception& e) {
481 TemplateParameters tp(&e);
482 tp.m_map["requestURL"] = targetURL.substr(0,targetURL.find('?'));
483 return make_pair(true,sendError(request, app, "session", tp, true));
487 TemplateParameters tp;
488 tp.m_map["errorType"] = "Unexpected Error";
489 tp.m_map["errorText"] = "Caught an unknown exception.";
490 tp.m_map["requestURL"] = targetURL.substr(0,targetURL.find('?'));
491 return make_pair(true,sendError(request, app, "session", tp, true));