Skip to content

Commit 515cad2

Browse files
committed
fix: Clear legacy sIdRefreshToken and fix other linter issues
1 parent efa73c1 commit 515cad2

File tree

14 files changed

+116
-73
lines changed

14 files changed

+116
-73
lines changed

supertokens_python/recipe/jwt/recipe.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,10 @@ async def handle_api_request(
9393
self.recipe_implementation,
9494
)
9595

96-
return await jwks_get(self.api_implementation, options)
96+
if request_id == GET_JWKS_API:
97+
return await jwks_get(self.api_implementation, options)
98+
99+
return None
97100

98101
async def handle_error(
99102
self, request: BaseRequest, err: SuperTokensError, response: BaseResponse

supertokens_python/recipe/session/access_token.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ def get_info_from_access_token(
5050
do_anti_csrf_check: bool,
5151
):
5252
# TODO: Add different tests to verify this works as expected
53-
try:
53+
try: # pylint: disable=too-many-nested-blocks
5454
payload: Optional[Dict[str, Any]] = None
5555

5656
if jwt_info.version < 3:

supertokens_python/recipe/session/asyncio/__init__.py

Lines changed: 20 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -11,38 +11,37 @@
1111
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
1212
# License for the specific language governing permissions and limitations
1313
# under the License.
14-
from typing import Any, Dict, List, Union, TypeVar, Callable, Optional
14+
from typing import Any, Callable, Dict, List, Optional, TypeVar, Union
1515

1616
from supertokens_python.recipe.openid.interfaces import (
1717
GetOpenIdDiscoveryConfigurationResult,
1818
)
1919
from supertokens_python.recipe.session.interfaces import (
20+
ClaimsValidationResult,
21+
GetClaimValueOkResult,
22+
JSONObject,
2023
RegenerateAccessTokenOkResult,
21-
SessionContainer,
22-
SessionInformationResult,
2324
SessionClaim,
2425
SessionClaimValidator,
26+
SessionContainer,
2527
SessionDoesNotExistError,
26-
ClaimsValidationResult,
27-
JSONObject,
28-
GetClaimValueOkResult,
29-
)
30-
from supertokens_python.recipe.session.recipe import (
31-
SessionRecipe,
32-
)
33-
from ..session_request_functions import (
34-
get_session_from_request,
35-
create_new_session_in_request,
36-
refresh_session_in_request,
28+
SessionInformationResult,
3729
)
30+
from supertokens_python.recipe.session.recipe import SessionRecipe
3831
from supertokens_python.types import MaybeAwaitable
3932
from supertokens_python.utils import FRAMEWORKS, resolve
40-
from ..utils import get_required_claim_validators
33+
4134
from ...jwt.interfaces import (
4235
CreateJwtOkResult,
4336
CreateJwtResultUnsupportedAlgorithm,
4437
GetJWKSResult,
4538
)
39+
from ..session_request_functions import (
40+
create_new_session_in_request,
41+
get_session_from_request,
42+
refresh_session_in_request,
43+
)
44+
from ..utils import get_required_claim_validators
4645

4746
_T = TypeVar("_T")
4847

@@ -332,7 +331,7 @@ async def get_session_without_request_response(
332331
- override_global_claim_validators: Alter the
333332
- user_context: user context
334333
335-
Returned statuses:
334+
Results:
336335
- OK: The session was successfully validated, including claim validation
337336
- CLAIM_VALIDATION_ERROR: While the access token is valid, one or more claim validators have failed. Our frontend SDKs expect a 403 response the contents matching the value returned from this function.
338337
- TRY_REFRESH_TOKEN_ERROR: This means, that the access token structure was valid, but it didn't pass validation for some reason and the user should call the refresh API.
@@ -347,7 +346,7 @@ async def get_session_without_request_response(
347346

348347
recipe_interface_impl = SessionRecipe.get_instance().recipe_implementation
349348

350-
session_ = await recipe_interface_impl.get_session(
349+
session = await recipe_interface_impl.get_session(
351350
access_token,
352351
anti_csrf_token,
353352
anti_csrf_check,
@@ -357,13 +356,13 @@ async def get_session_without_request_response(
357356
user_context,
358357
)
359358

360-
if session_ is not None:
359+
if session is not None:
361360
claim_validators = await get_required_claim_validators(
362-
session_, override_global_claim_validators, user_context
361+
session, override_global_claim_validators, user_context
363362
)
364-
await session_.assert_claims(claim_validators, user_context)
363+
await session.assert_claims(claim_validators, user_context)
365364

366-
return session_
365+
return session
367366

368367

369368
async def refresh_session(

supertokens_python/recipe/session/constants.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@
3333

3434
available_token_transfer_methods: List[TokenTransferMethod] = ["cookie", "header"]
3535

36-
JWKCacheMaxAgeInMs = 60 * 1000 # 1min
37-
JWKRequestCooldownInMs = 500 # 0.5s
36+
JWKCacheMaxAgeInMs = 60 * 1000 # 1min
37+
JWKRequestCooldownInMs = 500 # 0.5s
3838
protected_props = [
3939
"sub",
4040
"iat",

supertokens_python/recipe/session/cookie_and_header.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,7 @@ def _set_token(
262262
expires: int,
263263
transfer_method: TokenTransferMethod,
264264
):
265-
log_debug_message(f"Setting {token_type} token as {transfer_method}")
265+
log_debug_message("Setting %s token as %s", token_type, transfer_method)
266266
if transfer_method == "cookie":
267267
_set_cookie(
268268
response,

supertokens_python/recipe/session/exceptions.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,17 @@ def raise_try_refresh_token_exception(ex: Union[str, Exception]) -> NoReturn:
3131
raise TryRefreshTokenError(ex) from None
3232

3333

34-
def raise_unauthorised_exception(msg: str, clear_tokens: bool = True) -> NoReturn:
35-
raise UnauthorisedError(msg, clear_tokens) from None
34+
def raise_unauthorised_exception(
35+
msg: str,
36+
clear_tokens: bool = True,
37+
response_mutators: Optional[List[ResponseMutator]] = None,
38+
) -> NoReturn:
39+
if response_mutators is not None:
40+
response_mutators = []
41+
42+
err = UnauthorisedError(msg, clear_tokens)
43+
err.response_mutators.extend(UnauthorisedError.response_mutators)
44+
raise err
3645

3746

3847
class SuperTokensSessionError(SuperTokensError):

supertokens_python/recipe/session/jwt.py

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,10 @@
1616

1717
from supertokens_python.utils import utf_base64decode, utf_base64encode
1818

19-
"""
20-
why separators is used in dumps:
21-
- without it's use, output of dumps is: '{"alg": "RS256", "typ": "JWT", "version": "1"}'
22-
- with it's use, output of dumps is: '{"alg":"RS256","typ":"JWT","version":"1"}'
23-
24-
we require the non-spaced version, else the base64 encoding string will end up different than required
25-
"""
19+
# why separators is used in dumps:
20+
# - without it's use, output of dumps is: '{"alg": "RS256", "typ": "JWT", "version": "1"}'
21+
# - with it's use, output of dumps is: '{"alg":"RS256","typ":"JWT","version":"1"}'
22+
# we require the non-spaced version, else the base64 encoding string will end up different than required
2623
_allowed_headers = [
2724
utf_base64encode(
2825
dumps(

supertokens_python/recipe/session/recipe.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
from .recipe_implementation import (
5959
RecipeImplementation,
6060
)
61+
from .api import handle_refresh_api, handle_signout_api
6162
from .utils import (
6263
InputErrorHandlers,
6364
InputOverrideConfig,
@@ -191,9 +192,6 @@ async def handle_api_request(
191192
method: str,
192193
response: BaseResponse,
193194
) -> Union[BaseResponse, None]:
194-
# TODO: See if this import can be done globally without triggering cyclic import issues
195-
from .api import handle_refresh_api, handle_signout_api
196-
197195
if request_id == SESSION_REFRESH:
198196
return await handle_refresh_api(
199197
self.api_implementation,
@@ -216,7 +214,7 @@ async def handle_api_request(
216214
self.recipe_implementation,
217215
),
218216
)
219-
return await self.openid_recipe.handle_api_request( # FIXME: Update nested functions to also return None if nothing matches
217+
return await self.openid_recipe.handle_api_request(
220218
request_id, request, path, method, response
221219
)
222220

supertokens_python/recipe/session/recipe_implementation.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -203,9 +203,7 @@ async def get_session(
203203
access_token_obj.payload, access_token_obj.version
204204
)
205205
except Exception:
206-
if (
207-
session_required is False
208-
): # FIXME: But we are setting the default to None instead of False
206+
if session_required is False:
209207
log_debug_message(
210208
"getSession: Returning undefined because parsing failed and session_required is False"
211209
)

supertokens_python/recipe/session/session_functions.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,9 @@ async def create_new_session(
117117

118118
return CreateOrRefreshAPIResponse(
119119
CreateOrRefreshAPIResponseSession(
120-
response["handle"], response["userId"], response["userDataInJWT"]
120+
response["session"]["handle"],
121+
response["session"]["userId"],
122+
response["session"]["userDataInJWT"],
121123
),
122124
TokenInfo(
123125
response["accessToken"]["token"],
@@ -327,7 +329,9 @@ async def refresh_session(
327329
response.pop("status", None)
328330
return CreateOrRefreshAPIResponse(
329331
CreateOrRefreshAPIResponseSession(
330-
response["handle"], response["userId"], response["userDataInJWT"]
332+
response["session"]["handle"],
333+
response["session"]["userId"],
334+
response["session"]["userDataInJWT"],
331335
),
332336
TokenInfo(
333337
response["accessToken"]["token"],

supertokens_python/recipe/session/session_request_functions.py

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,9 @@
1313
# under the License.
1414
from __future__ import annotations
1515

16-
from typing import Any, Callable, Dict, List, Optional, Union
16+
from typing import Any, Callable, Dict, List, Optional, Union, TYPE_CHECKING
1717

1818
from supertokens_python.logger import log_debug_message
19-
from supertokens_python.recipe.session import SessionRecipe
2019
from supertokens_python.recipe.session.access_token import (
2120
validate_access_token_structure,
2221
)
@@ -38,8 +37,8 @@
3837
SessionClaimValidator,
3938
SessionContainer,
4039
)
41-
from supertokens_python.exceptions import SuperTokensError
4240
from supertokens_python.recipe.session.exceptions import (
41+
SuperTokensSessionError,
4342
TokenTheftError,
4443
UnauthorisedError,
4544
)
@@ -61,6 +60,10 @@
6160
normalise_http_method,
6261
set_request_in_user_context_if_not_defined,
6362
)
63+
from supertokens_python import Supertokens
64+
65+
if TYPE_CHECKING:
66+
from supertokens_python.recipe.session.recipe import SessionRecipe
6467

6568
LEGACY_ID_REFRESH_TOKEN_COOKIE_NAME = "sIdRefreshToken"
6669

@@ -84,7 +87,7 @@ async def get_session_from_request(
8487

8588
if not hasattr(request, "wrapper_used") or not request.wrapper_used:
8689
request = FRAMEWORKS[
87-
SessionRecipe.get_instance().app_info.framework
90+
Supertokens.get_instance().app_info.framework
8891
].wrap_request(request)
8992

9093
log_debug_message("getSession: Wrapping done")
@@ -219,7 +222,7 @@ async def create_new_session_in_request(
219222

220223
if not hasattr(request, "wrapper_used") or not request.wrapper_used:
221224
request = FRAMEWORKS[
222-
SessionRecipe.get_instance().app_info.framework
225+
Supertokens.get_instance().app_info.framework
223226
].wrap_request(request)
224227

225228
log_debug_message("createNewSession: Wrapping done")
@@ -265,14 +268,12 @@ async def create_new_session_in_request(
265268
)
266269

267270
disable_anti_csrf = output_transfer_method == "header"
268-
session = (
269-
await SessionRecipe.get_instance().recipe_implementation.create_new_session(
270-
user_id,
271-
final_access_token_payload,
272-
session_data_in_database,
273-
disable_anti_csrf,
274-
user_context=user_context,
275-
)
271+
session = await recipe_instance.recipe_implementation.create_new_session(
272+
user_id,
273+
final_access_token_payload,
274+
session_data_in_database,
275+
disable_anti_csrf,
276+
user_context=user_context,
276277
)
277278

278279
log_debug_message("createNewSession: Session created in core built")
@@ -310,7 +311,7 @@ async def refresh_session_in_request(
310311

311312
if not hasattr(request, "wrapper_used") or not request.wrapper_used:
312313
request = FRAMEWORKS[
313-
SessionRecipe.get_instance().app_info.framework
314+
Supertokens.get_instance().app_info.framework
314315
].wrap_request(request)
315316

316317
log_debug_message("refreshSession: Wrapping done")
@@ -370,6 +371,7 @@ async def refresh_session_in_request(
370371
return raise_unauthorised_exception(
371372
"Refresh token not found. Are you sending the refresh token in the request?",
372373
clear_tokens=False,
374+
response_mutators=response_mutators,
373375
)
374376

375377
assert refresh_token is not None
@@ -394,7 +396,7 @@ async def refresh_session_in_request(
394396
session = await recipe_interface_impl.refresh_session(
395397
refresh_token, anti_csrf_token, disable_anti_csrf, user_context
396398
)
397-
except SuperTokensError as e:
399+
except SuperTokensSessionError as e:
398400
if isinstance(e, TokenTheftError) or (
399401
isinstance(e, UnauthorisedError) and getattr(e, "clear_tokens") is True
400402
):
@@ -414,6 +416,7 @@ async def refresh_session_in_request(
414416
)
415417
)
416418

419+
e.response_mutators.extend(response_mutators)
417420
raise e
418421

419422
log_debug_message(

tests/Fastapi/test_fastapi.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from fastapi.requests import Request
1919
from fastapi.testclient import TestClient
2020
from pytest import fixture, mark
21+
2122
from supertokens_python import InputAppInfo, SupertokensConfig, init
2223
from supertokens_python.framework.fastapi import get_middleware
2324
from supertokens_python.recipe import emailpassword, session
@@ -599,6 +600,9 @@ async def refresh_post(
599600
assert res.status_code == 401
600601
assert_info_clears_tokens(info, token_transfer_method)
601602

603+
assert info["sIdRefreshToken"]["value"] == ""
604+
assert info["sIdRefreshToken"]["expires"] == "Thu, 01 Jan 1970 00:00:00 GMT"
605+
602606

603607
@mark.asyncio
604608
@mark.parametrize("token_transfer_method", ["cookie", "header"])
@@ -623,3 +627,39 @@ async def test_revoking_session_after_create_new_session_with_throwing_unauthori
623627

624628
assert res.status_code == 401
625629
assert_info_clears_tokens(info, token_transfer_method)
630+
631+
632+
@mark.asyncio
633+
async def test_session_with_legacy_refresh_token_and_unauthorized_should_clear_legacy_token(
634+
driver_config_client: TestClient,
635+
):
636+
init(
637+
**get_st_init_args(
638+
[
639+
session.init(
640+
anti_csrf="VIA_TOKEN",
641+
)
642+
]
643+
)
644+
) # type: ignore
645+
start_st()
646+
647+
headers: Dict[str, Any] = {}
648+
cookies: Dict[str, Any] = {}
649+
650+
cookies.update(
651+
# Invalid refresh token so that core throws an unauthorized error
652+
{"sRefreshToken": "non-existing-token", "sIdRefreshToken": "irrelevant-value"}
653+
)
654+
655+
res = driver_config_client.post(
656+
"/auth/session/refresh", headers=headers, cookies=cookies
657+
)
658+
info = extract_info(res)
659+
660+
assert res.status_code == 401
661+
assert res.json() == {"message": "unauthorised"}
662+
assert_info_clears_tokens(info, "cookie")
663+
664+
assert info["sIdRefreshToken"]["value"] == ""
665+
assert info["sIdRefreshToken"]["expires"] == "Thu, 01 Jan 1970 00:00:00 GMT"

0 commit comments

Comments
 (0)