1 | """Session handling middleware module |
---|
2 | |
---|
3 | Refactored authn module moving session specific code to here |
---|
4 | |
---|
5 | NERC 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: $" |
---|
13 | import logging |
---|
14 | log = logging.getLogger(__name__) |
---|
15 | |
---|
16 | from ndg.security.server.wsgi import (NDGSecurityMiddlewareBase, |
---|
17 | NDGSecurityMiddlewareError) |
---|
18 | |
---|
19 | |
---|
20 | class 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 | |
---|
45 | class SessionHandlerMiddlewareError(NDGSecurityMiddlewareError): |
---|
46 | """Base exception for SessionHandlerMiddleware""" |
---|
47 | |
---|
48 | |
---|
49 | class SessionHandlerMiddlewareConfigError(SessionHandlerMiddlewareError): |
---|
50 | """Configuration errors from SessionHandlerMiddleware""" |
---|
51 | |
---|
52 | |
---|
53 | class OpenIdAXConfigError(SessionHandlerMiddlewareError): |
---|
54 | """Error parsing OpenID Ax (Attribute Exchange) parameters""" |
---|
55 | |
---|
56 | |
---|
57 | class 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 |
---|