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

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

Testing SSL Client Authentication middleware with session and redirect middleware to enable wget support for NDG Security.

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