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

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@7119
Revision 7119, 13.3 KB checked in by pjkersha, 10 years ago (diff)

Incomplete - task 10: OpenID Provider HTML/Javascript response incompatible with OpenID4Java

  • Removed old Session Manager code from 1.5.x branch
  • started updating certificates for new test CA.
  • 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
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.