@@ -69,16 +69,6 @@ def test_not_same_origin(mock_redirect_handler):
6969 assert not mock_redirect_handler ._same_origin (origin1 , origin2 )
7070
7171
72- def test_is_https_redirect (mock_redirect_handler ):
73- url = httpx .URL ("http://example.com" )
74- location = httpx .URL (BASE_URL )
75- assert mock_redirect_handler .is_https_redirect (url , location )
76-
77-
78- def test_is_not_https_redirect (mock_redirect_handler ):
79- url = httpx .URL (BASE_URL )
80- location = httpx .URL ("http://www.example.com" )
81- assert not mock_redirect_handler .is_https_redirect (url , location )
8272
8373
8474@pytest .mark .asyncio
@@ -281,3 +271,208 @@ def request_handler(request: httpx.Request):
281271 with pytest .raises (Exception ) as e :
282272 await handler .send (request , mock_transport )
283273 assert "Too many redirects" in str (e .value )
274+
275+
276+ @pytest .mark .asyncio
277+ async def test_redirect_cross_host_removes_auth_and_cookie ():
278+ """Test that cross-host redirects remove both Authorization and Cookie headers"""
279+
280+ def request_handler (request : httpx .Request ):
281+ if request .url == "https://other.example.com/api" :
282+ return httpx .Response (200 , )
283+ return httpx .Response (
284+ MOVED_PERMANENTLY ,
285+ headers = {LOCATION_HEADER : "https://other.example.com/api" },
286+ )
287+
288+ handler = RedirectHandler ()
289+ request = httpx .Request (
290+ 'GET' ,
291+ BASE_URL ,
292+ headers = {
293+ AUTHORIZATION_HEADER : "Bearer token" ,
294+ "Cookie" : "session=SECRET"
295+ },
296+ )
297+ mock_transport = httpx .MockTransport (request_handler )
298+ resp = await handler .send (request , mock_transport )
299+ assert resp .status_code == 200
300+ assert AUTHORIZATION_HEADER not in resp .request .headers
301+ assert "Cookie" not in resp .request .headers
302+
303+
304+ @pytest .mark .asyncio
305+ async def test_redirect_scheme_change_removes_auth_and_cookie ():
306+ """Test that scheme changes remove both Authorization and Cookie headers"""
307+
308+ def request_handler (request : httpx .Request ):
309+ if request .url == "http://example.com/api" :
310+ return httpx .Response (200 , )
311+ return httpx .Response (
312+ MOVED_PERMANENTLY ,
313+ headers = {LOCATION_HEADER : "http://example.com/api" },
314+ )
315+
316+ handler = RedirectHandler (RedirectHandlerOption (allow_redirect_on_scheme_change = True ))
317+ request = httpx .Request (
318+ 'GET' ,
319+ BASE_URL ,
320+ headers = {
321+ AUTHORIZATION_HEADER : "Bearer token" ,
322+ "Cookie" : "session=SECRET"
323+ },
324+ )
325+ mock_transport = httpx .MockTransport (request_handler )
326+ resp = await handler .send (request , mock_transport )
327+ assert resp .status_code == 200
328+ assert AUTHORIZATION_HEADER not in resp .request .headers
329+ assert "Cookie" not in resp .request .headers
330+
331+
332+ @pytest .mark .asyncio
333+ async def test_redirect_same_host_and_scheme_keeps_all_headers ():
334+ """Test that same-host and same-scheme redirects keep Authorization and Cookie headers"""
335+
336+ def request_handler (request : httpx .Request ):
337+ if request .url == f"{ BASE_URL } /v2/api" :
338+ return httpx .Response (200 , )
339+ return httpx .Response (
340+ MOVED_PERMANENTLY ,
341+ headers = {LOCATION_HEADER : f"{ BASE_URL } /v2/api" },
342+ )
343+
344+ handler = RedirectHandler ()
345+ request = httpx .Request (
346+ 'GET' ,
347+ f"{ BASE_URL } /v1/api" ,
348+ headers = {
349+ AUTHORIZATION_HEADER : "Bearer token" ,
350+ "Cookie" : "session=SECRET"
351+ },
352+ )
353+ mock_transport = httpx .MockTransport (request_handler )
354+ resp = await handler .send (request , mock_transport )
355+ assert resp .status_code == 200
356+ assert AUTHORIZATION_HEADER in resp .request .headers
357+ assert resp .request .headers [AUTHORIZATION_HEADER ] == "Bearer token"
358+ assert "Cookie" in resp .request .headers
359+ assert resp .request .headers ["Cookie" ] == "session=SECRET"
360+
361+
362+ @pytest .mark .asyncio
363+ async def test_redirect_with_custom_scrubber ():
364+ """Test that custom scrubber can be provided and is used"""
365+
366+ def custom_scrubber (headers , original_url , new_url ):
367+ # Custom logic: never remove headers
368+ pass
369+
370+ def request_handler (request : httpx .Request ):
371+ if request .url == "https://evil.attacker.com/steal" :
372+ return httpx .Response (200 , )
373+ return httpx .Response (
374+ MOVED_PERMANENTLY ,
375+ headers = {LOCATION_HEADER : "https://evil.attacker.com/steal" },
376+ )
377+
378+ options = RedirectHandlerOption (scrub_sensitive_headers = custom_scrubber )
379+ handler = RedirectHandler (options )
380+ request = httpx .Request (
381+ 'GET' ,
382+ BASE_URL ,
383+ headers = {
384+ AUTHORIZATION_HEADER : "Bearer token" ,
385+ "Cookie" : "session=SECRET"
386+ },
387+ )
388+ mock_transport = httpx .MockTransport (request_handler )
389+ resp = await handler .send (request , mock_transport )
390+ assert resp .status_code == 200
391+ # Headers should be kept because custom scrubber doesn't remove them
392+ assert AUTHORIZATION_HEADER in resp .request .headers
393+ assert "Cookie" in resp .request .headers
394+
395+
396+ def test_default_scrub_sensitive_headers_removes_on_host_change ():
397+ """Test that default scrubber removes Authorization and Cookie when host changes"""
398+ from kiota_http .middleware .options .redirect_handler_option import default_scrub_sensitive_headers
399+
400+ headers = httpx .Headers ({
401+ AUTHORIZATION_HEADER : "Bearer token" ,
402+ "Cookie" : "session=SECRET" ,
403+ "Content-Type" : "application/json"
404+ })
405+ original_url = httpx .URL ("https://example.com/v1/api" )
406+ new_url = httpx .URL ("https://other.com/api" )
407+
408+ default_scrub_sensitive_headers (headers , original_url , new_url )
409+
410+ assert AUTHORIZATION_HEADER not in headers
411+ assert "Cookie" not in headers
412+ assert "Content-Type" in headers # Other headers should remain
413+
414+
415+ def test_default_scrub_sensitive_headers_removes_on_scheme_change ():
416+ """Test that default scrubber removes Authorization and Cookie when scheme changes"""
417+ from kiota_http .middleware .options .redirect_handler_option import default_scrub_sensitive_headers
418+
419+ headers = httpx .Headers ({
420+ AUTHORIZATION_HEADER : "Bearer token" ,
421+ "Cookie" : "session=SECRET" ,
422+ "Content-Type" : "application/json"
423+ })
424+ original_url = httpx .URL ("https://example.com/v1/api" )
425+ new_url = httpx .URL ("http://example.com/v1/api" )
426+
427+ default_scrub_sensitive_headers (headers , original_url , new_url )
428+
429+ assert AUTHORIZATION_HEADER not in headers
430+ assert "Cookie" not in headers
431+ assert "Content-Type" in headers
432+
433+
434+ def test_default_scrub_sensitive_headers_keeps_on_same_origin ():
435+ """Test that default scrubber keeps headers when host and scheme are the same"""
436+ from kiota_http .middleware .options .redirect_handler_option import default_scrub_sensitive_headers
437+
438+ headers = httpx .Headers ({
439+ AUTHORIZATION_HEADER : "Bearer token" ,
440+ "Cookie" : "session=SECRET" ,
441+ "Content-Type" : "application/json"
442+ })
443+ original_url = httpx .URL ("https://example.com/v1/api" )
444+ new_url = httpx .URL ("https://example.com/v2/api" )
445+
446+ default_scrub_sensitive_headers (headers , original_url , new_url )
447+
448+ assert AUTHORIZATION_HEADER in headers
449+ assert "Cookie" in headers
450+ assert "Content-Type" in headers
451+
452+
453+ def test_default_scrub_sensitive_headers_handles_none_gracefully ():
454+ """Test that default scrubber handles None/empty inputs gracefully"""
455+ from kiota_http .middleware .options .redirect_handler_option import default_scrub_sensitive_headers
456+
457+ # Should not raise exceptions
458+ default_scrub_sensitive_headers (None , httpx .URL (BASE_URL ), httpx .URL (BASE_URL ))
459+ default_scrub_sensitive_headers (httpx .Headers (), None , httpx .URL (BASE_URL ))
460+ default_scrub_sensitive_headers (httpx .Headers (), httpx .URL (BASE_URL ), None )
461+
462+
463+ def test_custom_scrub_sensitive_headers ():
464+ """Test that custom scrubber can be set on options"""
465+ def custom_scrubber (headers , original_url , new_url ):
466+ # Custom logic
467+ pass
468+
469+ options = RedirectHandlerOption (scrub_sensitive_headers = custom_scrubber )
470+ assert options .scrub_sensitive_headers == custom_scrubber
471+
472+
473+ def test_default_options_uses_default_scrubber ():
474+ """Test that default options use the default scrubber"""
475+ from kiota_http .middleware .options .redirect_handler_option import default_scrub_sensitive_headers
476+
477+ options = RedirectHandlerOption ()
478+ assert options .scrub_sensitive_headers == default_scrub_sensitive_headers
0 commit comments