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

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg-security/TI12-security/trunk/NDGSecurity/python/ndg_security_server/ndg/security/server/wsgi/session.py@6440
Revision 6440, 13.3 KB checked in by pjkersha, 11 years ago (diff)
  • #1088 Important fix to AuthnRedirectResponseMiddleware? to set redirect ONLY when SSL client authentication has just succeeded in the upstream middleware AuthKitSSLAuthnMiddleware. This bug was causing the browser to redirect to the wrong place following OpenID sign in in the case where the user is already logged into their provider and selects a new relying party to sign into.
    • Improvements to Provider decide page interface: leave out messages about attributes that the provider can't retrieve for the RP. Also included NDG style help icon.
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
16from ndg.security.server.wsgi import (NDGSecurityMiddlewareBase,
17                                      NDGSecurityMiddlewareError)
18
19
20class SessionMiddlewareBase(NDGSecurityMiddlewareBase):
21    """Base class for Authentication redirect middleware and Session Handler
22    middleware
23   
24    @type propertyDefaults: dict
25    @cvar propertyDefaults: valid configuration property keywords
26    """   
27    propertyDefaults = {
28        'sessionKey': 'beaker.session.ndg.security'
29    }
30
31    # Key names for PEP context information
32    PEPCTX_SESSION_KEYNAME = 'pepCtx'
33    PEPCTX_REQUEST_SESSION_KEYNAME = 'request'
34    PEPCTX_RESPONSE_SESSION_KEYNAME = 'response'
35    PEPCTX_TIMESTAMP_SESSION_KEYNAME = 'timestamp'
36   
37    _isAuthenticated = lambda self: \
38        SessionMiddlewareBase.USERNAME_SESSION_KEYNAME in \
39        self.environ.get(self.sessionKey, ())
40       
41    isAuthenticated = property(fget=_isAuthenticated,
42                               doc='boolean to indicate is user logged in')
43       
44
45class SessionHandlerMiddlewareError(NDGSecurityMiddlewareError):
46    """Base exception for SessionHandlerMiddleware"""
47           
48           
49class SessionHandlerMiddlewareConfigError(SessionHandlerMiddlewareError):
50    """Configuration errors from SessionHandlerMiddleware"""
51   
52   
53class OpenIdAXConfigError(SessionHandlerMiddlewareError):
54    """Error parsing OpenID Ax (Attribute Exchange) parameters"""
55   
56   
57class SessionHandlerMiddleware(SessionMiddlewareBase):
58    '''Middleware to:
59    - establish user session details following redirect from OpenID Relying
60    Party sign-in or SSL Client authentication
61    - end session redirecting back to referrer URI following call to a logout
62    URI as implemented in AuthKit
63    '''
64    AX_SESSION_KEYNAME = 'openid.ax'
65    SM_URI_SESSION_KEYNAME = 'sessionManagerURI'
66    ID_SESSION_KEYNAME = 'sessionId'
67    PEP_CTX_SESSION_KEYNAME = 'pepCtx'
68    CREDENTIAL_WALLET_SESSION_KEYNAME = 'credentialWallet'
69   
70    SESSION_KEYNAMES = (
71        SessionMiddlewareBase.USERNAME_SESSION_KEYNAME, 
72        SM_URI_SESSION_KEYNAME, 
73        ID_SESSION_KEYNAME, 
74        PEP_CTX_SESSION_KEYNAME, 
75        CREDENTIAL_WALLET_SESSION_KEYNAME
76    )
77   
78    AX_KEYNAME = 'ax'
79    SM_URI_AX_KEYNAME = 'value.sessionManagerURI.1'
80    SESSION_ID_AX_KEYNAME = 'value.sessionId.1'
81   
82    AUTHKIT_COOKIE_SIGNOUT_PARAMNAME = 'authkit.cookie.signoutpath'
83    SIGNOUT_PATH_PARAMNAME = 'signoutPath'
84    SESSION_KEY_PARAMNAME = 'sessionKey'
85    propertyDefaults = {
86        SIGNOUT_PATH_PARAMNAME: None,
87        SESSION_KEY_PARAMNAME: 'beaker.session.ndg.security'
88    }
89   
90    AUTH_TKT_SET_USER_ENVIRON_KEYNAME = 'paste.auth_tkt.set_user'
91   
92    PARAM_PREFIX = 'sessionHandler.'
93   
94    def __init__(self, app, global_conf, prefix=PARAM_PREFIX, **app_conf):
95        '''
96        @type app: callable following WSGI interface
97        @param app: next middleware application in the chain     
98        @type global_conf: dict       
99        @param global_conf: PasteDeploy global configuration dictionary
100        @type prefix: basestring
101        @param prefix: prefix for configuration items
102        @type app_conf: dict       
103        @param app_conf: PasteDeploy application specific configuration
104        dictionary
105        '''
106        signoutPathParamName = prefix + \
107                                SessionHandlerMiddleware.SIGNOUT_PATH_PARAMNAME
108       
109        if signoutPathParamName not in app_conf:
110            authKitSignOutPath = app_conf.get(
111                    SessionHandlerMiddleware.AUTHKIT_COOKIE_SIGNOUT_PARAMNAME)
112           
113            if authKitSignOutPath:
114                app_conf[signoutPathParamName] = authKitSignOutPath
115               
116                log.info('Set signoutPath=%s from "%s" setting', 
117                     authKitSignOutPath,
118                     SessionHandlerMiddleware.AUTHKIT_COOKIE_SIGNOUT_PARAMNAME)
119            else:
120                raise SessionHandlerMiddlewareConfigError(
121                                        '"signoutPath" parameter is not set')
122           
123        super(SessionHandlerMiddleware, self).__init__(app,
124                                                       global_conf,
125                                                       prefix=prefix, 
126                                                       **app_conf)
127       
128    @NDGSecurityMiddlewareBase.initCall
129    def __call__(self, environ, start_response):
130        """Manage setting of session from AuthKit following OpenID Relying
131        Party sign in and manage logout
132       
133        @type environ: dict
134        @param environ: WSGI environment variables dictionary
135        @type start_response: function
136        @param start_response: standard WSGI start response function
137        """
138        log.debug("SessionHandlerMiddleware.__call__ ...")
139       
140        session = environ.get(self.sessionKey)
141        if session is None:
142            raise SessionHandlerMiddlewareConfigError(
143                   'SessionHandlerMiddleware.__call__: No beaker session key '
144                   '"%s" found in environ' % self.sessionKey)
145       
146        if self.signoutPath and self.pathInfo == self.signoutPath:
147            log.debug("SessionHandlerMiddleware.__call__: caught sign out "
148                      "path [%s]", self.signoutPath)
149           
150            _start_response = self._doLogout(environ, start_response, session)
151        else:
152            log.debug("SessionHandlerMiddleware.__call__: checking for "
153                      "REMOTE_* environment variable settings set by OpenID "
154                      "Relying Party signin...")
155            self._setSession(environ, session)
156
157            _start_response = start_response
158           
159        return self._app(environ, _start_response)
160   
161    def _doLogout(self, environ, start_response, session):
162        """Execute logout action,
163         - clear the beaker session
164         - set the referrer URI to redirect back to by setting a custom
165        start_response function which modifies the HTTP header setting the
166        location field for a redirect
167       
168        @param environ: environment dictionary
169        @type environ: dict like object
170        @type start_response: function
171        @param start_response: standard WSGI start response function
172        @param session: beaker session
173        @type session: beaker.session.SessionObject
174        """
175           
176        # Clear user details from beaker session
177        for keyName in self.__class__.SESSION_KEYNAMES:
178            session.pop(keyName, None)
179        session.save()
180       
181        referrer = environ.get('HTTP_REFERER')
182        if referrer is not None:
183            def _start_response(status, header, exc_info=None):
184                """Alter the header to send a redirect to the logout
185                referrer address"""
186                filteredHeader = [(field, val) for field, val in header
187                                  if field.lower() != 'location']       
188                filteredHeader.extend([('Location', referrer)])
189                return start_response(self.getStatusMessage(302), 
190                                      filteredHeader,
191                                      exc_info)
192               
193            return _start_response       
194        else:
195            log.error('No referrer set for redirect following logout')
196            return start_response
197       
198    def _setSession(self, environ, session):
199        """Check for REMOTE_USER and REMOTE_USER_DATA set by authentication
200        handlers and set a new session from them if present
201       
202        @type environ: dict like object
203        @param environ: WSGI environment variables dictionary
204        @param session: beaker session
205        @type session: beaker.session.SessionObject
206        """
207       
208        # Set user id
209        if (SessionHandlerMiddleware.USERNAME_SESSION_KEYNAME not in session
210            and SessionHandlerMiddleware.USERNAME_ENVIRON_KEYNAME in environ):
211           
212            log.debug("SessionHandlerMiddleware.__call__: updating session "
213                      "username=%s", environ[
214                        SessionHandlerMiddleware.USERNAME_ENVIRON_KEYNAME])
215           
216            session[SessionHandlerMiddleware.USERNAME_SESSION_KEYNAME
217                    ] = environ[
218                        SessionHandlerMiddleware.USERNAME_ENVIRON_KEYNAME]
219            session.save()
220           
221        # Check for auxiliary user data
222        remoteUserData = environ.get(
223                        SessionHandlerMiddleware.USERDATA_ENVIRON_KEYNAME, '')   
224        if remoteUserData:
225            log.debug("SessionHandlerMiddleware.__call__: found "
226                      "REMOTE_USER_DATA=%s, set from OpenID Relying Party "
227                      "signin", 
228                      environ[
229                          SessionHandlerMiddleware.USERDATA_ENVIRON_KEYNAME
230                      ])
231           
232            if (SessionHandlerMiddleware.SM_URI_SESSION_KEYNAME not in 
233                session or 
234                SessionHandlerMiddleware.ID_SESSION_KEYNAME not in session):
235               
236                # eval is safe here because AuthKit cookie is signed and
237                # AuthKit middleware checks for tampering           
238                axData = eval(remoteUserData)
239                if (isinstance(axData, dict) and 
240                    SessionHandlerMiddleware.AX_KEYNAME in axData):
241                   
242                    ax = axData[SessionHandlerMiddleware.AX_KEYNAME]
243                   
244                    # Save attributes keyed by attribute name
245                    session[SessionHandlerMiddleware.AX_SESSION_KEYNAME
246                            ] = SessionHandlerMiddleware._parseOpenIdAX(ax)
247                   
248                    log.debug("SessionHandlerMiddleware.__call__: updated "
249                              "session with OpenID AX values: %r",
250                              session[
251                                SessionHandlerMiddleware.AX_SESSION_KEYNAME
252                              ])
253                       
254                    # Save Session Manager specific attributes
255                    sessionManagerURI = ax.get(
256                            SessionHandlerMiddleware.SM_URI_AX_KEYNAME)
257                       
258                    session[SessionHandlerMiddleware.SM_URI_SESSION_KEYNAME
259                            ] = sessionManagerURI
260
261                    sessionId = ax.get(
262                            SessionHandlerMiddleware.SESSION_ID_AX_KEYNAME)
263                    session[SessionHandlerMiddleware.ID_SESSION_KEYNAME
264                            ] = sessionId
265                           
266                    session.save()
267                   
268                    log.debug("SessionHandlerMiddleware.__call__: updated "
269                              "session "
270                              "with sessionManagerURI=%s and "
271                              "sessionId=%s", 
272                              sessionManagerURI, 
273                              sessionId)
274               
275            # Reset cookie removing user data by accessing the Auth ticket
276            # function available from environ
277            setUser = environ[
278                    SessionHandlerMiddleware.AUTH_TKT_SET_USER_ENVIRON_KEYNAME]
279            setUser(session[SessionHandlerMiddleware.USERNAME_SESSION_KEYNAME])
280        else:
281            log.debug("SessionHandlerMiddleware.__call__: REMOTE_USER_DATA "
282                      "is not set")
283                   
284    @staticmethod                   
285    def _parseOpenIdAX(ax):
286        """Return a dictionary of attribute exchange attributes parsed from the
287        OpenID Provider response set in the REMOTE_USER_DATA AuthKit environ
288        key
289       
290        @param ax: dictionary of AX parameters - format of keys is e.g.
291        count.paramName, value.paramName.<n>, type.paramName
292        @type ax: dict
293        @return: dictionary of parameters keyed by parameter with values for
294        each parameter a tuple of count.paramName values
295        @rtype: dict
296        """
297       
298        # Copy Attributes into session
299        outputKeys = [k.replace('type.', '') for k in ax.keys()
300                      if k.startswith('type.')]
301       
302        output = {}
303        for outputKey in outputKeys:
304            axCountKeyName = 'count.' + outputKey
305            axCount = int(ax[axCountKeyName])
306           
307            axValueKeyPrefix = 'value.%s.' % outputKey
308            output[outputKey] = tuple([v for k, v in ax.items() 
309                                       if k.startswith(axValueKeyPrefix)])
310           
311            nVals = len(output[outputKey])
312            if nVals != axCount:
313                raise OpenIdAXConfigError('Got %d parameters for AX attribute '
314                                          '"%s"; but "%s" AX key is set to %d'
315                                          % (nVals,
316                                             axCountKeyName,
317                                             axCountKeyName,
318                                             axCount))
319                                             
320        return output
Note: See TracBrowser for help on using the repository browser.