• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

zopefoundation / Products.PluggableAuthService / 14325864773

08 Apr 2025 05:29AM UTC coverage: 90.685% (+0.08%) from 90.603%
14325864773

Pull #122

github

dataflake
Merge branch 'master' into dataflake/improve_sessioning
Pull Request #122: Improve session handling, guard against session fixation

1046 of 1468 branches covered (71.25%)

Branch coverage included in aggregate %.

141 of 144 new or added lines in 9 files covered. (97.92%)

9 existing lines in 1 file now uncovered.

9848 of 10545 relevant lines covered (93.39%)

0.93 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.75
/src/Products/PluggableAuthService/plugins/tests/test_CookieAuthHelper.py
1
##############################################################################
2
#
3
# Copyright (c) 2001 Zope Foundation and Contributors
4
#
5
# This software is subject to the provisions of the Zope Public License,
6
# Version 2.1 (ZPL).  A copy of the ZPL should accompany this
7
# distribution.
8
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
9
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
10
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
11
# FOR A PARTICULAR PURPOSE.
12
#
13
##############################################################################
14
import codecs
1✔
15
import unittest
1✔
16
from base64 import encodebytes
1✔
17

18
from zope.component import adapter
1✔
19
from zope.component import provideHandler
1✔
20

21
from ...interfaces.events import IUserSessionStartedEvent
1✔
22
from ...interfaces.plugins import IChallengePlugin
1✔
23
from ...tests import pastc
1✔
24
from ...tests.conformance import IChallengePlugin_conformance
1✔
25
from ...tests.conformance import ICredentialsResetPlugin_conformance
1✔
26
from ...tests.conformance import ICredentialsUpdatePlugin_conformance
1✔
27
from ...tests.conformance import ILoginPasswordHostExtractionPlugin_conformance
1✔
28
from ...tests.test_PluggableAuthService import FauxContainer
1✔
29
from ...tests.test_PluggableAuthService import FauxObject
1✔
30
from ...tests.test_PluggableAuthService import FauxRequest
1✔
31
from ...tests.test_PluggableAuthService import FauxResponse
1✔
32
from ...tests.test_PluggableAuthService import FauxRoot
1✔
33

34

35
EVENTS = []
1✔
36

37

38
def _getTestEvents():
1✔
39
    global EVENTS
40
    return EVENTS
1✔
41

42

43
def _resetTestEvents():
1✔
44
    global EVENTS
NEW
45
    EVENTS = []
×
46

47

48
@adapter(IUserSessionStartedEvent)
1✔
49
def userSessionStartedHandler(event):
1✔
50
    events = _getTestEvents()
1✔
51
    events.append(event)
1✔
52

53

54
class FauxSettableRequest(FauxRequest):
1✔
55

56
    def set(self, name, value):
1✔
57
        self._dict[name] = value
1✔
58

59

60
class FauxCookieResponse(FauxResponse):
1✔
61

62
    def __init__(self):
1✔
63
        self.cookies = {}
1✔
64
        self.cookie_attributes = {}
1✔
65
        self.redirected = False
1✔
66
        self.status = '200'
1✔
67
        self.headers = {}
1✔
68

69
    def setCookie(self, cookie_name, cookie_value, path, **kw):
1✔
70
        self.cookies[(cookie_name, path)] = cookie_value
1✔
71
        self.cookie_attributes[(cookie_name, path)] = kw
1✔
72

73
    def expireCookie(self, cookie_name, path):
1✔
74
        if (cookie_name, path) in self.cookies:
1!
75
            del self.cookies[(cookie_name, path)]
×
76

77
    def redirect(self, location, status=302, lock=0):
1✔
78
        self.status = status
1✔
79
        self.headers['Location'] = location
1✔
80

81
    def setHeader(self, name, value):
1✔
82
        self.headers[name] = value
1✔
83

84

85
class CookieAuthHelperTests(unittest.TestCase,
1✔
86
                            ILoginPasswordHostExtractionPlugin_conformance,
87
                            IChallengePlugin_conformance,
88
                            ICredentialsResetPlugin_conformance,
89
                            ICredentialsUpdatePlugin_conformance):
90

91
    def _getTargetClass(self):
1✔
92

93
        from ...plugins.CookieAuthHelper import CookieAuthHelper
1✔
94

