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