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

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/TI12-security/trunk/NDGSecurity/python/ndg_security_server/ndg/security/server/wsgi/authn.py@6264
Revision 6264, 17.0 KB checked in by pjkersha, 10 years ago (diff)
  • Refactored PEP result handler code from authz into separate ndg.security.server.wsgi.authz.result_handler package
  • Refactored session handling classes from ndg.security.server.wsgi.authn to new ndg.security.server.wsgi.session module
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
32
33class AuthnException(NDGSecurityMiddlewareError):
34    """Base exception for this module"""
35   
36   
37class HTTPBasicAuthMiddlewareError(AuthnException):
38    """Base exception type for HTTPBasicAuthMiddleware"""
39   
40   
41class HTTPBasicAuthMiddlewareConfigError(NDGSecurityMiddlewareConfigError):
42    """Configuration error with HTTP Basic Auth middleware"""
43
44
45class HTTPBasicAuthUnauthorized(HTTPBasicAuthMiddlewareError): 
46    """Raise from custom authentication interface in order to set HTTP
47    401 Unuathorized response"""
48   
49   
50class HTTPBasicAuthMiddleware(NDGSecurityMiddlewareBase):
51    '''HTTP Basic Authentication Middleware
52    '''
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    @NDGSecurityMiddlewareBase.initCall
329    def __call__(self, environ, start_response):
330        session = environ[self.sessionKey]
331       
332        # Check for return to address in URI query args set by
333        # AuthnRedirectInitiatorMiddleware in application code stack
334        if environ['REQUEST_METHOD'] == "GET":
335            params = dict(parse_querystring(environ))
336        else:
337            params = {}
338       
339        # Store the return URI query argument in a beaker session
340        quotedReferrer = params.get(self.__class__.RETURN2URI_ARGNAME, '')
341        referrerURI = urllib.unquote(quotedReferrer)
342        if referrerURI:
343            session[self.__class__.RETURN2URI_ARGNAME] = referrerURI
344            session.save()
345           
346        # Check for a return URI setting in the beaker session and if the user
347        # is authenticated, redirect to this URL deleting the beaker session
348        # URL setting
349        return2URI = session.get(self.__class__.RETURN2URI_ARGNAME)   
350        if self.isAuthenticated and return2URI:
351            del session[self.__class__.RETURN2URI_ARGNAME]
352            session.save()
353            return self.redirect(return2URI)
354
355        return self._app(environ, start_response)
356
357
358class AuthKitRedirectResponseMiddleware(AuthnRedirectResponseMiddleware):
359    """Overload isAuthenticated method in parent class to set Authenticated
360    state based on presence of AuthKit 'REMOTE_USER' environ variable
361    """
362    _isAuthenticated = lambda self: \
363        AuthnRedirectResponseMiddleware.USERNAME_ENVIRON_KEYNAME in self.environ
364       
365    isAuthenticated = property(fget=_isAuthenticated,
366                               doc="Boolean indicating if AuthKit "
367                                   "'REMOTE_USER' environment variable is set")
368    def __init__(self, app, app_conf, **local_conf):
369        super(AuthKitRedirectResponseMiddleware, self).__init__(app, app_conf,
370                                                                **local_conf)
371    @NDGSecurityMiddlewareBase.initCall
372    def __call__(self, environ, start_response):
373        return super(AuthKitRedirectResponseMiddleware, self).__call__(environ,
374                                                                start_response)
375
376
377class AuthenticationMiddlewareConfigError(NDGSecurityMiddlewareConfigError):
378    '''Authentication Middleware Configuration error'''
379
380
381class AuthenticationMiddleware(MultiHandler, NDGSecurityMiddlewareBase):
382    '''Top-level class encapsulates session and authentication handlers
383    in this module
384   
385    Handler to intercept 401 Unauthorized HTTP responses and redirect to an
386    authentication URI.  This class also implements a redirect handler to
387    redirect back to the referrer if logout is invoked.
388    '''
389
390    def __init__(self, app, global_conf, prefix='', **app_conf):
391        '''
392        @type app: callable following WSGI interface
393        @param app: next middleware application in the chain     
394        @type global_conf: dict       
395        @param global_conf: PasteDeploy global configuration dictionary
396        @type prefix: basestring
397        @param prefix: prefix for configuration items
398        @type app_conf: dict       
399        @param app_conf: PasteDeploy application specific configuration
400        dictionary
401        '''
402       
403        # Set logout URI parameter from AuthKit settings if not otherwise set
404        sessionHandlerPrefix = prefix + SessionHandlerMiddleware.PARAM_PREFIX       
405        app = SessionHandlerMiddleware(app, 
406                                       global_conf, 
407                                       prefix=sessionHandlerPrefix,
408                                       **app_conf)
409       
410        # Remove session handler middleware specific parameters
411        for k in app_conf.keys():
412            if k.startswith(sessionHandlerPrefix):
413                del app_conf[k]
414       
415        app = authkit.authenticate.middleware(app, app_conf)       
416       
417        MultiHandler.__init__(self, app)
418
419        # Redirection middleware is invoked based on a check method which
420        # catches HTTP 401 responses.
421        self.add_method(AuthnRedirectInitiatorMiddleware.MIDDLEWARE_ID, 
422                        AuthnRedirectInitiatorMiddleware.filter_app_factory, 
423                        global_conf,
424                        prefix=prefix,
425                        **app_conf)
426       
427        self.add_checker(AuthnRedirectInitiatorMiddleware.MIDDLEWARE_ID, 
428                         AuthnRedirectInitiatorMiddleware.checker)
Note: See TracBrowser for help on using the repository browser.