Skip to content

fix: Improvements in session jwt changes #317

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
Show file tree
Hide file tree
Changes from 8 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
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@
],
keywords="",
install_requires=[
"PyJWT>=2.0.0 ,<3.0.0",
"PyJWT>=2.6.0 ,<3.0.0",
"httpx>=0.15.0 ,<0.24.0",
"pycryptodome==3.10.*",
"tldextract==3.1.0",
Expand Down
5 changes: 4 additions & 1 deletion supertokens_python/recipe/jwt/recipe.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,10 @@ async def handle_api_request(
self.recipe_implementation,
)

return await jwks_get(self.api_implementation, options)
if request_id == GET_JWKS_API:
return await jwks_get(self.api_implementation, options)

return None

async def handle_error(
self, request: BaseRequest, err: SuperTokensError, response: BaseResponse
Expand Down
4 changes: 2 additions & 2 deletions supertokens_python/recipe/session/access_token.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ def get_info_from_access_token(
do_anti_csrf_check: bool,
):
# TODO: Add different tests to verify this works as expected
try:
try: # pylint: disable=too-many-nested-blocks
payload: Optional[Dict[str, Any]] = None

if jwt_info.version < 3:
Expand Down Expand Up @@ -82,7 +82,7 @@ def get_info_from_access_token(
)
payload = jwt.decode( # type: ignore
jwt_info.raw_token_string,
matching_key,
matching_key.key, # type: ignore
algorithms=["RS256"],
options={"verify_signature": True, "verify_exp": True},
)
Expand Down
100 changes: 39 additions & 61 deletions supertokens_python/recipe/session/asyncio/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,49 +11,37 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from typing import Any, Dict, List, Union, TypeVar, Callable, Optional
from typing import Any, Callable, Dict, List, Optional, TypeVar, Union

from supertokens_python.exceptions import SuperTokensError
from supertokens_python.recipe.openid.interfaces import (
GetOpenIdDiscoveryConfigurationResult,
)
from supertokens_python.recipe.session.interfaces import (
ClaimsValidationResult,
GetClaimValueOkResult,
JSONObject,
RegenerateAccessTokenOkResult,
SessionContainer,
SessionInformationResult,
SessionClaim,
SessionClaimValidator,
SessionContainer,
SessionDoesNotExistError,
ClaimsValidationResult,
JSONObject,
GetClaimValueOkResult,
GetSessionUnauthorizedErrorResult,
GetSessionTryRefreshTokenErrorResult,
GetSessionClaimValidationErrorResult,
GetSessionClaimValidationErrorResponseObject,
CreateNewSessionResult,
GetSessionOkResult,
RefreshSessionOkResult,
RefreshSessionUnauthorizedResult,
RefreshSessionTokenTheftErrorResult,
)
from supertokens_python.recipe.session.recipe import (
SessionRecipe,
)
from ..session_request_functions import (
get_session_from_request,
create_new_session_in_request,
refresh_session_in_request,
SessionInformationResult,
)
from supertokens_python.recipe.session.recipe import SessionRecipe
from supertokens_python.types import MaybeAwaitable
from supertokens_python.utils import FRAMEWORKS, resolve
from ..exceptions import InvalidClaimsError
from ..utils import get_required_claim_validators

from ...jwt.interfaces import (
CreateJwtOkResult,
CreateJwtResultUnsupportedAlgorithm,
GetJWKSResult,
)
from ..session_request_functions import (
create_new_session_in_request,
get_session_from_request,
refresh_session_in_request,
)
from ..utils import get_required_claim_validators

_T = TypeVar("_T")

Expand Down Expand Up @@ -94,7 +82,7 @@ async def create_new_session_without_request_response(
session_data_in_database: Union[Dict[str, Any], None] = None,
disable_anti_csrf: bool = False,
user_context: Union[None, Dict[str, Any]] = None,
) -> CreateNewSessionResult:
) -> SessionContainer:
if user_context is None:
user_context = {}
if session_data_in_database is None:
Expand Down Expand Up @@ -294,6 +282,9 @@ async def get_session(
if user_context is None:
user_context = {}

if session_required is None:
session_required = True

recipe_instance = SessionRecipe.get_instance()
recipe_interface_impl = recipe_instance.recipe_implementation
config = recipe_instance.config
Expand All @@ -314,6 +305,7 @@ async def get_session_without_request_response(
access_token: str,
anti_csrf_token: Optional[str] = None,
anti_csrf_check: Optional[bool] = None,
session_required: Optional[bool] = None,
check_database: Optional[bool] = None,
override_global_claim_validators: Optional[
Callable[
Expand All @@ -322,12 +314,7 @@ async def get_session_without_request_response(
]
] = None,
user_context: Union[None, Dict[str, Any]] = None,
) -> Union[
GetSessionOkResult,
GetSessionUnauthorizedErrorResult,
GetSessionTryRefreshTokenErrorResult,
GetSessionClaimValidationErrorResult,
]:
) -> Optional[SessionContainer]:
"""Tries to validate an access token and build a Session object from it.

Notes about anti-csrf checking:
Expand All @@ -338,49 +325,44 @@ async def get_session_without_request_response(
Args:
- access_token: The access token extracted from the authorization header or cookies
- anti_csrf_token: The anti-csrf token extracted from the authorization header or cookies. Can be undefined if antiCsrfCheck is false
- anti_csrf_check: If true, anti-csrf checking will be done. If false, it will be skipped. Defaults behaviour to check.
- check_database: If true, the session will be checked in the database. If false, it will be skipped. Defaults behaviour to skip.
- anti_csrf_check: If true, anti-csrf checking will be done. If false, it will be skipped. Default behaviour is to check.
- session_required: If true, throws an error if the session does not exist. Default is True.
- check_database: If true, the session will be checked in the database. If false, it will be skipped. Default behaviour is to skip.
- override_global_claim_validators: Alter the
- user_context: user context

Returned values:
- GetSessionOkResult: The session was successfully validated, including claim validation
- GetSessionClaimValidationErrorResult: 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.
- GetSessionTryRefreshTokenErrorResult: 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.
- You can send a 401 response to trigger this behaviour if you are using our frontend SDKs
- GetSessionUnauthorizedErrorResult: This means that the access token likely doesn't belong to a SuperTokens session. If this is unexpected, it's best handled by sending a 401 response.
Results:
- OK: The session was successfully validated, including claim validation
- 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.
- 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.
You can send a 401 response to trigger this behaviour if you are using our frontend SDKs
- UNAUTHORISED: This means that the access token likely doesn't belong to a SuperTokens session. If this is unexpected, it's best handled by sending a 401 response.
"""
if user_context is None:
user_context = {}

if session_required is None:
session_required = True

recipe_interface_impl = SessionRecipe.get_instance().recipe_implementation

res = await recipe_interface_impl.get_session(
session = await recipe_interface_impl.get_session(
access_token,
anti_csrf_token,
anti_csrf_check,
session_required,
check_database,
override_global_claim_validators,
user_context,
)

if isinstance(res, GetSessionOkResult):
if session is not None:
claim_validators = await get_required_claim_validators(
res.session, override_global_claim_validators, user_context
session, override_global_claim_validators, user_context
)
try:
await res.session.assert_claims(claim_validators, user_context)
except SuperTokensError as e:
if isinstance(e, InvalidClaimsError):
return GetSessionClaimValidationErrorResult(
error=e,
response=GetSessionClaimValidationErrorResponseObject(
message="invalid claim", claim_validation_errors=e.payload
),
)
raise e
await session.assert_claims(claim_validators, user_context)

return res
return session


async def refresh_session(
Expand Down Expand Up @@ -412,11 +394,7 @@ async def refresh_session_without_request_response(
disable_anti_csrf: bool = False,
anti_csrf_token: Optional[str] = None,
user_context: Optional[Dict[str, Any]] = None,
) -> Union[
RefreshSessionOkResult,
RefreshSessionUnauthorizedResult,
RefreshSessionTokenTheftErrorResult,
]:
) -> SessionContainer:
if user_context is None:
user_context = {}

Expand Down
4 changes: 2 additions & 2 deletions supertokens_python/recipe/session/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@

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

JWKCacheMaxAgeInMs = 60 * 1000
JWKRequestCooldownInMs = 500 * 1000
JWKCacheMaxAgeInMs = 60 * 1000 # 1min
JWKRequestCooldownInMs = 500 # 0.5s
protected_props = [
"sub",
"iat",
Expand Down
2 changes: 1 addition & 1 deletion supertokens_python/recipe/session/cookie_and_header.py
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ def _set_token(
expires: int,
transfer_method: TokenTransferMethod,
):
log_debug_message(f"Setting {token_type} token as {transfer_method}")
log_debug_message("Setting %s token as %s", token_type, transfer_method)
if transfer_method == "cookie":
_set_cookie(
response,
Expand Down
13 changes: 11 additions & 2 deletions supertokens_python/recipe/session/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,17 @@ def raise_try_refresh_token_exception(ex: Union[str, Exception]) -> NoReturn:
raise TryRefreshTokenError(ex) from None


def raise_unauthorised_exception(msg: str, clear_tokens: bool = True) -> NoReturn:
raise UnauthorisedError(msg, clear_tokens) from None
def raise_unauthorised_exception(
msg: str,
clear_tokens: bool = True,
response_mutators: Optional[List[ResponseMutator]] = None,
) -> NoReturn:
if response_mutators is None:
response_mutators = []

err = UnauthorisedError(msg, clear_tokens)
err.response_mutators.extend(UnauthorisedError.response_mutators)
raise err


class SuperTokensSessionError(SuperTokensError):
Expand Down
Loading