Skip to content

Commit eddb922

Browse files
committed
test: Ensure access token expiry time is correct
1 parent 40953fb commit eddb922

File tree

6 files changed

+104
-36
lines changed

6 files changed

+104
-36
lines changed

CHANGELOG.md

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

88
## unreleased
99

10+
## [0.12.2] - 2023-02-23
11+
- Fix expiry time of access token cookie.
12+
13+
1014
## [0.12.1] - 2023-02-06
1115

1216
- Email template updates

supertokens_python/framework/fastapi/fastapi_request.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ def method(self) -> str:
4545
return self.request.method
4646

4747
def get_cookie(self, key: str) -> Union[str, None]:
48+
# Note: Unlike other frameworks, FastAPI wraps the value in quotes in Set-Cookie header
49+
# It also takes care of escaping the quotes while fetching the value
4850
return self.request.cookies.get(key)
4951

5052
def get_header(self, key: str) -> Union[str, None]:

supertokens_python/framework/fastapi/fastapi_response.py

Lines changed: 13 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -52,31 +52,19 @@ def set_cookie(
5252
# Note: For FastAPI response object, the expires value
5353
# doesn't mean the absolute time in ms, but the duration in seconds
5454
# So we need to convert our absolute expiry time (ms) to a duration (seconds)
55-
if domain is None:
56-
# we do ceil because if we do floor, we tests may fail where the access
57-
# token lifetime is set to 1 second
58-
self.response.set_cookie(
59-
key=key,
60-
value=value,
61-
expires=ceil((expires - get_timestamp_ms()) / 1000),
62-
path=path,
63-
secure=secure,
64-
httponly=httponly,
65-
samesite=samesite,
66-
)
67-
else:
68-
# we do ceil because if we do floor, we tests may fail where the access
69-
# token lifetime is set to 1 second
70-
self.response.set_cookie(
71-
key=key,
72-
value=value,
73-
expires=ceil((expires - get_timestamp_ms()) / 1000),
74-
path=path,
75-
domain=domain,
76-
secure=secure,
77-
httponly=httponly,
78-
samesite=samesite,
79-
)
55+
56+
# we do ceil because if we do floor, we tests may fail where the access
57+
# token lifetime is set to 1 second
58+
self.response.set_cookie(
59+
key=key,
60+
value=value, # Note: Unlike other frameworks, FastAPI wraps the value in quotes in Set-Cookie header
61+
expires=ceil((expires - get_timestamp_ms()) / 1000),
62+
path=path,
63+
domain=domain, # type: ignore # starlette didn't set domain as optional type but their default value is None anyways
64+
secure=secure,
65+
httponly=httponly,
66+
samesite=samesite,
67+
)
8068

8169
def set_header(self, key: str, value: str):
8270
self.response.headers[key] = value

supertokens_python/recipe/session/recipe_implementation.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,7 @@ async def create_new_session(
254254
# We set the expiration to 100 years, because we can't really access the expiration of the refresh token everywhere we are setting it.
255255
# This should be safe to do, since this is only the validity of the cookie (set here or on the frontend) but we check the expiration of the JWT anyway.
256256
# Even if the token is expired the presence of the token indicates that the user could have a valid refresh
257-
# Setting them to infinity would require special case handling on the frontend and just adding 10 years seems enough.
257+
# Setting them to infinity would require special case handling on the frontend and just adding 100 years seems enough.
258258
response_mutators.append(
259259
token_response_mutator(
260260
self.config,
@@ -269,7 +269,9 @@ async def create_new_session(
269269
self.config,
270270
"refresh",
271271
new_refresh_token_info["token"],
272-
new_refresh_token_info["expiry"],
272+
new_refresh_token_info[
273+
"expiry"
274+
], # This comes from the core and is 100 days
273275
new_session.transfer_method,
274276
)
275277
)
@@ -466,7 +468,7 @@ async def get_session(
466468
# We set the expiration to 100 years, because we can't really access the expiration of the refresh token everywhere we are setting it.
467469
# This should be safe to do, since this is only the validity of the cookie (set here or on the frontend) but we check the expiration of the JWT anyway.
468470
# Even if the token is expired the presence of the token indicates that the user could have a valid refresh
469-
# Setting them to infinity would require special case handling on the frontend and just adding 10 years seems enough.
471+
# Setting them to infinity would require special case handling on the frontend and just adding 100 years seems enough.
470472
session.response_mutators.append(
471473
token_response_mutator(
472474
self.config,
@@ -617,7 +619,7 @@ async def refresh_session(
617619
# We set the expiration to 100 years, because we can't really access the expiration of the refresh token everywhere we are setting it.
618620
# This should be safe to do, since this is only the validity of the cookie (set here or on the frontend) but we check the expiration of the JWT anyway.
619621
# Even if the token is expired the presence of the token indicates that the user could have a valid refresh
620-
# Setting them to infinity would require special case handling on the frontend and just adding 10 years seems enough.
622+
# Setting them to infinity would require special case handling on the frontend and just adding 100 years seems enough.
621623
response_mutators.append(
622624
token_response_mutator(
623625
self.config,
@@ -633,7 +635,9 @@ async def refresh_session(
633635
self.config,
634636
"refresh",
635637
new_refresh_token_info["token"],
636-
new_refresh_token_info["expiry"],
638+
new_refresh_token_info[
639+
"expiry"
640+
], # This comes from the core and is 100 days
637641
session.transfer_method,
638642
)
639643
)

supertokens_python/recipe/session/session_class.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ async def update_access_token_payload(
9999
# We set the expiration to 100 years, because we can't really access the expiration of the refresh token everywhere we are setting it.
100100
# This should be safe to do, since this is only the validity of the cookie (set here or on the frontend) but we check the expiration of the JWT anyway.
101101
# Even if the token is expired the presence of the token indicates that the user could have a valid refresh
102-
# Setting them to infinity would require special case handling on the frontend and just adding 10 years seems enough.
102+
# Setting them to infinity would require special case handling on the frontend and just adding 100 years seems enough.
103103
self.response_mutators.append(
104104
token_response_mutator(
105105
self.config,

tests/test_session.py

Lines changed: 75 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
# License for the specific language governing permissions and limitations
1313
# under the License.
1414

15+
from datetime import datetime, timedelta
1516
from typing import Any, Dict, List
1617
from unittest.mock import MagicMock
1718

@@ -28,9 +29,6 @@
2829
from supertokens_python.recipe.session.asyncio import (
2930
create_new_session as async_create_new_session,
3031
)
31-
from supertokens_python.recipe.session.jwt import (
32-
parse_jwt_without_signature_verification,
33-
)
3432
from supertokens_python.recipe.session.asyncio import (
3533
get_all_session_handles_for_user,
3634
get_session_information,
@@ -44,6 +42,9 @@
4442
update_session_data,
4543
)
4644
from supertokens_python.recipe.session.interfaces import RecipeInterface
45+
from supertokens_python.recipe.session.jwt import (
46+
parse_jwt_without_signature_verification,
47+
)
4748
from supertokens_python.recipe.session.recipe_implementation import RecipeImplementation
4849
from supertokens_python.recipe.session.session_functions import (
4950
create_new_session,
@@ -356,10 +357,10 @@ async def get_session_information(
356357
from supertokens_python.recipe.session.exceptions import raise_unauthorised_exception
357358
from supertokens_python.recipe.session.interfaces import APIInterface, APIOptions
358359
from tests.utils import (
360+
assert_info_clears_tokens,
359361
extract_all_cookies,
360-
get_st_init_args,
361362
extract_info,
362-
assert_info_clears_tokens,
363+
get_st_init_args,
363364
)
364365

365366

@@ -578,3 +579,72 @@ async def refresh_post(api_options: APIOptions, user_context: Dict[str, Any]):
578579

579580
assert cookies["sAccessToken"]["value"] != ""
580581
assert cookies["sRefreshToken"]["value"] != ""
582+
583+
584+
async def test_token_cookie_expires(
585+
driver_config_client: TestClient,
586+
):
587+
init_args = get_st_init_args(
588+
[
589+
session.init(
590+
anti_csrf="VIA_TOKEN",
591+
get_token_transfer_method=lambda _, __, ___: "cookie",
592+
),
593+
]
594+
)
595+
init(**init_args)
596+
start_st()
597+
598+
response = driver_config_client.post("/create")
599+
assert response.status_code == 200
600+
601+
cookies = extract_all_cookies(response)
602+
603+
assert "sAccessToken" in cookies
604+
assert "sRefreshToken" in cookies
605+
606+
for c in response.cookies:
607+
if c.name == "sAccessToken": # 100 years (set by the SDK)
608+
# some time must have elasped since the cookie was set. So less than current time
609+
assert (
610+
datetime.fromtimestamp(c.expires or 0) - timedelta(days=365.25 * 100)
611+
< datetime.now()
612+
)
613+
if c.name == "sRefreshToken": # 100 days (set by the core)
614+
assert (
615+
datetime.fromtimestamp(c.expires or 0) - timedelta(days=100)
616+
< datetime.now()
617+
)
618+
619+
assert response.headers["anti-csrf"] != ""
620+
assert response.headers["front-token"] != ""
621+
622+
response = driver_config_client.post(
623+
"/auth/session/refresh",
624+
cookies={
625+
"sRefreshToken": cookies["sRefreshToken"]["value"],
626+
},
627+
headers={"anti-csrf": response.headers["anti-csrf"]},
628+
)
629+
630+
assert response.status_code == 200
631+
cookies = extract_all_cookies(response)
632+
633+
assert "sAccessToken" in cookies
634+
assert "sRefreshToken" in cookies
635+
636+
for c in response.cookies:
637+
if c.name == "sAccessToken": # 100 years (set by the SDK)
638+
# some time must have elasped since the cookie was set. So less than current time
639+
assert (
640+
datetime.fromtimestamp(c.expires or 0) - timedelta(days=365.25 * 100)
641+
< datetime.now()
642+
)
643+
if c.name == "sRefreshToken": # 100 days (set by the core)
644+
assert (
645+
datetime.fromtimestamp(c.expires or 0) - timedelta(days=100)
646+
< datetime.now()
647+
)
648+
649+
assert response.headers["anti-csrf"] != ""
650+
assert response.headers["front-token"] != ""

0 commit comments

Comments
 (0)