@@ -46,6 +46,11 @@ <h1 class="title">Module <code>supertokens_python.recipe.session.cookie_and_head
46
46
47
47
from typing_extensions import Literal
48
48
49
+ from supertokens_python.recipe.session.exceptions import (
50
+ raise_clear_duplicate_session_cookies_exception,
51
+ )
52
+ from supertokens_python.recipe.session.interfaces import ResponseMutator
53
+
49
54
from .constants import (
50
55
ACCESS_CONTROL_EXPOSE_HEADERS,
51
56
ACCESS_TOKEN_COOKIE_KEY,
@@ -139,9 +144,9 @@ <h1 class="title">Module <code>supertokens_python.recipe.session.cookie_and_head
139
144
expires: int,
140
145
path_type: Literal["refresh_token_path", "access_token_path"],
141
146
request: BaseRequest,
147
+ domain: Optional[str],
142
148
user_context: Dict[str, Any],
143
149
):
144
- domain = config.cookie_domain
145
150
secure = config.cookie_secure
146
151
same_site = config.get_cookie_same_site(request, user_context)
147
152
path = ""
@@ -169,10 +174,21 @@ <h1 class="title">Module <code>supertokens_python.recipe.session.cookie_and_head
169
174
expires: int,
170
175
path_type: Literal["refresh_token_path", "access_token_path"],
171
176
request: BaseRequest,
177
+ domain: Optional[str] = None,
172
178
):
179
+ domain = domain if domain is not None else config.cookie_domain
180
+
173
181
def mutator(response: BaseResponse, user_context: Dict[str, Any]):
174
182
return _set_cookie(
175
- response, config, key, value, expires, path_type, request, user_context
183
+ response,
184
+ config,
185
+ key,
186
+ value,
187
+ expires,
188
+ path_type,
189
+ request,
190
+ domain,
191
+ user_context,
176
192
)
177
193
178
194
return mutator
@@ -322,6 +338,7 @@ <h1 class="title">Module <code>supertokens_python.recipe.session.cookie_and_head
322
338
expires,
323
339
"refresh_token_path" if token_type == "refresh" else "access_token_path",
324
340
request,
341
+ config.cookie_domain,
325
342
user_context,
326
343
)
327
344
elif transfer_method == "header":
@@ -425,7 +442,97 @@ <h1 class="title">Module <code>supertokens_python.recipe.session.cookie_and_head
425
442
"header",
426
443
request,
427
444
user_context,
428
- )</ code > </ pre >
445
+ )
446
+
447
+
448
+ # This function addresses an edge case where changing the cookie_domain config on the server can
449
+ # lead to session integrity issues. For instance, if the API server URL is 'api.example.com'
450
+ # with a cookie domain of '.example.com', and the server updates the cookie domain to 'api.example.com',
451
+ # the client may retain cookies with both '.example.com' and 'api.example.com' domains.
452
+
453
+ # Consequently, if the server chooses the older cookie, session invalidation occurs, potentially
454
+ # resulting in an infinite refresh loop. To fix this, users are asked to specify "older_cookie_domain" in
455
+ # the config.
456
+
457
+ # This function checks for multiple cookies with the same name and clears the cookies for the older domain.
458
+ def clear_session_cookies_from_older_cookie_domain(
459
+ request: BaseRequest, config: SessionConfig, user_context: Dict[str, Any]
460
+ ):
461
+ allowed_transfer_method = config.get_token_transfer_method(
462
+ request, False, user_context
463
+ )
464
+ # If the transfer method is 'header', there's no need to clear cookies immediately, even if there are multiple in the request.
465
+ if allowed_transfer_method == "header":
466
+ return
467
+
468
+ did_clear_cookies = False
469
+ response_mutators: List[ResponseMutator] = []
470
+
471
+ token_types: List[TokenType] = ["access", "refresh"]
472
+ for token_type in token_types:
473
+ if has_multiple_cookies_for_token_type(request, token_type):
474
+ # If a request has multiple session cookies and 'older_cookie_domain' is
475
+ # unset, we can't identify the correct cookie for refreshing the session.
476
+ # Using the wrong cookie can cause an infinite refresh loop. To avoid this,
477
+ # we throw a 500 error asking the user to set 'older_cookie_domain''.
478
+ if config.older_cookie_domain is None:
479
+ raise Exception(
480
+ "The request contains multiple session cookies. This may happen if you've changed the 'cookie_domain' setting in your configuration. To clear tokens from the previous domain, set 'older_cookie_domain' in your config."
481
+ )
482
+
483
+ log_debug_message(
484
+ "Clearing duplicate %s cookie with domain %s",
485
+ token_type,
486
+ config.cookie_domain,
487
+ )
488
+ response_mutators.append(
489
+ set_cookie_response_mutator(
490
+ config,
491
+ get_cookie_name_from_token_type(token_type),
492
+ "",
493
+ 0,
494
+ "refresh_token_path"
495
+ if token_type == "refresh"
496
+ else "access_token_path",
497
+ request,
498
+ domain=config.older_cookie_domain,
499
+ )
500
+ )
501
+ did_clear_cookies = True
502
+ if did_clear_cookies:
503
+ raise_clear_duplicate_session_cookies_exception(
504
+ "The request contains multiple session cookies. We are clearing the cookie from older_cookie_domain. Session will be refreshed in the next refresh call.",
505
+ response_mutators=response_mutators,
506
+ )
507
+
508
+
509
+ def has_multiple_cookies_for_token_type(
510
+ request: BaseRequest, token_type: TokenType
511
+ ) -> bool:
512
+ cookie_string = request.get_header("cookie")
513
+ if cookie_string is None:
514
+ return False
515
+
516
+ cookies = _parse_cookie_string_from_request_header_allow_duplicates(cookie_string)
517
+ cookie_name = get_cookie_name_from_token_type(token_type)
518
+ return cookie_name in cookies and len(cookies[cookie_name]) > 1
519
+
520
+
521
+ def _parse_cookie_string_from_request_header_allow_duplicates(
522
+ cookie_string: str,
523
+ ) -> Dict[str, List[str]]:
524
+ cookies: Dict[str, List[str]] = {}
525
+ cookie_pairs = cookie_string.split(";")
526
+ for cookie_pair in cookie_pairs:
527
+ name_value = cookie_pair.split("=")
528
+ if len(name_value) != 2:
529
+ raise Exception("Invalid cookie string in request header")
530
+ name, value = unquote(name_value[0].strip()), unquote(name_value[1].strip())
531
+ if name in cookies:
532
+ cookies[name].append(value)
533
+ else:
534
+ cookies[name] = [value]
535
+ return cookies</ code > </ pre >
429
536
</ details >
430
537
</ section >
431
538
< section >
@@ -507,6 +614,66 @@ <h2 class="section-title" id="header-functions">Functions</h2>
507
614
)</ code > </ pre >
508
615
</ details >
509
616
</ dd >
617
+ < dt id ="supertokens_python.recipe.session.cookie_and_header.clear_session_cookies_from_older_cookie_domain "> < code class ="name flex ">
618
+ < span > def < span class ="ident "> clear_session_cookies_from_older_cookie_domain</ span > </ span > (< span > request: BaseRequest, config: SessionConfig, user_context: Dict[str, Any])</ span >
619
+ </ code > </ dt >
620
+ < dd >
621
+ < div class ="desc "> </ div >
622
+ < details class ="source ">
623
+ < summary >
624
+ < span > Expand source code</ span >
625
+ </ summary >
626
+ < pre > < code class ="python "> def clear_session_cookies_from_older_cookie_domain(
627
+ request: BaseRequest, config: SessionConfig, user_context: Dict[str, Any]
628
+ ):
629
+ allowed_transfer_method = config.get_token_transfer_method(
630
+ request, False, user_context
631
+ )
632
+ # If the transfer method is 'header', there's no need to clear cookies immediately, even if there are multiple in the request.
633
+ if allowed_transfer_method == "header":
634
+ return
635
+
636
+ did_clear_cookies = False
637
+ response_mutators: List[ResponseMutator] = []
638
+
639
+ token_types: List[TokenType] = ["access", "refresh"]
640
+ for token_type in token_types:
641
+ if has_multiple_cookies_for_token_type(request, token_type):
642
+ # If a request has multiple session cookies and 'older_cookie_domain' is
643
+ # unset, we can't identify the correct cookie for refreshing the session.
644
+ # Using the wrong cookie can cause an infinite refresh loop. To avoid this,
645
+ # we throw a 500 error asking the user to set 'older_cookie_domain''.
646
+ if config.older_cookie_domain is None:
647
+ raise Exception(
648
+ "The request contains multiple session cookies. This may happen if you've changed the 'cookie_domain' setting in your configuration. To clear tokens from the previous domain, set 'older_cookie_domain' in your config."
649
+ )
650
+
651
+ log_debug_message(
652
+ "Clearing duplicate %s cookie with domain %s",
653
+ token_type,
654
+ config.cookie_domain,
655
+ )
656
+ response_mutators.append(
657
+ set_cookie_response_mutator(
658
+ config,
659
+ get_cookie_name_from_token_type(token_type),
660
+ "",
661
+ 0,
662
+ "refresh_token_path"
663
+ if token_type == "refresh"
664
+ else "access_token_path",
665
+ request,
666
+ domain=config.older_cookie_domain,
667
+ )
668
+ )
669
+ did_clear_cookies = True
670
+ if did_clear_cookies:
671
+ raise_clear_duplicate_session_cookies_exception(
672
+ "The request contains multiple session cookies. We are clearing the cookie from older_cookie_domain. Session will be refreshed in the next refresh call.",
673
+ response_mutators=response_mutators,
674
+ )</ code > </ pre >
675
+ </ details >
676
+ </ dd >
510
677
< dt id ="supertokens_python.recipe.session.cookie_and_header.clear_session_from_all_token_transfer_methods "> < code class ="name flex ">
511
678
< span > def < span class ="ident "> clear_session_from_all_token_transfer_methods</ span > </ span > (< span > response: BaseResponse, recipe: SessionRecipe, request: BaseRequest, user_context: Dict[str, Any])</ span >
512
679
</ code > </ dt >
@@ -699,6 +866,27 @@ <h2 class="section-title" id="header-functions">Functions</h2>
699
866
raise Exception("Should never happen: Unknown transferMethod: " + transfer_method)</ code > </ pre >
700
867
</ details >
701
868
</ dd >
869
+ < dt id ="supertokens_python.recipe.session.cookie_and_header.has_multiple_cookies_for_token_type "> < code class ="name flex ">
870
+ < span > def < span class ="ident "> has_multiple_cookies_for_token_type</ span > </ span > (< span > request: BaseRequest, token_type: TokenType) ‑> bool</ span >
871
+ </ code > </ dt >
872
+ < dd >
873
+ < div class ="desc "> </ div >
874
+ < details class ="source ">
875
+ < summary >
876
+ < span > Expand source code</ span >
877
+ </ summary >
878
+ < pre > < code class ="python "> def has_multiple_cookies_for_token_type(
879
+ request: BaseRequest, token_type: TokenType
880
+ ) -> bool:
881
+ cookie_string = request.get_header("cookie")
882
+ if cookie_string is None:
883
+ return False
884
+
885
+ cookies = _parse_cookie_string_from_request_header_allow_duplicates(cookie_string)
886
+ cookie_name = get_cookie_name_from_token_type(token_type)
887
+ return cookie_name in cookies and len(cookies[cookie_name]) > 1</ code > </ pre >
888
+ </ details >
889
+ </ dd >
702
890
< dt id ="supertokens_python.recipe.session.cookie_and_header.remove_header "> < code class ="name flex ">
703
891
< span > def < span class ="ident "> remove_header</ span > </ span > (< span > response: BaseResponse, key: str)</ span >
704
892
</ code > </ dt >
@@ -714,7 +902,7 @@ <h2 class="section-title" id="header-functions">Functions</h2>
714
902
</ details >
715
903
</ dd >
716
904
< dt id ="supertokens_python.recipe.session.cookie_and_header.set_cookie_response_mutator "> < code class ="name flex ">
717
- < span > def < span class ="ident "> set_cookie_response_mutator</ span > </ span > (< span > config: SessionConfig, key: str, value: str, expires: int, path_type: "Literal['refresh_token_path', 'access_token_path']", request: BaseRequest)</ span >
905
+ < span > def < span class ="ident "> set_cookie_response_mutator</ span > </ span > (< span > config: SessionConfig, key: str, value: str, expires: int, path_type: "Literal['refresh_token_path', 'access_token_path']", request: BaseRequest, domain: Optional[str] = None )</ span >
718
906
</ code > </ dt >
719
907
< dd >
720
908
< div class ="desc "> </ div >
@@ -729,10 +917,21 @@ <h2 class="section-title" id="header-functions">Functions</h2>
729
917
expires: int,
730
918
path_type: Literal["refresh_token_path", "access_token_path"],
731
919
request: BaseRequest,
920
+ domain: Optional[str] = None,
732
921
):
922
+ domain = domain if domain is not None else config.cookie_domain
923
+
733
924
def mutator(response: BaseResponse, user_context: Dict[str, Any]):
734
925
return _set_cookie(
735
- response, config, key, value, expires, path_type, request, user_context
926
+ response,
927
+ config,
928
+ key,
929
+ value,
930
+ expires,
931
+ path_type,
932
+ request,
933
+ domain,
934
+ user_context,
736
935
)
737
936
738
937
return mutator</ code > </ pre >
@@ -828,6 +1027,7 @@ <h2>Index</h2>
828
1027
< li > < code > < a title ="supertokens_python.recipe.session.cookie_and_header.access_token_mutator " href ="#supertokens_python.recipe.session.cookie_and_header.access_token_mutator "> access_token_mutator</ a > </ code > </ li >
829
1028
< li > < code > < a title ="supertokens_python.recipe.session.cookie_and_header.anti_csrf_response_mutator " href ="#supertokens_python.recipe.session.cookie_and_header.anti_csrf_response_mutator "> anti_csrf_response_mutator</ a > </ code > </ li >
830
1029
< li > < code > < a title ="supertokens_python.recipe.session.cookie_and_header.build_front_token " href ="#supertokens_python.recipe.session.cookie_and_header.build_front_token "> build_front_token</ a > </ code > </ li >
1030
+ < li > < code > < a title ="supertokens_python.recipe.session.cookie_and_header.clear_session_cookies_from_older_cookie_domain " href ="#supertokens_python.recipe.session.cookie_and_header.clear_session_cookies_from_older_cookie_domain "> clear_session_cookies_from_older_cookie_domain</ a > </ code > </ li >
831
1031
< li > < code > < a title ="supertokens_python.recipe.session.cookie_and_header.clear_session_from_all_token_transfer_methods " href ="#supertokens_python.recipe.session.cookie_and_header.clear_session_from_all_token_transfer_methods "> clear_session_from_all_token_transfer_methods</ a > </ code > </ li >
832
1032
< li > < code > < a title ="supertokens_python.recipe.session.cookie_and_header.clear_session_mutator " href ="#supertokens_python.recipe.session.cookie_and_header.clear_session_mutator "> clear_session_mutator</ a > </ code > </ li >
833
1033
< li > < code > < a title ="supertokens_python.recipe.session.cookie_and_header.clear_session_response_mutator " href ="#supertokens_python.recipe.session.cookie_and_header.clear_session_response_mutator "> clear_session_response_mutator</ a > </ code > </ li >
@@ -838,6 +1038,7 @@ <h2>Index</h2>
838
1038
< li > < code > < a title ="supertokens_python.recipe.session.cookie_and_header.get_response_header_name_for_token_type " href ="#supertokens_python.recipe.session.cookie_and_header.get_response_header_name_for_token_type "> get_response_header_name_for_token_type</ a > </ code > </ li >
839
1039
< li > < code > < a title ="supertokens_python.recipe.session.cookie_and_header.get_rid_header " href ="#supertokens_python.recipe.session.cookie_and_header.get_rid_header "> get_rid_header</ a > </ code > </ li >
840
1040
< li > < code > < a title ="supertokens_python.recipe.session.cookie_and_header.get_token " href ="#supertokens_python.recipe.session.cookie_and_header.get_token "> get_token</ a > </ code > </ li >
1041
+ < li > < code > < a title ="supertokens_python.recipe.session.cookie_and_header.has_multiple_cookies_for_token_type " href ="#supertokens_python.recipe.session.cookie_and_header.has_multiple_cookies_for_token_type "> has_multiple_cookies_for_token_type</ a > </ code > </ li >
841
1042
< li > < code > < a title ="supertokens_python.recipe.session.cookie_and_header.remove_header " href ="#supertokens_python.recipe.session.cookie_and_header.remove_header "> remove_header</ a > </ code > </ li >
842
1043
< li > < code > < a title ="supertokens_python.recipe.session.cookie_and_header.set_cookie_response_mutator " href ="#supertokens_python.recipe.session.cookie_and_header.set_cookie_response_mutator "> set_cookie_response_mutator</ a > </ code > </ li >
843
1044
< li > < code > < a title ="supertokens_python.recipe.session.cookie_and_header.set_header " href ="#supertokens_python.recipe.session.cookie_and_header.set_header "> set_header</ a > </ code > </ li >
0 commit comments