Skip to content

Commit 6e982fc

Browse files
Merge pull request #287 from supertokens/fix/access-token-cookie-expiry
fix: Update access token cookie expiry logic and add comments
2 parents 7eb9f11 + 68cfa52 commit 6e982fc

File tree

9 files changed

+145
-53
lines changed

9 files changed

+145
-53
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

setup.py

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

7171
setup(
7272
name="supertokens_python",
73-
version="0.12.1",
73+
version="0.12.2",
7474
author="SuperTokens",
7575
license="Apache 2.0",
7676
author_email="[email protected]",

supertokens_python/constants.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
# License for the specific language governing permissions and limitations
1313
# under the License.
1414
SUPPORTED_CDI_VERSIONS = ["2.9", "2.10", "2.11", "2.12", "2.13", "2.14", "2.15"]
15-
VERSION = "0.12.1"
15+
VERSION = "0.12.2"
1616
TELEMETRY = "/telemetry"
1717
USER_COUNT = "/users/count"
1818
USER_DELETE = "/user/remove"

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: 17 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,10 @@
1313
# under the License.
1414
import json
1515
from math import ceil
16-
from time import time
1716
from typing import Any, Dict, Optional
1817

1918
from supertokens_python.framework.response import BaseResponse
19+
from supertokens_python.utils import get_timestamp_ms
2020

2121

2222
class FastApiResponse(BaseResponse):
@@ -49,31 +49,22 @@ def set_cookie(
4949
httponly: bool = False,
5050
samesite: str = "lax",
5151
):
52-
if domain is None:
53-
# we do ceil because if we do floor, we tests may fail where the access
54-
# token lifetime is set to 1 second
55-
self.response.set_cookie(
56-
key=key,
57-
value=value,
58-
expires=ceil((expires - int(time() * 1000)) / 1000),
59-
path=path,
60-
secure=secure,
61-
httponly=httponly,
62-
samesite=samesite,
63-
)
64-
else:
65-
# we do ceil because if we do floor, we tests may fail where the access
66-
# token lifetime is set to 1 second
67-
self.response.set_cookie(
68-
key=key,
69-
value=value,
70-
expires=ceil((expires - int(time() * 1000)) / 1000),
71-
path=path,
72-
domain=domain,
73-
secure=secure,
74-
httponly=httponly,
75-
samesite=samesite,
76-
)
52+
# Note: For FastAPI response object, the expires value
53+
# doesn't mean the absolute time in ms, but the duration in seconds
54+
# So we need to convert our absolute expiry time (ms) to a duration (seconds)
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+
)
7768

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

supertokens_python/recipe/session/recipe_implementation.py

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
from __future__ import annotations
1515

1616
import json
17-
from datetime import datetime
1817
from typing import TYPE_CHECKING, Any, Callable, Dict, Optional
1918

2019
from supertokens_python.framework import BaseRequest
@@ -33,14 +32,14 @@
3332
from . import session_functions
3433
from .access_token import validate_access_token_structure
3534
from .cookie_and_header import (
35+
anti_csrf_response_mutator,
36+
clear_session_response_mutator,
37+
front_token_response_mutator,
3638
get_anti_csrf_header,
3739
get_rid_header,
3840
get_token,
39-
token_response_mutator,
40-
front_token_response_mutator,
41-
anti_csrf_response_mutator,
4241
set_cookie_response_mutator,
43-
clear_session_response_mutator,
42+
token_response_mutator,
4443
)
4544
from .exceptions import (
4645
TokenTheftError,
@@ -50,12 +49,12 @@
5049
)
5150
from .interfaces import (
5251
AccessTokenObj,
53-
ResponseMutator,
5452
ClaimsValidationResult,
5553
GetClaimValueOkResult,
5654
JSONObject,
5755
RecipeInterface,
5856
RegenerateAccessTokenOkResult,
57+
ResponseMutator,
5958
SessionClaim,
6059
SessionClaimValidator,
6160
SessionDoesNotExistError,
@@ -64,14 +63,18 @@
6463
)
6564
from .jwt import ParsedJWTInfo, parse_jwt_without_signature_verification
6665
from .session_class import Session
67-
from .utils import SessionConfig, TokenTransferMethod, validate_claims_in_payload
66+
from .utils import (
67+
HUNDRED_YEARS_IN_MS,
68+
SessionConfig,
69+
TokenTransferMethod,
70+
validate_claims_in_payload,
71+
)
6872

6973
if TYPE_CHECKING:
7074
from typing import List, Union
7175
from supertokens_python import AppInfo
7276
from supertokens_python.querier import Querier
7377

74-
7578
from .constants import available_token_transfer_methods
7679
from .interfaces import SessionContainer
7780

