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