4242
4343TEST_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
4750class 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
188371if __name__ == '__main__' :
189372 testmain ()
0 commit comments