@@ -248,12 +251,16 @@ async def create_new_session(
248251
new_session.access_token_payload,
249252
)
250253
)
254+
# We set the expiration to 100 years, because we can't really access the expiration of the refresh token everywhere we are setting it.
255+
# 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.
256+
# 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 100 years seems enough.
251258
response_mutators.append(
252259
token_response_mutator(
253260
self.config,
254261
"access",
255262
new_access_token_info["token"],
256-
int(datetime.now().timestamp()) + 3153600000000,
263+
get_timestamp_ms() + HUNDRED_YEARS_IN_MS,
257264
new_session.transfer_method,
258265
)
259266
)
@@ -262,7 +269,9 @@ async def create_new_session(
262269
self.config,
263270
"refresh",
264271
new_refresh_token_info["token"],
265-
new_refresh_token_info["expiry"],
272+
new_refresh_token_info[
273+
"expiry"
274+
], # This comes from the core and is 100 days
266275
new_session.transfer_method,
267276
)
268277
)
@@ -456,12 +465,16 @@ async def get_session(
456465
session.access_token_payload,
457466
)
458467
)
468+
# We set the expiration to 100 years, because we can't really access the expiration of the refresh token everywhere we are setting it.
469+
# 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.
470+
# Even if the token is expired the presence of the token indicates that the user could have a valid refresh
471+
# Setting them to infinity would require special case handling on the frontend and just adding 100 years seems enough.
459472
session.response_mutators.append(
460473
token_response_mutator(
461474
self.config,
462475
"access",
463476
session.access_token,
464-
int(datetime.now().timestamp()) + 3153600000000,
477+
get_timestamp_ms() + HUNDRED_YEARS_IN_MS,
465478
session.transfer_method,
466479
)
467480
)
@@ -603,12 +616,16 @@ async def refresh_session(
603616
session.access_token_payload,
604617
)
605618
)
619+
# We set the expiration to 100 years, because we can't really access the expiration of the refresh token everywhere we are setting it.
620+
# 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.
621+
# Even if the token is expired the presence of the token indicates that the user could have a valid refresh
622+
# Setting them to infinity would require special case handling on the frontend and just adding 100 years seems enough.
606623
response_mutators.append(
607624
token_response_mutator(
608625
self.config,
609626
"access",
610627
new_access_token_info["token"],
611-
int(datetime.now().timestamp()) + 3153600000000,
628+
get_timestamp_ms() + HUNDRED_YEARS_IN_MS, # 100 years
612629
session.transfer_method,
613630
)
614631
)
@@ -618,7 +635,9 @@ async def refresh_session(
618635
self.config,
619636
"refresh",
620637
new_refresh_token_info["token"],
621-
new_refresh_token_info["expiry"],
638+
new_refresh_token_info[
639+
"expiry"
640+
], # This comes from the core and is 100 days
622641
session.transfer_method,
623642
)
624643
)

supertokens_python/recipe/session/session_class.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,20 +11,21 @@
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 datetime import datetime
1514
from typing import Any, Dict, List, TypeVar, Union
1615

1716
from supertokens_python.recipe.session.exceptions import (
1817
raise_invalid_claims_exception,
1918
raise_unauthorised_exception,
2019
)
20+
from supertokens_python.utils import get_timestamp_ms
2121

2222
from .cookie_and_header import (
2323
clear_session_response_mutator,
2424
front_token_response_mutator,
2525
token_response_mutator,
2626
)
2727
from .interfaces import SessionClaim, SessionClaimValidator, SessionContainer
28+
from .utils import HUNDRED_YEARS_IN_MS
2829

2930
_T = TypeVar("_T")
3031

@@ -95,12 +96,16 @@ async def update_access_token_payload(
9596
self.access_token_payload,
9697
)
9798
)
99+
# We set the expiration to 100 years, because we can't really access the expiration of the refresh token everywhere we are setting it.
100+
# 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.
101+
# 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 100 years seems enough.
98103
self.response_mutators.append(
99104
token_response_mutator(
100105
self.config,
101106
"access",
102107
result.access_token.token,
103-
int(datetime.now().timestamp()) + 3153600000000,
108+
get_timestamp_ms() + HUNDRED_YEARS_IN_MS,
104109
self.transfer_method,
105110
)
106111
)

supertokens_python/recipe/session/utils.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
from typing import TYPE_CHECKING, Any, Awaitable, Callable, Dict, List, Optional, Union
1818
from urllib.parse import urlparse
1919

20+
from typing_extensions import Literal
21+
2022
from supertokens_python.exceptions import raise_general_exception
2123
from supertokens_python.framework import BaseResponse
2224
from supertokens_python.normalised_url_path import NormalisedURLPath
@@ -29,13 +31,10 @@
2931
send_non_200_response,
3032
send_non_200_response_with_message,
3133
)
32-
from typing_extensions import Literal
3334

3435
from ...types import MaybeAwaitable
35-
from .constants import SESSION_REFRESH, AUTH_MODE_HEADER_KEY
36-
from .cookie_and_header import (
37-
clear_session_from_all_token_transfer_methods,
38-
)
36+
from .constants import AUTH_MODE_HEADER_KEY, SESSION_REFRESH
37+
from .cookie_and_header import clear_session_from_all_token_transfer_methods
3938
from .exceptions import ClaimValidationError
4039
from .with_jwt.constants import (
4140
ACCESS_TOKEN_PAYLOAD_JWT_PROPERTY_NAME_KEY,
@@ -56,6 +55,8 @@
5655

5756
from supertokens_python.logger import log_debug_message
5857

58+
HUNDRED_YEARS_IN_MS = 3153600000000
59+
5960

6061
def normalise_session_scope(session_scope: str) -> str:
6162
def helper(scope: str) -> str:

0 commit comments

Comments
 (0)