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