source: TI12-security/trunk/NDGSecurity/python/ndg_security_server/ndg/security/server/wsgi/authn.py @ 7077

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/TI12-security/trunk/NDGSecurity/python/ndg_security_server/ndg/security/server/wsgi/authn.py@7077
Revision 7077, 18.2 KB checked in by pjkersha, 9 years ago (diff)
  • Property svn:keywords set to Id
Line 
1"""Module containing:
2 * HTTP Basic Authentication Middleware
3 * middleware to enable redirection to OpenID Relying Party for login
4 * logout middleware for deleting AuthKit cookie and redirecting back to
5   referrer
6 
7NERC DataGrid Project
8"""
9__author__ = "P J Kershaw"
10__date__ = "13/01/09"
11__copyright__ = "(C) 2009 Science and Technology Facilities Council"
12__license__ = "BSD - see LICENSE file in top-level directory"
13__contact__ = "Philip.Kershaw@stfc.ac.uk"
14__revision__ = "$Id$"
15import logging
16log = logging.getLogger(__name__)
17
18import re
19import base64
20import httplib
21import urllib
22from paste.request import construct_url, parse_querystring
23import authkit.authenticate
24from authkit.authenticate.multi import MultiHandler
25
26from ndg.security.server.wsgi import (NDGSecurityMiddlewareBase, 
27                                      NDGSecurityMiddlewareError, 
28                                      NDGSecurityMiddlewareConfigError) 
29from ndg.security.server.wsgi.session import (SessionMiddlewareBase,
30                                              SessionHandlerMiddleware) 
31
32from ndg.security.server.wsgi.ssl import AuthKitSSLAuthnMiddleware
33
34class AuthnException(NDGSecurityMiddlewareError):
35    """Base exception for this module"""
36   
37   
38class HTTPBasicAuthMiddlewareError(AuthnException):
39    """Base exception type for HTTPBasicAuthMiddleware"""
40   
41   
42class HTTPBasicAuthMiddlewareConfigError(NDGSecurityMiddlewareConfigError):
43    """Configuration error with HTTP Basic Auth middleware"""
44
45
46class HTTPBasicAuthUnauthorized(HTTPBasicAuthMiddlewareError): 
47    """Raise from custom authentication interface in order to set HTTP
48    401 Unuathorized response"""
49   
50   
51class HTTPBasicAuthMiddleware(NDGSecurityMiddlewareBase):
52    '''HTTP Basic Authentication Middleware
53    '''
54    AUTHN_FUNC_ENV_KEYNAME = ('ndg.security.server.wsgi.authn.'
55                              'HTTPBasicAuthMiddleware.authenticate')
56    AUTHN_FUNC_ENV_KEYNAME_OPTNAME = 'authnFuncEnvKeyName'       
57    PARAM_PREFIX = 'http.auth.basic.'
58    HTTP_HDR_FIELDNAME = 'basic'
59    FIELD_SEP = ':'
60    AUTHZ_ENV_KEYNAME = 'HTTP_AUTHORIZATION'
61   
62    RE_PATH_MATCH_LIST_OPTNAME = 'rePathMatchList'
63   
64    def __init__(self, app, app_conf, prefix=PARAM_PREFIX, **local_conf):
65        self.__rePathMatchList = None
66        self.__authnFuncEnvironKeyName = None
67       
68        super(HTTPBasicAuthMiddleware, self).__init__(app, app_conf, 
69                                                      **local_conf)
70
71        rePathMatchListOptName = prefix + \
72                            HTTPBasicAuthMiddleware.RE_PATH_MATCH_LIST_OPTNAME
73        rePathMatchListVal = app_conf.pop(rePathMatchListOptName, '')
74       
75        self.rePathMatchList = [re.compile(i) 
76                                for i in rePathMatchListVal.split()]
77
78        paramName = prefix + \
79                    HTTPBasicAuthMiddleware.AUTHN_FUNC_ENV_KEYNAME_OPTNAME
80                   
81        self.authnFuncEnvironKeyName = local_conf.get(paramName,
82                                HTTPBasicAuthMiddleware.AUTHN_FUNC_ENV_KEYNAME)
83
84    def _getAuthnFuncEnvironKeyName(self):
85        return self.__authnFuncEnvironKeyName
86
87    def _setAuthnFuncEnvironKeyName(self, value):
88        if not isinstance(value, basestring):
89            raise TypeError('Expecting string type for '
90                            '"authnFuncEnvironKeyName"; got %r type' % 
91                            type(value))
92        self.__authnFuncEnvironKeyName = value
93
94    authnFuncEnvironKeyName = property(fget=_getAuthnFuncEnvironKeyName, 
95                                       fset=_setAuthnFuncEnvironKeyName, 
96                                       doc="key name in environ for the "
97                                           "custom authentication function "
98                                           "used by this class")
99
100    def _getRePathMatchList(self):
101        return self.__rePathMatchList
102
103    def _setRePathMatchList(self, value):
104        if not isinstance(value, (list, tuple)):
105            raise TypeError('Expecting list or tuple type for '
106                            '"rePathMatchList"; got %r' % type(value))
107       
108        self.__rePathMatchList = value
109
110    rePathMatchList = property(fget=_getRePathMatchList, 
111                               fset=_setRePathMatchList, 
112                               doc="List of regular expressions determine the "
113                                   "URI paths intercepted by this middleware")
114
115    def _pathMatch(self):
116        """Apply a list of regular expression matching patterns to the contents
117        of environ['PATH_INFO'], if any match, return True.  This method is
118        used to determine whether to apply SSL client authentication
119        """
120        path = self.pathInfo
121        for regEx in self.rePathMatchList:
122            if regEx.match(path):
123                return True
124           
125        return False   
126
127    def _parseCredentials(self):
128        """Extract username and password from HTTP_AUTHORIZATION environ key
129       
130        @rtype: tuple
131        @return: username and password.  If the key is not set or the auth
132        method is not basic return a two element tuple with elements both set
133        to None
134        """
135        basicAuthHdr = self.environ.get(
136                                    HTTPBasicAuthMiddleware.AUTHZ_ENV_KEYNAME)
137        if basicAuthHdr is None:
138            log.debug("No %r setting in environ: skipping HTTP Basic Auth",
139                      HTTPBasicAuthMiddleware.AUTHZ_ENV_KEYNAME)
140            return None, None
141                       
142        method, encodedCreds = basicAuthHdr.split(None, 1)
143        if method.lower() != HTTPBasicAuthMiddleware.HTTP_HDR_FIELDNAME:
144            log.debug("Auth method is %r not %r: skipping request",
145                      method, HTTPBasicAuthMiddleware.HTTP_HDR_FIELDNAME)
146            return None, None
147           
148        creds = base64.decodestring(encodedCreds)
149        username, password = creds.split(HTTPBasicAuthMiddleware.FIELD_SEP, 1)
150        return username, password
151
152    @NDGSecurityMiddlewareBase.initCall
153    def __call__(self, environ, start_response):
154        """Authenticate based HTTP header elements as specified by the HTTP
155        Basic Authentication spec."""
156        log.debug("HTTPBasicAuthNMiddleware.__call__ ...")
157       
158        if not self._pathMatch():
159            return self._app(environ, start_response)
160       
161        authenticate = environ.get(self.authnFuncEnvironKeyName)
162        if authenticate is None:
163            # HTTP 500 default is right for this error
164            raise HTTPBasicAuthMiddlewareConfigError("No authentication "
165                                                     "function set in environ")
166           
167        username, password = self._parseCredentials()
168        if username is None:
169            return self._setErrorResponse(code=httplib.UNAUTHORIZED)
170       
171        # Call authentication application
172        try:
173            return authenticate(environ, start_response, username, password)
174       
175        except HTTPBasicAuthUnauthorized, e:
176            log.error(e)
177            return self._setErrorResponse(code=httplib.UNAUTHORIZED)
178        else:
179            return self._app(environ, start_response)
180
181
182# AuthKit based HTTP basic authentication plugin not currently needed but may
183# need resurrecting
184from authkit.permissions import UserIn
185           
186class HTTPBasicAuthentication(object):
187    '''Authkit based HTTP Basic Authentication.   __call__ defines a
188    validation function to fit with the pattern for the AuthKit interface
189    '''
190   
191    def __init__(self):
192        self._userIn = UserIn([])
193       
194    def __call__(self, environ, username, password):
195        """AuthKit HTTP Basic Auth validation function - return True/False"""
196        raise NotImplementedError()
197
198       
199class AuthnRedirectMiddleware(SessionMiddlewareBase):
200    """Base class for Authentication HTTP redirect initiator and redirect
201    response WSGI middleware
202
203    @type RETURN2URI_ARGNAME: basestring
204    @cvar RETURN2URI_ARGNAME: name of URI query argument used to pass the
205    return to URI between initiator and consumer classes"""
206    RETURN2URI_ARGNAME = 'ndg.security.r'
207
208
209class AuthnRedirectInitiatorMiddleware(AuthnRedirectMiddleware):
210    '''Middleware to initiate a redirect to another URI if a user is not
211    authenticated i.e. security cookie is not set
212   
213    AuthKit.authenticate.middleware must be in place upstream of this
214    middleware.  AuthenticationMiddleware wrapper handles this.
215   
216    @type propertyDefaults: dict
217    @cvar propertyDefaults: valid configuration property keywords   
218    '''
219    propertyDefaults = {
220        'redirectURI': None,
221    }
222    propertyDefaults.update(AuthnRedirectMiddleware.propertyDefaults)
223   
224    TRIGGER_HTTP_STATUS_CODE = '401'
225    MIDDLEWARE_ID = 'AuthnRedirectInitiatorMiddleware'
226
227    def __init__(self, app, global_conf, **app_conf):
228        '''
229        @type app: callable following WSGI interface
230        @param app: next middleware application in the chain     
231        @type global_conf: dict       
232        @param global_conf: PasteDeploy global configuration dictionary
233        @type prefix: basestring
234        @param prefix: prefix for configuration items
235        @type app_conf: dict       
236        @param app_conf: PasteDeploy application specific configuration
237        dictionary
238        '''
239        self.__redirectURI = None
240        super(AuthnRedirectInitiatorMiddleware, self).__init__(app, 
241                                                               global_conf, 
242                                                               **app_conf)
243
244    @NDGSecurityMiddlewareBase.initCall
245    def __call__(self, environ, start_response):
246        '''Invoke redirect if user is not authenticated'''
247       
248        log.debug("AuthnRedirectInitiatorMiddleware.__call__ ...")
249       
250        if self.isAuthenticated:
251            # Call next app in stack
252            return self._app(environ, start_response)       
253        else:
254            # User is not authenticated - Redirect to OpenID Relying Party URI
255            # for user OpenID entry
256            return self._setRedirectResponse()
257   
258    def _setRedirectURI(self, uri):
259        if not isinstance(uri, basestring):
260            raise TypeError("Redirect URI must be set to string type")   
261         
262        self.__redirectURI = uri
263       
264    def _getRedirectURI(self):
265        return self.__redirectURI
266   
267    redirectURI = property(fget=_getRedirectURI,
268                           fset=_setRedirectURI,
269                           doc="URI to redirect to if user is not "
270                               "authenticated")
271
272    def _setRedirectResponse(self):
273        """Construct a redirect response adding in a return to address in a
274        URI query argument
275       
276        @rtype: basestring
277        @return: redirect response
278        """       
279        return2URI = construct_url(self.environ)
280        quotedReturn2URI = urllib.quote(return2URI, safe='')
281        return2URIQueryArg = urllib.urlencode(
282            {AuthnRedirectInitiatorMiddleware.RETURN2URI_ARGNAME: 
283             quotedReturn2URI})
284
285        redirectURI = self.redirectURI
286       
287        if '?' in redirectURI:
288            if redirectURI.endswith('&'):
289                redirectURI += return2URIQueryArg
290            else:
291                redirectURI += '&' + return2URIQueryArg
292        else:
293            if redirectURI.endswith('?'):
294                redirectURI += return2URIQueryArg
295            else:
296                redirectURI += '?' + return2URIQueryArg
297         
298        # Call NDGSecurityMiddlewareBase.redirect utility method     
299        return self.redirect(redirectURI)
300       
301    @classmethod
302    def checker(cls, environ, status, headers):
303        """Set the MultiHandler checker function for triggering this
304        middleware.  In this case, it's a HTTP 401 Unauthorized response
305        detected in the middleware chain
306        """
307        if status.startswith(cls.TRIGGER_HTTP_STATUS_CODE):
308            log.debug("%s.checker caught status [%s]: invoking authentication "
309                      "handler", cls.__name__, cls.TRIGGER_HTTP_STATUS_CODE)
310            return True
311        else:
312            log.debug("%s.checker skipping status [%s]", cls.__name__, status)
313            return False
314
315
316class AuthnRedirectResponseMiddleware(AuthnRedirectMiddleware):
317    """Compliment to AuthnRedirectInitiatorMiddleware
318    functioning as the opposite end of the HTTP redirect interface.  It
319    performs the following tasks:
320    - Detect a redirect URI set in a URI query argument and copy it into
321    a user session object.
322    - Redirect back to the redirect URI once a user is authenticated
323   
324    Also see,
325    ndg.security.server.wsgi.openid.relyingparty.OpenIDRelyingPartyMiddleware
326    which performs a similar function.
327    """   
328    _sslAuthnSucceeded = lambda self: self.environ.get(
329                    AuthKitSSLAuthnMiddleware.AUTHN_SUCCEEDED_ENVIRON_KEYNAME,
330                    False)
331       
332    sslAuthnSucceeded = property(fget=_sslAuthnSucceeded,
333                                 doc="Boolean indicating SSL authentication "
334                                     "has succeeded in "
335                                     "AuthKitSSLAuthnMiddleware upstream of "
336                                     "this middleware")   
337    _sslAuthnSucceeded = lambda self: self.environ.get(
338                    AuthKitSSLAuthnMiddleware.AUTHN_SUCCEEDED_ENVIRON_KEYNAME,
339                    False)
340       
341    sslAuthnSucceeded = property(fget=_sslAuthnSucceeded,
342                                 doc="Boolean indicating SSL authentication "
343                                     "has succeeded in "
344                                     "AuthKitSSLAuthnMiddleware upstream of "
345                                     "this middleware")
346       
347    @NDGSecurityMiddlewareBase.initCall
348    def __call__(self, environ, start_response):
349        session = environ[self.sessionKey]
350       
351        # Check for return to address in URI query args set by
352        # AuthnRedirectInitiatorMiddleware in application code stack
353        if environ['REQUEST_METHOD'] == "GET":
354            params = dict(parse_querystring(environ))
355        else:
356            params = {}
357       
358        # Store the return URI query argument in a beaker session
359        quotedReferrer = params.get(self.__class__.RETURN2URI_ARGNAME, '')
360        referrerURI = urllib.unquote(quotedReferrer)
361        if referrerURI:
362            session[self.__class__.RETURN2URI_ARGNAME] = referrerURI
363            session.save()
364           
365        # Check for a return URI setting in the beaker session and if the user
366        # has just been authenticated by the AuthKit SSL Client authentication
367        # middleware.  If so, redirect to this URL deleting the beaker session
368        # URL setting
369        return2URI = session.get(self.__class__.RETURN2URI_ARGNAME)   
370        if self.sslAuthnSucceeded and return2URI:
371            del session[self.__class__.RETURN2URI_ARGNAME]
372            session.save()
373            return self.redirect(return2URI)
374
375        return self._app(environ, start_response)
376
377
378class AuthKitRedirectResponseMiddleware(AuthnRedirectResponseMiddleware):
379    """Overload isAuthenticated method in parent class to set Authenticated
380    state based on presence of AuthKit 'REMOTE_USER' environ variable
381    """
382    _isAuthenticated = lambda self: \
383        AuthnRedirectResponseMiddleware.USERNAME_ENVIRON_KEYNAME in self.environ
384       
385    isAuthenticated = property(fget=_isAuthenticated,
386                               doc="Boolean indicating if AuthKit "
387                                   "'REMOTE_USER' environment variable is set")
388   
389    def __init__(self, app, app_conf, **local_conf):
390        super(AuthKitRedirectResponseMiddleware, self).__init__(app, app_conf,
391                                                                **local_conf)
392    @NDGSecurityMiddlewareBase.initCall
393    def __call__(self, environ, start_response):
394        return super(AuthKitRedirectResponseMiddleware, self).__call__(environ,
395                                                                start_response)
396
397
398class AuthenticationMiddlewareConfigError(NDGSecurityMiddlewareConfigError):
399    '''Authentication Middleware Configuration error'''
400
401
402class AuthenticationMiddleware(MultiHandler, NDGSecurityMiddlewareBase):
403    '''Top-level class encapsulates session and authentication handlers
404    in this module
405   
406    Handler to intercept 401 Unauthorized HTTP responses and redirect to an
407    authentication URI.  This class also implements a redirect handler to
408    redirect back to the referrer if logout is invoked.
409    '''
410
411    def __init__(self, app, global_conf, prefix='', **app_conf):
412        '''
413        @type app: callable following WSGI interface
414        @param app: next middleware application in the chain     
415        @type global_conf: dict       
416        @param global_conf: PasteDeploy global configuration dictionary
417        @type prefix: basestring
418        @param prefix: prefix for configuration items
419        @type app_conf: dict       
420        @param app_conf: PasteDeploy application specific configuration
421        dictionary
422        '''
423       
424        # Set logout URI parameter from AuthKit settings if not otherwise set
425        sessionHandlerPrefix = prefix + SessionHandlerMiddleware.PARAM_PREFIX       
426        app = SessionHandlerMiddleware(app, 
427                                       global_conf, 
428                                       prefix=sessionHandlerPrefix,
429                                       **app_conf)
430       
431        # Remove session handler middleware specific parameters
432        for k in app_conf.keys():
433            if k.startswith(sessionHandlerPrefix):
434                del app_conf[k]
435       
436        app = authkit.authenticate.middleware(app, app_conf)       
437       
438        MultiHandler.__init__(self, app)
439
440        # Redirection middleware is invoked based on a check method which
441        # catches HTTP 401 responses.
442        self.add_method(AuthnRedirectInitiatorMiddleware.MIDDLEWARE_ID, 
443                        AuthnRedirectInitiatorMiddleware.filter_app_factory, 
444                        global_conf,
445                        prefix=prefix,
446                        **app_conf)
447       
448        self.add_checker(AuthnRedirectInitiatorMiddleware.MIDDLEWARE_ID, 
449                         AuthnRedirectInitiatorMiddleware.checker)
Note: See TracBrowser for help on using the repository browser.