source: TI12-security/trunk/NDGSecurity/python/ndg_security_server/ndg/security/server/wsgi/session.py @ 7077

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/TI12-security/trunk/NDGSecurity/python/ndg_security_server/ndg/security/server/wsgi/session.py@7077
Revision 7077, 15.6 KB checked in by pjkersha, 9 years ago (diff)
  • Property svn:keywords set to Id
Line 
1"""Session handling middleware module
2
3Refactored authn module moving session specific code to here
4 
5NERC DataGrid Project
6"""
7__author__ = "P J Kershaw"
8__date__ = "05/01/10"
9__copyright__ = "(C) 2010 Science and Technology Facilities Council"
10__license__ = "BSD - see LICENSE file in top-level directory"
11__contact__ = "Philip.Kershaw@stfc.ac.uk"
12__revision__ = "$Id$"
13import logging
14log = logging.getLogger(__name__)
15
16import urllib
17from paste.request import parse_querystring
18
19from ndg.security.server.wsgi import (NDGSecurityMiddlewareBase,
20                                      NDGSecurityMiddlewareError)
21
22
23class SessionMiddlewareBase(NDGSecurityMiddlewareBase):
24    """Base class for Authentication redirect middleware and Session Handler
25    middleware
26   
27    @type propertyDefaults: dict
28    @cvar propertyDefaults: valid configuration property keywords
29    """   
30    propertyDefaults = {
31        'sessionKey': 'beaker.session.ndg.security'
32    }
33
34    # Key names for PEP context information
35    PEPCTX_SESSION_KEYNAME = 'pepCtx'
36    PEPCTX_REQUEST_SESSION_KEYNAME = 'request'
37    PEPCTX_RESPONSE_SESSION_KEYNAME = 'response'
38    PEPCTX_TIMESTAMP_SESSION_KEYNAME = 'timestamp'
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 SessionHandlerMiddlewareError(NDGSecurityMiddlewareError):
49    """Base exception for SessionHandlerMiddleware"""
50           
51           
52class SessionHandlerMiddlewareConfigError(SessionHandlerMiddlewareError):
53    """Configuration errors from SessionHandlerMiddleware"""
54   
55   
56class OpenIdAXConfigError(SessionHandlerMiddlewareError):
57    """Error parsing OpenID Ax (Attribute Exchange) parameters"""
58   
59   
60class SessionHandlerMiddleware(SessionMiddlewareBase):
61    '''Middleware to:
62    - establish user session details following redirect from OpenID Relying
63    Party sign-in or SSL Client authentication
64    - end session redirecting back to referrer URI following call to a logout
65    URI as implemented in AuthKit
66    '''
67    AX_SESSION_KEYNAME = 'openid.ax'
68    SM_URI_SESSION_KEYNAME = 'sessionManagerURI'
69    ID_SESSION_KEYNAME = 'sessionId'
70    PEP_CTX_SESSION_KEYNAME = 'pepCtx'
71    CREDENTIAL_WALLET_SESSION_KEYNAME = 'credentialWallet'
72   
73    SESSION_KEYNAMES = (
74        SessionMiddlewareBase.USERNAME_SESSION_KEYNAME, 
75        SM_URI_SESSION_KEYNAME, 
76        ID_SESSION_KEYNAME, 
77        PEP_CTX_SESSION_KEYNAME, 
78        CREDENTIAL_WALLET_SESSION_KEYNAME
79    )
80   
81    AX_KEYNAME = 'ax'
82    SM_URI_AX_KEYNAME = 'value.sessionManagerURI.1'
83    SESSION_ID_AX_KEYNAME = 'value.sessionId.1'
84   
85    AUTHKIT_COOKIE_SIGNOUT_PARAMNAME = 'authkit.cookie.signoutpath'
86    SIGNOUT_PATH_PARAMNAME = 'signoutPath'
87    SESSION_KEY_PARAMNAME = 'sessionKey'
88    DEFAULT_LOGOUT_RETURN2URI_PARAMNAME = 'defaultLogoutReturnToURI'
89   
90    propertyDefaults = {
91        SIGNOUT_PATH_PARAMNAME: None,
92        SESSION_KEY_PARAMNAME: 'beaker.session.ndg.security',
93        DEFAULT_LOGOUT_RETURN2URI_PARAMNAME: '/'
94    }
95   
96    AUTH_TKT_SET_USER_ENVIRON_KEYNAME = 'paste.auth_tkt.set_user'
97   
98    LOGOUT_RETURN2URI_ARGNAME = 'ndg.security.logout.r'
99    LOGOUT_REDIRECT_STATUS_CODE = 302
100   
101    PARAM_PREFIX = 'sessionHandler.'
102   
103    def __init__(self, app, global_conf, prefix=PARAM_PREFIX, **app_conf):
104        '''
105        @type app: callable following WSGI interface
106        @param app: next middleware application in the chain     
107        @type global_conf: dict       
108        @param global_conf: PasteDeploy global configuration dictionary
109        @type prefix: basestring
110        @param prefix: prefix for configuration items
111        @type app_conf: dict       
112        @param app_conf: PasteDeploy application specific configuration
113        dictionary
114        '''
115        cls = SessionHandlerMiddleware
116        signoutPathParamName = prefix + cls.SIGNOUT_PATH_PARAMNAME
117       
118        if signoutPathParamName not in app_conf:
119            authKitSignOutPath = app_conf.get(
120                                        cls.AUTHKIT_COOKIE_SIGNOUT_PARAMNAME)
121           
122            if authKitSignOutPath:
123                app_conf[signoutPathParamName] = authKitSignOutPath
124               
125                log.info('Set signoutPath=%s from "%s" setting', 
126                         authKitSignOutPath,
127                         cls.AUTHKIT_COOKIE_SIGNOUT_PARAMNAME)
128            else:
129                raise SessionHandlerMiddlewareConfigError(
130                                        '"signoutPath" parameter is not set')
131               
132        defaultLogoutReturnToURIParamName = prefix + \
133                                        cls.DEFAULT_LOGOUT_RETURN2URI_PARAMNAME
134       
135        self.__defaultLogoutReturnToURI = app_conf.get(
136                defaultLogoutReturnToURIParamName,
137                cls.propertyDefaults[cls.DEFAULT_LOGOUT_RETURN2URI_PARAMNAME])
138       
139        super(SessionHandlerMiddleware, self).__init__(app,
140                                                       global_conf,
141                                                       prefix=prefix, 
142                                                       **app_conf)
143       
144    @NDGSecurityMiddlewareBase.initCall
145    def __call__(self, environ, start_response):
146        """Manage setting of session from AuthKit following OpenID Relying
147        Party sign in and manage logout
148       
149        @type environ: dict
150        @param environ: WSGI environment variables dictionary
151        @type start_response: function
152        @param start_response: standard WSGI start response function
153        """
154        log.debug("SessionHandlerMiddleware.__call__ ...")
155       
156        session = environ.get(self.sessionKey)
157        if session is None:
158            raise SessionHandlerMiddlewareConfigError(
159                   'SessionHandlerMiddleware.__call__: No beaker session key '
160                   '"%s" found in environ' % self.sessionKey)
161       
162        if self.signoutPath and self.pathInfo == self.signoutPath:
163            log.debug("SessionHandlerMiddleware.__call__: caught sign out "
164                      "path [%s]", self.signoutPath)
165           
166            _start_response = self._doLogout(environ, start_response, session)
167        else:
168            log.debug("SessionHandlerMiddleware.__call__: checking for "
169                      "REMOTE_* environment variable settings set by OpenID "
170                      "Relying Party signin...")
171            self._setSession(environ, session)
172
173            _start_response = start_response
174           
175        return self._app(environ, _start_response)
176   
177    def _doLogout(self, environ, start_response, session):
178        """Execute logout action,
179         - clear the beaker session
180         - set the referrer URI to redirect back to by setting a custom
181        start_response function which modifies the HTTP header setting the
182        location field for a redirect
183       
184        @param environ: environment dictionary
185        @type environ: dict like object
186        @type start_response: function
187        @param start_response: standard WSGI start response function
188        @param session: beaker session
189        @type session: beaker.session.SessionObject
190        """
191           
192        # Clear user details from beaker session
193        for keyName in self.__class__.SESSION_KEYNAMES:
194            session.pop(keyName, None)
195        session.save()
196       
197       
198        if self.__class__.LOGOUT_RETURN2URI_ARGNAME in environ['QUERY_STRING']:
199            params = dict(parse_querystring(environ))
200       
201            # Store the return URI query argument in a beaker session
202            quotedReferrer = params.get(
203                                self.__class__.LOGOUT_RETURN2URI_ARGNAME, '')
204            referrer = urllib.unquote(quotedReferrer)
205           
206            log.debug('Set redirect URI following logout based on %r URI query '
207                      'string = %r', 
208                      self.__class__.LOGOUT_RETURN2URI_ARGNAME,
209                      referrer)
210        else:
211            referrer = environ.get('HTTP_REFERER')
212            if referrer is None:
213                log.warning('No HTTP return to URI set for redirect following '
214                            'logout, either via the return to query string %r '
215                            'or the "HTTP_REFERER" environment variable: '
216                            'redirecting based on the %r config file option = '
217                            '%r', 
218                            self.__class__.LOGOUT_RETURN2URI_ARGNAME,
219                            self.__class__.DEFAULT_LOGOUT_RETURN2URI_PARAMNAME,
220                            self.__defaultLogoutReturnToURI)
221               
222                referrer = self.__defaultLogoutReturnToURI
223            else:
224                log.debug('Set redirect URI following logout based on '
225                          '"HTTP_REFERER" environment variable = %r',
226                          referrer)
227               
228        def _start_response(status, header, exc_info=None):
229            """Alter the header to send a redirect to the logout referrer
230            address"""
231           
232            # Filter out any existing location field setting
233            filteredHeader = [(field, val) for field, val in header
234                              if field.lower() != 'location'] 
235           
236            # Add redirect destination to new location field setting     
237            filteredHeader.extend([('Location', referrer)])
238           
239            statusMsg = self.getStatusMessage(
240                                    self.__class__.LOGOUT_REDIRECT_STATUS_CODE)
241           
242            return start_response(statusMsg, filteredHeader, exc_info)
243               
244        return _start_response
245       
246    def _setSession(self, environ, session):
247        """Check for REMOTE_USER and REMOTE_USER_DATA set by authentication
248        handlers and set a new session from them if present
249       
250        @type environ: dict like object
251        @param environ: WSGI environment variables dictionary
252        @param session: beaker session
253        @type session: beaker.session.SessionObject
254        """
255       
256        # Set user id
257        if (SessionHandlerMiddleware.USERNAME_SESSION_KEYNAME not in session
258            and SessionHandlerMiddleware.USERNAME_ENVIRON_KEYNAME in environ):
259           
260            log.debug("SessionHandlerMiddleware.__call__: updating session "
261                      "username=%s", environ[
262                        SessionHandlerMiddleware.USERNAME_ENVIRON_KEYNAME])
263           
264            session[SessionHandlerMiddleware.USERNAME_SESSION_KEYNAME
265                    ] = environ[
266                        SessionHandlerMiddleware.USERNAME_ENVIRON_KEYNAME]
267            session.save()
268           
269        # Check for auxiliary user data
270        remoteUserData = environ.get(
271                        SessionHandlerMiddleware.USERDATA_ENVIRON_KEYNAME, '')   
272        if remoteUserData:
273            log.debug("SessionHandlerMiddleware.__call__: found "
274                      "REMOTE_USER_DATA=%s, set from OpenID Relying Party "
275                      "signin", 
276                      environ[
277                          SessionHandlerMiddleware.USERDATA_ENVIRON_KEYNAME
278                      ])
279           
280            if (SessionHandlerMiddleware.SM_URI_SESSION_KEYNAME not in 
281                session or 
282                SessionHandlerMiddleware.ID_SESSION_KEYNAME not in session):
283               
284                # eval is safe here because AuthKit cookie is signed and
285                # AuthKit middleware checks for tampering           
286                axData = eval(remoteUserData)
287                if (isinstance(axData, dict) and 
288                    SessionHandlerMiddleware.AX_KEYNAME in axData):
289                   
290                    ax = axData[SessionHandlerMiddleware.AX_KEYNAME]
291                   
292                    # Save attributes keyed by attribute name
293                    session[SessionHandlerMiddleware.AX_SESSION_KEYNAME
294                            ] = SessionHandlerMiddleware._parseOpenIdAX(ax)
295                   
296                    log.debug("SessionHandlerMiddleware.__call__: updated "
297                              "session with OpenID AX values: %r",
298                              session[
299                                SessionHandlerMiddleware.AX_SESSION_KEYNAME
300                              ])
301                       
302                    # Save Session Manager specific attributes
303                    sessionManagerURI = ax.get(
304                            SessionHandlerMiddleware.SM_URI_AX_KEYNAME)
305                       
306                    session[SessionHandlerMiddleware.SM_URI_SESSION_KEYNAME
307                            ] = sessionManagerURI
308
309                    sessionId = ax.get(
310                            SessionHandlerMiddleware.SESSION_ID_AX_KEYNAME)
311                    session[SessionHandlerMiddleware.ID_SESSION_KEYNAME
312                            ] = sessionId
313                           
314                    session.save()
315                   
316                    log.debug("SessionHandlerMiddleware.__call__: updated "
317                              "session "
318                              "with sessionManagerURI=%s and "
319                              "sessionId=%s", 
320                              sessionManagerURI, 
321                              sessionId)
322               
323            # Reset cookie removing user data by accessing the Auth ticket
324            # function available from environ
325            setUser = environ[
326                    SessionHandlerMiddleware.AUTH_TKT_SET_USER_ENVIRON_KEYNAME]
327            setUser(session[SessionHandlerMiddleware.USERNAME_SESSION_KEYNAME])
328           
329            # Also reset the environment variable to prevent AuthKit from
330            # restoring the AX values in the cookie.
331            environ[SessionHandlerMiddleware.USERDATA_ENVIRON_KEYNAME] = ''     
332        else:
333            log.debug("SessionHandlerMiddleware.__call__: REMOTE_USER_DATA "
334                      "is not set")
335                   
336    @staticmethod                   
337    def _parseOpenIdAX(ax):
338        """Return a dictionary of attribute exchange attributes parsed from the
339        OpenID Provider response set in the REMOTE_USER_DATA AuthKit environ
340        key
341       
342        @param ax: dictionary of AX parameters - format of keys is e.g.
343        count.paramName, value.paramName.<n>, type.paramName
344        @type ax: dict
345        @return: dictionary of parameters keyed by parameter with values for
346        each parameter a tuple of count.paramName values
347        @rtype: dict
348        """
349       
350        # Copy Attributes into session
351        outputKeys = [k.replace('type.', '') for k in ax.keys()
352                      if k.startswith('type.')]
353       
354        output = {}
355        for outputKey in outputKeys:
356            axCountKeyName = 'count.' + outputKey
357            axCount = int(ax[axCountKeyName])
358           
359            axValueKeyPrefix = 'value.%s.' % outputKey
360            output[outputKey] = tuple([v for k, v in ax.items() 
361                                       if k.startswith(axValueKeyPrefix)])
362           
363            nVals = len(output[outputKey])
364            if nVals != axCount:
365                raise OpenIdAXConfigError('Got %d parameters for AX attribute '
366                                          '"%s"; but "%s" AX key is set to %d'
367                                          % (nVals,
368                                             axCountKeyName,
369                                             axCountKeyName,
370                                             axCount))
371                                             
372        return output
Note: See TracBrowser for help on using the repository browser.