Skip to content

Session related changes #497

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
May 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@
"python.linting.pylintEnabled": true,
"python.linting.enabled": true,
"editor.codeActionsOnSave": {
"source.organizeImports": true
"source.organizeImports": "explicit"
},
"python.analysis.typeCheckingMode": "strict",
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true,
"python.analysis.autoImportCompletions": true
"python.analysis.autoImportCompletions": true,
"circleci.persistedProjectSelection": []
}
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [unreleased]

## [0.19.0] - 2024-05-06

- `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`.
- Enable smooth switching between `use_dynamic_access_token_signing_key` settings by allowing refresh calls to change the signing key type of a session.

### Breaking change:
- A session is not required when calling the sign out API. Otherwise the API will return a 401 error.

## [0.18.11] - 2024-04-26

- 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.
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@

setup(
name="supertokens_python",
version="0.18.11",
version="0.19.0",
author="SuperTokens",
license="Apache 2.0",
author_email="[email protected]",
Expand Down
2 changes: 1 addition & 1 deletion supertokens_python/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from __future__ import annotations

SUPPORTED_CDI_VERSIONS = ["3.0"]
VERSION = "0.18.11"
VERSION = "0.19.0"
TELEMETRY = "/telemetry"
USER_COUNT = "/users/count"
USER_DELETE = "/user/remove"
Expand Down
2 changes: 1 addition & 1 deletion supertokens_python/recipe/session/api/signout.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ async def handle_signout_api(
api_options.request,
api_options.config,
api_options.recipe_implementation,
session_required=False,
session_required=True,
override_global_claim_validators=lambda _, __, ___: [],
user_context=user_context,
)
Expand Down
1 change: 1 addition & 0 deletions supertokens_python/recipe/session/recipe_implementation.py
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,7 @@ async def refresh_session(
refresh_token,
anti_csrf_token,
disable_anti_csrf,
self.config.use_dynamic_access_token_signing_key,
user_context=user_context,
)

Expand Down
20 changes: 12 additions & 8 deletions supertokens_python/recipe/session/session_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ async def get_session(
)

raise_try_refresh_token_exception(
"The access token doesn't match the useDynamicAccessTokenSigningKey setting"
"The access token doesn't match the use_dynamic_access_token_signing_key setting"
)

# If we get here we either have a V2 token that doesn't pass verification or a valid V3> token
Expand Down Expand Up @@ -308,13 +308,15 @@ async def get_session(
response["session"].get("tenantId")
or (access_token_info or {}).get("tenantId"),
),
GetSessionAPIResponseAccessToken(
response["accessToken"]["token"],
response["accessToken"]["expiry"],
response["accessToken"]["createdTime"],
)
if "accessToken" in response
else None,
(
GetSessionAPIResponseAccessToken(
response["accessToken"]["token"],
response["accessToken"]["expiry"],
response["accessToken"]["createdTime"],
)
if "accessToken" in response
else None
),
)
if response["status"] == "UNAUTHORISED":
log_debug_message("getSession: Returning UNAUTHORISED because of core response")
Expand All @@ -331,6 +333,7 @@ async def refresh_session(
refresh_token: str,
anti_csrf_token: Union[str, None],
disable_anti_csrf: bool,
use_dynamic_access_token_signing_key: bool,
user_context: Optional[Dict[str, Any]],
) -> CreateOrRefreshAPIResponse:
data = {
Expand All @@ -339,6 +342,7 @@ async def refresh_session(
not disable_anti_csrf
and recipe_implementation.config.anti_csrf_function_or_string == "VIA_TOKEN"
),
"useDynamicSigningKey": use_dynamic_access_token_signing_key,
}

