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

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

Updated integration tests. These test various configurations of the security middleware:

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