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