Skip to content

Commit 357a184

Browse files
committed
Refactor cookie init into shared helper and expand test coverage.
1 parent d3283de commit 357a184

File tree

1 file changed

+192
-9
lines changed

1 file changed

+192
-9
lines changed

tests/test_mig_shared_auth.py

Lines changed: 192 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@
4242

4343
TEST_USER_DN = \
4444
'/C=DK/ST=NA/L=NA/O=Test Org/OU=NA/CN=Test User/emailAddress=test@example.com'
45+
GDP_USER_DN = f'{TEST_USER_DN}/GDP=projectx'
46+
TEST_CLIENT_PREFIX = \
47+
'2e1c3d78bddf637ed6b83067c15ac9b9893545ff6a549519178cb4a252ed38b5'
4548

4649

4750
class MigSharedAuth__twofactor(MigTestCase):
@@ -50,6 +53,19 @@ class MigSharedAuth__twofactor(MigTestCase):
5053
def _provide_configuration(self):
5154
return 'testconfig'
5255

56+
def _mimic_cookie_init(self, session_key):
57+
"""Mimic twofactor session cookie setup"""
58+
cookie = http.cookies.SimpleCookie()
59+
session_start = time.time()
60+
cookie['2FA_Auth'] = session_key
61+
cookie['2FA_Auth']['path'] = '/'
62+
# NOTE: SimpleCookie translates expires ttl to actual date from now
63+
cookie['2FA_Auth']['expires'] = twofactor_cookie_ttl
64+
cookie['2FA_Auth']['secure'] = True
65+
cookie['2FA_Auth']['httponly'] = True
66+
environ = {'HTTP_COOKIE': cookie}
67+
return environ
68+
5369
def before_each(self):
5470
"""Setup test environment before each test method"""
5571
ensure_dirs_exist(self.configuration.user_cache)
@@ -58,6 +74,18 @@ def before_each(self):
5874
ensure_dirs_exist(self.configuration.mrsl_files_dir)
5975
ensure_dirs_exist(self.configuration.resource_pending)
6076
ensure_dirs_exist(self.configuration.twofactor_home)
77+
self.configuration.site_enable_gdp = False # Disable GDP by default
78+
79+
def test_twofactor_available(self):
80+
"""Test pyotp availability detection"""
81+
# Positive case (assuming pyotp is installed)
82+
self.assertTrue(auth.twofactor_available(self.configuration))
83+
84+
# Simulate missing pyotp
85+
original_pyotp = auth.pyotp
86+
auth.pyotp = None
87+
self.assertFalse(auth.twofactor_available(self.configuration))
88+
auth.pyotp = original_pyotp
6189

6290
def test_get_twofactor_secrets_generates_valid_key(self):
6391
"""Test get_twofactor_secrets generates and stores valid key"""
@@ -165,16 +193,8 @@ def test_twofactor_session_lifecycle(self):
165193
'session should have correct TTL')
166194

167195
# Mimic cookie init
168-
cookie = http.cookies.SimpleCookie()
169-
session_start = time.time()
170-
cookie['2FA_Auth'] = session_key
171-
cookie['2FA_Auth']['path'] = '/'
172-
# NOTE: SimpleCookie translates expires ttl to actual date from now
173-
cookie['2FA_Auth']['expires'] = twofactor_cookie_ttl
174-
cookie['2FA_Auth']['secure'] = True
175-
cookie['2FA_Auth']['httponly'] = True
196+
environ = self._mimic_cookie_init(session_key)
176197

177-
environ = {'HTTP_COOKIE': cookie}
178198
# Expire session
179199
expire_result = auth.expire_twofactor_session(self.configuration,
180200
TEST_USER_DN, environ)
@@ -184,6 +204,169 @@ def test_twofactor_session_lifecycle(self):
184204
self.assertNotIn(session_key, sessions_after,
185205
'session should be removed')
186206