95
        return CookieAuthHelper
1✔
96

97
    def _makeOne(self, id='test', *args, **kw):
1✔
98

99
        return self._getTargetClass()(id=id, *args, **kw)
1✔
100

101
    def _makeTree(self):
1✔
102

103
        rc = FauxObject('rc')
1✔
104
        root = FauxRoot('root').__of__(rc)
1✔
105
        folder = FauxContainer('folder').__of__(root)
1✔
106
        object = FauxObject('object').__of__(folder)
1✔
107

108
        return rc, root, folder, object
1✔
109

110
    def afterSetUp(self):
1✔
NEW
111
        _resetTestEvents()
×
112

113
    def test_extractCredentials_no_creds(self):
1✔
114

115
        helper = self._makeOne()
1✔
116
        response = FauxCookieResponse()
1✔
117
        request = FauxRequest(RESPONSE=response)
1✔
118

119
        self.assertEqual(helper.extractCredentials(request), {})
1✔
120

121
    def test_extractCredentials_with_form_creds(self):
1✔
122

123
        helper = self._makeOne()
1✔
124
        response = FauxCookieResponse()
1✔
125
        request = FauxSettableRequest(__ac_name='foo',
1✔
126
                                      __ac_password='b:ar',
127
                                      RESPONSE=response)
128

129
        self.assertEqual(len(response.cookies), 0)
1✔
130
        self.assertEqual(helper.extractCredentials(request),
1✔
131
                         {'login': 'foo',
132
                          'password': 'b:ar',
133
                          'remote_host': '',
134
                          'remote_address': ''})
135
        self.assertEqual(len(response.cookies), 0)
1✔
136

137
    def test_extractCredentials_with_deleted_cookie(self):
1✔
138
        # http://www.zope.org/Collectors/PAS/43
139
        # Edge case: The ZPublisher sets a cookie's value to "deleted"
140
        # in the current request if expireCookie is called. If we hit
141
        # extractCredentials in the same request after this, it would
142
        # blow up trying to deal with the invalid cookie value.
143
        helper = self._makeOne()
1✔
144
        response = FauxCookieResponse()
1✔
145
        req_data = {helper.cookie_name: 'deleted', 'RESPONSE': response}
1✔
146
        request = FauxSettableRequest(**req_data)
1✔
147
        self.assertEqual(len(response.cookies), 0)
1✔
148

149
        self.assertEqual(helper.extractCredentials(request), {})
1✔
150

151
    def test_challenge(self):
1✔
152
        rc, root, folder, object = self._makeTree()
1✔
153
        response = FauxCookieResponse()
1✔
154
        testPath = '/some/path?arg1=val1&arg2=val2'
1✔
155
        testURL = 'http://test' + testPath
1✔
156
        request = FauxRequest(RESPONSE=response, URL=testURL,
1✔
157
                              ACTUAL_URL=testURL)
158
        root.REQUEST = request
1✔
159

160
        helper = self._makeOne().__of__(root)
1✔
161

162
        helper.challenge(request, response)
1✔
163
        self.assertEqual(response.status, 302)
1✔
164
        self.assertEqual(len(response.headers), 3)
1✔
165
        self.assertEqual(
1✔
166
            response.headers['Location'],
167
            '/login_form?came_from=/some/path%3Farg1%3Dval1%26arg2%3Dval2')
168
        self.assertEqual(response.headers['Cache-Control'], 'no-cache')
1✔
169
        self.assertEqual(response.headers['Expires'],
1✔
170
                         'Sat, 01 Jan 2000 00:00:00 GMT')
171

172
    def test_challenge_with_vhm(self):
1✔
173
        rc, root, folder, object = self._makeTree()
1✔
174
        response = FauxCookieResponse()
1✔
175
        vhm = 'http://localhost/VirtualHostBase/http/test/VirtualHostRoot/xxx'
1✔
176
        actualURL = 'http://test/xxx?arg1=val1&arg2=val2'
1✔
177

178
        request = FauxRequest(RESPONSE=response, URL=vhm,
1✔
179
                              ACTUAL_URL=actualURL)
180
        root.REQUEST = request
1✔
181

182
        helper = self._makeOne().__of__(root)
1✔
183

184
        helper.challenge(request, response)
1✔
185
        self.assertEqual(response.status, 302)
