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

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/TI12-security/trunk/python/ndg_security_server/ndg/security/server/wsgi/authn.py@5766
Revision 5766, 20.1 KB checked in by pjkersha, 10 years ago (diff)

Working unit tests for Authentication redirect handler with SSL Client based authentication.

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 urllib
19from urlparse import urlsplit
20from paste.request import construct_url
21from paste.request import parse_querystring
22from beaker.middleware import SessionMiddleware
23import authkit.authenticate
24
25from ndg.security.server.wsgi import NDGSecurityMiddlewareBase, \
26    NDGSecurityMiddlewareConfigError       
27
28
29class AuthenticationMiddlewareBase(NDGSecurityMiddlewareBase):
30    """Base class for Authentication redirect middleware and Session Handler
31    middleware
32   
33    @type propertyDefaults: dict
34    @cvar propertyDefaults: valid configuration property keywords
35    """   
36    propertyDefaults = {
37        'sessionKey': 'beaker.session.ndg.security'
38    }
39    USERNAME_SESSION_KEYNAME = 'username'
40   
41    _isAuthenticated = lambda self: \
42        AuthenticationMiddlewareBase.USERNAME_SESSION_KEYNAME in \
43        self.environ.get(self.sessionKey, ())
44       
45    isAuthenticated = property(fget=_isAuthenticated,
46                               doc='boolean to indicate is user logged in')
47
48       
49class AuthnRedirectMiddleware(AuthenticationMiddlewareBase):
50    """Base class for Authentication HTTP redirect initiator and redirect
51    response WSGI middleware
52
53    @type RETURN2URI_ARGNAME: basestring
54    @cvar RETURN2URI_ARGNAME: name of URI query argument used to pass the
55    return to URI between initiator and consumer classes"""
56    RETURN2URI_ARGNAME = 'ndg.security.r'
57#   
58#    _isAuthenticated = lambda self: \
59#            AuthnRedirectMiddleware.USERNAME_ENVIRON_KEYNAME in self.environ
60#           
61#    isAuthenticated = property(fget=_isAuthenticated,
62#                               doc='boolean for, "is user logged in?"')
63
64class AuthnRedirectInitiatorMiddleware(AuthnRedirectMiddleware):
65    '''Middleware to initiate a redirect to another URI if a user is not
66    authenticated i.e. security cookie is not set
67   
68    AuthKit.authenticate.middleware must be in place upstream of this
69    middleware.  AuthenticationMiddleware wrapper handles this.
70   
71    @type propertyDefaults: dict
72    @cvar propertyDefaults: valid configuration property keywords   
73    '''
74    propertyDefaults = {
75        'redirectURI': None,
76    }
77    propertyDefaults.update(AuthnRedirectMiddleware.propertyDefaults)
78   
79
80    TRIGGER_HTTP_STATUS_CODE = '401'
81    MIDDLEWARE_ID = 'AuthnRedirectInitiatorMiddleware'
82
83    def __init__(self, app, global_conf, **app_conf):
84        '''
85        @type app: callable following WSGI interface
86        @param app: next middleware application in the chain     
87        @type global_conf: dict       
88        @param global_conf: PasteDeploy global configuration dictionary
89        @type prefix: basestring
90        @param prefix: prefix for configuration items
91        @type app_conf: dict       
92        @param app_conf: PasteDeploy application specific configuration
93        dictionary
94        '''
95        self._redirectURI = None
96        super(AuthnRedirectInitiatorMiddleware, self).__init__(app, 
97                                                               global_conf, 
98                                                               **app_conf)
99       
100    @NDGSecurityMiddlewareBase.initCall
101    def __call__(self, environ, start_response):
102        '''Invoke redirect if user is not authenticated'''
103       
104        log.debug("AuthnRedirectInitiatorMiddleware.__call__ ...")
105       
106        if self.isAuthenticated:
107            # Call next app in stack
108            return self._app(environ, start_response)       
109        else:
110            # User is not authenticated - Redirect to OpenID Relying Party URI
111            # for user OpenID entry
112            return self._setRedirectResponse()
113   
114    def _setRedirectURI(self, uri):
115        if not isinstance(uri, basestring):
116            raise TypeError("Redirect URI must be set to string type")   
117         
118        self._redirectURI = uri
119       
120    def _getRedirectURI(self):
121        return self._redirectURI
122   
123    redirectURI = property(fget=_getRedirectURI,
124                       fset=_setRedirectURI,
125                       doc="URI to redirect to if user is not authenticated")
126
127    def _setRedirectResponse(self):
128        """Construct a redirect response adding in a return to address in a
129        URI query argument
130       
131        @rtype: basestring
132        @return: redirect response
133        """       
134        return2URI = construct_url(self.environ)
135        quotedReturn2URI = urllib.quote(return2URI, safe='')
136        return2URIQueryArg = urllib.urlencode(
137                    {AuthnRedirectInitiatorMiddleware.RETURN2URI_ARGNAME: 
138                     quotedReturn2URI})
139
140        redirectURI = self.redirectURI
141       
142        if '?' in redirectURI:
143            if redirectURI.endswith('&'):
144                redirectURI += return2URIQueryArg
145            else:
146                redirectURI += '&' + return2URIQueryArg
147        else:
148            if redirectURI.endswith('?'):
149                redirectURI += return2URIQueryArg
150            else:
151                redirectURI += '?' + return2URIQueryArg
152         
153        # Call NDGSecurityMiddlewareBase.redirect utility method     
154        return self.redirect(redirectURI)
155       
156    @classmethod
157    def checker(cls, environ, status, headers):
158        """Set the MultiHandler checker function for triggering this
159        middleware.  In this case, it's a HTTP 401 Unauthorized response
160        detected in the middleware chain
161        """
162        if status.startswith(cls.TRIGGER_HTTP_STATUS_CODE):
163            log.debug("%s.checker caught status [%s]: invoking authentication"
164                      " handler", cls.__name__, cls.TRIGGER_HTTP_STATUS_CODE)
165            return True
166        else:
167            log.debug("%s.checker skipping status [%s]", cls.__name__, status)
168            return False
169
170
171class AuthnRedirectResponseMiddleware(AuthnRedirectMiddleware):
172    """Compliment to AuthnRedirectInitiatorMiddleware
173    functioning as the opposite end of the HTTP redirect interface.  It
174    performs the following tasks:
175    - Detect a redirect URI set in a URI query argument and copy it into
176    a user session object.
177    - Redirect back to the redirect URI once a user is authenticated
178   
179    Also see,
180    ndg.security.server.wsgi.openid.relyingparty.OpenIDRelyingPartyMiddleware
181    which performs a similar function.
182    """
183    @NDGSecurityMiddlewareBase.initCall
184    def __call__(self, environ, start_response):
185        session = environ[self.sessionKey]
186       
187        # Check for return to address in URI query args set by
188        # AuthnRedirectInitiatorMiddleware in application code stack
189        if environ['REQUEST_METHOD'] == "GET":
190            params = dict(parse_querystring(environ))
191        else:
192            params = {}
193       
194        quotedReferrer = params.get(self.__class__.RETURN2URI_ARGNAME, '')
195        referrerURI = urllib.unquote(quotedReferrer)
196        if referrerURI:
197            session[self.__class__.RETURN2URI_ARGNAME] = referrerURI
198            session.save()
199           
200        return2URI = session.get(self.__class__.RETURN2URI_ARGNAME)   
201        if self.isAuthenticated and return2URI:
202            del session[self.__class__.RETURN2URI_ARGNAME]
203            session.save()
204            return self.redirect(return2URI)
205
206        return self._app(environ, start_response)
207
208
209class SessionHandlerMiddlewareConfigError(Exception):
210    """Configuration errors from SessionHandlerMiddleware"""
211   
212   
213class SessionHandlerMiddleware(AuthenticationMiddlewareBase):
214    '''Middleware to handle:
215    - establish user session details following redirect from OpenID Relying
216    Party sign-in or SSL Client authentication
217    - end session redirecting back to referrer URI following call to a logout
218    URI as implemented in AuthKit
219    '''
220   
221    SM_URI_SESSION_KEYNAME = 'sessionManagerURI'
222    ID_SESSION_KEYNAME = 'sessionId'
223    PEP_CTX_SESSION_KEYNAME = 'pepCtx'
224    CREDENTIAL_WALLET_SESSION_KEYNAME = 'credentialWallet'
225   
226    SESSION_KEYNAMES = (
227        AuthenticationMiddlewareBase.USERNAME_SESSION_KEYNAME, 
228        SM_URI_SESSION_KEYNAME, 
229        ID_SESSION_KEYNAME, 
230        PEP_CTX_SESSION_KEYNAME, 
231        CREDENTIAL_WALLET_SESSION_KEYNAME
232    )
233   
234    AX_KEYNAME = 'ax'
235    SM_URI_AX_KEYNAME = 'value.sessionManagerURI.1'
236    SESSION_ID_AX_KEYNAME = 'value.sessionId.1'
237   
238    AUTHKIT_COOKIE_SIGNOUT_PARAMNAME = 'authkit.cookie.signoutpath'
239    SIGNOUT_PATH_PARAMNAME = 'signoutPath'
240    SESSION_KEY_PARAMNAME = 'sessionKey'
241    propertyDefaults = {
242        SIGNOUT_PATH_PARAMNAME: None,
243        SESSION_KEY_PARAMNAME: 'beaker.session.ndg.security'
244    }
245   
246    AUTH_TKT_SET_USER_ENVIRON_KEYNAME = 'paste.auth_tkt.set_user'
247   
248    PARAM_PREFIX = 'sessionHandler.'
249   
250    def __init__(self, app, global_conf, prefix=PARAM_PREFIX, **app_conf):
251        '''
252        @type app: callable following WSGI interface
253        @param app: next middleware application in the chain     
254        @type global_conf: dict       
255        @param global_conf: PasteDeploy global configuration dictionary
256        @type prefix: basestring
257        @param prefix: prefix for configuration items
258        @type app_conf: dict       
259        @param app_conf: PasteDeploy application specific configuration
260        dictionary
261        '''
262        signoutPathParamName = prefix + \
263                                SessionHandlerMiddleware.SIGNOUT_PATH_PARAMNAME
264       
265        if signoutPathParamName not in app_conf:
266            authKitSignOutPath = app_conf.get(
267                    SessionHandlerMiddleware.AUTHKIT_COOKIE_SIGNOUT_PARAMNAME)
268           
269            if authKitSignOutPath:
270                app_conf[signoutPathParamName] = authKitSignOutPath
271               
272                log.info('Set signoutPath=%s from "%s" setting', 
273                     authKitSignOutPath,
274                     SessionHandlerMiddleware.AUTHKIT_COOKIE_SIGNOUT_PARAMNAME)
275            else:
276                raise SessionHandlerMiddlewareConfigError(
277                                        '"signoutPath" parameter is not set')
278           
279        super(SessionHandlerMiddleware, self).__init__(app,
280                                                       global_conf,
281                                                       prefix=prefix, 
282                                                       **app_conf)
283       
284    @NDGSecurityMiddlewareBase.initCall
285    def __call__(self, environ, start_response):
286        """Manage setting of session from AuthKit following OpenID Relying
287        Party sign in and manage logout
288       
289        @type environ: dict
290        @param environ: WSGI environment variables dictionary
291        @type start_response: function
292        @param start_response: standard WSGI start response function
293        """
294        log.debug("SessionHandlerMiddleware.__call__ ...")
295       
296        session = environ.get(self.sessionKey)
297        if session is None:
298            raise SessionHandlerMiddlewareConfigError(
299                   'SessionHandlerMiddleware.__call__: No beaker session key '
300                   '"%s" found in environ' % self.sessionKey)
301       
302        if self.signoutPath and self.pathInfo == self.signoutPath:
303            log.debug("SessionHandlerMiddleware.__call__: caught sign out "
304                      "path [%s]", self.signoutPath)
305           
306            referrer = environ.get('HTTP_REFERER')
307            if referrer is not None:
308                def _start_response(status, header, exc_info=None):
309                    """Alter the header to send a redirect to the logout
310                    referrer address"""
311                    filteredHeader = [(field, val) for field, val in header
312                                      if field.lower() != 'location']       
313                    filteredHeader.extend([('Location', referrer)])
314                    return start_response(self.getStatusMessage(302), 
315                                          filteredHeader,
316                                          exc_info)
317                   
318            else:
319                log.error('No referrer set for redirect following logout')
320                _start_response = start_response
321               
322            # Clear user details from beaker session
323            for keyName in self.__class__.SESSION_KEYNAMES:
324                session.pop(keyName, None)
325            session.save()
326        else:
327            log.debug("SessionHandlerMiddleware.__call__: checking for "
328                      "REMOTE_* environment variable settings set by OpenID "
329                      "Relying Party signin...")
330           
331            if SessionHandlerMiddleware.USERNAME_SESSION_KEYNAME not in session\
332               and SessionHandlerMiddleware.USERNAME_ENVIRON_KEYNAME in environ:
333                log.debug("SessionHandlerMiddleware: updating session "
334                          "username=%s", environ[
335                            SessionHandlerMiddleware.USERNAME_ENVIRON_KEYNAME])
336               
337                session[SessionHandlerMiddleware.USERNAME_SESSION_KEYNAME
338                        ] = environ[
339                            SessionHandlerMiddleware.USERNAME_ENVIRON_KEYNAME]
340                session.save()
341               
342            remoteUserData = environ.get(
343                        SessionHandlerMiddleware.USERDATA_ENVIRON_KEYNAME, '')   
344            if remoteUserData:
345                log.debug("SessionHandlerMiddleware: found REMOTE_USER_DATA="
346                          "%s, set from OpenID Relying Party signin",
347                          environ[
348                          SessionHandlerMiddleware.USERDATA_ENVIRON_KEYNAME])
349               
350                # eval is safe here because AuthKit cookie is signed and
351                # AuthKit middleware checks for tampering
352                if SessionHandlerMiddleware.SM_URI_SESSION_KEYNAME not in \
353                   session or \
354                   SessionHandlerMiddleware.ID_SESSION_KEYNAME not in session:
355                   
356                    axData = eval(remoteUserData)
357                    if isinstance(axData, dict) and \
358                       SessionHandlerMiddleware.AX_KEYNAME in axData:
359                       
360                        sessionManagerURI = axData[
361                            SessionHandlerMiddleware.AX_KEYNAME].get(
362                                SessionHandlerMiddleware.SM_URI_AX_KEYNAME)
363                           
364                        session[SessionHandlerMiddleware.SM_URI_SESSION_KEYNAME
365                                ] = sessionManagerURI
366   
367                        sessionId = axData[
368                            SessionHandlerMiddleware.AX_KEYNAME].get(
369                                SessionHandlerMiddleware.SESSION_ID_AX_KEYNAME)
370                        session[SessionHandlerMiddleware.ID_SESSION_KEYNAME
371                                ] = sessionId
372                        session.save()
373                       
374                        log.debug("SessionHandlerMiddleware: updated session "
375                                  "with sessionManagerURI=%s and "
376                                  "sessionId=%s", 
377                                  sessionManagerURI, 
378                                  sessionId)
379                   
380                # Reset cookie removing user data
381                environ[
382                    SessionHandlerMiddleware.AUTH_TKT_SET_USER_ENVIRON_KEYNAME](
383                        session[SessionHandlerMiddleware.ID_SESSION_KEYNAME])
384
385            _start_response = start_response
386           
387        return self._app(environ, _start_response)
388
389
390from authkit.authenticate.multi import MultiHandler
391
392class AuthenticationMiddlewareConfigError(NDGSecurityMiddlewareConfigError):
393    '''Authentication Middleware Configuration error'''
394
395
396class AuthenticationMiddleware(MultiHandler, NDGSecurityMiddlewareBase):
397    '''Top-level class encapsulates session and authentication handlers
398    in this module
399   
400    Handler to intercept 401 Unauthorized HTTP responses and redirect to an
401    authentication URI.  This class also implements a redirect handler to
402    redirect back to the referrer if logout is invoked.
403    '''
404
405    def __init__(self, app, global_conf, prefix='', **app_conf):
406        '''
407        @type app: callable following WSGI interface
408        @param app: next middleware application in the chain     
409        @type global_conf: dict       
410        @param global_conf: PasteDeploy global configuration dictionary
411        @type prefix: basestring
412        @param prefix: prefix for configuration items
413        @type app_conf: dict       
414        @param app_conf: PasteDeploy application specific configuration
415        dictionary
416        '''
417       
418        # Set logout URI parameter from AuthKit settings if not otherwise set
419        sessionHandlerPrefix = prefix + SessionHandlerMiddleware.PARAM_PREFIX       
420        app = SessionHandlerMiddleware(app, 
421                                       global_conf, 
422                                       prefix=sessionHandlerPrefix,
423                                       **app_conf)
424       
425        # Remove session handler middleware specific parameters
426        for k in app_conf.keys():
427            if k.startswith(sessionHandlerPrefix):
428                del app_conf[k]
429       
430        app = authkit.authenticate.middleware(app, app_conf)       
431       
432        MultiHandler.__init__(self, app)
433
434        # Redirection middleware is invoked based on a check method which
435        # catches HTTP 401 responses.
436        self.add_method(AuthnRedirectInitiatorMiddleware.MIDDLEWARE_ID, 
437                        AuthnRedirectInitiatorMiddleware.filter_app_factory, 
438                        global_conf,
439                        prefix=prefix,
440                        **app_conf)
441       
442        self.add_checker(AuthnRedirectInitiatorMiddleware.MIDDLEWARE_ID, 
443                         AuthnRedirectInitiatorMiddleware.checker)
444
445
446# AuthKit based HTTP basic authentication not currently needed but may need
447# resurrecting
448
449#from authkit.permissions import UserIn
450#from ndg.security.server.wsgi.utils.sessionmanagerclient import \
451#    WSGISessionManagerClient
452#class HTTPBasicAuthNMiddleware(NDGSecurityMiddlewareBase):
453#    '''HTTP Basic Authentication Middleware
454#   
455#    TODO: implement authN interface and username/password retrieval from
456#    HTTP header.'''
457#   
458#    def __init__(self, app, app_conf, **local_conf):
459#
460#        super(HTTPBasicAuthentication).__init__(self,
461#                                                app,
462#                                                app_conf,
463#                                                **local_conf)
464#       
465#    def __call__(self, environ, start_response):
466#        """Authenticate based HTTP header elements as specified by the HTTP
467#        Basic Authentication spec."""
468#        log.debug("HTTPBasicAuthNMiddleware.__call__ ...")
469#       
470#        try:
471#            self.authNInterface.logon(username, password)
472#               
473#        except Exception, e:
474#            return self._errorResponse(code=401)
475#        else:
476#            return self._app(environ, start_response)
477#           
478#class HTTPBasicAuthentication(object):
479#    '''Authkit based HTTP Basic Authentication.   __call__ defines a
480#    validation function to fit with the pattern for the AuthKit interface
481#    '''
482#   
483#    def __init__(self):
484#        self._userIn = UserIn([])
485#       
486#    def __call__(self, environ, username, password):
487#        """validation function"""
488#        try:
489#            client = WSGISessionManagerClient(environ=environ,
490#                                environKeyName=self.sessionManagerFilterID)
491#            res = client.connect(username, passphrase=password)
492#
493#            if username not in self._userIn.users:
494#                self._userIn.users += [username]
495#           
496#            # TODO: set session
497#               
498#        except Exception, e:
499#            return False
500#        else:
501#            return True
Note: See TracBrowser for help on using the repository browser.