207+
def test_load_twofactor_key_missing(self):
208+
"""Test handling of missing twofactor key"""
209+
self._provision_test_user(self, TEST_USER_DN)
210+
result = auth.load_twofactor_key(TEST_USER_DN, self.configuration,
211+
allow_missing=True)
212+
self.assertIsNone(result, 'should return None for missing key')
213+
214+
def test_generate_session_prefix(self):
215+
"""Test session prefix generation"""
216+
prefix = auth.generate_session_prefix(self.configuration, TEST_USER_DN)
217+
self.assertEqual(prefix, TEST_CLIENT_PREFIX)
218+
219+
def test_gdp_client_id_transformation(self):
220+
"""Test GDP client ID normalization"""
221+
self.configuration.site_enable_gdp = True
222+
from mig.shared.gdp.all import get_base_client_id
223+
base_dn = get_base_client_id(self.configuration, GDP_USER_DN,
224+
expand_oid_alias=False)
225+
226+
# With GDP enabled, ID should be transformed to base project
227+
session_key = auth.generate_session_key(self.configuration,
228+
GDP_USER_DN)
229+
base_prefix = auth.generate_session_prefix(self.configuration, base_dn)
230+
self.assertTrue(session_key.startswith(base_prefix))
231+
232+
def test_client_twofactor_session_parsing(self):
233+
"""Test cookie parsing for session ID extraction"""
234+
cookie = http.cookies.SimpleCookie()
235+
cookie['2FA_Auth'] = 'test_session_id'
236+
environ = {'HTTP_COOKIE': cookie.output(header='')}
237+
238+
session_id = auth.client_twofactor_session(self.configuration,
239+
TEST_USER_DN, environ)
240+
self.assertEqual(session_id, 'test_session_id',
241+
'should extract session ID from cookies')
242+
243+
def test_multiple_active_sessions(self):
244+
"""Test handling of multiple concurrent sessions"""
245+
client_dir = self._provision_test_user(self, TEST_USER_DN)
246+
247+
# Create 3 sessions from different addresses
248+
sessions = []
249+
for addr in ['192.168.0.1', '10.0.0.1', '172.16.0.1']:
250+
session_key = auth.generate_session_key(self.configuration,
251+
TEST_USER_DN)
252+
auth.save_twofactor_session(
253+
self.configuration, TEST_USER_DN, session_key,
254+
addr, 'TestAgent', time.time()
255+
)
256+
sessions.append(session_key)
257+
258+
# List all sessions
259+
all_sessions = auth.list_twofactor_sessions(self.configuration,
260+
TEST_USER_DN)
261+
self.assertEqual(len(all_sessions), 3,
262+
'should list all active sessions')
263+
264+
# Filter by address
265+
filtered = auth.list_twofactor_sessions(
266+
self.configuration, TEST_USER_DN, user_addr='10.0.0.1')
267+
self.assertEqual(len(filtered), 1,
268+
'should filter sessions by address')
269+
270+
def test_expired_session_cleanup(self):
271+
"""Test automatic exclusion of expired sessions"""
272+
client_dir = self._provision_test_user(self, TEST_USER_DN)
273+
valid_key = auth.generate_session_key(self.configuration,
274+
TEST_USER_DN)
275+
expired_key = auth.generate_session_key(self.configuration,
276+
TEST_USER_DN)
277+
278+
# Create valid session (expires future)
279+
auth.save_twofactor_session(
280+
self.configuration, TEST_USER_DN, valid_key,
281+
'127.0.0.1', 'TestAgent', time.time()
282+
)
283+
284+
# Create expired session
285+
auth.save_twofactor_session(
286+
self.configuration, TEST_USER_DN, expired_key,
287+
'127.0.0.1', 'TestAgent', time.time() - twofactor_cookie_ttl - 100
288+
)
289+
290+
# Only valid session should appear in active listings
291+
active = auth.active_twofactor_session(
292+
self.configuration, TEST_USER_DN, '127.0.0.1')
293+
self.assertEqual(active['session_key'], valid_key,
294+
'should only return active sessions')
295+
296+
def test_custom_token_interval(self):
297+
"""Test token verification with custom interval"""
298+
self._provision_test_user(self, TEST_USER_DN)
299+
300+
# Save custom interval
301+
client_dir = client_id_dir(TEST_USER_DN)
302+
interval_path = os.path.join(
303+
self.configuration.user_settings,
304+
client_dir,
305+
'twofactor_interval'
306+
)
307+
with open(interval_path, 'w') as fh:
308+
fh.write('60') # 1 minute interval
309+
310+
# Generate key with custom interval
311+
b32_key = auth.reset_twofactor_key(
312+
TEST_USER_DN, self.configuration, interval=60)
313+
314+
totp = auth.get_totp(TEST_USER_DN, b32_key, self.configuration)
315+
valid_token = totp.now()
316+
317+
# Verify works with custom interval
318+
result = auth.verify_twofactor_token(
319+
self.configuration, TEST_USER_DN, b32_key, valid_token)
320+
self.assertTrue(result, 'should accept token with custom interval')
321+
322+
def test_strict_address_session_handling(self):
323+
"""Test session handling with strict address enforcement"""
324+
self.configuration.site_twofactor_strict_address = True
325+
session_key = auth.generate_session_key(self.configuration,
326+
TEST_USER_DN)
327+
user_addr = '192.168.1.100'
328+
329+
auth.save_twofactor_session(
330+
self.configuration, TEST_USER_DN, session_key,
331+
user_addr, 'TestAgent', time.time()
332+
)
333+
334+
# Should have address-linked file
335+
addr_file = os.path.join(
336+
self.configuration.twofactor_home,
337+
f"{user_addr}_{session_key}"
338+
)
339+
self.assertTrue(os.path.exists(addr_file),
340+
'should create address-linked session file')
341+
342+
# Mimic cookie init
343+
environ = self._mimic_cookie_init(session_key)
344+
345+
# Expire should remove both files
346+
auth.expire_twofactor_session(self.configuration, TEST_USER_DN,
347+
environ, user_addr=user_addr)
348+
self.assertFalse(os.path.exists(addr_file),
349+
'should remove address-linked session file')
350+
self.assertFalse(os.path.exists(
351+
os.path.join(self.configuration.twofactor_home, session_key)
352+
), 'should remove main session file')
353+
354+
def test_check_twofactor_active_missing_session(self):
355+
"""Test session check when session cookie is missing"""
356+
self._provision_test_user(self, TEST_USER_DN)
357+
# Empty environment
358+
result = auth.check_twofactor_active(self.configuration,
359+
TEST_USER_DN, '127.0.0.1', {})
360+
self.assertFalse(result, 'should reject missing session')
361+
362+
# Invalid cookie
363+
cookie = http.cookies.SimpleCookie()
364+
cookie['invalid'] = 'value'
365+
environ = {'HTTP_COOKIE': cookie.output(header='')}
366+
result = auth.check_twofactor_active(self.configuration,
367+
TEST_USER_DN, '127.0.0.1', environ)
368+
self.assertFalse(result, 'should reject invalid session cookie')
369+
187370

188371
if __name__ == '__main__':
189372
testmain()

0 commit comments

Comments
 (0)