Skip to content

Commit bd23c3d

Browse files
Merge pull request #497 from supertokens/session-related-changes
Session related changes
2 parents e1650b8 + d77067a commit bd23c3d

File tree

12 files changed

+244
-20
lines changed

12 files changed

+244
-20
lines changed

.vscode/settings.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@
22
"python.linting.pylintEnabled": true,
33
"python.linting.enabled": true,
44
"editor.codeActionsOnSave": {
5-
"source.organizeImports": true
5+
"source.organizeImports": "explicit"
66
},
77
"python.analysis.typeCheckingMode": "strict",
88
"python.testing.unittestEnabled": false,
99
"python.testing.pytestEnabled": true,
10-
"python.analysis.autoImportCompletions": true
10+
"python.analysis.autoImportCompletions": true,
11+
"circleci.persistedProjectSelection": []
1112
}

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88

99
## [unreleased]
1010

11+
## [0.19.0] - 2024-05-06
12+
13+
- `create_new_session` now defaults to the value of the `st-auth-mode` header (if available) if the configured `get_token_transfer_method` returns `any`.
14+
- Enable smooth switching between `use_dynamic_access_token_signing_key` settings by allowing refresh calls to change the signing key type of a session.
15+
16+
### Breaking change:
17+
- A session is not required when calling the sign out API. Otherwise the API will return a 401 error.
18+
1119
## [0.18.11] - 2024-04-26
1220

1321
- Fixes issues with the propagation of session creation/updates with django-rest-framework because the django-rest-framework wrapped the original request with it's own request object. Updates on that object were not reflecting on the original request object.

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@
8282

8383
setup(
8484
name="supertokens_python",
85-
version="0.18.11",
85+
version="0.19.0",
8686
author="SuperTokens",
8787
license="Apache 2.0",
8888
author_email="[email protected]",

supertokens_python/constants.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from __future__ import annotations
1515

1616
SUPPORTED_CDI_VERSIONS = ["3.0"]
17-
VERSION = "0.18.11"
17+
VERSION = "0.19.0"
1818
TELEMETRY = "/telemetry"
1919
USER_COUNT = "/users/count"
2020
USER_DELETE = "/user/remove"

supertokens_python/recipe/session/api/signout.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ async def handle_signout_api(
4343
api_options.request,
4444
api_options.config,
4545
api_options.recipe_implementation,
46-
session_required=False,
46+
session_required=True,
4747
override_global_claim_validators=lambda _, __, ___: [],
4848
user_context=user_context,
4949
)

supertokens_python/recipe/session/recipe_implementation.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,7 @@ async def refresh_session(
297297
refresh_token,
298298
anti_csrf_token,
299299
disable_anti_csrf,
300+
self.config.use_dynamic_access_token_signing_key,
300301
user_context=user_context,
301302
)
302303

supertokens_python/recipe/session/session_functions.py

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@ async def get_session(
218218
)
219219

220220
raise_try_refresh_token_exception(
221-
"The access token doesn't match the useDynamicAccessTokenSigningKey setting"
221+
"The access token doesn't match the use_dynamic_access_token_signing_key setting"
222222
)
223223

224224
# If we get here we either have a V2 token that doesn't pass verification or a valid V3> token
@@ -308,13 +308,15 @@ async def get_session(
308308
response["session"].get("tenantId")
309309
or (access_token_info or {}).get("tenantId"),
310310
),
311-
GetSessionAPIResponseAccessToken(
312-
response["accessToken"]["token"],
313-
response["accessToken"]["expiry"],
314-
response["accessToken"]["createdTime"],
315-
)
316-
if "accessToken" in response
317-
else None,
311+
(
312+
GetSessionAPIResponseAccessToken(
313+
response["accessToken"]["token"],
314+
response["accessToken"]["expiry"],
315+
response["accessToken"]["createdTime"],
316+
)
317+
if "accessToken" in response
318+
else None
319+
),
318320
)
319321
if response["status"] == "UNAUTHORISED":
320322
log_debug_message("getSession: Returning UNAUTHORISED because of core response")
@@ -331,6 +333,7 @@ async def refresh_session(
331333
refresh_token: str,
332334
anti_csrf_token: Union[str, None],
333335
disable_anti_csrf: bool,
336+
use_dynamic_access_token_signing_key: bool,
334337
user_context: Optional[Dict[str, Any]],
335338
) -> CreateOrRefreshAPIResponse:
336339
data = {
@@ -339,6 +342,7 @@ async def refresh_session(
339342
not disable_anti_csrf
340343
and recipe_implementation.config.anti_csrf_function_or_string == "VIA_TOKEN"
341344
),
345+
"useDynamicSigningKey": use_dynamic_access_token_signing_key,
342346
}
343347

