Bypass xmltooling/signing bug by using a fresh document once the signature is added.
[shibboleth/sp.git] / shibsp / ServiceProvider.cpp
1 /*
2  *  Copyright 2001-2009 Internet2
3  *
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
7  *
8  *     http://www.apache.org/licenses/LICENSE-2.0
9  *
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.
15  */
16
17 /**
18  * ServiceProvider.cpp
19  *
20  * Interface to a Shibboleth ServiceProvider instance.
21  */
22
23 #include "internal.h"
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"
33
34 #include <fstream>
35 #include <sstream>
36 #ifndef SHIBSP_LITE
37 # include <saml/exceptions.h>
38 # include <saml/saml2/metadata/MetadataProvider.h>
39 #endif
40 #include <xmltooling/XMLToolingConfig.h>
41 #include <xmltooling/util/NDC.h>
42 #include <xmltooling/util/PathResolver.h>
43 #include <xmltooling/util/URLEncoder.h>
44 #include <xmltooling/util/XMLHelper.h>
45
46 using namespace shibsp;
47 using namespace xmltooling::logging;
48 using namespace xmltooling;
49 using namespace std;
50
51 namespace shibsp {
52     SHIBSP_DLLLOCAL PluginManager<ServiceProvider,string,const DOMElement*>::Factory XMLServiceProviderFactory;
53
54     long SHIBSP_DLLLOCAL sendError(
55         Category& log, SPRequest& request, const Application* app, const char* page, TemplateParameters& tp, bool mayRedirect=true
56         )
57     {
58         // The properties we need can be set in the RequestMap, or the Errors element.
59         bool mderror = dynamic_cast<const opensaml::saml2md::MetadataException*>(tp.getRichException())!=NULL;
60         pair<bool,const char*> redirectErrors = pair<bool,const char*>(false,NULL);
61         pair<bool,const char*> pathname = pair<bool,const char*>(false,NULL);
62
63         // Strictly for error handling, detect a NULL application and point at the default.
64         if (!app)
65             app = request.getServiceProvider().getApplication("default");
66
67         const PropertySet* props=app->getPropertySet("Errors");
68
69         try {
70             RequestMapper::Settings settings = request.getRequestSettings();
71             if (mderror)
72                 pathname = settings.first->getString("metadataError");
73             if (!pathname.first) {
74                 string pagename(page);
75                 pagename += "Error";
76                 pathname = settings.first->getString(pagename.c_str());
77             }
78             if (mayRedirect)
79                 redirectErrors = settings.first->getString("redirectErrors");
80         }
81         catch (exception& ex) {
82             log.error(ex.what());
83         }
84
85         if (mayRedirect) {
86             // Check for redirection on errors instead of template.
87             if (!redirectErrors.first && props)
88                 redirectErrors = props->getString("redirectErrors");
89             if (redirectErrors.first) {
90                 string loc(redirectErrors.second);
91                 loc = loc + '?' + tp.toQueryString();
92                 return request.sendRedirect(loc.c_str());
93             }
94         }
95
96         request.setContentType("text/html");
97         request.setResponseHeader("Expires","01-Jan-1997 12:00:00 GMT");
98         request.setResponseHeader("Cache-Control","private,no-store,no-cache");
99
100         if (!pathname.first && props) {
101             if (mderror)
102                 pathname=props->getString("metadata");
103             if (!pathname.first)
104                 pathname=props->getString(page);
105         }
106         if (pathname.first) {
107             string fname(pathname.second);
108             ifstream infile(XMLToolingConfig::getConfig().getPathResolver()->resolve(fname, PathResolver::XMLTOOLING_CFG_FILE).c_str());
109             if (infile) {
110                 tp.setPropertySet(props);
111                 stringstream str;
112                 XMLToolingConfig::getConfig().getTemplateEngine()->run(infile, str, tp, tp.getRichException());
113                 return request.sendError(str);
114             }
115         }
116
117         if (!strcmp(page,"access")) {
118             istringstream msg("Access Denied");
119             return request.sendResponse(msg, HTTPResponse::XMLTOOLING_HTTP_STATUS_FORBIDDEN);
120         }
121
122         log.error("sendError could not process error template (%s)", page);
123         istringstream msg("Internal Server Error. Please contact the site administrator.");
124         return request.sendError(msg);
125     }
126
127     void SHIBSP_DLLLOCAL clearHeaders(SPRequest& request) {
128         const Application& app = request.getApplication();
129         app.clearHeader(request, "Shib-Session-ID", "HTTP_SHIB_SESSION_ID");
130         app.clearHeader(request, "Shib-Identity-Provider", "HTTP_SHIB_IDENTITY_PROVIDER");
131         app.clearHeader(request, "Shib-Authentication-Method", "HTTP_SHIB_AUTHENTICATION_METHOD");
132         app.clearHeader(request, "Shib-Authentication-Instant", "HTTP_SHIB_AUTHENTICATION_INSTANT");
133         app.clearHeader(request, "Shib-AuthnContext-Class", "HTTP_SHIB_AUTHNCONTEXT_CLASS");
134         app.clearHeader(request, "Shib-AuthnContext-Decl", "HTTP_SHIB_AUTHNCONTEXT_DECL");
135         app.clearHeader(request, "Shib-Assertion-Count", "HTTP_SHIB_ASSERTION_COUNT");
136         app.clearAttributeHeaders(request);
137         request.clearHeader("REMOTE_USER", "HTTP_REMOTE_USER");
138     }
139 };
140
141 void SHIBSP_API shibsp::registerServiceProviders()
142 {
143     SPConfig::getConfig().ServiceProviderManager.registerFactory(XML_SERVICE_PROVIDER, XMLServiceProviderFactory);
144 }
145
146 ServiceProvider::ServiceProvider()
147 {
148 }
149
150 ServiceProvider::~ServiceProvider()
151 {
152 }
153
154 pair<bool,long> ServiceProvider::doAuthentication(SPRequest& request, bool handler) const
155 {
156 #ifdef _DEBUG
157     xmltooling::NDC ndc("doAuthentication");
158 #endif
159     Category& log = Category::getInstance(SHIBSP_LOGCAT".ServiceProvider");
160
161     const Application* app=NULL;
162     string targetURL = request.getRequestURL();
163
164     try {
165         RequestMapper::Settings settings = request.getRequestSettings();
166         app = &(request.getApplication());
167
168         // If not SSL, check to see if we should block or redirect it.
169         if (!request.isSecure()) {
170             pair<bool,const char*> redirectToSSL = settings.first->getString("redirectToSSL");
171             if (redirectToSSL.first) {
172 #ifdef HAVE_STRCASECMP
173                 if (!strcasecmp("GET",request.getMethod()) || !strcasecmp("HEAD",request.getMethod())) {
174 #else
175                 if (!stricmp("GET",request.getMethod()) || !stricmp("HEAD",request.getMethod())) {
176 #endif
177                     // Compute the new target URL
178                     string redirectURL = string("https://") + request.getHostname();
179                     if (strcmp(redirectToSSL.second,"443")) {
180                         redirectURL = redirectURL + ':' + redirectToSSL.second;
181                     }
182                     redirectURL += request.getRequestURI();
183                     return make_pair(true, request.sendRedirect(redirectURL.c_str()));
184                 }
185                 else {
186                     TemplateParameters tp;
187                     tp.m_map["requestURL"] = targetURL.substr(0,targetURL.find('?'));
188                     return make_pair(true,sendError(log, request, app, "ssl", tp, false));
189                 }
190             }
191         }
192
193         const char* handlerURL=request.getHandlerURL(targetURL.c_str());
194         if (!handlerURL)
195             throw ConfigurationException("Cannot determine handler from resource URL, check configuration.");
196
197         // If the request URL contains the handler base URL for this application, either dispatch
198         // directly (mainly Apache 2.0) or just pass back control.
199         if (strstr(targetURL.c_str(),handlerURL)) {
200             if (handler)
201                 return doHandler(request);
202             else
203                 return make_pair(true, request.returnOK());
204         }
205
206         // Three settings dictate how to proceed.
207         pair<bool,const char*> authType = settings.first->getString("authType");
208         pair<bool,bool> requireSession = settings.first->getBool("requireSession");
209         pair<bool,const char*> requireSessionWith = settings.first->getString("requireSessionWith");
210
211         // If no session is required AND the AuthType (an Apache-derived concept) isn't shibboleth,
212         // then we ignore this request and consider it unprotected. Apache might lie to us if
213         // ShibBasicHijack is on, but that's up to it.
214         if ((!requireSession.first || !requireSession.second) && !requireSessionWith.first &&
215 #ifdef HAVE_STRCASECMP
216                 (!authType.first || strcasecmp(authType.second,"shibboleth")))
217 #else
218                 (!authType.first || _stricmp(authType.second,"shibboleth")))
219 #endif
220             return make_pair(true,request.returnDecline());
221
222         // Fix for secadv 20050901
223         clearHeaders(request);
224
225         Session* session = NULL;
226         try {
227             session = request.getSession();
228         }
229         catch (exception& e) {
230             log.warn("error during session lookup: %s", e.what());
231             // If it's not a retryable session failure, we throw to the outer handler for reporting.
232             if (dynamic_cast<opensaml::RetryableProfileException*>(&e)==NULL)
233                 throw;
234         }
235
236         if (!session) {
237             // No session.  Maybe that's acceptable?
238             if ((!requireSession.first || !requireSession.second) && !requireSessionWith.first)
239                 return make_pair(true,request.returnOK());
240
241             // No session, but we require one. Initiate a new session using the indicated method.
242             const SessionInitiator* initiator=NULL;
243             if (requireSessionWith.first) {
244                 initiator=app->getSessionInitiatorById(requireSessionWith.second);
245                 if (!initiator) {
246                     throw ConfigurationException(
247                         "No session initiator found with id ($1), check requireSessionWith command.", params(1,requireSessionWith.second)
248                         );
249                 }
250             }
251             else {
252                 initiator=app->getDefaultSessionInitiator();
253                 if (!initiator)
254                     throw ConfigurationException("No default session initiator found, check configuration.");
255             }
256
257             return initiator->run(request,false);
258         }
259
260         request.setAuthType("shibboleth");
261
262         // We're done.  Everything is okay.  Nothing to report.  Nothing to do..
263         // Let the caller decide how to proceed.
264         log.debug("doAuthentication succeeded");
265         return make_pair(false,0L);
266     }
267     catch (exception& e) {
268         request.log(SPRequest::SPError, e.what());
269         TemplateParameters tp(&e);
270         tp.m_map["requestURL"] = targetURL.substr(0,targetURL.find('?'));
271         return make_pair(true,sendError(log, request, app, "session", tp));
272     }
273 }
274
275 pair<bool,long> ServiceProvider::doAuthorization(SPRequest& request) const
276 {
277 #ifdef _DEBUG
278     xmltooling::NDC ndc("doAuthorization");
279 #endif
280     Category& log = Category::getInstance(SHIBSP_LOGCAT".ServiceProvider");
281
282     const Application* app=NULL;
283     string targetURL = request.getRequestURL();
284
285     try {
286         RequestMapper::Settings settings = request.getRequestSettings();
287         app = &(request.getApplication());
288
289         // Three settings dictate how to proceed.
290         pair<bool,const char*> authType = settings.first->getString("authType");
291         pair<bool,bool> requireSession = settings.first->getBool("requireSession");
292         pair<bool,const char*> requireSessionWith = settings.first->getString("requireSessionWith");
293
294         // If no session is required AND the AuthType (an Apache-derived concept) isn't shibboleth,
295         // then we ignore this request and consider it unprotected. Apache might lie to us if
296         // ShibBasicHijack is on, but that's up to it.
297         if ((!requireSession.first || !requireSession.second) && !requireSessionWith.first &&
298 #ifdef HAVE_STRCASECMP
299                 (!authType.first || strcasecmp(authType.second,"shibboleth")))
300 #else
301                 (!authType.first || _stricmp(authType.second,"shibboleth")))
302 #endif
303             return make_pair(true,request.returnDecline());
304
305         // Do we have an access control plugin?
306         if (settings.second) {
307             const Session* session = NULL;
308             try {
309                 session = request.getSession(false);
310             }
311             catch (exception& e) {
312                 log.warn("unable to obtain session to pass to access control provider: %s", e.what());
313             }
314
315             Locker acllock(settings.second);
316             switch (settings.second->authorized(request,session)) {
317                 case AccessControl::shib_acl_true:
318                     log.debug("access control provider granted access");
319                     return make_pair(true,request.returnOK());
320
321                 case AccessControl::shib_acl_false:
322                 {
323                     log.warn("access control provider denied access");
324                     TemplateParameters tp;
325                     tp.m_map["requestURL"] = targetURL;
326                     return make_pair(true,sendError(log, request, app, "access", tp, false));
327                 }
328
329                 default:
330                     // Use the "DECLINE" interface to signal we don't know what to do.
331                     return make_pair(true,request.returnDecline());
332             }
333         }
334         else {
335             return make_pair(true,request.returnDecline());
336         }
337     }
338     catch (exception& e) {
339         request.log(SPRequest::SPError, e.what());
340         TemplateParameters tp(&e);
341         tp.m_map["requestURL"] = targetURL.substr(0,targetURL.find('?'));
342         return make_pair(true,sendError(log, request, app, "access", tp));
343     }
344 }
345
346 pair<bool,long> ServiceProvider::doExport(SPRequest& request, bool requireSession) const
347 {
348 #ifdef _DEBUG
349     xmltooling::NDC ndc("doExport");
350 #endif
351     Category& log = Category::getInstance(SHIBSP_LOGCAT".ServiceProvider");
352
353     const Application* app=NULL;
354     string targetURL = request.getRequestURL();
355
356     try {
357         RequestMapper::Settings settings = request.getRequestSettings();
358         app = &(request.getApplication());
359
360         const Session* session = NULL;
361         try {
362             session = request.getSession(false);
363         }
364         catch (exception& e) {
365             log.warn("unable to obtain session to export to request: %s", e.what());
366                 // If we have to have a session, then this is a fatal error.
367                 if (requireSession)
368                         throw;
369         }
370
371                 // Still no data?
372         if (!session) {
373                 if (requireSession)
374                 throw opensaml::RetryableProfileException("Unable to obtain session to export to request.");
375                 else
376                         return make_pair(false,0L);     // just bail silently
377         }
378
379         app->setHeader(request, "Shib-Application-ID", app->getId());
380         app->setHeader(request, "Shib-Session-ID", session->getID());
381
382         // Export the IdP name and Authn method/context info.
383         const char* hval = session->getEntityID();
384         if (hval)
385             app->setHeader(request, "Shib-Identity-Provider", hval);
386         hval = session->getAuthnInstant();
387         if (hval)
388             app->setHeader(request, "Shib-Authentication-Instant", hval);
389         hval = session->getAuthnContextClassRef();
390         if (hval) {
391             app->setHeader(request, "Shib-Authentication-Method", hval);
392             app->setHeader(request, "Shib-AuthnContext-Class", hval);
393         }
394         hval = session->getAuthnContextDeclRef();
395         if (hval)
396             app->setHeader(request, "Shib-AuthnContext-Decl", hval);
397
398         // Maybe export the assertion keys.
399         pair<bool,bool> exp=settings.first->getBool("exportAssertion");
400         if (exp.first && exp.second) {
401             const PropertySet* sessions=app->getPropertySet("Sessions");
402             pair<bool,const char*> exportLocation = sessions ? sessions->getString("exportLocation") : pair<bool,const char*>(false,NULL);
403             if (!exportLocation.first)
404                 log.warn("can't export assertions without an exportLocation Sessions property");
405             else {
406                 const URLEncoder* encoder = XMLToolingConfig::getConfig().getURLEncoder();
407                 string exportName = "Shib-Assertion-00";
408                 string baseURL;
409                 if (!strncmp(exportLocation.second, "http", 4))
410                     baseURL = exportLocation.second;
411                 else
412                     baseURL = string(request.getHandlerURL(targetURL.c_str())) + exportLocation.second;
413                 baseURL = baseURL + "?key=" + session->getID() + "&ID=";
414                 const vector<const char*>& tokens = session->getAssertionIDs();
415                 vector<const char*>::size_type count = 0;
416                 for (vector<const char*>::const_iterator tokenids = tokens.begin(); tokenids!=tokens.end(); ++tokenids) {
417                     count++;
418                     *(exportName.rbegin()) = '0' + (count%10);
419                     *(++exportName.rbegin()) = '0' + (count/10);
420                     string fullURL = baseURL + encoder->encode(*tokenids);
421                     app->setHeader(request, exportName.c_str(), fullURL.c_str());
422                 }
423                 app->setHeader(request, "Shib-Assertion-Count", exportName.c_str() + 15);
424             }
425         }
426
427         // Export the attributes.
428         const multimap<string,const Attribute*>& attributes = session->getIndexedAttributes();
429         for (multimap<string,const Attribute*>::const_iterator a = attributes.begin(); a!=attributes.end(); ++a) {
430             if (a->second->isInternal())
431                 continue;
432             string header(app->getSecureHeader(request, a->first.c_str()));
433             const vector<string>& vals = a->second->getSerializedValues();
434             for (vector<string>::const_iterator v = vals.begin(); v!=vals.end(); ++v) {
435                 if (!header.empty())
436                     header += ";";
437                 string::size_type pos = v->find_first_of(';',string::size_type(0));
438                 if (pos!=string::npos) {
439                     string value(*v);
440                     for (; pos != string::npos; pos = value.find_first_of(';',pos)) {
441                         value.insert(pos, "\\");
442                         pos += 2;
443                     }
444                     header += value;
445                 }
446                 else {
447                     header += (*v);
448                 }
449             }
450             app->setHeader(request, a->first.c_str(), header.c_str());
451         }
452
453         // Check for REMOTE_USER.
454         bool remoteUserSet = false;
455         const vector<string>& rmids = app->getRemoteUserAttributeIds();
456         for (vector<string>::const_iterator rmid = rmids.begin(); !remoteUserSet && rmid != rmids.end(); ++rmid) {
457             pair<multimap<string,const Attribute*>::const_iterator,multimap<string,const Attribute*>::const_iterator> matches =
458                 attributes.equal_range(*rmid);
459             for (; matches.first != matches.second; ++matches.first) {
460                 const vector<string>& vals = matches.first->second->getSerializedValues();
461                 if (!vals.empty()) {
462                     request.setRemoteUser(vals.front().c_str());
463                     remoteUserSet = true;
464                     break;
465                 }
466             }
467         }
468
469         return make_pair(false,0L);
470     }
471     catch (exception& e) {
472         request.log(SPRequest::SPError, e.what());
473         TemplateParameters tp(&e);
474         tp.m_map["requestURL"] = targetURL.substr(0,targetURL.find('?'));
475         return make_pair(true,sendError(log, request, app, "session", tp));
476     }
477 }
478
479 pair<bool,long> ServiceProvider::doHandler(SPRequest& request) const
480 {
481 #ifdef _DEBUG
482     xmltooling::NDC ndc("doHandler");
483 #endif
484     Category& log = Category::getInstance(SHIBSP_LOGCAT".ServiceProvider");
485
486     const Application* app=NULL;
487     string targetURL = request.getRequestURL();
488
489     try {
490         RequestMapper::Settings settings = request.getRequestSettings();
491         app = &(request.getApplication());
492
493         // If not SSL, check to see if we should block or redirect it.
494         if (!request.isSecure()) {
495             pair<bool,const char*> redirectToSSL = settings.first->getString("redirectToSSL");
496             if (redirectToSSL.first) {
497 #ifdef HAVE_STRCASECMP
498                 if (!strcasecmp("GET",request.getMethod()) || !strcasecmp("HEAD",request.getMethod())) {
499 #else
500                 if (!stricmp("GET",request.getMethod()) || !stricmp("HEAD",request.getMethod())) {
501 #endif
502                     // Compute the new target URL
503                     string redirectURL = string("https://") + request.getHostname();
504                     if (strcmp(redirectToSSL.second,"443")) {
505                         redirectURL = redirectURL + ':' + redirectToSSL.second;
506                     }
507                     redirectURL += request.getRequestURI();
508                     return make_pair(true, request.sendRedirect(redirectURL.c_str()));
509                 }
510                 else {
511                     TemplateParameters tp;
512                     tp.m_map["requestURL"] = targetURL.substr(0,targetURL.find('?'));
513                     return make_pair(true,sendError(log, request, app, "ssl", tp, false));
514                 }
515             }
516         }
517
518         const char* handlerURL=request.getHandlerURL(targetURL.c_str());
519         if (!handlerURL)
520             throw ConfigurationException("Cannot determine handler from resource URL, check configuration.");
521
522         // Make sure we only process handler requests.
523         if (!strstr(targetURL.c_str(),handlerURL))
524             return make_pair(true, request.returnDecline());
525
526         const PropertySet* sessionProps=app->getPropertySet("Sessions");
527         if (!sessionProps)
528             throw ConfigurationException("Unable to map request to application session settings, check configuration.");
529
530         // Process incoming request.
531         pair<bool,bool> handlerSSL=sessionProps->getBool("handlerSSL");
532
533         // Make sure this is SSL, if it should be
534         if ((!handlerSSL.first || handlerSSL.second) && !request.isSecure())
535             throw opensaml::FatalProfileException("Blocked non-SSL access to Shibboleth handler.");
536
537         // We dispatch based on our path info. We know the request URL begins with or equals the handler URL,
538         // so the path info is the next character (or null).
539         const Handler* handler=app->getHandler(targetURL.c_str() + strlen(handlerURL));
540         if (!handler)
541             throw ConfigurationException("Shibboleth handler invoked at an unconfigured location.");
542
543         pair<bool,long> hret=handler->run(request);
544
545         // Did the handler run successfully?
546         if (hret.first)
547             return hret;
548
549         throw ConfigurationException("Configured Shibboleth handler failed to process the request.");
550     }
551     catch (exception& e) {
552         request.log(SPRequest::SPError, e.what());
553         TemplateParameters tp(&e);
554         tp.m_map["requestURL"] = targetURL.substr(0,targetURL.find('?'));
555         tp.m_request = &request;
556         return make_pair(true,sendError(log, request, app, "session", tp));
557     }
558 }