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

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

Fixes for testing OpenID Relying Party running in the application code stack instead of the separate 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   
271    propertyDefaults = {
272        'signoutPath': None,
273        'sessionKey': 'beaker.session.ndg.security'
274    }
275    propertyDefaults.update(NDGSecurityMiddlewareBase.propertyDefaults)
276
277
278    def __init__(self, app, global_conf, prefix='', **app_conf):
279        '''
280        @type app: callable following WSGI interface
281        @param app: next middleware application in the chain     
282        @type global_conf: dict       
283        @param global_conf: PasteDeploy global configuration dictionary
284        @type prefix: basestring
285        @param prefix: prefix for configuration items
286        @type app_conf: dict       
287        @param app_conf: PasteDeploy application specific configuration
288        dictionary
289        '''
290        super(SessionHandlerMiddleware, self).__init__(app,
291                                                       global_conf,
292                                                       prefix=prefix, 
293                                                       **app_conf)
294       
295    @NDGSecurityMiddlewareBase.initCall
296    def __call__(self, environ, start_response):
297        """Manage setting of session from AuthKit following OpenID Relying
298        Party sign in and manage logout
299       
300        @type environ: dict
301        @param environ: WSGI environment variables dictionary
302        @type start_response: function
303        @param start_response: standard WSGI start response function
304
305        """
306        log.debug("SessionHandlerMiddleware.__call__ ...")
307       
308        session = environ.get(self.sessionKey)
309        if session is None:
310            raise SessionHandlerMiddlewareConfigError('No beaker session key '
311                                                      '"%s" found in environ' %
312                                                      self.sessionKey)
313       
314        if self.signoutPath and self.pathInfo == self.signoutPath:
315            log.debug("SessionHandlerMiddleware: caught sign out path [%s]",
316                      self.signoutPath)
317           
318            referrer = environ.get('HTTP_REFERER')
319            if referrer is not None:
320                def _start_response(status, header, exc_info=None):
321                    """Alter the header to send a redirect to the logout
322                    referrer address"""
323                    header.extend([('Location', referrer)])
324                    return start_response(self.getStatusMessage(302), 
325                                          header,
326                                          exc_info)
327                   
328            else:
329                log.error('No referrer set for redirect following logout')
330                _start_response = start_response
331               
332            # Clear user details from beaker session
333            for keyName in self.__class__.sessionKeyNames:
334                session.pop(keyName, None)
335            session.save()
336        else:
337            log.debug("SessionHandlerMiddleware: checking for REMOTE_* "
338                      "environment variable settings set by OpenID Relying "
339                      "Party signin...")
340           
341            if 'username' not in session and 'REMOTE_USER' in environ:
342                log.debug("SessionHandlerMiddleware: updating session with "
343                          "username=%s", environ['REMOTE_USER'])
344               
345                session['username'] = environ['REMOTE_USER']
346                session.save()
347               
348            remoteUserData = environ.get('REMOTE_USER_DATA', '')   
349            if remoteUserData:
350                log.debug("SessionHandlerMiddleware: found REMOTE_USER_DATA="
351                          "%s, set from OpenID Relying Party signin",
352                          environ['REMOTE_USER_DATA'])
353               
354                # eval is safe here because AuthKit cookie is signed and
355                # AuthKit middleware checks for tampering
356                if 'sessionManagerURI' not in session or \
357                   'sessionId' not in session:
358                   
359                    axData = eval(remoteUserData)
360                    if isinstance(axData, dict) and 'ax' in axData:
361                        sessionManagerURI = \
362                                axData['ax'].get('value.sessionManagerURI.1')
363                        session['sessionManagerURI'] = sessionManagerURI
364   
365                        sessionId = axData['ax'].get('value.sessionId.1')
366                        session['sessionId'] = sessionId
367                        session.save()
368                       
369                        log.debug("SessionHandlerMiddleware: updated session "
370                                  "with sessionManagerURI=%s and "
371                                  "sessionId=%s", 
372                                  sessionManagerURI, 
373                                  sessionId)
374                   
375                # Reset cookie removing user data
376                environ['paste.auth_tkt.set_user'](session['username'])
377
378            _start_response = start_response
379           
380        return self._app(environ, _start_response)
381
382
383from authkit.authenticate.multi import MultiHandler
384
385class AuthenticationMiddlewareConfigError(NDGSecurityMiddlewareConfigError):
386    '''Authentication Middleware Configuration error'''
387
388class AuthenticationMiddleware(MultiHandler, NDGSecurityMiddlewareBase):
389    '''Handler to intercept 401 Unauthorized HTTP responses and redirect to an
390    authentication URI.  This class also implements a redirect handler to
391    redirect back to the referrer if logout is invoked.
392    '''
393
394    def __init__(self, app, global_conf, prefix='', **app_conf):
395        '''
396        @type app: callable following WSGI interface
397        @param app: next middleware application in the chain     
398        @type global_conf: dict       
399        @param global_conf: PasteDeploy global configuration dictionary
400        @type prefix: basestring
401        @param prefix: prefix for configuration items
402        @type app_conf: dict       
403        @param app_conf: PasteDeploy application specific configuration
404        dictionary
405        '''
406       
407        # Set logout URI parameter from AuthKit settings if not otherwise set
408        sessionHandlerPrefix = prefix + SessionHandlerMiddleware.prefix
409        signoutPathParamName = sessionHandlerPrefix + 'signoutPath'
410        if signoutPathParamName not in app_conf:
411            try:                   
412                app_conf[signoutPathParamName] = app_conf[
413                                                'authkit.cookie.signoutpath']
414            except KeyError, e:
415                raise AuthenticationMiddlewareConfigError("No %s or %s keys "
416                                                    "set" % 
417                                                    (signoutPathParamName, e))
418       
419        app = SessionHandlerMiddleware(app, 
420                                       global_conf, 
421                                       prefix=sessionHandlerPrefix,
422                                       **app_conf)
423       
424        # Remove session handler middleware specific parameters
425        for k in app_conf.keys():
426            if k.startswith(sessionHandlerPrefix):
427                del app_conf[k]
428       
429        app = authkit.authenticate.middleware(app, app_conf)       
430        app = SessionMiddleware(app, 
431                                environ_key='beaker.session.ndg.security',
432                                **app_conf)
433       
434        MultiHandler.__init__(self, app)
435
436        self.add_method(AuthNRedirectInitiatorMiddleware.id, 
437                        AuthNRedirectInitiatorMiddleware.filter_app_factory, 
438                        global_conf,
439                        prefix=prefix,
440                        **app_conf)
441       
442        self.add_checker(AuthNRedirectInitiatorMiddleware.id, 
443                         AuthNRedirectInitiatorMiddleware.checker)
444
Note: See TracBrowser for help on using the repository browser.