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

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

Adding SSL Client authentication step into authz_lite integration test. Broken redirecting back from authn step to requested resource.

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