344348
if anti_csrf_token is not None:

supertokens_python/recipe/session/session_request_functions.py

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
SessionConfig,
5151
TokenTransferMethod,
5252
get_required_claim_validators,
53+
get_auth_mode_from_header,
5354
)
5455
from supertokens_python.types import MaybeAwaitable
5556
from supertokens_python.utils import (
@@ -179,9 +180,11 @@ async def get_session_from_request(
179180
log_debug_message("getSession: Value of antiCsrfToken is: %s", do_anti_csrf_check)
180181

181182
session = await recipe_interface_impl.get_session(
182-
access_token=request_access_token.raw_token_string
183-
if request_access_token is not None
184-
else None,
183+
access_token=(
184+
request_access_token.raw_token_string
185+
if request_access_token is not None
186+
else None
187+
),
185188
anti_csrf_token=anti_csrf_token,
186189
anti_csrf_check=do_anti_csrf_check,
187190
session_required=session_required,
@@ -229,6 +232,7 @@ async def create_new_session_in_request(
229232
) -> SessionContainer:
230233
log_debug_message("createNewSession: Started")
231234

235+
# Handling framework specific request/response wrapping
232236
if not hasattr(request, "wrapper_used") or not request.wrapper_used:
233237
request = FRAMEWORKS[
234238
Supertokens.get_instance().app_info.framework
@@ -238,7 +242,6 @@ async def create_new_session_in_request(
238242
user_context = set_request_in_user_context_if_not_defined(user_context, request)
239243

240244
claims_added_by_other_recipes = recipe_instance.get_claims_added_by_other_recipes()
241-
app_info = recipe_instance.app_info
242245
issuer = (
243246
app_info.api_domain.get_as_string_dangerous()
244247
+ app_info.api_base_path.get_as_string_dangerous()
@@ -252,15 +255,20 @@ async def create_new_session_in_request(
252255

253256
for claim in claims_added_by_other_recipes:
254257
update = await claim.build(user_id, tenant_id, user_context)
255-
final_access_token_payload = {**final_access_token_payload, **update}
258+
final_access_token_payload.update(update)
256259

257260
log_debug_message("createNewSession: Access token payload built")
258261

259262
output_transfer_method = config.get_token_transfer_method(
260263
request, True, user_context
261264
)
262265
if output_transfer_method == "any":
263-
output_transfer_method = "header"
266+
auth_mode_header = get_auth_mode_from_header(request)
267+
if auth_mode_header == "cookie":
268+
output_transfer_method = auth_mode_header
269+
else:
270+
output_transfer_method = "header"
271+
264272
log_debug_message(
265273
"createNewSession: using transfer method %s", output_transfer_method
266274
)

tests/sessions/test_auth_mode.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,91 @@ def check_extracted_info(
162162
assert False, "Invalid expected_transfer_method"
163163

164164

165+
@mark.asyncio
166+
async def test_use_headers_if_get_token_transfer_method_returns_any_and_no_st_auth_mode_header(
167+
app: TestClient,
168+
):
169+
init(
170+
**get_st_init_args(
171+
[
172+
session.init(
173+
anti_csrf="VIA_TOKEN",
174+
get_token_transfer_method=lambda _, __, ___: "any", # Always return "any"
175+
)
176+
]
177+
)
178+
)
179+
start_st()
180+
181+
# Create session without specifying st-auth-mode
182+
res = create_session(app)
183+
184+
# Assert that no tokens are set in the cookies
185+
assert res.get("accessToken") is None
186+
assert res.get("refreshToken") is None
187+
assert res.get("antiCsrf") is None
188+
189+
# Assert that tokens are set in the headers
190+
assert res.get("accessTokenFromHeader") is not None
191+
assert res.get("refreshTokenFromHeader") is not None
192+
193+
194+
@mark.asyncio
195+
async def test_should_use_cookies_if_get_token_transfer_method_returns_any_and_st_auth_mode_is_set_to_cookie(
196+
app: TestClient,
197+
):
198+
init(
199+
**get_st_init_args(
200+
[
201+
session.init(
202+
anti_csrf="VIA_TOKEN",
203+
get_token_transfer_method=lambda _, __, ___: "any", # Always returns "any"
204+
)
205+
]
206+
)
207+
)
208+
start_st()
209+
210+
# Creating session with st-auth-mode set to 'cookie'
211+
res = create_session(app, auth_mode_header="cookie")
212+
213+
# Checking that the tokens are not set in headers
214+
assert res.get("accessToken") is not None
215+
assert res.get("refreshToken") is not None
216+
assert res.get("antiCsrf") is not None
217+
assert res.get("accessTokenFromHeader") is None
218+
assert res.get("refreshTokenFromHeader") is None
219+
220+
221+
@mark.asyncio
222+
async def test_use_headers_if_get_token_transfer_method_returns_any_and_st_auth_mode_is_set_to_header(
223+
app: TestClient,
224+
):
225+
init(
226+
**get_st_init_args(
227+
[
228+
session.init(
229+
anti_csrf="VIA_TOKEN",
230+
get_token_transfer_method=lambda _, __, ___: "any", # Always returns "any"
231+
)
232+
]
233+
)
234+
)
235+
start_st()
236+
237+
# Creating session with st-auth-mode set to 'header'
238+
res = create_session(app, auth_mode_header="header")
239+
240+
# Assert that no tokens are set in the cookies
241+
assert res.get("accessToken") is None
242+
assert res.get("refreshToken") is None
243+
assert res.get("antiCsrf") is None
244+
245+
# Assert that tokens are set in the headers
246+
assert res.get("accessTokenFromHeader") is not None
247+
assert res.get("refreshTokenFromHeader") is not None
248+
249+
165250
@mark.parametrize(
166251
"auth_mode_header, expected_transfer_method",
167252
[

tests/sessions/test_jwks.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -698,5 +698,5 @@ async def test_session_verification_of_jwt_with_dynamic_signing_key_mode_works_a
698698
except TryRefreshTokenError as e:
699699
assert (
700700
str(e)
701-
== "The access token doesn't match the useDynamicAccessTokenSigningKey setting"
701+
== "The access token doesn't match the use_dynamic_access_token_signing_key setting"
702702
)
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import pytest
2+
from supertokens_python import init
3+
from supertokens_python.recipe import session
4+
from supertokens_python.recipe.session.asyncio import (
5+
create_new_session_without_request_response,
6+
get_session_without_request_response,
7+
refresh_session_without_request_response,
8+
)
9+
from supertokens_python.recipe.session.session_class import SessionContainer
10+
from tests.utils import (
11+
get_st_init_args,
12+
setup_function,
13+
start_st,
14+
teardown_function,
15+
reset,
16+
)
17+
from supertokens_python.recipe.session.jwt import (
18+
parse_jwt_without_signature_verification,
19+
)
20+
from supertokens_python.recipe.session.interfaces import GetSessionTokensDangerouslyDict
21+
22+
_ = setup_function # type:ignore
23+
_ = teardown_function # type:ignore
24+
25+
pytestmark = pytest.mark.asyncio
26+
27+
28+
async def test_dynamic_key_switching():
29+
init(**get_st_init_args([session.init(use_dynamic_access_token_signing_key=True)]))
30+
start_st()
31+
32+
# Create a new session without an actual HTTP request-response flow
33+
create_res: SessionContainer = await create_new_session_without_request_response(
34+
"public", "test-user-id", {"tokenProp": True}, {"dbProp": True}
35+
)
36+
37+
# Extract session tokens for further testing
38+
tokens = create_res.get_all_session_tokens_dangerously()
39+
check_access_token_signing_key_type(tokens, True)
40+
41+
# Reset and reinitialize with dynamic signing key disabled
42+
reset(stop_core=False)
43+
init(**get_st_init_args([session.init(use_dynamic_access_token_signing_key=False)]))
44+
45+
caught_exception = None
46+
try:
47+
# Attempt to retrieve the session using previously obtained tokens
48+
await get_session_without_request_response(
49+
tokens["accessToken"], tokens["antiCsrfToken"]
50+
)
51+
except Exception as e:
52+
caught_exception = e
53+
54+
# Check for the expected exception due to token signing key mismatch
55+
assert (
56+
caught_exception is not None
57+
), "Expected an exception to be thrown, but none was."
58+
assert (
59+
str(caught_exception)
60+
== "The access token doesn't match the use_dynamic_access_token_signing_key setting"
61+
), f"Unexpected exception message: {str(caught_exception)}"
62+
63+
64+
async def test_refresh_session():
65+
init(**get_st_init_args([session.init(use_dynamic_access_token_signing_key=True)]))
66+
start_st()
67+
68+
# Create a new session without an actual HTTP request-response flow
69+
create_res: SessionContainer = await create_new_session_without_request_response(
70+
"public", "test-user-id", {"tokenProp": True}, {"dbProp": True}
71+
)
72+
73+
# Extract session tokens for further testing
74+
tokens = create_res.get_all_session_tokens_dangerously()
75+
check_access_token_signing_key_type(tokens, True)
76+
77+
# Reset and reinitialize with dynamic signing key disabled
78+
reset(stop_core=False)
79+
init(**get_st_init_args([session.init(use_dynamic_access_token_signing_key=False)]))
80+
81+
assert tokens["refreshToken"] is not None
82+
83+
# Refresh session
84+
refreshed_session = await refresh_session_without_request_response(
85+
tokens["refreshToken"], True, tokens["antiCsrfToken"]
86+
)
87+
tokens_after_refresh = refreshed_session.get_all_session_tokens_dangerously()
88+
assert tokens_after_refresh["accessAndFrontTokenUpdated"] is True
89+
check_access_token_signing_key_type(tokens_after_refresh, False)
90+
91+
# Verify session after refresh
92+
verified_session = await get_session_without_request_response(
93+
tokens_after_refresh["accessToken"], tokens_after_refresh["antiCsrfToken"]
94+
)
95+
assert verified_session is not None
96+
tokens_after_verify = verified_session.get_all_session_tokens_dangerously()
97+
assert tokens_after_verify["accessAndFrontTokenUpdated"] is True
98+
check_access_token_signing_key_type(tokens_after_verify, False)
99+
100+
# Verify session again
101+
verified2_session = await get_session_without_request_response(
102+
tokens_after_verify["accessToken"], tokens_after_verify["antiCsrfToken"]
103+
)
104+
assert verified2_session is not None
105+
tokens_after_verify2 = verified2_session.get_all_session_tokens_dangerously()
106+
assert tokens_after_verify2["accessAndFrontTokenUpdated"] is False
107+
108+
109+
def check_access_token_signing_key_type(
110+
tokens: GetSessionTokensDangerouslyDict, is_dynamic: bool
111+
):
112+
info = parse_jwt_without_signature_verification(tokens["accessToken"])
113+
if is_dynamic:
114+
assert info.kid is not None and info.kid.startswith("d-")
115+
else:
116+
assert info.kid is not None and info.kid.startswith("s-")

tests/test_session.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ async def test_that_once_the_info_is_loaded_it_doesnt_query_again():
119119
response.refreshToken.token,
120120
response.antiCsrfToken,
121121
False,
122+
True,
122123
None,
123124
)
124125

0 commit comments

Comments
 (0)