1✔
186
        self.assertEqual(len(response.headers), 3)
1✔
187
        self.assertEqual(
1✔
188
            response.headers['Location'],
189
            '/login_form?came_from=/xxx%3Farg1%3Dval1%26arg2%3Dval2')
190
        self.assertEqual(response.headers['Cache-Control'], 'no-cache')
1✔
191
        self.assertEqual(response.headers['Expires'],
1✔
192
                         'Sat, 01 Jan 2000 00:00:00 GMT')
193

194
    def test_resetCredentials(self):
1✔
195
        helper = self._makeOne()
1✔
196
        response = FauxCookieResponse()
1✔
197
        request = FauxRequest(RESPONSE=response)
1✔
198

199
        helper.resetCredentials(request, response)
1✔
200
        self.assertEqual(len(response.cookies), 0)
1✔
201

202
    def test_login_redirect(self):
1✔
203
        helper = self._makeOne()
1✔
204
        response = FauxCookieResponse()
1✔
205
        request = FauxSettableRequest(RESPONSE=response)
1✔
206
        helper.REQUEST = request
1✔
207

208
        # came_from empty, redirect is empty as well
209
        url = ''
1✔
210
        request.form['came_from'] = url
1✔
211
        helper.login()
1✔
212
        self.assertEqual(response.headers['Location'], url)
1✔
213

214
        # came_from is site-local, redirect won't change the URL
215
        url = '/foo?arg1=val1&arg2=val2'
1✔
216
        request.form['came_from'] = url
1✔
217
        helper.login()
1✔
218
        self.assertEqual(response.headers['Location'], url)
1✔
219

220
        # Protocol and host parts will be chopped off
221
        url = 'http://evil.com/foo?arg1=val1&arg2=val2'
1✔
222
        request.form['came_from'] = url
1✔
223
        helper.login()
1✔
224
        self.assertEqual(response.headers['Location'],
1✔
225
                         '/foo?arg1=val1&arg2=val2')
226

227
    def test_loginWithoutCredentialsUpdate(self):
1✔
228
        helper = self._makeOne()
1✔
229
        response = FauxCookieResponse()
1✔
230
        request = FauxSettableRequest(__ac_name='foo', __ac_password='bar',
1✔
231
                                      RESPONSE=response)
232
        request.form['came_from'] = ''
1✔
233
        helper.REQUEST = request
1✔
234

235
        helper.login()
1✔
236
        self.assertEqual(len(response.cookies), 0)
1✔
237

238
    def test_extractCredentials_from_cookie_with_colon_in_password(self):
1✔
239
        # http://www.zope.org/Collectors/PAS/51
240
        # Passwords with ":" characters broke authentication
241
        helper = self._makeOne()
1✔
242
        response = FauxCookieResponse()
1✔
243
        request = FauxSettableRequest(RESPONSE=response)
1✔
244

245
        username = codecs.encode(b'foo', 'hex_codec')
1✔
246
        password = codecs.encode(b'b:ar', 'hex_codec')
1✔
247
        cookie_str = b'%s:%s' % (username, password)
1✔
248
        cookie_val = encodebytes(cookie_str)
1✔
249
        cookie_val = cookie_val.rstrip()
1✔
250
        cookie_val = cookie_val.decode('utf8')
1✔
251
        request.set(helper.cookie_name, cookie_val)
1✔
252

253
        self.assertEqual(helper.extractCredentials(request),
1✔
254
                         {'login': 'foo',
255
                          'password': 'b:ar',
256
                          'remote_host': '',
257
                          'remote_address': ''})
258

259
    def test_extractCredentials_from_cookie_with_colon_that_is_not_ours(self):
1✔
260
        # http://article.gmane.org/gmane.comp.web.zope.plone.product-developers/5145
261
        helper = self._makeOne()
1✔
262
        response = FauxCookieResponse()
1✔
263
        request = FauxSettableRequest(RESPONSE=response)
1✔
264

265
        cookie_str = b'cookie:from_other_plugin'
1✔
266
        cookie_val = encodebytes(cookie_str)
1✔
267
        cookie_val = cookie_val.rstrip()
1✔
268
        cookie_val = cookie_val.decode('utf8')
1✔
269
        request.set(helper.cookie_name, cookie_val)
1✔
270

271
        self.assertEqual(helper.extractCredentials(request), {})