if anti_csrf_token is not None:
Expand Down
20 changes: 14 additions & 6 deletions supertokens_python/recipe/session/session_request_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
SessionConfig,
TokenTransferMethod,
get_required_claim_validators,
get_auth_mode_from_header,
)
from supertokens_python.types import MaybeAwaitable
from supertokens_python.utils import (
Expand Down Expand Up @@ -179,9 +180,11 @@ async def get_session_from_request(
log_debug_message("getSession: Value of antiCsrfToken is: %s", do_anti_csrf_check)

session = await recipe_interface_impl.get_session(
access_token=request_access_token.raw_token_string
if request_access_token is not None
else None,
access_token=(
request_access_token.raw_token_string
if request_access_token is not None
else None
),
anti_csrf_token=anti_csrf_token,
anti_csrf_check=do_anti_csrf_check,
session_required=session_required,
Expand Down Expand Up @@ -229,6 +232,7 @@ async def create_new_session_in_request(
) -> SessionContainer:
log_debug_message("createNewSession: Started")

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

claims_added_by_other_recipes = recipe_instance.get_claims_added_by_other_recipes()
app_info = recipe_instance.app_info
issuer = (
app_info.api_domain.get_as_string_dangerous()
+ app_info.api_base_path.get_as_string_dangerous()
Expand All @@ -252,15 +255,20 @@ async def create_new_session_in_request(

for claim in claims_added_by_other_recipes:
update = await claim.build(user_id, tenant_id, user_context)
final_access_token_payload = {**final_access_token_payload, **update}
final_access_token_payload.update(update)

log_debug_message("createNewSession: Access token payload built")

output_transfer_method = config.get_token_transfer_method(
request, True, user_context
)
if output_transfer_method == "any":
output_transfer_method = "header"
auth_mode_header = get_auth_mode_from_header(request)
if auth_mode_header == "cookie":
output_transfer_method = auth_mode_header
else:
output_transfer_method = "header"

log_debug_message(
"createNewSession: using transfer method %s", output_transfer_method
)
Expand Down
85 changes: 85 additions & 0 deletions tests/sessions/test_auth_mode.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,91 @@ def check_extracted_info(
assert False, "Invalid expected_transfer_method"


@mark.asyncio
async def test_use_headers_if_get_token_transfer_method_returns_any_and_no_st_auth_mode_header(
app: TestClient,
):
init(
**get_st_init_args(
[
session.init(
anti_csrf="VIA_TOKEN",
get_token_transfer_method=lambda _, __, ___: "any", # Always return "any"
)
]
)
)
start_st()

# Create session without specifying st-auth-mode
res = create_session(app)

# Assert that no tokens are set in the cookies
assert res.get("accessToken") is None
assert res.get("refreshToken") is None
assert res.get("antiCsrf") is None

# Assert that tokens are set in the headers
assert res.get("accessTokenFromHeader") is not None
assert res.get("refreshTokenFromHeader") is not None


@mark.asyncio
async def test_should_use_cookies_if_get_token_transfer_method_returns_any_and_st_auth_mode_is_set_to_cookie(
app: TestClient,
):
init(
**get_st_init_args(
[
session.init(
anti_csrf="VIA_TOKEN",
get_token_transfer_method=lambda _, __, ___: "any", # Always returns "any"
)
]
)
)
start_st()

# Creating session with st-auth-mode set to 'cookie'
res = create_session(app, auth_mode_header="cookie")

# Checking that the tokens are not set in headers
assert res.get("accessToken") is not None
assert res.get("refreshToken") is not None
assert res.get("antiCsrf") is not None
assert res.get("accessTokenFromHeader") is None
assert res.get("refreshTokenFromHeader") is None


@mark.asyncio
async def test_use_headers_if_get_token_transfer_method_returns_any_and_st_auth_mode_is_set_to_header(
app: TestClient,
):
init(
**get_st_init_args(
[
session.init(
anti_csrf="VIA_TOKEN",
get_token_transfer_method=lambda _, __, ___: "any", # Always returns "any"
)
]
)
)
start_st()

# Creating session with st-auth-mode set to 'header'
res = create_session(app, auth_mode_header="header")

# Assert that no tokens are set in the cookies
assert res.get("accessToken") is None
assert res.get("refreshToken") is None
assert res.get("antiCsrf") is None

# Assert that tokens are set in the headers
assert res.get("accessTokenFromHeader") is not None
assert res.get("refreshTokenFromHeader") is not None


@mark.parametrize(
"auth_mode_header, expected_transfer_method",
[
Expand Down
2 changes: 1 addition & 1 deletion tests/sessions/test_jwks.py
Original file line number Diff line number Diff line change
Expand Up @@ -698,5 +698,5 @@ async def test_session_verification_of_jwt_with_dynamic_signing_key_mode_works_a
except TryRefreshTokenError as e:
assert (
str(e)
== "The access token doesn't match the useDynamicAccessTokenSigningKey setting"
== "The access token doesn't match the use_dynamic_access_token_signing_key setting"
)
116 changes: 116 additions & 0 deletions tests/sessions/test_use_dynamic_signing_key_switching.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import pytest
from supertokens_python import init
from supertokens_python.recipe import session
from supertokens_python.recipe.session.asyncio import (
create_new_session_without_request_response,
get_session_without_request_response,
refresh_session_without_request_response,
)
from supertokens_python.recipe.session.session_class import SessionContainer
from tests.utils import (
get_st_init_args,
setup_function,
start_st,
teardown_function,
reset,
)
from supertokens_python.recipe.session.jwt import (
parse_jwt_without_signature_verification,
)
from supertokens_python.recipe.session.interfaces import GetSessionTokensDangerouslyDict

_ = setup_function # type:ignore
_ = teardown_function # type:ignore

pytestmark = pytest.mark.asyncio


async def test_dynamic_key_switching():
init(**get_st_init_args([session.init(use_dynamic_access_token_signing_key=True)]))
start_st()

# Create a new session without an actual HTTP request-response flow
create_res: SessionContainer = await create_new_session_without_request_response(
"public", "test-user-id", {"tokenProp": True}, {"dbProp": True}
)

# Extract session tokens for further testing
tokens = create_res.get_all_session_tokens_dangerously()
check_access_token_signing_key_type(tokens, True)

# Reset and reinitialize with dynamic signing key disabled
reset(stop_core=False)
init(**get_st_init_args([session.init(use_dynamic_access_token_signing_key=False)]))

caught_exception = None
try:
# Attempt to retrieve the session using previously obtained tokens
await get_session_without_request_response(
tokens["accessToken"], tokens["antiCsrfToken"]
)
except Exception as e:
caught_exception = e

# Check for the expected exception due to token signing key mismatch
assert (
caught_exception is not None
), "Expected an exception to be thrown, but none was."
assert (
str(caught_exception)
== "The access token doesn't match the use_dynamic_access_token_signing_key setting"
), f"Unexpected exception message: {str(caught_exception)}"


async def test_refresh_session():
init(**get_st_init_args([session.init(use_dynamic_access_token_signing_key=True)]))
start_st()

# Create a new session without an actual HTTP request-response flow
create_res: SessionContainer = await create_new_session_without_request_response(
"public", "test-user-id", {"tokenProp": True}, {"dbProp": True}
)

# Extract session tokens for further testing
tokens = create_res.get_all_session_tokens_dangerously()
check_access_token_signing_key_type(tokens, True)

# Reset and reinitialize with dynamic signing key disabled
reset(stop_core=False)
init(**get_st_init_args([session.init(use_dynamic_access_token_signing_key=False)]))

assert tokens["refreshToken"] is not None

# Refresh session
refreshed_session = await refresh_session_without_request_response(
tokens["refreshToken"], True, tokens["antiCsrfToken"]
)
tokens_after_refresh = refreshed_session.get_all_session_tokens_dangerously()
assert tokens_after_refresh["accessAndFrontTokenUpdated"] is True
check_access_token_signing_key_type(tokens_after_refresh, False)

# Verify session after refresh
verified_session = await get_session_without_request_response(
tokens_after_refresh["accessToken"], tokens_after_refresh["antiCsrfToken"]
)
assert verified_session is not None
tokens_after_verify = verified_session.get_all_session_tokens_dangerously()
assert tokens_after_verify["accessAndFrontTokenUpdated"] is True
check_access_token_signing_key_type(tokens_after_verify, False)

# Verify session again
verified2_session = await get_session_without_request_response(
tokens_after_verify["accessToken"], tokens_after_verify["antiCsrfToken"]
)
assert verified2_session is not None
tokens_after_verify2 = verified2_session.get_all_session_tokens_dangerously()
assert tokens_after_verify2["accessAndFrontTokenUpdated"] is False


def check_access_token_signing_key_type(
tokens: GetSessionTokensDangerouslyDict, is_dynamic: bool
):
info = parse_jwt_without_signature_verification(tokens["accessToken"])
if is_dynamic:
assert info.kid is not None and info.kid.startswith("d-")
else:
assert info.kid is not None and info.kid.startswith("s-")
1 change: 1 addition & 0 deletions tests/test_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ async def test_that_once_the_info_is_loaded_it_doesnt_query_again():
response.refreshToken.token,
response.antiCsrfToken,
False,
True,
None,
)

Expand Down
Loading