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

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

Re-testing OpenID Attribute Exchange interface - added CSV file based test AX Response class.

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 SessionMiddlewareBase(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   
40    _isAuthenticated = lambda self: \
41        SessionMiddlewareBase.USERNAME_SESSION_KEYNAME in \
42        self.environ.get(self.sessionKey, ())
43       
44    isAuthenticated = property(fget=_isAuthenticated,
45                               doc='boolean to indicate is user logged in')
46
47       
48class AuthnRedirectMiddleware(SessionMiddlewareBase):
49    """Base class for Authentication HTTP redirect initiator and redirect
50    response WSGI middleware
51
52    @type RETURN2URI_ARGNAME: basestring
53    @cvar RETURN2URI_ARGNAME: name of URI query argument used to pass the
54    return to URI between initiator and consumer classes"""
55    RETURN2URI_ARGNAME = 'ndg.security.r'
56
57
58class AuthnRedirectInitiatorMiddleware(AuthnRedirectMiddleware):
59    '''Middleware to initiate a redirect to another URI if a user is not
60    authenticated i.e. security cookie is not set
61   
62    AuthKit.authenticate.middleware must be in place upstream of this
63    middleware.  AuthenticationMiddleware wrapper handles this.
64   
65    @type propertyDefaults: dict
66    @cvar propertyDefaults: valid configuration property keywords   
67    '''
68    propertyDefaults = {
69        'redirectURI': None,
70    }
71    propertyDefaults.update(AuthnRedirectMiddleware.propertyDefaults)
72   
73
74    TRIGGER_HTTP_STATUS_CODE = '401'
75    MIDDLEWARE_ID = 'AuthnRedirectInitiatorMiddleware'
76
77    def __init__(self, app, global_conf, **app_conf):
78        '''
79        @type app: callable following WSGI interface
80        @param app: next middleware application in the chain     
81        @type global_conf: dict       
82        @param global_conf: PasteDeploy global configuration dictionary
83        @type prefix: basestring
84        @param prefix: prefix for configuration items
85        @type app_conf: dict       
86        @param app_conf: PasteDeploy application specific configuration
87        dictionary
88        '''
89        self._redirectURI = None
90        super(AuthnRedirectInitiatorMiddleware, self).__init__(app, 
91                                                               global_conf, 
92                                                               **app_conf)
93       
94    @NDGSecurityMiddlewareBase.initCall
95    def __call__(self, environ, start_response):
96        '''Invoke redirect if user is not authenticated'''
97       
98        log.debug("AuthnRedirectInitiatorMiddleware.__call__ ...")
99       
100        if self.isAuthenticated:
101            # Call next app in stack
102            return self._app(environ, start_response)       
103        else:
104            # User is not authenticated - Redirect to OpenID Relying Party URI
105            # for user OpenID entry
106            return self._setRedirectResponse()
107   
108    def _setRedirectURI(self, uri):
109        if not isinstance(uri, basestring):
110            raise TypeError("Redirect URI must be set to string type")   
111         
112        self._redirectURI = uri
113       
114    def _getRedirectURI(self):
115        return self._redirectURI
116   
117    redirectURI = property(fget=_getRedirectURI,
118                       fset=_setRedirectURI,
119                       doc="URI to redirect to if user is not authenticated")
120
121    def _setRedirectResponse(self):
122        """Construct a redirect response adding in a return to address in a
123        URI query argument
124       
125        @rtype: basestring
126        @return: redirect response
127        """       
128        return2URI = construct_url(self.environ)
129        quotedReturn2URI = urllib.quote(return2URI, safe='')
130        return2URIQueryArg = urllib.urlencode(
131                    {AuthnRedirectInitiatorMiddleware.RETURN2URI_ARGNAME: 
132                     quotedReturn2URI})
133
134        redirectURI = self.redirectURI
135       
136        if '?' in redirectURI:
137            if redirectURI.endswith('&'):
138                redirectURI += return2URIQueryArg
139            else:
140                redirectURI += '&' + return2URIQueryArg
141        else:
142            if redirectURI.endswith('?'):
143                redirectURI += return2URIQueryArg
144            else:
145                redirectURI += '?' + return2URIQueryArg
146         
147        # Call NDGSecurityMiddlewareBase.redirect utility method     
148        return self.redirect(redirectURI)
149       
150    @classmethod
151    def checker(cls, environ, status, headers):
152        """Set the MultiHandler checker function for triggering this
153        middleware.  In this case, it's a HTTP 401 Unauthorized response
154        detected in the middleware chain
155        """
156        if status.startswith(cls.TRIGGER_HTTP_STATUS_CODE):
157            log.debug("%s.checker caught status [%s]: invoking authentication"
158                      " handler", cls.__name__, cls.TRIGGER_HTTP_STATUS_CODE)
159            return True
160        else:
161            log.debug("%s.checker skipping status [%s]", cls.__name__, status)
162            return False
163
164
165class AuthnRedirectResponseMiddleware(AuthnRedirectMiddleware):
166    """Compliment to AuthnRedirectInitiatorMiddleware
167    functioning as the opposite end of the HTTP redirect interface.  It
168    performs the following tasks:
169    - Detect a redirect URI set in a URI query argument and copy it into
170    a user session object.
171    - Redirect back to the redirect URI once a user is authenticated
172   
173    Also see,
174    ndg.security.server.wsgi.openid.relyingparty.OpenIDRelyingPartyMiddleware
175    which performs a similar function.
176    """
177    @NDGSecurityMiddlewareBase.initCall
178    def __call__(self, environ, start_response):
179        session = environ[self.sessionKey]
180       
181        # Check for return to address in URI query args set by
182        # AuthnRedirectInitiatorMiddleware in application code stack
183        if environ['REQUEST_METHOD'] == "GET":
184            params = dict(parse_querystring(environ))
185        else:
186            params = {}
187       
188        # Store the return URI query argument in a beaker session
189        quotedReferrer = params.get(self.__class__.RETURN2URI_ARGNAME, '')
190        referrerURI = urllib.unquote(quotedReferrer)
191        if referrerURI:
192            session[self.__class__.RETURN2URI_ARGNAME] = referrerURI
193            session.save()
194           
195        # Check for a return URI setting in the beaker session and if the user
196        # is authenticated, redirect to this URL deleting the beaker session
197        # URL setting
198        return2URI = session.get(self.__class__.RETURN2URI_ARGNAME)   
199        if self.isAuthenticated and return2URI:
200            del session[self.__class__.RETURN2URI_ARGNAME]
201            session.save()
202            return self.redirect(return2URI)
203
204        return self._app(environ, start_response)
205
206
207class AuthKitRedirectResponseMiddleware(AuthnRedirectResponseMiddleware):
208    """Overload isAuthenticated method in parent class to set Authenticated
209    state based on presence of AuthKit 'REMOTE_USER' environ variable
210    """
211    _isAuthenticated = lambda self: \
212        AuthnRedirectResponseMiddleware.USERNAME_ENVIRON_KEYNAME in self.environ
213       
214    isAuthenticated = property(fget=_isAuthenticated,
215                               doc="Boolean indicating if AuthKit "
216                                   "'REMOTE_USER' environment variable is set")
217    def __init__(self, app, app_conf, **local_conf):
218        super(AuthKitRedirectResponseMiddleware, self).__init__(app, app_conf,
219                                                                **local_conf)
220    @NDGSecurityMiddlewareBase.initCall
221    def __call__(self, environ, start_response):
222        return super(AuthKitRedirectResponseMiddleware, self).__call__(environ,
223                                                                start_response)
224       
225       
226class SessionHandlerMiddlewareConfigError(Exception):
227    """Configuration errors from SessionHandlerMiddleware"""
228   
229   
230class SessionHandlerMiddleware(SessionMiddlewareBase):
231    '''Middleware to:
232    - establish user session details following redirect from OpenID Relying
233    Party sign-in or SSL Client authentication
234    - end session redirecting back to referrer URI following call to a logout
235    URI as implemented in AuthKit
236    '''
237   
238    SM_URI_SESSION_KEYNAME = 'sessionManagerURI'
239    ID_SESSION_KEYNAME = 'sessionId'
240    PEP_CTX_SESSION_KEYNAME = 'pepCtx'
241    CREDENTIAL_WALLET_SESSION_KEYNAME = 'credentialWallet'
242   
243    SESSION_KEYNAMES = (
244        SessionMiddlewareBase.USERNAME_SESSION_KEYNAME, 
245        SM_URI_SESSION_KEYNAME, 
246        ID_SESSION_KEYNAME, 
247        PEP_CTX_SESSION_KEYNAME, 
248        CREDENTIAL_WALLET_SESSION_KEYNAME
249    )
250   
251    AX_KEYNAME = 'ax'
252    SM_URI_AX_KEYNAME = 'value.sessionManagerURI.1'
253    SESSION_ID_AX_KEYNAME = 'value.sessionId.1'
254   
255    AUTHKIT_COOKIE_SIGNOUT_PARAMNAME = 'authkit.cookie.signoutpath'
256    SIGNOUT_PATH_PARAMNAME = 'signoutPath'
257    SESSION_KEY_PARAMNAME = 'sessionKey'
258    propertyDefaults = {
259        SIGNOUT_PATH_PARAMNAME: None,
260        SESSION_KEY_PARAMNAME: 'beaker.session.ndg.security'
261    }
262   
263    AUTH_TKT_SET_USER_ENVIRON_KEYNAME = 'paste.auth_tkt.set_user'
264   
265    PARAM_PREFIX = 'sessionHandler.'
266   
267    def __init__(self, app, global_conf, prefix=PARAM_PREFIX, **app_conf):
268        '''
269        @type app: callable following WSGI interface
270        @param app: next middleware application in the chain     
271        @type global_conf: dict       
272        @param global_conf: PasteDeploy global configuration dictionary
273        @type prefix: basestring
274        @param prefix: prefix for configuration items
275        @type app_conf: dict       
276        @param app_conf: PasteDeploy application specific configuration
277        dictionary
278        '''
279        signoutPathParamName = prefix + \
280                                SessionHandlerMiddleware.SIGNOUT_PATH_PARAMNAME
281       
282        if signoutPathParamName not in app_conf:
283            authKitSignOutPath = app_conf.get(
284                    SessionHandlerMiddleware.AUTHKIT_COOKIE_SIGNOUT_PARAMNAME)
285           
286            if authKitSignOutPath:
287                app_conf[signoutPathParamName] = authKitSignOutPath
288               
289                log.info('Set signoutPath=%s from "%s" setting', 
290                     authKitSignOutPath,
291                     SessionHandlerMiddleware.AUTHKIT_COOKIE_SIGNOUT_PARAMNAME)
292            else:
293                raise SessionHandlerMiddlewareConfigError(
294                                        '"signoutPath" parameter is not set')
295           
296        super(SessionHandlerMiddleware, self).__init__(app,
297                                                       global_conf,
298                                                       prefix=prefix, 
299                                                       **app_conf)
300       
301    @NDGSecurityMiddlewareBase.initCall
302    def __call__(self, environ, start_response):
303        """Manage setting of session from AuthKit following OpenID Relying
304        Party sign in and manage logout
305       
306        @type environ: dict
307        @param environ: WSGI environment variables dictionary
308        @type start_response: function
309        @param start_response: standard WSGI start response function
310        """
311        log.debug("SessionHandlerMiddleware.__call__ ...")
312       
313        session = environ.get(self.sessionKey)
314        if session is None:
315            raise SessionHandlerMiddlewareConfigError(
316                   'SessionHandlerMiddleware.__call__: No beaker session key '
317                   '"%s" found in environ' % self.sessionKey)
318       
319        if self.signoutPath and self.pathInfo == self.signoutPath:
320            log.debug("SessionHandlerMiddleware.__call__: caught sign out "
321                      "path [%s]", self.signoutPath)
322           
323            referrer = environ.get('HTTP_REFERER')
324            if referrer is not None:
325                def _start_response(status, header, exc_info=None):
326                    """Alter the header to send a redirect to the logout
327                    referrer address"""
328                    filteredHeader = [(field, val) for field, val in header
329                                      if field.lower() != 'location']       
330                    filteredHeader.extend([('Location', referrer)])
331                    return start_response(self.getStatusMessage(302), 
332                                          filteredHeader,
333                                          exc_info)
334                   
335            else:
336                log.error('No referrer set for redirect following logout')
337                _start_response = start_response
338               
339            # Clear user details from beaker session
340            for keyName in self.__class__.SESSION_KEYNAMES:
341                session.pop(keyName, None)
342            session.save()
343        else:
344            log.debug("SessionHandlerMiddleware.__call__: checking for "
345                      "REMOTE_* environment variable settings set by OpenID "
346                      "Relying Party signin...")
347           
348            if SessionHandlerMiddleware.USERNAME_SESSION_KEYNAME not in session\
349               and SessionHandlerMiddleware.USERNAME_ENVIRON_KEYNAME in environ:
350                log.debug("SessionHandlerMiddleware: updating session "
351                          "username=%s", environ[
352                            SessionHandlerMiddleware.USERNAME_ENVIRON_KEYNAME])
353               
354                session[SessionHandlerMiddleware.USERNAME_SESSION_KEYNAME
355                        ] = environ[
356                            SessionHandlerMiddleware.USERNAME_ENVIRON_KEYNAME]
357                session.save()
358               
359            remoteUserData = environ.get(
360                        SessionHandlerMiddleware.USERDATA_ENVIRON_KEYNAME, '')   
361            if remoteUserData:
362                log.debug("SessionHandlerMiddleware: found REMOTE_USER_DATA="
363                          "%s, set from OpenID Relying Party signin",
364                          environ[
365                          SessionHandlerMiddleware.USERDATA_ENVIRON_KEYNAME])
366               
367                # eval is safe here because AuthKit cookie is signed and
368                # AuthKit middleware checks for tampering
369                if SessionHandlerMiddleware.SM_URI_SESSION_KEYNAME not in \
370                   session or \
371                   SessionHandlerMiddleware.ID_SESSION_KEYNAME not in session:
372                   
373                    axData = eval(remoteUserData)
374                    if isinstance(axData, dict) and \
375                       SessionHandlerMiddleware.AX_KEYNAME in axData:
376                       
377                        sessionManagerURI = axData[
378                            SessionHandlerMiddleware.AX_KEYNAME].get(
379                                SessionHandlerMiddleware.SM_URI_AX_KEYNAME)
380                           
381                        session[SessionHandlerMiddleware.SM_URI_SESSION_KEYNAME
382                                ] = sessionManagerURI
383   
384                        sessionId = axData[
385                            SessionHandlerMiddleware.AX_KEYNAME].get(
386                                SessionHandlerMiddleware.SESSION_ID_AX_KEYNAME)
387                        session[SessionHandlerMiddleware.ID_SESSION_KEYNAME
388                                ] = sessionId
389                        session.save()
390                       
391                        log.debug("SessionHandlerMiddleware: updated session "
392                                  "with sessionManagerURI=%s and "
393                                  "sessionId=%s", 
394                                  sessionManagerURI, 
395                                  sessionId)
396                   
397                # Reset cookie removing user data
398                setUser = environ[
399                    SessionHandlerMiddleware.AUTH_TKT_SET_USER_ENVIRON_KEYNAME]
400                setUser(
401                    session[SessionHandlerMiddleware.USERNAME_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.