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

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

OpenID Relying Party flexible configuration

Fixed security WSGI configuration so that the OpenID Relying Party can run in the same middleware as the application it protects or independently in the security services middleware stack. There are two applications involved in applying security:

  1. the app to be secured
  2. app running security services


  1. is configured with middleware to intercept requests and apply the security policy. 2. runs services such as the Attribute Authority and OpenID Provider used by 1. The OpenID Relying Party can now be incorporated in either. For cases where an application runs in a different domain to the security services stack it's easier to deploy a Relying Party with the app in 1. as otherwise cookies set by the RP won't be in the scope of the secured app. 2. is useful for where the app is in the same domain as 2. and there's a need to run the RP over SSL.

Configurations can be set at deployment from Paste ini file pipeline settings.

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.