1✔
272

273
    def test_extractCredentials_from_cookie_with_bad_binascii(self):
1✔
274
        # this might happen between browser implementations
275
        helper = self._makeOne()
1✔
276
        response = FauxCookieResponse()
1✔
277
        request = FauxSettableRequest(RESPONSE=response)
1✔
278

279
        cookie_val = 'NjE2NDZkNjk2ZTo3MDZjNmY2ZTY1MzQ3NQ%3D%3D'[:-1]
1✔
280
        request.set(helper.cookie_name, cookie_val)
1✔
281

282
        self.assertEqual(helper.extractCredentials(request), {})
1✔
283

284
    def test_updateCredentials(self):
1✔
285
        provideHandler(userSessionStartedHandler)
1✔
286
        helper = self._makeOne()
1✔
287
        response = FauxCookieResponse()
1✔
288
        request = FauxSettableRequest(RESPONSE=response)
1✔
289

290
        username = codecs.encode(b'foo', 'hex_codec')
1✔
291
        password = codecs.encode(b'b:ar', 'hex_codec')
1✔
292
        cookie_str = b'%s:%s' % (username, password)
1✔
293
        cookie_val = encodebytes(cookie_str)
1✔
294
        cookie_val = cookie_val.rstrip()
1✔
295
        cookie_val = cookie_val.decode('utf8')
1✔
296
        request.set(helper.cookie_name, cookie_val)
1✔
297

298
        # Defaults
299
        helper.updateCredentials(request, response, 'new_user', 'new_pass')
1✔
300
        cookie_attrs = response.cookie_attributes[(helper.cookie_name, '/')]
1✔
301
        self.assertEqual(cookie_attrs['same_site'], 'Lax')
1✔
302
        self.assertFalse(cookie_attrs['secure'])
1✔
303
        self.assertEqual(len(_getTestEvents()), 1)
1✔
304

305
        # If the login does not change no event is fired.
306
        helper.updateCredentials(request, response, 'foo', 'new_pass')
1✔
307
        self.assertEqual(len(_getTestEvents()), 1)
1✔
308

309
        # Setting the cookie same site value to "None" forces secure flag
310
        helper.cookie_same_site = 'None'
1✔
311
        helper.updateCredentials(request, response, 'new_user', 'new_pass')
1✔
312
        cookie_attrs = response.cookie_attributes[(helper.cookie_name, '/')]
1✔
313
        self.assertEqual(cookie_attrs['same_site'], 'None')
1✔
314
        self.assertTrue(cookie_attrs['secure'])
1✔
315
        self.assertEqual(len(_getTestEvents()), 2)
1✔
316

317
        # Setting the Secure flag manually
318
        helper.cookie_same_site = 'Lax'
1✔
319
        helper.cookie_secure = True
1✔
320
        helper.updateCredentials(request, response, 'new_user', 'new_pass')
1✔
321
        cookie_attrs = response.cookie_attributes[(helper.cookie_name, '/')]
1✔
322
        self.assertEqual(cookie_attrs['same_site'], 'Lax')
1✔
323
        self.assertTrue(cookie_attrs['secure'])
1✔
324
        self.assertEqual(len(_getTestEvents()), 3)
1✔
325

326

327
class CookieAuthHelperIntegrationTests(pastc.PASTestCase):
1✔
328

329
    def test_login_with_missing_came_from(self):
1✔
330
        pas = self.folder.acl_users
1✔
331
        factory = pas.manage_addProduct['PluggableAuthService']
1✔
332
        factory.addCookieAuthHelper('cookie_auth')
1✔
333
        plugins = pas.plugins
1✔
334
        plugins.activatePlugin(IChallengePlugin, 'cookie_auth')
1✔
335

336
        response = FauxCookieResponse()
1✔
337
        request = FauxSettableRequest(RESPONSE=response)
1✔
338

339
        # find cookie auth
340
        for id_, plugin in pas.plugins.items():
1!
341
            if id_ == 'cookie_auth':
1✔
342
                cookie_auth = plugin
1✔
343
                break
1✔
344

345
        cookie_auth.REQUEST = request
1✔
346
        cookie_auth.login()
1✔
347

348
        self.assertEqual(
1✔
349
            response.headers['Location'], 'http://nohost/test_folder_1_')
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc