source: TI12-security/branches/ndg-security-1.5.x/ndg_security_server/ndg/security/server/wsgi/session.py @ 7474

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/TI12-security/branches/ndg-security-1.5.x/ndg_security_server/ndg/security/server/wsgi/session.py@7474
Revision 7474, 15.4 KB checked in by pjkersha, 11 years ago (diff)

Incomplete - task 13: OpenID Provider doesn't validate OpenID against username

  • Include fix from trunk enabling a return to URL to be specified as a query argument to the logout action. This is useful in cases where HTTP_REFERER is not set in environ.
  • 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    propertyDefaults = {
90        SIGNOUT_PATH_PARAMNAME: None,
91        SESSION_KEY_PARAMNAME: 'beaker.session.ndg.security',
92        DEFAULT_LOGOUT_RETURN2URI_PARAMNAME: '/'
93    }
94   
95    AUTH_TKT_SET_USER_ENVIRON_KEYNAME = 'paste.auth_tkt.set_user'
96   
97    LOGOUT_RETURN2URI_ARGNAME = 'ndg.security.logout.r'
98    LOGOUT_REDIRECT_STATUS_CODE = 302
99   
100    PARAM_PREFIX = 'sessionHandler.'
101   
102    def __init__(self, app, global_conf, prefix=PARAM_PREFIX, **app_conf):
103        '''
104        @type app: callable following WSGI interface
105        @param app: next middleware application in the chain     
106        @type global_conf: dict       
107        @param global_conf: PasteDeploy global configuration dictionary
108        @type prefix: basestring
109        @param prefix: prefix for configuration items
110        @type app_conf: dict       
111        @param app_conf: PasteDeploy application specific configuration
112        dictionary
113        '''
114        signoutPathParamName = prefix + \
115                                SessionHandlerMiddleware.SIGNOUT_PATH_PARAMNAME
116       
117        if signoutPathParamName not in app_conf:
118            authKitSignOutPath = app_conf.get(
119                    SessionHandlerMiddleware.AUTHKIT_COOKIE_SIGNOUT_PARAMNAME)
120           
121            if authKitSignOutPath:
122                app_conf[signoutPathParamName] = authKitSignOutPath
123               
124                log.info('Set signoutPath=%s from "%s" setting', 
125                     authKitSignOutPath,
126                     SessionHandlerMiddleware.AUTHKIT_COOKIE_SIGNOUT_PARAMNAME)
127            else:
128                raise SessionHandlerMiddlewareConfigError(
129                                        '"signoutPath" parameter is not set')
130               
131        defaultLogoutReturnToURIParamName = prefix + \
132                                        cls.DEFAULT_LOGOUT_RETURN2URI_PARAMNAME
133       
134        self.__defaultLogoutReturnToURI = app_conf.get(
135                defaultLogoutReturnToURIParamName,
136                cls.propertyDefaults[cls.DEFAULT_LOGOUT_RETURN2URI_PARAMNAME])
137                   
138        super(SessionHandlerMiddleware, self).__init__(app,
139                                                       global_conf,
140                                                       prefix=prefix, 
141                                                       **app_conf)
142       
143    @NDGSecurityMiddlewareBase.initCall
144    def __call__(self, environ, start_response):
145        """Manage setting of session from AuthKit following OpenID Relying
146        Party sign in and manage logout
147       
148        @type environ: dict
149        @param environ: WSGI environment variables dictionary
150        @type start_response: function
151        @param start_response: standard WSGI start response function
152        """
153        log.debug("SessionHandlerMiddleware.__call__ ...")
154       
155        session = environ.get(self.sessionKey)
156        if session is None:
157            raise SessionHandlerMiddlewareConfigError(
158                   'SessionHandlerMiddleware.__call__: No beaker session key '
159                   '"%s" found in environ' % self.sessionKey)
160       
161        if self.signoutPath and self.pathInfo == self.signoutPath:
162            log.debug("SessionHandlerMiddleware.__call__: caught sign out "
163                      "path [%s]", self.signoutPath)
164           
165            _start_response = self._doLogout(environ, start_response, session)
166        else:
167            log.debug("SessionHandlerMiddleware.__call__: checking for "
168                      "REMOTE_* environment variable settings set by OpenID "
169                      "Relying Party signin...")
170            self._setSession(environ, session)
171
172            _start_response = start_response
173           
174        return self._app(environ, _start_response)
175   
176    def _doLogout(self, environ, start_response, session):
177        """Execute logout action,
178         - clear the beaker session
179         - set the referrer URI to redirect back to by setting a custom
180        start_response function which modifies the HTTP header setting the
181        location field for a redirect
182       
183        @param environ: environment dictionary
184        @type environ: dict like object
185        @type start_response: function
186        @param start_response: standard WSGI start response function
187        @param session: beaker session
188        @type session: beaker.session.SessionObject
189        """
190           
191        # Clear user details from beaker session
192        for keyName in self.__class__.SESSION_KEYNAMES:
193            session.pop(keyName, None)
194        session.save()
195               
196        if self.__class__.LOGOUT_RETURN2URI_ARGNAME in environ['QUERY_STRING']:
197            params = dict(parse_querystring(environ))
198       
199            # Store the return URI query argument in a beaker session
200            quotedReferrer = params.get(
201                                self.__class__.LOGOUT_RETURN2URI_ARGNAME, '')
202            referrer = urllib.unquote(quotedReferrer)
203           
204            log.debug('Set redirect URI following logout based on %r URI query '
205                      'string = %r', 
206                      self.__class__.LOGOUT_RETURN2URI_ARGNAME,
207                      referrer)
208        else:
209            referrer = environ.get('HTTP_REFERER')
210            if referrer is None:
211                log.warning('No HTTP return to URI set for redirect following '
212                            'logout, either via the return to query string %r '
213                            'or the "HTTP_REFERER" environment variable: '
214                            'redirecting based on the %r config file option = '
215                            '%r', 
216                            self.__class__.LOGOUT_RETURN2URI_ARGNAME,
217                            self.__class__.DEFAULT_LOGOUT_RETURN2URI_PARAMNAME,
218                            self.__defaultLogoutReturnToURI)
219               
220                referrer = self.__defaultLogoutReturnToURI
221            else:
222                log.debug('Set redirect URI following logout based on '
223                          '"HTTP_REFERER" environment variable = %r',
224                          referrer)
225               
226        def _start_response(status, header, exc_info=None):
227            """Alter the header to send a redirect to the logout referrer
228            address"""
229           
230            # Filter out any existing location field setting
231            filteredHeader = [(field, val) for field, val in header
232                              if field.lower() != 'location'] 
233           
234            # Add redirect destination to new location field setting     
235            filteredHeader.extend([('Location', referrer)])
236           
237            statusMsg = self.getStatusMessage(
238                                    self.__class__.LOGOUT_REDIRECT_STATUS_CODE)
239           
240            return start_response(statusMsg, filteredHeader, exc_info)
241               
242        return _start_response
243       
244    def _setSession(self, environ, session):
245        """Check for REMOTE_USER and REMOTE_USER_DATA set by authentication
246        handlers and set a new session from them if present
247       
248        @type environ: dict like object
249        @param environ: WSGI environment variables dictionary
250        @param session: beaker session
251        @type session: beaker.session.SessionObject
252        """
253       
254        # Set user id
255        if (SessionHandlerMiddleware.USERNAME_SESSION_KEYNAME not in session
256            and SessionHandlerMiddleware.USERNAME_ENVIRON_KEYNAME in environ):
257           
258            log.debug("SessionHandlerMiddleware.__call__: updating session "
259                      "username=%s", environ[
260                        SessionHandlerMiddleware.USERNAME_ENVIRON_KEYNAME])
261           
262            session[SessionHandlerMiddleware.USERNAME_SESSION_KEYNAME
263                    ] = environ[
264                        SessionHandlerMiddleware.USERNAME_ENVIRON_KEYNAME]
265            session.save()
266           
267        # Check for auxiliary user data
268        remoteUserData = environ.get(
269                        SessionHandlerMiddleware.USERDATA_ENVIRON_KEYNAME, '')   
270        if remoteUserData:
271            log.debug("SessionHandlerMiddleware.__call__: found "
272                      "REMOTE_USER_DATA=%s, set from OpenID Relying Party "
273                      "signin", 
274                      environ[
275                          SessionHandlerMiddleware.USERDATA_ENVIRON_KEYNAME
276                      ])
277           
278            if (SessionHandlerMiddleware.SM_URI_SESSION_KEYNAME not in 
279                session or 
280                SessionHandlerMiddleware.ID_SESSION_KEYNAME not in session):
281               
282                # eval is safe here because AuthKit cookie is signed and
283                # AuthKit middleware checks for tampering           
284                axData = eval(remoteUserData)
285                if (isinstance(axData, dict) and 
286                    SessionHandlerMiddleware.AX_KEYNAME in axData):
287                   
288                    ax = axData[SessionHandlerMiddleware.AX_KEYNAME]
289                   
290                    # Save attributes keyed by attribute name
291                    session[SessionHandlerMiddleware.AX_SESSION_KEYNAME
292                            ] = SessionHandlerMiddleware._parseOpenIdAX(ax)
293                   
294                    log.debug("SessionHandlerMiddleware.__call__: updated "
295                              "session with OpenID AX values: %r",
296                              session[
297                                SessionHandlerMiddleware.AX_SESSION_KEYNAME
298                              ])
299                       
300                    # Save Session Manager specific attributes
301                    sessionManagerURI = ax.get(
302                            SessionHandlerMiddleware.SM_URI_AX_KEYNAME)
303                       
304                    session[SessionHandlerMiddleware.SM_URI_SESSION_KEYNAME
305                            ] = sessionManagerURI
306
307                    sessionId = ax.get(
308                            SessionHandlerMiddleware.SESSION_ID_AX_KEYNAME)
309                    session[SessionHandlerMiddleware.ID_SESSION_KEYNAME
310                            ] = sessionId
311                           
312                    session.save()
313                   
314                    log.debug("SessionHandlerMiddleware.__call__: updated "
315                              "session "
316                              "with sessionManagerURI=%s and "
317                              "sessionId=%s", 
318                              sessionManagerURI, 
319                              sessionId)
320               
321            # Reset cookie removing user data by accessing the Auth ticket
322            # function available from environ
323            setUser = environ[
324                    SessionHandlerMiddleware.AUTH_TKT_SET_USER_ENVIRON_KEYNAME]
325            setUser(session[SessionHandlerMiddleware.USERNAME_SESSION_KEYNAME])
326        else:
327            log.debug("SessionHandlerMiddleware.__call__: REMOTE_USER_DATA "
328                      "is not set")
329                   
330    @staticmethod                   
331    def _parseOpenIdAX(ax):
332        """Return a dictionary of attribute exchange attributes parsed from the
333        OpenID Provider response set in the REMOTE_USER_DATA AuthKit environ
334        key
335       
336        @param ax: dictionary of AX parameters - format of keys is e.g.
337        count.paramName, value.paramName.<n>, type.paramName
338        @type ax: dict
339        @return: dictionary of parameters keyed by parameter with values for
340        each parameter a tuple of count.paramName values
341        @rtype: dict
342        """
343       
344        # Copy Attributes into session
345        outputKeys = [k.replace('type.', '') for k in ax.keys()
346                      if k.startswith('type.')]
347       
348        output = {}
349        for outputKey in outputKeys:
350            axCountKeyName = 'count.' + outputKey
351            axCount = int(ax[axCountKeyName])
352           
353            axValueKeyPrefix = 'value.%s.' % outputKey
354            output[outputKey] = tuple([v for k, v in ax.items() 
355                                       if k.startswith(axValueKeyPrefix)])
356           
357            nVals = len(output[outputKey])
358            if nVals != axCount:
359                raise OpenIdAXConfigError('Got %d parameters for AX attribute '
360                                          '"%s"; but "%s" AX key is set to %d'
361                                          % (nVals,
362                                             axCountKeyName,
363                                             axCountKeyName,
364                                             axCount))
365                                             
366        return output
Note: See TracBrowser for help on using the repository browser.