1
Origin: upstream, abc532fa90eac1cc970423339347e318aa8d1b1a
2
Description: Horizon session fixation and reuse
3
https://lists.launchpad.net/openstack/msg11263.html
4
Bug: https://bugs.launchpad.net/horizon/+bug/978896
6
Index: horizon-2012.1/horizon/exceptions.py
7
===================================================================
8
--- horizon-2012.1.orig/horizon/exceptions.py 2012-05-05 07:19:50.000000000 -0500
9
+++ horizon-2012.1/horizon/exceptions.py 2012-05-05 07:20:36.000000000 -0500
11
if issubclass(exc_type, UNAUTHORIZED):
14
- request.session.clear()
15
+ request.user_logout()
17
LOG.debug("Unauthorized: %s" % exc_value)
18
# We get some pretty useless error messages back from
19
Index: horizon-2012.1/horizon/middleware.py
20
===================================================================
21
--- horizon-2012.1.orig/horizon/middleware.py 2012-05-05 07:19:50.000000000 -0500
22
+++ horizon-2012.1/horizon/middleware.py 2012-05-05 07:20:36.000000000 -0500
25
Adds a :class:`~horizon.users.User` object to ``request.user``.
27
+ # A quick and dirty way to log users out
28
+ def user_logout(request):
29
+ if hasattr(request, '_cached_user'):
30
+ del request._cached_user
31
+ # Use flush instead of clear, so we rotate session keys in
32
+ # addition to clearing all the session data
33
+ request.session.flush()
34
+ request.__class__.user_logout = user_logout
36
request.__class__.user = users.LazyUser()
37
request.horizon = {'dashboard': None, 'panel': None}
39
Index: horizon-2012.1/horizon/tests/auth_tests.py
40
===================================================================
41
--- horizon-2012.1.orig/horizon/tests/auth_tests.py 2012-05-05 07:19:50.000000000 -0500
42
+++ horizon-2012.1/horizon/tests/auth_tests.py 2012-05-05 07:20:36.000000000 -0500
44
# License for the specific language governing permissions and limitations
49
from django import http
50
from django.core.urlresolvers import reverse
51
from keystoneclient import exceptions as keystone_exceptions
54
self.assertRedirectsNoFollow(res, reverse('splash'))
55
self.assertNotIn(KEY, self.client.session)
57
+ def test_session_fixation(self):
59
+ form_data = {'method': 'Login',
60
+ 'region': 'http://localhost:5000/v2.0',
61
+ 'password': self.user.password,
62
+ 'username': self.user.name}
64
+ self.mox.StubOutWithMock(api, 'token_create')
65
+ self.mox.StubOutWithMock(api, 'tenant_list_for_token')
66
+ self.mox.StubOutWithMock(api, 'token_create_scoped')
68
+ aToken = self.tokens.unscoped_token
69
+ bToken = self.tokens.scoped_token
71
+ api.token_create(IsA(http.HttpRequest), "", self.user.name,
72
+ self.user.password).AndReturn(aToken)
73
+ api.tenant_list_for_token(IsA(http.HttpRequest),
74
+ aToken.id).AndReturn([self.tenants.first()])
75
+ api.token_create_scoped(IsA(http.HttpRequest),
77
+ aToken.id).AndReturn(bToken)
79
+ api.token_create(IsA(http.HttpRequest), "", self.user.name,
80
+ self.user.password).AndReturn(aToken)
81
+ api.tenant_list_for_token(IsA(http.HttpRequest),
82
+ aToken.id).AndReturn([self.tenants.first()])
83
+ api.token_create_scoped(IsA(http.HttpRequest),
85
+ aToken.id).AndReturn(bToken)
86
+ self.mox.ReplayAll()
88
+ res = self.client.get(reverse('horizon:auth_login'))
89
+ self.assertEqual(res.cookies.get('sessionid'), None)
90
+ res = self.client.post(reverse('horizon:auth_login'), form_data)
91
+ session_ids.append(res.cookies['sessionid'].value)
93
+ self.assertEquals(self.client.session['user_name'],
95
+ self.client.session['foobar'] = 'MY TEST VALUE'
96
+ res = self.client.get(reverse('horizon:auth_logout'))
97
+ session_ids.append(res.cookies['sessionid'].value)
98
+ self.assertEqual(len(self.client.session.items()), 0)
99
+ # Sleep for 1 second so the session values are different if
100
+ # using the signed_cookies backend.
102
+ res = self.client.post(reverse('horizon:auth_login'), form_data)
103
+ session_ids.append(res.cookies['sessionid'].value)
104
+ # Make sure all 3 session id values are different
105
+ self.assertEqual(len(session_ids), len(set(session_ids)))
106
Index: horizon-2012.1/horizon/users.py
107
===================================================================
108
--- horizon-2012.1.orig/horizon/users.py 2012-05-05 07:19:50.000000000 -0500
109
+++ horizon-2012.1/horizon/users.py 2012-05-05 07:20:36.000000000 -0500
111
# If any of those keys are missing from the session it is
112
# overwhelmingly likely that we're dealing with an outdated session.
113
LOG.exception("Error while creating User from session.")
114
- request.session.clear()
115
+ request.user_logout()
116
raise exceptions.NotAuthorized(_("Your session has expired. "
117
"Please log in again."))
119
Index: horizon-2012.1/horizon/views/auth.py
120
===================================================================
121
--- horizon-2012.1.orig/horizon/views/auth.py 2012-05-05 07:19:50.000000000 -0500
122
+++ horizon-2012.1/horizon/views/auth.py 2012-05-05 07:20:36.000000000 -0500
126
""" Clears the session and logs the current user out. """
127
- request.session.clear()
128
+ request.user_logout()
129
# FIXME(gabriel): we don't ship a view named splash
130
return shortcuts.redirect('splash')
131
Index: horizon-2012.1/horizon/views/auth_forms.py
132
===================================================================
133
--- horizon-2012.1.orig/horizon/views/auth_forms.py 2012-05-05 07:19:50.000000000 -0500
134
+++ horizon-2012.1/horizon/views/auth_forms.py 2012-05-05 07:20:36.000000000 -0500
136
self.fields['region'].widget = forms.widgets.HiddenInput()
138
def handle(self, request, data):
139
+ if 'user_name' in request.session:
140
+ if request.session['user_name'] != data['username']:
141
+ # To avoid reusing another user's session, create a
142
+ # new, empty session if the existing session
143
+ # corresponds to a different authenticated user.
144
+ request.session.flush()
145
+ # Always cycle the session key when viewing the login form to
146
+ # prevent session fixation
147
+ request.session.cycle_key()
149
# For now we'll allow fallback to OPENSTACK_KEYSTONE_URL if the
150
# form post doesn't include a region.
151
endpoint = data.get('region', None) or settings.OPENSTACK_KEYSTONE_URL
153
# If we get here we don't want to show a stack trace to the
154
# user. However, if we fail here, there may be bad session
155
# data that's been cached already.
156
- request.session.clear()
157
+ request.user_logout()
158
exceptions.handle(request,
159
message=_("An error occurred authenticating."
160
" Please try again later."),