From 75811d4083e36f0dbef9e7d8c16a4766fcaf580e Mon Sep 17 00:00:00 2001 From: KShivendu Date: Mon, 20 Feb 2023 13:10:54 +0530 Subject: [PATCH 001/192] chores: Use header based auth in examples --- .../with-thirdpartyemailpassword/project/settings.py | 2 +- examples/with-fastapi/with-thirdpartyemailpassword/main.py | 2 +- examples/with-flask/with-thirdpartyemailpassword/app.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/with-django/with-thirdpartyemailpassword/project/settings.py b/examples/with-django/with-thirdpartyemailpassword/project/settings.py index bb17f2c80..df4d739a3 100644 --- a/examples/with-django/with-thirdpartyemailpassword/project/settings.py +++ b/examples/with-django/with-thirdpartyemailpassword/project/settings.py @@ -65,7 +65,7 @@ def get_website_domain(): framework="django", mode="wsgi", recipe_list=[ - session.init(get_token_transfer_method=lambda _, __, ___: "cookie"), + session.init(), emailverification.init("REQUIRED"), thirdpartyemailpassword.init( providers=[ diff --git a/examples/with-fastapi/with-thirdpartyemailpassword/main.py b/examples/with-fastapi/with-thirdpartyemailpassword/main.py index a94b94884..d3c9d3e8e 100644 --- a/examples/with-fastapi/with-thirdpartyemailpassword/main.py +++ b/examples/with-fastapi/with-thirdpartyemailpassword/main.py @@ -55,7 +55,7 @@ def get_website_domain(): ), framework="fastapi", recipe_list=[ - session.init(get_token_transfer_method=lambda _, __, ___: "cookie"), + session.init(), emailverification.init("REQUIRED"), thirdpartyemailpassword.init( providers=[ diff --git a/examples/with-flask/with-thirdpartyemailpassword/app.py b/examples/with-flask/with-thirdpartyemailpassword/app.py index a29cd215d..56e3163be 100644 --- a/examples/with-flask/with-thirdpartyemailpassword/app.py +++ b/examples/with-flask/with-thirdpartyemailpassword/app.py @@ -48,7 +48,7 @@ def get_website_domain(): ), framework="flask", recipe_list=[ - session.init(get_token_transfer_method=lambda _, __, ___: "cookie"), + session.init(), emailverification.init("REQUIRED"), thirdpartyemailpassword.init( providers=[ From 40953fb9e8bc1fd9690103bd6372b117e8ea545d Mon Sep 17 00:00:00 2001 From: KShivendu Date: Thu, 23 Feb 2023 12:29:10 +0530 Subject: [PATCH 002/192] fix: Update access token cookie expiry logic and add comments --- .../framework/fastapi/fastapi_response.py | 9 +++-- .../recipe/session/recipe_implementation.py | 37 +++++++++++++------ .../recipe/session/session_class.py | 9 ++++- supertokens_python/recipe/session/utils.py | 11 +++--- 4 files changed, 45 insertions(+), 21 deletions(-) diff --git a/supertokens_python/framework/fastapi/fastapi_response.py b/supertokens_python/framework/fastapi/fastapi_response.py index 43cc22887..62ef3ed81 100644 --- a/supertokens_python/framework/fastapi/fastapi_response.py +++ b/supertokens_python/framework/fastapi/fastapi_response.py @@ -13,10 +13,10 @@ # under the License. import json from math import ceil -from time import time from typing import Any, Dict, Optional from supertokens_python.framework.response import BaseResponse +from supertokens_python.utils import get_timestamp_ms class FastApiResponse(BaseResponse): @@ -49,13 +49,16 @@ def set_cookie( httponly: bool = False, samesite: str = "lax", ): + # Note: For FastAPI response object, the expires value + # doesn't mean the absolute time in ms, but the duration in seconds + # So we need to convert our absolute expiry time (ms) to a duration (seconds) if domain is None: # we do ceil because if we do floor, we tests may fail where the access # token lifetime is set to 1 second self.response.set_cookie( key=key, value=value, - expires=ceil((expires - int(time() * 1000)) / 1000), + expires=ceil((expires - get_timestamp_ms()) / 1000), path=path, secure=secure, httponly=httponly, @@ -67,7 +70,7 @@ def set_cookie( self.response.set_cookie( key=key, value=value, - expires=ceil((expires - int(time() * 1000)) / 1000), + expires=ceil((expires - get_timestamp_ms()) / 1000), path=path, domain=domain, secure=secure, diff --git a/supertokens_python/recipe/session/recipe_implementation.py b/supertokens_python/recipe/session/recipe_implementation.py index 8c7a296bc..28db79765 100644 --- a/supertokens_python/recipe/session/recipe_implementation.py +++ b/supertokens_python/recipe/session/recipe_implementation.py @@ -14,7 +14,6 @@ from __future__ import annotations import json -from datetime import datetime from typing import TYPE_CHECKING, Any, Callable, Dict, Optional from supertokens_python.framework import BaseRequest @@ -33,14 +32,14 @@ from . import session_functions from .access_token import validate_access_token_structure from .cookie_and_header import ( + anti_csrf_response_mutator, + clear_session_response_mutator, + front_token_response_mutator, get_anti_csrf_header, get_rid_header, get_token, - token_response_mutator, - front_token_response_mutator, - anti_csrf_response_mutator, set_cookie_response_mutator, - clear_session_response_mutator, + token_response_mutator, ) from .exceptions import ( TokenTheftError, @@ -50,12 +49,12 @@ ) from .interfaces import ( AccessTokenObj, - ResponseMutator, ClaimsValidationResult, GetClaimValueOkResult, JSONObject, RecipeInterface, RegenerateAccessTokenOkResult, + ResponseMutator, SessionClaim, SessionClaimValidator, SessionDoesNotExistError, @@ -64,14 +63,18 @@ ) from .jwt import ParsedJWTInfo, parse_jwt_without_signature_verification from .session_class import Session -from .utils import SessionConfig, TokenTransferMethod, validate_claims_in_payload +from .utils import ( + HUNDRED_YEARS_IN_MS, + SessionConfig, + TokenTransferMethod, + validate_claims_in_payload, +) if TYPE_CHECKING: from typing import List, Union from supertokens_python import AppInfo from supertokens_python.querier import Querier - from .constants import available_token_transfer_methods from .interfaces import SessionContainer @@ -248,12 +251,16 @@ async def create_new_session( new_session.access_token_payload, ) ) + # We set the expiration to 100 years, because we can't really access the expiration of the refresh token everywhere we are setting it. + # 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. + # Even if the token is expired the presence of the token indicates that the user could have a valid refresh + # Setting them to infinity would require special case handling on the frontend and just adding 10 years seems enough. response_mutators.append( token_response_mutator( self.config, "access", new_access_token_info["token"], - int(datetime.now().timestamp()) + 3153600000000, + get_timestamp_ms() + HUNDRED_YEARS_IN_MS, new_session.transfer_method, ) ) @@ -456,12 +463,16 @@ async def get_session( session.access_token_payload, ) ) + # We set the expiration to 100 years, because we can't really access the expiration of the refresh token everywhere we are setting it. + # 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. + # Even if the token is expired the presence of the token indicates that the user could have a valid refresh + # Setting them to infinity would require special case handling on the frontend and just adding 10 years seems enough. session.response_mutators.append( token_response_mutator( self.config, "access", session.access_token, - int(datetime.now().timestamp()) + 3153600000000, + get_timestamp_ms() + HUNDRED_YEARS_IN_MS, session.transfer_method, ) ) @@ -603,12 +614,16 @@ async def refresh_session( session.access_token_payload, ) ) + # We set the expiration to 100 years, because we can't really access the expiration of the refresh token everywhere we are setting it. + # 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. + # Even if the token is expired the presence of the token indicates that the user could have a valid refresh + # Setting them to infinity would require special case handling on the frontend and just adding 10 years seems enough. response_mutators.append( token_response_mutator( self.config, "access", new_access_token_info["token"], - int(datetime.now().timestamp()) + 3153600000000, + get_timestamp_ms() + HUNDRED_YEARS_IN_MS, # 100 years session.transfer_method, ) ) diff --git a/supertokens_python/recipe/session/session_class.py b/supertokens_python/recipe/session/session_class.py index 9334ce603..c6ee1bea5 100644 --- a/supertokens_python/recipe/session/session_class.py +++ b/supertokens_python/recipe/session/session_class.py @@ -11,13 +11,13 @@ # 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 datetime import datetime from typing import Any, Dict, List, TypeVar, Union from supertokens_python.recipe.session.exceptions import ( raise_invalid_claims_exception, raise_unauthorised_exception, ) +from supertokens_python.utils import get_timestamp_ms from .cookie_and_header import ( clear_session_response_mutator, @@ -25,6 +25,7 @@ token_response_mutator, ) from .interfaces import SessionClaim, SessionClaimValidator, SessionContainer +from .utils import HUNDRED_YEARS_IN_MS _T = TypeVar("_T") @@ -95,12 +96,16 @@ async def update_access_token_payload( self.access_token_payload, ) ) + # We set the expiration to 100 years, because we can't really access the expiration of the refresh token everywhere we are setting it. + # 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. + # Even if the token is expired the presence of the token indicates that the user could have a valid refresh + # Setting them to infinity would require special case handling on the frontend and just adding 10 years seems enough. self.response_mutators.append( token_response_mutator( self.config, "access", result.access_token.token, - int(datetime.now().timestamp()) + 3153600000000, + get_timestamp_ms() + HUNDRED_YEARS_IN_MS, self.transfer_method, ) ) diff --git a/supertokens_python/recipe/session/utils.py b/supertokens_python/recipe/session/utils.py index 84245a1f1..82aac353d 100644 --- a/supertokens_python/recipe/session/utils.py +++ b/supertokens_python/recipe/session/utils.py @@ -17,6 +17,8 @@ from typing import TYPE_CHECKING, Any, Awaitable, Callable, Dict, List, Optional, Union from urllib.parse import urlparse +from typing_extensions import Literal + from supertokens_python.exceptions import raise_general_exception from supertokens_python.framework import BaseResponse from supertokens_python.normalised_url_path import NormalisedURLPath @@ -29,13 +31,10 @@ send_non_200_response, send_non_200_response_with_message, ) -from typing_extensions import Literal from ...types import MaybeAwaitable -from .constants import SESSION_REFRESH, AUTH_MODE_HEADER_KEY -from .cookie_and_header import ( - clear_session_from_all_token_transfer_methods, -) +from .constants import AUTH_MODE_HEADER_KEY, SESSION_REFRESH +from .cookie_and_header import clear_session_from_all_token_transfer_methods from .exceptions import ClaimValidationError from .with_jwt.constants import ( ACCESS_TOKEN_PAYLOAD_JWT_PROPERTY_NAME_KEY, @@ -56,6 +55,8 @@ from supertokens_python.logger import log_debug_message +HUNDRED_YEARS_IN_MS = 3153600000000 + def normalise_session_scope(session_scope: str) -> str: def helper(scope: str) -> str: From eddb922ecd2eb2ec57c7b980c85c0b80fdca0220 Mon Sep 17 00:00:00 2001 From: KShivendu Date: Thu, 23 Feb 2023 14:45:50 +0530 Subject: [PATCH 003/192] test: Ensure access token expiry time is correct --- CHANGELOG.md | 4 + .../framework/fastapi/fastapi_request.py | 2 + .../framework/fastapi/fastapi_response.py | 38 +++------ .../recipe/session/recipe_implementation.py | 14 ++-- .../recipe/session/session_class.py | 2 +- tests/test_session.py | 80 +++++++++++++++++-- 6 files changed, 104 insertions(+), 36 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 157646efe..ee61d051e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## unreleased +## [0.12.2] - 2023-02-23 +- Fix expiry time of access token cookie. + + ## [0.12.1] - 2023-02-06 - Email template updates diff --git a/supertokens_python/framework/fastapi/fastapi_request.py b/supertokens_python/framework/fastapi/fastapi_request.py index 95c63b6d7..5d27c66b0 100644 --- a/supertokens_python/framework/fastapi/fastapi_request.py +++ b/supertokens_python/framework/fastapi/fastapi_request.py @@ -45,6 +45,8 @@ def method(self) -> str: return self.request.method def get_cookie(self, key: str) -> Union[str, None]: + # Note: Unlike other frameworks, FastAPI wraps the value in quotes in Set-Cookie header + # It also takes care of escaping the quotes while fetching the value return self.request.cookies.get(key) def get_header(self, key: str) -> Union[str, None]: diff --git a/supertokens_python/framework/fastapi/fastapi_response.py b/supertokens_python/framework/fastapi/fastapi_response.py index 62ef3ed81..70c867ac8 100644 --- a/supertokens_python/framework/fastapi/fastapi_response.py +++ b/supertokens_python/framework/fastapi/fastapi_response.py @@ -52,31 +52,19 @@ def set_cookie( # Note: For FastAPI response object, the expires value # doesn't mean the absolute time in ms, but the duration in seconds # So we need to convert our absolute expiry time (ms) to a duration (seconds) - if domain is None: - # we do ceil because if we do floor, we tests may fail where the access - # token lifetime is set to 1 second - self.response.set_cookie( - key=key, - value=value, - expires=ceil((expires - get_timestamp_ms()) / 1000), - path=path, - secure=secure, - httponly=httponly, - samesite=samesite, - ) - else: - # we do ceil because if we do floor, we tests may fail where the access - # token lifetime is set to 1 second - self.response.set_cookie( - key=key, - value=value, - expires=ceil((expires - get_timestamp_ms()) / 1000), - path=path, - domain=domain, - secure=secure, - httponly=httponly, - samesite=samesite, - ) + + # we do ceil because if we do floor, we tests may fail where the access + # token lifetime is set to 1 second + self.response.set_cookie( + key=key, + value=value, # Note: Unlike other frameworks, FastAPI wraps the value in quotes in Set-Cookie header + expires=ceil((expires - get_timestamp_ms()) / 1000), + path=path, + domain=domain, # type: ignore # starlette didn't set domain as optional type but their default value is None anyways + secure=secure, + httponly=httponly, + samesite=samesite, + ) def set_header(self, key: str, value: str): self.response.headers[key] = value diff --git a/supertokens_python/recipe/session/recipe_implementation.py b/supertokens_python/recipe/session/recipe_implementation.py index 28db79765..c35bb4260 100644 --- a/supertokens_python/recipe/session/recipe_implementation.py +++ b/supertokens_python/recipe/session/recipe_implementation.py @@ -254,7 +254,7 @@ async def create_new_session( # We set the expiration to 100 years, because we can't really access the expiration of the refresh token everywhere we are setting it. # 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. # Even if the token is expired the presence of the token indicates that the user could have a valid refresh - # Setting them to infinity would require special case handling on the frontend and just adding 10 years seems enough. + # Setting them to infinity would require special case handling on the frontend and just adding 100 years seems enough. response_mutators.append( token_response_mutator( self.config, @@ -269,7 +269,9 @@ async def create_new_session( self.config, "refresh", new_refresh_token_info["token"], - new_refresh_token_info["expiry"], + new_refresh_token_info[ + "expiry" + ], # This comes from the core and is 100 days new_session.transfer_method, ) ) @@ -466,7 +468,7 @@ async def get_session( # We set the expiration to 100 years, because we can't really access the expiration of the refresh token everywhere we are setting it. # 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. # Even if the token is expired the presence of the token indicates that the user could have a valid refresh - # Setting them to infinity would require special case handling on the frontend and just adding 10 years seems enough. + # Setting them to infinity would require special case handling on the frontend and just adding 100 years seems enough. session.response_mutators.append( token_response_mutator( self.config, @@ -617,7 +619,7 @@ async def refresh_session( # We set the expiration to 100 years, because we can't really access the expiration of the refresh token everywhere we are setting it. # 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. # Even if the token is expired the presence of the token indicates that the user could have a valid refresh - # Setting them to infinity would require special case handling on the frontend and just adding 10 years seems enough. + # Setting them to infinity would require special case handling on the frontend and just adding 100 years seems enough. response_mutators.append( token_response_mutator( self.config, @@ -633,7 +635,9 @@ async def refresh_session( self.config, "refresh", new_refresh_token_info["token"], - new_refresh_token_info["expiry"], + new_refresh_token_info[ + "expiry" + ], # This comes from the core and is 100 days session.transfer_method, ) ) diff --git a/supertokens_python/recipe/session/session_class.py b/supertokens_python/recipe/session/session_class.py index c6ee1bea5..8db135363 100644 --- a/supertokens_python/recipe/session/session_class.py +++ b/supertokens_python/recipe/session/session_class.py @@ -99,7 +99,7 @@ async def update_access_token_payload( # We set the expiration to 100 years, because we can't really access the expiration of the refresh token everywhere we are setting it. # 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. # Even if the token is expired the presence of the token indicates that the user could have a valid refresh - # Setting them to infinity would require special case handling on the frontend and just adding 10 years seems enough. + # Setting them to infinity would require special case handling on the frontend and just adding 100 years seems enough. self.response_mutators.append( token_response_mutator( self.config, diff --git a/tests/test_session.py b/tests/test_session.py index 2b6c3e7ed..d4e7e5008 100644 --- a/tests/test_session.py +++ b/tests/test_session.py @@ -12,6 +12,7 @@ # License for the specific language governing permissions and limitations # under the License. +from datetime import datetime, timedelta from typing import Any, Dict, List from unittest.mock import MagicMock @@ -28,9 +29,6 @@ from supertokens_python.recipe.session.asyncio import ( create_new_session as async_create_new_session, ) -from supertokens_python.recipe.session.jwt import ( - parse_jwt_without_signature_verification, -) from supertokens_python.recipe.session.asyncio import ( get_all_session_handles_for_user, get_session_information, @@ -44,6 +42,9 @@ update_session_data, ) from supertokens_python.recipe.session.interfaces import RecipeInterface +from supertokens_python.recipe.session.jwt import ( + parse_jwt_without_signature_verification, +) from supertokens_python.recipe.session.recipe_implementation import RecipeImplementation from supertokens_python.recipe.session.session_functions import ( create_new_session, @@ -356,10 +357,10 @@ async def get_session_information( from supertokens_python.recipe.session.exceptions import raise_unauthorised_exception from supertokens_python.recipe.session.interfaces import APIInterface, APIOptions from tests.utils import ( + assert_info_clears_tokens, extract_all_cookies, - get_st_init_args, extract_info, - assert_info_clears_tokens, + get_st_init_args, ) @@ -578,3 +579,72 @@ async def refresh_post(api_options: APIOptions, user_context: Dict[str, Any]): assert cookies["sAccessToken"]["value"] != "" assert cookies["sRefreshToken"]["value"] != "" + + +async def test_token_cookie_expires( + driver_config_client: TestClient, +): + init_args = get_st_init_args( + [ + session.init( + anti_csrf="VIA_TOKEN", + get_token_transfer_method=lambda _, __, ___: "cookie", + ), + ] + ) + init(**init_args) + start_st() + + response = driver_config_client.post("/create") + assert response.status_code == 200 + + cookies = extract_all_cookies(response) + + assert "sAccessToken" in cookies + assert "sRefreshToken" in cookies + + for c in response.cookies: + if c.name == "sAccessToken": # 100 years (set by the SDK) + # some time must have elasped since the cookie was set. So less than current time + assert ( + datetime.fromtimestamp(c.expires or 0) - timedelta(days=365.25 * 100) + < datetime.now() + ) + if c.name == "sRefreshToken": # 100 days (set by the core) + assert ( + datetime.fromtimestamp(c.expires or 0) - timedelta(days=100) + < datetime.now() + ) + + assert response.headers["anti-csrf"] != "" + assert response.headers["front-token"] != "" + + response = driver_config_client.post( + "/auth/session/refresh", + cookies={ + "sRefreshToken": cookies["sRefreshToken"]["value"], + }, + headers={"anti-csrf": response.headers["anti-csrf"]}, + ) + + assert response.status_code == 200 + cookies = extract_all_cookies(response) + + assert "sAccessToken" in cookies + assert "sRefreshToken" in cookies + + for c in response.cookies: + if c.name == "sAccessToken": # 100 years (set by the SDK) + # some time must have elasped since the cookie was set. So less than current time + assert ( + datetime.fromtimestamp(c.expires or 0) - timedelta(days=365.25 * 100) + < datetime.now() + ) + if c.name == "sRefreshToken": # 100 days (set by the core) + assert ( + datetime.fromtimestamp(c.expires or 0) - timedelta(days=100) + < datetime.now() + ) + + assert response.headers["anti-csrf"] != "" + assert response.headers["front-token"] != "" From 68cfa52acc6876a928a2584659b67984613166af Mon Sep 17 00:00:00 2001 From: KShivendu Date: Thu, 23 Feb 2023 14:52:00 +0530 Subject: [PATCH 004/192] chores: Bump version to 0.12.2 --- setup.py | 2 +- supertokens_python/constants.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 7f9e294b2..41d7812f4 100644 --- a/setup.py +++ b/setup.py @@ -70,7 +70,7 @@ setup( name="supertokens_python", - version="0.12.1", + version="0.12.2", author="SuperTokens", license="Apache 2.0", author_email="team@supertokens.com", diff --git a/supertokens_python/constants.py b/supertokens_python/constants.py index 122a37fb8..abb38620e 100644 --- a/supertokens_python/constants.py +++ b/supertokens_python/constants.py @@ -12,7 +12,7 @@ # License for the specific language governing permissions and limitations # under the License. SUPPORTED_CDI_VERSIONS = ["2.9", "2.10", "2.11", "2.12", "2.13", "2.14", "2.15"] -VERSION = "0.12.1" +VERSION = "0.12.2" TELEMETRY = "/telemetry" USER_COUNT = "/users/count" USER_DELETE = "/user/remove" From 550c1260ed7b9653deb26e8ca0f3039556767b54 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Thu, 23 Feb 2023 14:56:37 +0530 Subject: [PATCH 005/192] adding dev-v0.12.2 tag to this commit to ensure building --- html/supertokens_python/constants.html | 2 +- .../framework/fastapi/fastapi_request.html | 6 + .../framework/fastapi/fastapi_response.html | 125 +++++++----------- .../recipe/session/recipe_implementation.html | 97 ++++++++++---- .../recipe/session/session_class.html | 15 ++- .../recipe/session/utils.html | 11 +- 6 files changed, 148 insertions(+), 108 deletions(-) diff --git a/html/supertokens_python/constants.html b/html/supertokens_python/constants.html index a76743305..06d1f8628 100644 --- a/html/supertokens_python/constants.html +++ b/html/supertokens_python/constants.html @@ -40,7 +40,7 @@

Module supertokens_python.constants

# License for the specific language governing permissions and limitations # under the License. SUPPORTED_CDI_VERSIONS = ["2.9", "2.10", "2.11", "2.12", "2.13", "2.14", "2.15"] -VERSION = "0.12.1" +VERSION = "0.12.2" TELEMETRY = "/telemetry" USER_COUNT = "/users/count" USER_DELETE = "/user/remove" diff --git a/html/supertokens_python/framework/fastapi/fastapi_request.html b/html/supertokens_python/framework/fastapi/fastapi_request.html index dea124fd5..65fd1b6fa 100644 --- a/html/supertokens_python/framework/fastapi/fastapi_request.html +++ b/html/supertokens_python/framework/fastapi/fastapi_request.html @@ -73,6 +73,8 @@

Module supertokens_python.framework.fastapi.fastapi_requ return self.request.method def get_cookie(self, key: str) -> Union[str, None]: + # Note: Unlike other frameworks, FastAPI wraps the value in quotes in Set-Cookie header + # It also takes care of escaping the quotes while fetching the value return self.request.cookies.get(key) def get_header(self, key: str) -> Union[str, None]: @@ -141,6 +143,8 @@

Classes

return self.request.method def get_cookie(self, key: str) -> Union[str, None]: + # Note: Unlike other frameworks, FastAPI wraps the value in quotes in Set-Cookie header + # It also takes care of escaping the quotes while fetching the value return self.request.cookies.get(key) def get_header(self, key: str) -> Union[str, None]: @@ -203,6 +207,8 @@

Methods

Expand source code
def get_cookie(self, key: str) -> Union[str, None]:
+    # Note: Unlike other frameworks, FastAPI wraps the value in quotes in Set-Cookie header
+    # It also takes care of escaping the quotes while fetching the value
     return self.request.cookies.get(key)
diff --git a/html/supertokens_python/framework/fastapi/fastapi_response.html b/html/supertokens_python/framework/fastapi/fastapi_response.html index bf7454bae..4a581ceb3 100644 --- a/html/supertokens_python/framework/fastapi/fastapi_response.html +++ b/html/supertokens_python/framework/fastapi/fastapi_response.html @@ -41,10 +41,10 @@

Module supertokens_python.framework.fastapi.fastapi_resp # under the License. import json from math import ceil -from time import time from typing import Any, Dict, Optional from supertokens_python.framework.response import BaseResponse +from supertokens_python.utils import get_timestamp_ms class FastApiResponse(BaseResponse): @@ -77,31 +77,22 @@

Module supertokens_python.framework.fastapi.fastapi_resp httponly: bool = False, samesite: str = "lax", ): - if domain is None: - # we do ceil because if we do floor, we tests may fail where the access - # token lifetime is set to 1 second - self.response.set_cookie( - key=key, - value=value, - expires=ceil((expires - int(time() * 1000)) / 1000), - path=path, - secure=secure, - httponly=httponly, - samesite=samesite, - ) - else: - # we do ceil because if we do floor, we tests may fail where the access - # token lifetime is set to 1 second - self.response.set_cookie( - key=key, - value=value, - expires=ceil((expires - int(time() * 1000)) / 1000), - path=path, - domain=domain, - secure=secure, - httponly=httponly, - samesite=samesite, - ) + # Note: For FastAPI response object, the expires value + # doesn't mean the absolute time in ms, but the duration in seconds + # So we need to convert our absolute expiry time (ms) to a duration (seconds) + + # we do ceil because if we do floor, we tests may fail where the access + # token lifetime is set to 1 second + self.response.set_cookie( + key=key, + value=value, # Note: Unlike other frameworks, FastAPI wraps the value in quotes in Set-Cookie header + expires=ceil((expires - get_timestamp_ms()) / 1000), + path=path, + domain=domain, # type: ignore # starlette didn't set domain as optional type but their default value is None anyways + secure=secure, + httponly=httponly, + samesite=samesite, + ) def set_header(self, key: str, value: str): self.response.headers[key] = value @@ -183,31 +174,22 @@

Classes

httponly: bool = False, samesite: str = "lax", ): - if domain is None: - # we do ceil because if we do floor, we tests may fail where the access - # token lifetime is set to 1 second - self.response.set_cookie( - key=key, - value=value, - expires=ceil((expires - int(time() * 1000)) / 1000), - path=path, - secure=secure, - httponly=httponly, - samesite=samesite, - ) - else: - # we do ceil because if we do floor, we tests may fail where the access - # token lifetime is set to 1 second - self.response.set_cookie( - key=key, - value=value, - expires=ceil((expires - int(time() * 1000)) / 1000), - path=path, - domain=domain, - secure=secure, - httponly=httponly, - samesite=samesite, - ) + # Note: For FastAPI response object, the expires value + # doesn't mean the absolute time in ms, but the duration in seconds + # So we need to convert our absolute expiry time (ms) to a duration (seconds) + + # we do ceil because if we do floor, we tests may fail where the access + # token lifetime is set to 1 second + self.response.set_cookie( + key=key, + value=value, # Note: Unlike other frameworks, FastAPI wraps the value in quotes in Set-Cookie header + expires=ceil((expires - get_timestamp_ms()) / 1000), + path=path, + domain=domain, # type: ignore # starlette didn't set domain as optional type but their default value is None anyways + secure=secure, + httponly=httponly, + samesite=samesite, + ) def set_header(self, key: str, value: str): self.response.headers[key] = value @@ -298,31 +280,22 @@

Methods

httponly: bool = False, samesite: str = "lax", ): - if domain is None: - # we do ceil because if we do floor, we tests may fail where the access - # token lifetime is set to 1 second - self.response.set_cookie( - key=key, - value=value, - expires=ceil((expires - int(time() * 1000)) / 1000), - path=path, - secure=secure, - httponly=httponly, - samesite=samesite, - ) - else: - # we do ceil because if we do floor, we tests may fail where the access - # token lifetime is set to 1 second - self.response.set_cookie( - key=key, - value=value, - expires=ceil((expires - int(time() * 1000)) / 1000), - path=path, - domain=domain, - secure=secure, - httponly=httponly, - samesite=samesite, - )
+ # Note: For FastAPI response object, the expires value + # doesn't mean the absolute time in ms, but the duration in seconds + # So we need to convert our absolute expiry time (ms) to a duration (seconds) + + # we do ceil because if we do floor, we tests may fail where the access + # token lifetime is set to 1 second + self.response.set_cookie( + key=key, + value=value, # Note: Unlike other frameworks, FastAPI wraps the value in quotes in Set-Cookie header + expires=ceil((expires - get_timestamp_ms()) / 1000), + path=path, + domain=domain, # type: ignore # starlette didn't set domain as optional type but their default value is None anyways + secure=secure, + httponly=httponly, + samesite=samesite, + )
diff --git a/html/supertokens_python/recipe/session/recipe_implementation.html b/html/supertokens_python/recipe/session/recipe_implementation.html index 656b6aaaa..4d8a68549 100644 --- a/html/supertokens_python/recipe/session/recipe_implementation.html +++ b/html/supertokens_python/recipe/session/recipe_implementation.html @@ -42,7 +42,6 @@

Module supertokens_python.recipe.session.recipe_implemen from __future__ import annotations import json -from datetime import datetime from typing import TYPE_CHECKING, Any, Callable, Dict, Optional from supertokens_python.framework import BaseRequest @@ -61,14 +60,14 @@

Module supertokens_python.recipe.session.recipe_implemen from . import session_functions from .access_token import validate_access_token_structure from .cookie_and_header import ( + anti_csrf_response_mutator, + clear_session_response_mutator, + front_token_response_mutator, get_anti_csrf_header, get_rid_header, get_token, - token_response_mutator, - front_token_response_mutator, - anti_csrf_response_mutator, set_cookie_response_mutator, - clear_session_response_mutator, + token_response_mutator, ) from .exceptions import ( TokenTheftError, @@ -78,12 +77,12 @@

Module supertokens_python.recipe.session.recipe_implemen ) from .interfaces import ( AccessTokenObj, - ResponseMutator, ClaimsValidationResult, GetClaimValueOkResult, JSONObject, RecipeInterface, RegenerateAccessTokenOkResult, + ResponseMutator, SessionClaim, SessionClaimValidator, SessionDoesNotExistError, @@ -92,14 +91,18 @@

Module supertokens_python.recipe.session.recipe_implemen ) from .jwt import ParsedJWTInfo, parse_jwt_without_signature_verification from .session_class import Session -from .utils import SessionConfig, TokenTransferMethod, validate_claims_in_payload +from .utils import ( + HUNDRED_YEARS_IN_MS, + SessionConfig, + TokenTransferMethod, + validate_claims_in_payload, +) if TYPE_CHECKING: from typing import List, Union from supertokens_python import AppInfo from supertokens_python.querier import Querier - from .constants import available_token_transfer_methods from .interfaces import SessionContainer @@ -276,12 +279,16 @@

Module supertokens_python.recipe.session.recipe_implemen new_session.access_token_payload, ) ) + # We set the expiration to 100 years, because we can't really access the expiration of the refresh token everywhere we are setting it. + # 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. + # Even if the token is expired the presence of the token indicates that the user could have a valid refresh + # Setting them to infinity would require special case handling on the frontend and just adding 100 years seems enough. response_mutators.append( token_response_mutator( self.config, "access", new_access_token_info["token"], - int(datetime.now().timestamp()) + 3153600000000, + get_timestamp_ms() + HUNDRED_YEARS_IN_MS, new_session.transfer_method, ) ) @@ -290,7 +297,9 @@

Module supertokens_python.recipe.session.recipe_implemen self.config, "refresh", new_refresh_token_info["token"], - new_refresh_token_info["expiry"], + new_refresh_token_info[ + "expiry" + ], # This comes from the core and is 100 days new_session.transfer_method, ) ) @@ -484,12 +493,16 @@

Module supertokens_python.recipe.session.recipe_implemen session.access_token_payload, ) ) + # We set the expiration to 100 years, because we can't really access the expiration of the refresh token everywhere we are setting it. + # 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. + # Even if the token is expired the presence of the token indicates that the user could have a valid refresh + # Setting them to infinity would require special case handling on the frontend and just adding 100 years seems enough. session.response_mutators.append( token_response_mutator( self.config, "access", session.access_token, - int(datetime.now().timestamp()) + 3153600000000, + get_timestamp_ms() + HUNDRED_YEARS_IN_MS, session.transfer_method, ) ) @@ -631,12 +644,16 @@

Module supertokens_python.recipe.session.recipe_implemen session.access_token_payload, ) ) + # We set the expiration to 100 years, because we can't really access the expiration of the refresh token everywhere we are setting it. + # 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. + # Even if the token is expired the presence of the token indicates that the user could have a valid refresh + # Setting them to infinity would require special case handling on the frontend and just adding 100 years seems enough. response_mutators.append( token_response_mutator( self.config, "access", new_access_token_info["token"], - int(datetime.now().timestamp()) + 3153600000000, + get_timestamp_ms() + HUNDRED_YEARS_IN_MS, # 100 years session.transfer_method, ) ) @@ -646,7 +663,9 @@

Module supertokens_python.recipe.session.recipe_implemen self.config, "refresh", new_refresh_token_info["token"], - new_refresh_token_info["expiry"], + new_refresh_token_info[ + "expiry" + ], # This comes from the core and is 100 days session.transfer_method, ) ) @@ -1082,12 +1101,16 @@

Methods

new_session.access_token_payload, ) ) + # We set the expiration to 100 years, because we can't really access the expiration of the refresh token everywhere we are setting it. + # 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. + # Even if the token is expired the presence of the token indicates that the user could have a valid refresh + # Setting them to infinity would require special case handling on the frontend and just adding 100 years seems enough. response_mutators.append( token_response_mutator( self.config, "access", new_access_token_info["token"], - int(datetime.now().timestamp()) + 3153600000000, + get_timestamp_ms() + HUNDRED_YEARS_IN_MS, new_session.transfer_method, ) ) @@ -1096,7 +1119,9 @@

Methods

self.config, "refresh", new_refresh_token_info["token"], - new_refresh_token_info["expiry"], + new_refresh_token_info[ + "expiry" + ], # This comes from the core and is 100 days new_session.transfer_method, ) ) @@ -1290,12 +1315,16 @@

Methods

session.access_token_payload, ) ) + # We set the expiration to 100 years, because we can't really access the expiration of the refresh token everywhere we are setting it. + # 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. + # Even if the token is expired the presence of the token indicates that the user could have a valid refresh + # Setting them to infinity would require special case handling on the frontend and just adding 100 years seems enough. session.response_mutators.append( token_response_mutator( self.config, "access", session.access_token, - int(datetime.now().timestamp()) + 3153600000000, + get_timestamp_ms() + HUNDRED_YEARS_IN_MS, session.transfer_method, ) ) @@ -1437,12 +1466,16 @@

Methods

session.access_token_payload, ) ) + # We set the expiration to 100 years, because we can't really access the expiration of the refresh token everywhere we are setting it. + # 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. + # Even if the token is expired the presence of the token indicates that the user could have a valid refresh + # Setting them to infinity would require special case handling on the frontend and just adding 100 years seems enough. response_mutators.append( token_response_mutator( self.config, "access", new_access_token_info["token"], - int(datetime.now().timestamp()) + 3153600000000, + get_timestamp_ms() + HUNDRED_YEARS_IN_MS, # 100 years session.transfer_method, ) ) @@ -1452,7 +1485,9 @@

Methods

self.config, "refresh", new_refresh_token_info["token"], - new_refresh_token_info["expiry"], + new_refresh_token_info[ + "expiry" + ], # This comes from the core and is 100 days session.transfer_method, ) ) @@ -1758,12 +1793,16 @@

Methods

new_session.access_token_payload, ) ) + # We set the expiration to 100 years, because we can't really access the expiration of the refresh token everywhere we are setting it. + # 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. + # Even if the token is expired the presence of the token indicates that the user could have a valid refresh + # Setting them to infinity would require special case handling on the frontend and just adding 100 years seems enough. response_mutators.append( token_response_mutator( self.config, "access", new_access_token_info["token"], - int(datetime.now().timestamp()) + 3153600000000, + get_timestamp_ms() + HUNDRED_YEARS_IN_MS, new_session.transfer_method, ) ) @@ -1772,7 +1811,9 @@

Methods

self.config, "refresh", new_refresh_token_info["token"], - new_refresh_token_info["expiry"], + new_refresh_token_info[ + "expiry" + ], # This comes from the core and is 100 days new_session.transfer_method, ) ) @@ -2060,12 +2101,16 @@

Methods

session.access_token_payload, ) ) + # We set the expiration to 100 years, because we can't really access the expiration of the refresh token everywhere we are setting it. + # 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. + # Even if the token is expired the presence of the token indicates that the user could have a valid refresh + # Setting them to infinity would require special case handling on the frontend and just adding 100 years seems enough. session.response_mutators.append( token_response_mutator( self.config, "access", session.access_token, - int(datetime.now().timestamp()) + 3153600000000, + get_timestamp_ms() + HUNDRED_YEARS_IN_MS, session.transfer_method, ) ) @@ -2261,12 +2306,16 @@

Methods

session.access_token_payload, ) ) + # We set the expiration to 100 years, because we can't really access the expiration of the refresh token everywhere we are setting it. + # 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. + # Even if the token is expired the presence of the token indicates that the user could have a valid refresh + # Setting them to infinity would require special case handling on the frontend and just adding 100 years seems enough. response_mutators.append( token_response_mutator( self.config, "access", new_access_token_info["token"], - int(datetime.now().timestamp()) + 3153600000000, + get_timestamp_ms() + HUNDRED_YEARS_IN_MS, # 100 years session.transfer_method, ) ) @@ -2276,7 +2325,9 @@

Methods

self.config, "refresh", new_refresh_token_info["token"], - new_refresh_token_info["expiry"], + new_refresh_token_info[ + "expiry" + ], # This comes from the core and is 100 days session.transfer_method, ) ) diff --git a/html/supertokens_python/recipe/session/session_class.html b/html/supertokens_python/recipe/session/session_class.html index f25435cde..f63759fe6 100644 --- a/html/supertokens_python/recipe/session/session_class.html +++ b/html/supertokens_python/recipe/session/session_class.html @@ -39,13 +39,13 @@

Module supertokens_python.recipe.session.session_classModule supertokens_python.recipe.session.session_classModule supertokens_python.recipe.session.session_classClasses

self.access_token_payload, ) ) + # We set the expiration to 100 years, because we can't really access the expiration of the refresh token everywhere we are setting it. + # 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. + # Even if the token is expired the presence of the token indicates that the user could have a valid refresh + # Setting them to infinity would require special case handling on the frontend and just adding 100 years seems enough. self.response_mutators.append( token_response_mutator( self.config, "access", result.access_token.token, - int(datetime.now().timestamp()) + 3153600000000, + get_timestamp_ms() + HUNDRED_YEARS_IN_MS, self.transfer_method, ) ) diff --git a/html/supertokens_python/recipe/session/utils.html b/html/supertokens_python/recipe/session/utils.html index af2760aea..cab8b23b5 100644 --- a/html/supertokens_python/recipe/session/utils.html +++ b/html/supertokens_python/recipe/session/utils.html @@ -45,6 +45,8 @@

Module supertokens_python.recipe.session.utilsModule supertokens_python.recipe.session.utilsModule supertokens_python.recipe.session.utils Date: Mon, 27 Feb 2023 12:42:37 +0530 Subject: [PATCH 006/192] added functions and updateed interface for email-pasword-login --- .../recipe/dashboard/api/__init__.py | 7 +- .../recipe/dashboard/api/signin.py | 24 +++++ .../recipe/dashboard/api/signout.py | 26 +++++ .../recipe/dashboard/constants.py | 2 + .../recipe/dashboard/interfaces.py | 10 +- supertokens_python/recipe/dashboard/recipe.py | 63 +++++------- supertokens_python/recipe/dashboard/utils.py | 99 ++++++++++--------- 7 files changed, 142 insertions(+), 89 deletions(-) create mode 100644 supertokens_python/recipe/dashboard/api/signin.py create mode 100644 supertokens_python/recipe/dashboard/api/signout.py diff --git a/supertokens_python/recipe/dashboard/api/__init__.py b/supertokens_python/recipe/dashboard/api/__init__.py index bdef6df16..badb48b82 100644 --- a/supertokens_python/recipe/dashboard/api/__init__.py +++ b/supertokens_python/recipe/dashboard/api/__init__.py @@ -13,10 +13,13 @@ # under the License. from .api_key_protector import api_key_protector from .dashboard import handle_dashboard_api +from .signin import handle_sign_in_api +from .signout import handle_signout from .userdetails.user_delete import handle_user_delete from .userdetails.user_email_verify_get import handle_user_email_verify_get from .userdetails.user_email_verify_put import handle_user_email_verify_put -from .userdetails.user_email_verify_token_post import handle_email_verify_token_post +from .userdetails.user_email_verify_token_post import \ + handle_email_verify_token_post from .userdetails.user_get import handle_user_get from .userdetails.user_metadata_get import handle_metadata_get from .userdetails.user_metadata_put import handle_metadata_put @@ -45,4 +48,6 @@ "handle_user_sessions_post", "handle_user_password_put", "handle_email_verify_token_post", + "handle_sign_in_api", + "handle_signout" ] diff --git a/supertokens_python/recipe/dashboard/api/signin.py b/supertokens_python/recipe/dashboard/api/signin.py new file mode 100644 index 000000000..db20f27e4 --- /dev/null +++ b/supertokens_python/recipe/dashboard/api/signin.py @@ -0,0 +1,24 @@ +# Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. +# +# This software is licensed under the Apache License, Version 2.0 (the +# "License") as published by the Apache Software Foundation. +# +# You may not use this file except in compliance with the License. You may +# obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# 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 __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from supertokens_python.recipe.dashboard.interfaces import (APIInterface, + APIOptions) + + +async def handle_sign_in_api(api_implementation: APIInterface, api_options: APIOptions): + pass \ No newline at end of file diff --git a/supertokens_python/recipe/dashboard/api/signout.py b/supertokens_python/recipe/dashboard/api/signout.py new file mode 100644 index 000000000..1f5a55fff --- /dev/null +++ b/supertokens_python/recipe/dashboard/api/signout.py @@ -0,0 +1,26 @@ +# Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. +# +# This software is licensed under the Apache License, Version 2.0 (the +# "License") as published by the Apache Software Foundation. +# +# You may not use this file except in compliance with the License. You may +# obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# 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 __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from supertokens_python.recipe.dashboard.interfaces import (APIInterface, + APIOptions) + +from ..interfaces import SignOutOK + + +async def handle_signout(api_implementation: APIInterface, api_options: APIOptions) -> SignOutOK: + pass \ No newline at end of file diff --git a/supertokens_python/recipe/dashboard/constants.py b/supertokens_python/recipe/dashboard/constants.py index 85652d3a2..9505bf658 100644 --- a/supertokens_python/recipe/dashboard/constants.py +++ b/supertokens_python/recipe/dashboard/constants.py @@ -8,3 +8,5 @@ USER_SESSION_API = "/api/user/sessions" USER_PASSWORD_API = "/api/user/password" USER_EMAIL_VERIFY_TOKEN_API = "/api/user/email/verify/token" +EMAIL_PASSWORD_SIGN_IN = "/api/signin" +EMAIL_PASSSWORD_SIGNOUT = "/api/signout" diff --git a/supertokens_python/recipe/dashboard/interfaces.py b/supertokens_python/recipe/dashboard/interfaces.py index ecc7f0016..5035a45bb 100644 --- a/supertokens_python/recipe/dashboard/interfaces.py +++ b/supertokens_python/recipe/dashboard/interfaces.py @@ -14,9 +14,11 @@ from __future__ import annotations from abc import ABC, abstractmethod -from typing import TYPE_CHECKING, Any, Awaitable, Callable, Dict, List, Optional, Union +from typing import (TYPE_CHECKING, Any, Awaitable, Callable, Dict, List, + Optional, Union) -from supertokens_python.recipe.session.interfaces import SessionInformationResult +from supertokens_python.recipe.session.interfaces import \ + SessionInformationResult from supertokens_python.types import User from ...supertokens import AppInfo @@ -285,3 +287,7 @@ def __init__(self, error: str) -> None: def to_json(self) -> Dict[str, Any]: return {"status": self.status, "error": self.error} + +class SignOutOK(APIResponse): + def to_json(self): + return {"status": "OK"} \ No newline at end of file diff --git a/supertokens_python/recipe/dashboard/recipe.py b/supertokens_python/recipe/dashboard/recipe.py index ad4d98718..02d0efb9c 100644 --- a/supertokens_python/recipe/dashboard/recipe.py +++ b/supertokens_python/recipe/dashboard/recipe.py @@ -19,24 +19,14 @@ from supertokens_python.normalised_url_path import NormalisedURLPath from supertokens_python.recipe_module import APIHandled, RecipeModule -from .api import ( - api_key_protector, - handle_dashboard_api, - handle_email_verify_token_post, - handle_metadata_get, - handle_metadata_put, - handle_sessions_get, - handle_user_delete, - handle_user_email_verify_get, - handle_user_email_verify_put, - handle_user_get, - handle_user_password_put, - handle_user_put, - handle_user_sessions_post, - handle_users_count_get_api, - handle_users_get_api, - handle_validate_key_api, -) +from .api import (api_key_protector, handle_dashboard_api, + handle_email_verify_token_post, handle_metadata_get, + handle_metadata_put, handle_sessions_get, handle_sign_in_api, + handle_signout, handle_user_delete, + handle_user_email_verify_get, handle_user_email_verify_put, + handle_user_get, handle_user_password_put, handle_user_put, + handle_user_sessions_post, handle_users_count_get_api, + handle_users_get_api, handle_validate_key_api) from .api.implementation import APIImplementation from .exceptions import SuperTokensDashboardError from .interfaces import APIInterface, APIOptions @@ -48,26 +38,16 @@ from supertokens_python.supertokens import AppInfo from supertokens_python.types import APIResponse -from supertokens_python.exceptions import SuperTokensError, raise_general_exception - -from .constants import ( - DASHBOARD_API, - USER_API, - USER_EMAIL_VERIFY_API, - USER_EMAIL_VERIFY_TOKEN_API, - USER_METADATA_API, - USER_PASSWORD_API, - USER_SESSION_API, - USERS_COUNT_API, - USERS_LIST_GET_API, - VALIDATE_KEY_API, -) -from .utils import ( - InputOverrideConfig, - get_api_if_matched, - is_api_path, - validate_and_normalise_user_input, -) +from supertokens_python.exceptions import (SuperTokensError, + raise_general_exception) + +from .constants import (DASHBOARD_API, EMAIL_PASSSWORD_SIGNOUT, + EMAIL_PASSWORD_SIGN_IN, USER_API, + USER_EMAIL_VERIFY_API, USER_EMAIL_VERIFY_TOKEN_API, + USER_METADATA_API, USER_PASSWORD_API, USER_SESSION_API, + USERS_COUNT_API, USERS_LIST_GET_API, VALIDATE_KEY_API) +from .utils import (InputOverrideConfig, get_api_if_matched, is_api_path, + validate_and_normalise_user_input) class DashboardRecipe(RecipeModule): @@ -136,6 +116,8 @@ async def handle_api_request( return await handle_dashboard_api(self.api_implementation, api_options) if request_id == VALIDATE_KEY_API: return await handle_validate_key_api(self.api_implementation, api_options) + if request_id == EMAIL_PASSWORD_SIGN_IN: + return await handle_sign_in_api(self.api_implementation, api_options) # Do API key validation for the remaining APIs api_function: Optional[ @@ -171,6 +153,8 @@ async def handle_api_request( api_function = handle_user_password_put elif request_id == USER_EMAIL_VERIFY_TOKEN_API: api_function = handle_email_verify_token_post + elif request_id == EMAIL_PASSSWORD_SIGNOUT: + api_function = handle_signout if api_function is not None: return await api_key_protector( @@ -221,7 +205,8 @@ def reset(): if ("SUPERTOKENS_ENV" not in environ) or ( environ["SUPERTOKENS_ENV"] != "testing" ): - raise_general_exception("calling testing function in non testing env") + raise_general_exception( + "calling testing function in non testing env") DashboardRecipe.__instance = None def return_api_id_if_can_handle_request( diff --git a/supertokens_python/recipe/dashboard/utils.py b/supertokens_python/recipe/dashboard/utils.py index 8ffeddb6b..5b39f9b0a 100644 --- a/supertokens_python/recipe/dashboard/utils.py +++ b/supertokens_python/recipe/dashboard/utils.py @@ -15,47 +15,36 @@ from typing import TYPE_CHECKING, Any, Callable, Dict, Optional, Union +if TYPE_CHECKING: + from supertokens_python.framework.request import BaseRequest + from supertokens_python.recipe.emailpassword import EmailPasswordRecipe -from supertokens_python.recipe.emailpassword.asyncio import ( - get_user_by_id as ep_get_user_by_id, -) +from supertokens_python.recipe.emailpassword.asyncio import \ + get_user_by_id as ep_get_user_by_id from supertokens_python.recipe.passwordless import PasswordlessRecipe -from supertokens_python.recipe.passwordless.asyncio import ( - get_user_by_id as pless_get_user_by_id, -) +from supertokens_python.recipe.passwordless.asyncio import \ + get_user_by_id as pless_get_user_by_id from supertokens_python.recipe.thirdparty import ThirdPartyRecipe -from supertokens_python.recipe.thirdparty.asyncio import ( - get_user_by_id as tp_get_user_by_idx, -) -from supertokens_python.recipe.thirdpartyemailpassword import ( - ThirdPartyEmailPasswordRecipe, -) -from supertokens_python.recipe.thirdpartyemailpassword.asyncio import ( - get_user_by_id as tpep_get_user_by_id, -) -from supertokens_python.recipe.thirdpartypasswordless import ( - ThirdPartyPasswordlessRecipe, -) -from supertokens_python.recipe.thirdpartypasswordless.asyncio import ( - get_user_by_id as tppless_get_user_by_id, -) +from supertokens_python.recipe.thirdparty.asyncio import \ + get_user_by_id as tp_get_user_by_idx +from supertokens_python.recipe.thirdpartyemailpassword import \ + ThirdPartyEmailPasswordRecipe +from supertokens_python.recipe.thirdpartyemailpassword.asyncio import \ + get_user_by_id as tpep_get_user_by_id +from supertokens_python.recipe.thirdpartypasswordless import \ + ThirdPartyPasswordlessRecipe +from supertokens_python.recipe.thirdpartypasswordless.asyncio import \ + get_user_by_id as tppless_get_user_by_id from supertokens_python.types import User from supertokens_python.utils import Awaitable from ...normalised_url_path import NormalisedURLPath from ...supertokens import AppInfo -from .constants import ( - DASHBOARD_API, - USER_API, - USER_EMAIL_VERIFY_API, - USER_EMAIL_VERIFY_TOKEN_API, - USER_METADATA_API, - USER_PASSWORD_API, - USER_SESSION_API, - USERS_COUNT_API, - USERS_LIST_GET_API, - VALIDATE_KEY_API, -) +from .constants import (DASHBOARD_API, EMAIL_PASSSWORD_SIGNOUT, + EMAIL_PASSWORD_SIGN_IN, USER_API, + USER_EMAIL_VERIFY_API, USER_EMAIL_VERIFY_TOKEN_API, + USER_METADATA_API, USER_PASSWORD_API, USER_SESSION_API, + USERS_COUNT_API, USERS_LIST_GET_API, VALIDATE_KEY_API) if TYPE_CHECKING: from .interfaces import APIInterface, RecipeInterface @@ -143,7 +132,8 @@ def to_json(self) -> Dict[str, Any]: class InputOverrideConfig: def __init__( self, - functions: Union[Callable[[RecipeInterface], RecipeInterface], None] = None, + functions: Union[Callable[[RecipeInterface], + RecipeInterface], None] = None, apis: Union[Callable[[APIInterface], APIInterface], None] = None, ): self.functions = functions @@ -153,7 +143,8 @@ def __init__( class OverrideConfig: def __init__( self, - functions: Union[Callable[[RecipeInterface], RecipeInterface], None] = None, + functions: Union[Callable[[RecipeInterface], + RecipeInterface], None] = None, apis: Union[Callable[[APIInterface], APIInterface], None] = None, ): self.functions = functions @@ -165,9 +156,11 @@ def __init__( self, api_key: str, override: OverrideConfig, + auth_mode: str ): self.api_key = api_key self.override = override + self.auth_mode = auth_mode def validate_and_normalise_user_input( @@ -175,8 +168,6 @@ def validate_and_normalise_user_input( api_key: str, override: Optional[InputOverrideConfig] = None, ) -> DashboardConfig: - if api_key.strip() == "": - raise Exception("apiKey provided to Dashboard recipe cannot be empty") if override is None: override = InputOverrideConfig() @@ -187,6 +178,7 @@ def validate_and_normalise_user_input( functions=override.functions, apis=override.apis, ), + "api-key" if api_key else "email-password" ) @@ -198,7 +190,8 @@ def is_api_path(path: NormalisedURLPath, app_info: AppInfo) -> bool: if not path.startswith(dashboard_recipe_base_path): return False - path_without_dashboard_path = path.get_as_string_dangerous().split(DASHBOARD_API)[1] + path_without_dashboard_path = path.get_as_string_dangerous().split(DASHBOARD_API)[ + 1] if len(path_without_dashboard_path) > 0 and path_without_dashboard_path[0] == "/": path_without_dashboard_path = path_without_dashboard_path[1:] @@ -230,6 +223,10 @@ def get_api_if_matched(path: NormalisedURLPath, method: str) -> Optional[str]: return USER_PASSWORD_API if path_str.endswith(USER_EMAIL_VERIFY_TOKEN_API) and method == "post": return USER_EMAIL_VERIFY_TOKEN_API + if path_str.endswith(EMAIL_PASSWORD_SIGN_IN) and method == "post": + return EMAIL_PASSWORD_SIGN_IN + if path_str.endswith(EMAIL_PASSSWORD_SIGNOUT) and method == "post": + return EMAIL_PASSSWORD_SIGNOUT return None @@ -245,15 +242,16 @@ def __init__(self, user: UserWithMetadata, recipe: str): if TYPE_CHECKING: - from supertokens_python.recipe.emailpassword.types import User as EmailPasswordUser - from supertokens_python.recipe.passwordless.types import User as PasswordlessUser - from supertokens_python.recipe.thirdparty.types import User as ThirdPartyUser - from supertokens_python.recipe.thirdpartyemailpassword.types import ( - User as ThirdPartyEmailPasswordUser, - ) - from supertokens_python.recipe.thirdpartypasswordless.types import ( - User as ThirdPartyPasswordlessUser, - ) + from supertokens_python.recipe.emailpassword.types import \ + User as EmailPasswordUser + from supertokens_python.recipe.passwordless.types import \ + User as PasswordlessUser + from supertokens_python.recipe.thirdparty.types import \ + User as ThirdPartyUser + from supertokens_python.recipe.thirdpartyemailpassword.types import \ + User as ThirdPartyEmailPasswordUser + from supertokens_python.recipe.thirdpartypasswordless.types import \ + User as ThirdPartyPasswordlessUser GetUserResult = Union[ EmailPasswordUser, @@ -385,3 +383,10 @@ def is_recipe_initialised(recipeId: str) -> bool: pass return isRecipeInitialised + + +def validate_APIKey(req: BaseRequest, config: DashboardConfig) -> bool: + apiKeyHeaderValue = req.get_header("authorization") + if not apiKeyHeaderValue: + return False + return apiKeyHeaderValue == config.api_key From dc5f099e1393172fb639a410456d13965ced8519 Mon Sep 17 00:00:00 2001 From: Iresh Sharma Date: Mon, 27 Feb 2023 12:46:10 +0530 Subject: [PATCH 007/192] versions updated --- CHANGELOG.md | 3 +++ coreDriverInterfaceSupported.json | 25 ++++++++++++++----------- setup.py | 2 +- supertokens_python/constants.py | 4 ++-- 4 files changed, 20 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 157646efe..67f5acbc7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## unreleased +## [0.12.2] - 2023-02-27 +- Email passowrd login for dashboard recipe + ## [0.12.1] - 2023-02-06 - Email template updates diff --git a/coreDriverInterfaceSupported.json b/coreDriverInterfaceSupported.json index 825609c69..260ce4863 100644 --- a/coreDriverInterfaceSupported.json +++ b/coreDriverInterfaceSupported.json @@ -1,12 +1,15 @@ { - "_comment": "contains a list of core-driver interfaces branch names that this core supports", - "versions": [ - "2.9", - "2.10", - "2.11", - "2.12", - "2.13", - "2.14", - "2.15" - ] -} \ No newline at end of file + "_comment": "contains a list of core-driver interfaces branch names that this core supports", + "versions": [ + "2.9", + "2.10", + "2.11", + "2.12", + "2.13", + "2.14", + "2.15", + "2.16", + "2.17", + "2.18" + ] +} diff --git a/setup.py b/setup.py index 7f9e294b2..41d7812f4 100644 --- a/setup.py +++ b/setup.py @@ -70,7 +70,7 @@ setup( name="supertokens_python", - version="0.12.1", + version="0.12.2", author="SuperTokens", license="Apache 2.0", author_email="team@supertokens.com", diff --git a/supertokens_python/constants.py b/supertokens_python/constants.py index 122a37fb8..dbfdc7b95 100644 --- a/supertokens_python/constants.py +++ b/supertokens_python/constants.py @@ -11,8 +11,8 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. -SUPPORTED_CDI_VERSIONS = ["2.9", "2.10", "2.11", "2.12", "2.13", "2.14", "2.15"] -VERSION = "0.12.1" +SUPPORTED_CDI_VERSIONS = ["2.9", "2.10", "2.11", "2.12", "2.13", "2.14", "2.15", "2.16", "2.17", "2.18"] +VERSION = "0.12.2" TELEMETRY = "/telemetry" USER_COUNT = "/users/count" USER_DELETE = "/user/remove" From 1776a532f29cc0d4c88a78688a9e9df4f8eb6f2d Mon Sep 17 00:00:00 2001 From: Iresh Sharma Date: Mon, 27 Feb 2023 12:46:10 +0530 Subject: [PATCH 008/192] versions updated --- CHANGELOG.md | 3 +++ coreDriverInterfaceSupported.json | 25 ++++++++++++++----------- setup.py | 2 +- supertokens_python/constants.py | 4 ++-- 4 files changed, 20 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 157646efe..67f5acbc7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## unreleased +## [0.12.2] - 2023-02-27 +- Email passowrd login for dashboard recipe + ## [0.12.1] - 2023-02-06 - Email template updates diff --git a/coreDriverInterfaceSupported.json b/coreDriverInterfaceSupported.json index 825609c69..260ce4863 100644 --- a/coreDriverInterfaceSupported.json +++ b/coreDriverInterfaceSupported.json @@ -1,12 +1,15 @@ { - "_comment": "contains a list of core-driver interfaces branch names that this core supports", - "versions": [ - "2.9", - "2.10", - "2.11", - "2.12", - "2.13", - "2.14", - "2.15" - ] -} \ No newline at end of file + "_comment": "contains a list of core-driver interfaces branch names that this core supports", + "versions": [ + "2.9", + "2.10", + "2.11", + "2.12", + "2.13", + "2.14", + "2.15", + "2.16", + "2.17", + "2.18" + ] +} diff --git a/setup.py b/setup.py index 7f9e294b2..41d7812f4 100644 --- a/setup.py +++ b/setup.py @@ -70,7 +70,7 @@ setup( name="supertokens_python", - version="0.12.1", + version="0.12.2", author="SuperTokens", license="Apache 2.0", author_email="team@supertokens.com", diff --git a/supertokens_python/constants.py b/supertokens_python/constants.py index 122a37fb8..dbfdc7b95 100644 --- a/supertokens_python/constants.py +++ b/supertokens_python/constants.py @@ -11,8 +11,8 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. -SUPPORTED_CDI_VERSIONS = ["2.9", "2.10", "2.11", "2.12", "2.13", "2.14", "2.15"] -VERSION = "0.12.1" +SUPPORTED_CDI_VERSIONS = ["2.9", "2.10", "2.11", "2.12", "2.13", "2.14", "2.15", "2.16", "2.17", "2.18"] +VERSION = "0.12.2" TELEMETRY = "/telemetry" USER_COUNT = "/users/count" USER_DELETE = "/user/remove" From bb515f4f9314cd4628d09881a57fa647b3f896ce Mon Sep 17 00:00:00 2001 From: Iresh Sharma Date: Mon, 27 Feb 2023 13:45:18 +0530 Subject: [PATCH 009/192] changed init function to accept None for api_key --- supertokens_python/constants.py | 13 ++- .../recipe/dashboard/api/__init__.py | 5 +- .../recipe/dashboard/api/signin.py | 7 +- .../recipe/dashboard/api/signout.py | 10 +- .../recipe/dashboard/interfaces.py | 9 +- supertokens_python/recipe/dashboard/recipe.py | 67 +++++++++----- supertokens_python/recipe/dashboard/utils.py | 91 ++++++++++--------- 7 files changed, 122 insertions(+), 80 deletions(-) diff --git a/supertokens_python/constants.py b/supertokens_python/constants.py index dbfdc7b95..51171c879 100644 --- a/supertokens_python/constants.py +++ b/supertokens_python/constants.py @@ -11,7 +11,18 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. -SUPPORTED_CDI_VERSIONS = ["2.9", "2.10", "2.11", "2.12", "2.13", "2.14", "2.15", "2.16", "2.17", "2.18"] +SUPPORTED_CDI_VERSIONS = [ + "2.9", + "2.10", + "2.11", + "2.12", + "2.13", + "2.14", + "2.15", + "2.16", + "2.17", + "2.18", +] VERSION = "0.12.2" TELEMETRY = "/telemetry" USER_COUNT = "/users/count" diff --git a/supertokens_python/recipe/dashboard/api/__init__.py b/supertokens_python/recipe/dashboard/api/__init__.py index badb48b82..204e3c6c9 100644 --- a/supertokens_python/recipe/dashboard/api/__init__.py +++ b/supertokens_python/recipe/dashboard/api/__init__.py @@ -18,8 +18,7 @@ from .userdetails.user_delete import handle_user_delete from .userdetails.user_email_verify_get import handle_user_email_verify_get from .userdetails.user_email_verify_put import handle_user_email_verify_put -from .userdetails.user_email_verify_token_post import \ - handle_email_verify_token_post +from .userdetails.user_email_verify_token_post import handle_email_verify_token_post from .userdetails.user_get import handle_user_get from .userdetails.user_metadata_get import handle_metadata_get from .userdetails.user_metadata_put import handle_metadata_put @@ -49,5 +48,5 @@ "handle_user_password_put", "handle_email_verify_token_post", "handle_sign_in_api", - "handle_signout" + "handle_signout", ] diff --git a/supertokens_python/recipe/dashboard/api/signin.py b/supertokens_python/recipe/dashboard/api/signin.py index db20f27e4..30740ae55 100644 --- a/supertokens_python/recipe/dashboard/api/signin.py +++ b/supertokens_python/recipe/dashboard/api/signin.py @@ -16,9 +16,8 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: - from supertokens_python.recipe.dashboard.interfaces import (APIInterface, - APIOptions) - + from supertokens_python.recipe.dashboard.interfaces import APIInterface, APIOptions +# pylint: disable=unused-argument async def handle_sign_in_api(api_implementation: APIInterface, api_options: APIOptions): - pass \ No newline at end of file + pass diff --git a/supertokens_python/recipe/dashboard/api/signout.py b/supertokens_python/recipe/dashboard/api/signout.py index 1f5a55fff..0b47409de 100644 --- a/supertokens_python/recipe/dashboard/api/signout.py +++ b/supertokens_python/recipe/dashboard/api/signout.py @@ -16,11 +16,13 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: - from supertokens_python.recipe.dashboard.interfaces import (APIInterface, - APIOptions) + from supertokens_python.recipe.dashboard.interfaces import APIInterface, APIOptions from ..interfaces import SignOutOK -async def handle_signout(api_implementation: APIInterface, api_options: APIOptions) -> SignOutOK: - pass \ No newline at end of file +# pylint: disable=unused-argument +async def handle_signout( + api_implementation: APIInterface, api_options: APIOptions +) -> SignOutOK: + return SignOutOK() diff --git a/supertokens_python/recipe/dashboard/interfaces.py b/supertokens_python/recipe/dashboard/interfaces.py index 5035a45bb..0d4569a7e 100644 --- a/supertokens_python/recipe/dashboard/interfaces.py +++ b/supertokens_python/recipe/dashboard/interfaces.py @@ -14,11 +14,9 @@ from __future__ import annotations from abc import ABC, abstractmethod -from typing import (TYPE_CHECKING, Any, Awaitable, Callable, Dict, List, - Optional, Union) +from typing import TYPE_CHECKING, Any, Awaitable, Callable, Dict, List, Optional, Union -from supertokens_python.recipe.session.interfaces import \ - SessionInformationResult +from supertokens_python.recipe.session.interfaces import SessionInformationResult from supertokens_python.types import User from ...supertokens import AppInfo @@ -288,6 +286,7 @@ def __init__(self, error: str) -> None: def to_json(self) -> Dict[str, Any]: return {"status": self.status, "error": self.error} + class SignOutOK(APIResponse): def to_json(self): - return {"status": "OK"} \ No newline at end of file + return {"status": "OK"} diff --git a/supertokens_python/recipe/dashboard/recipe.py b/supertokens_python/recipe/dashboard/recipe.py index 02d0efb9c..fd8b15629 100644 --- a/supertokens_python/recipe/dashboard/recipe.py +++ b/supertokens_python/recipe/dashboard/recipe.py @@ -19,14 +19,26 @@ from supertokens_python.normalised_url_path import NormalisedURLPath from supertokens_python.recipe_module import APIHandled, RecipeModule -from .api import (api_key_protector, handle_dashboard_api, - handle_email_verify_token_post, handle_metadata_get, - handle_metadata_put, handle_sessions_get, handle_sign_in_api, - handle_signout, handle_user_delete, - handle_user_email_verify_get, handle_user_email_verify_put, - handle_user_get, handle_user_password_put, handle_user_put, - handle_user_sessions_post, handle_users_count_get_api, - handle_users_get_api, handle_validate_key_api) +from .api import ( + api_key_protector, + handle_dashboard_api, + handle_email_verify_token_post, + handle_metadata_get, + handle_metadata_put, + handle_sessions_get, + handle_sign_in_api, + handle_signout, + handle_user_delete, + handle_user_email_verify_get, + handle_user_email_verify_put, + handle_user_get, + handle_user_password_put, + handle_user_put, + handle_user_sessions_post, + handle_users_count_get_api, + handle_users_get_api, + handle_validate_key_api, +) from .api.implementation import APIImplementation from .exceptions import SuperTokensDashboardError from .interfaces import APIInterface, APIOptions @@ -38,16 +50,28 @@ from supertokens_python.supertokens import AppInfo from supertokens_python.types import APIResponse -from supertokens_python.exceptions import (SuperTokensError, - raise_general_exception) - -from .constants import (DASHBOARD_API, EMAIL_PASSSWORD_SIGNOUT, - EMAIL_PASSWORD_SIGN_IN, USER_API, - USER_EMAIL_VERIFY_API, USER_EMAIL_VERIFY_TOKEN_API, - USER_METADATA_API, USER_PASSWORD_API, USER_SESSION_API, - USERS_COUNT_API, USERS_LIST_GET_API, VALIDATE_KEY_API) -from .utils import (InputOverrideConfig, get_api_if_matched, is_api_path, - validate_and_normalise_user_input) +from supertokens_python.exceptions import SuperTokensError, raise_general_exception + +from .constants import ( + DASHBOARD_API, + EMAIL_PASSSWORD_SIGNOUT, + EMAIL_PASSWORD_SIGN_IN, + USER_API, + USER_EMAIL_VERIFY_API, + USER_EMAIL_VERIFY_TOKEN_API, + USER_METADATA_API, + USER_PASSWORD_API, + USER_SESSION_API, + USERS_COUNT_API, + USERS_LIST_GET_API, + VALIDATE_KEY_API, +) +from .utils import ( + InputOverrideConfig, + get_api_if_matched, + is_api_path, + validate_and_normalise_user_input, +) class DashboardRecipe(RecipeModule): @@ -58,7 +82,7 @@ def __init__( self, recipe_id: str, app_info: AppInfo, - api_key: str, + api_key: Union[str, None], override: Union[InputOverrideConfig, None] = None, ): super().__init__(recipe_id, app_info) @@ -173,7 +197,7 @@ def get_all_cors_headers(self) -> List[str]: @staticmethod def init( - api_key: str, + api_key: Union[str, None], override: Union[InputOverrideConfig, None] = None, ): def func(app_info: AppInfo): @@ -205,8 +229,7 @@ def reset(): if ("SUPERTOKENS_ENV" not in environ) or ( environ["SUPERTOKENS_ENV"] != "testing" ): - raise_general_exception( - "calling testing function in non testing env") + raise_general_exception("calling testing function in non testing env") DashboardRecipe.__instance = None def return_api_id_if_can_handle_request( diff --git a/supertokens_python/recipe/dashboard/utils.py b/supertokens_python/recipe/dashboard/utils.py index 5b39f9b0a..0769fabd5 100644 --- a/supertokens_python/recipe/dashboard/utils.py +++ b/supertokens_python/recipe/dashboard/utils.py @@ -19,32 +19,48 @@ from supertokens_python.framework.request import BaseRequest from supertokens_python.recipe.emailpassword import EmailPasswordRecipe -from supertokens_python.recipe.emailpassword.asyncio import \ - get_user_by_id as ep_get_user_by_id +from supertokens_python.recipe.emailpassword.asyncio import ( + get_user_by_id as ep_get_user_by_id, +) from supertokens_python.recipe.passwordless import PasswordlessRecipe -from supertokens_python.recipe.passwordless.asyncio import \ - get_user_by_id as pless_get_user_by_id +from supertokens_python.recipe.passwordless.asyncio import ( + get_user_by_id as pless_get_user_by_id, +) from supertokens_python.recipe.thirdparty import ThirdPartyRecipe -from supertokens_python.recipe.thirdparty.asyncio import \ - get_user_by_id as tp_get_user_by_idx -from supertokens_python.recipe.thirdpartyemailpassword import \ - ThirdPartyEmailPasswordRecipe -from supertokens_python.recipe.thirdpartyemailpassword.asyncio import \ - get_user_by_id as tpep_get_user_by_id -from supertokens_python.recipe.thirdpartypasswordless import \ - ThirdPartyPasswordlessRecipe -from supertokens_python.recipe.thirdpartypasswordless.asyncio import \ - get_user_by_id as tppless_get_user_by_id +from supertokens_python.recipe.thirdparty.asyncio import ( + get_user_by_id as tp_get_user_by_idx, +) +from supertokens_python.recipe.thirdpartyemailpassword import ( + ThirdPartyEmailPasswordRecipe, +) +from supertokens_python.recipe.thirdpartyemailpassword.asyncio import ( + get_user_by_id as tpep_get_user_by_id, +) +from supertokens_python.recipe.thirdpartypasswordless import ( + ThirdPartyPasswordlessRecipe, +) +from supertokens_python.recipe.thirdpartypasswordless.asyncio import ( + get_user_by_id as tppless_get_user_by_id, +) from supertokens_python.types import User from supertokens_python.utils import Awaitable from ...normalised_url_path import NormalisedURLPath from ...supertokens import AppInfo -from .constants import (DASHBOARD_API, EMAIL_PASSSWORD_SIGNOUT, - EMAIL_PASSWORD_SIGN_IN, USER_API, - USER_EMAIL_VERIFY_API, USER_EMAIL_VERIFY_TOKEN_API, - USER_METADATA_API, USER_PASSWORD_API, USER_SESSION_API, - USERS_COUNT_API, USERS_LIST_GET_API, VALIDATE_KEY_API) +from .constants import ( + DASHBOARD_API, + EMAIL_PASSSWORD_SIGNOUT, + EMAIL_PASSWORD_SIGN_IN, + USER_API, + USER_EMAIL_VERIFY_API, + USER_EMAIL_VERIFY_TOKEN_API, + USER_METADATA_API, + USER_PASSWORD_API, + USER_SESSION_API, + USERS_COUNT_API, + USERS_LIST_GET_API, + VALIDATE_KEY_API, +) if TYPE_CHECKING: from .interfaces import APIInterface, RecipeInterface @@ -132,8 +148,7 @@ def to_json(self) -> Dict[str, Any]: class InputOverrideConfig: def __init__( self, - functions: Union[Callable[[RecipeInterface], - RecipeInterface], None] = None, + functions: Union[Callable[[RecipeInterface], RecipeInterface], None] = None, apis: Union[Callable[[APIInterface], APIInterface], None] = None, ): self.functions = functions @@ -143,8 +158,7 @@ def __init__( class OverrideConfig: def __init__( self, - functions: Union[Callable[[RecipeInterface], - RecipeInterface], None] = None, + functions: Union[Callable[[RecipeInterface], RecipeInterface], None] = None, apis: Union[Callable[[APIInterface], APIInterface], None] = None, ): self.functions = functions @@ -153,10 +167,7 @@ def __init__( class DashboardConfig: def __init__( - self, - api_key: str, - override: OverrideConfig, - auth_mode: str + self, api_key: Union[str, None], override: OverrideConfig, auth_mode: str ): self.api_key = api_key self.override = override @@ -165,7 +176,7 @@ def __init__( def validate_and_normalise_user_input( # app_info: AppInfo, - api_key: str, + api_key: Union[str, None], override: Optional[InputOverrideConfig] = None, ) -> DashboardConfig: @@ -178,7 +189,7 @@ def validate_and_normalise_user_input( functions=override.functions, apis=override.apis, ), - "api-key" if api_key else "email-password" + "api-key" if api_key else "email-password", ) @@ -190,8 +201,7 @@ def is_api_path(path: NormalisedURLPath, app_info: AppInfo) -> bool: if not path.startswith(dashboard_recipe_base_path): return False - path_without_dashboard_path = path.get_as_string_dangerous().split(DASHBOARD_API)[ - 1] + path_without_dashboard_path = path.get_as_string_dangerous().split(DASHBOARD_API)[1] if len(path_without_dashboard_path) > 0 and path_without_dashboard_path[0] == "/": path_without_dashboard_path = path_without_dashboard_path[1:] @@ -242,16 +252,15 @@ def __init__(self, user: UserWithMetadata, recipe: str): if TYPE_CHECKING: - from supertokens_python.recipe.emailpassword.types import \ - User as EmailPasswordUser - from supertokens_python.recipe.passwordless.types import \ - User as PasswordlessUser - from supertokens_python.recipe.thirdparty.types import \ - User as ThirdPartyUser - from supertokens_python.recipe.thirdpartyemailpassword.types import \ - User as ThirdPartyEmailPasswordUser - from supertokens_python.recipe.thirdpartypasswordless.types import \ - User as ThirdPartyPasswordlessUser + from supertokens_python.recipe.emailpassword.types import User as EmailPasswordUser + from supertokens_python.recipe.passwordless.types import User as PasswordlessUser + from supertokens_python.recipe.thirdparty.types import User as ThirdPartyUser + from supertokens_python.recipe.thirdpartyemailpassword.types import ( + User as ThirdPartyEmailPasswordUser, + ) + from supertokens_python.recipe.thirdpartypasswordless.types import ( + User as ThirdPartyPasswordlessUser, + ) GetUserResult = Union[ EmailPasswordUser, From ba0a2b91bddea37a9baf9a06aa3e1027b6a52791 Mon Sep 17 00:00:00 2001 From: Iresh Sharma Date: Mon, 27 Feb 2023 14:13:02 +0530 Subject: [PATCH 010/192] api implementation for email-password login for dashboard recipe --- CHANGELOG.md | 2 +- supertokens_python/constants.py | 13 ++- supertokens_python/querier.py | 11 ++- .../recipe/dashboard/api/__init__.py | 5 +- .../recipe/dashboard/api/signin.py | 30 +++++- .../recipe/dashboard/api/signout.py | 29 +++++- .../recipe/dashboard/interfaces.py | 9 +- supertokens_python/recipe/dashboard/recipe.py | 63 +++++++++---- .../recipe/dashboard/recipe_implementation.py | 38 +++++--- supertokens_python/recipe/dashboard/utils.py | 91 ++++++++++--------- 10 files changed, 196 insertions(+), 95 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 67f5acbc7..f3c6e813e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## unreleased ## [0.12.2] - 2023-02-27 -- Email passowrd login for dashboard recipe +- Adds APIs and logic to the dashboard recipe to enable email password based login ## [0.12.1] - 2023-02-06 diff --git a/supertokens_python/constants.py b/supertokens_python/constants.py index dbfdc7b95..51171c879 100644 --- a/supertokens_python/constants.py +++ b/supertokens_python/constants.py @@ -11,7 +11,18 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. -SUPPORTED_CDI_VERSIONS = ["2.9", "2.10", "2.11", "2.12", "2.13", "2.14", "2.15", "2.16", "2.17", "2.18"] +SUPPORTED_CDI_VERSIONS = [ + "2.9", + "2.10", + "2.11", + "2.12", + "2.13", + "2.14", + "2.15", + "2.16", + "2.17", + "2.18", +] VERSION = "0.12.2" TELEMETRY = "/telemetry" USER_COUNT = "/users/count" diff --git a/supertokens_python/querier.py b/supertokens_python/querier.py index 0c8deff63..1eaeb30d5 100644 --- a/supertokens_python/querier.py +++ b/supertokens_python/querier.py @@ -166,11 +166,18 @@ async def f(url: str) -> Response: return await self.__send_request_helper(path, "POST", f, len(self.__hosts)) - async def send_delete_request(self, path: NormalisedURLPath): + async def send_delete_request( + self, path: NormalisedURLPath, params: Union[Dict[str, Any], None] = None + ): + if not params: + params = {} + async def f(url: str) -> Response: async with AsyncClient() as client: return await client.delete( # type:ignore - url, headers=await self.__get_headers_with_api_version(path) + url, + params=params, + headers=await self.__get_headers_with_api_version(path), ) return await self.__send_request_helper(path, "DELETE", f, len(self.__hosts)) diff --git a/supertokens_python/recipe/dashboard/api/__init__.py b/supertokens_python/recipe/dashboard/api/__init__.py index badb48b82..204e3c6c9 100644 --- a/supertokens_python/recipe/dashboard/api/__init__.py +++ b/supertokens_python/recipe/dashboard/api/__init__.py @@ -18,8 +18,7 @@ from .userdetails.user_delete import handle_user_delete from .userdetails.user_email_verify_get import handle_user_email_verify_get from .userdetails.user_email_verify_put import handle_user_email_verify_put -from .userdetails.user_email_verify_token_post import \ - handle_email_verify_token_post +from .userdetails.user_email_verify_token_post import handle_email_verify_token_post from .userdetails.user_get import handle_user_get from .userdetails.user_metadata_get import handle_metadata_get from .userdetails.user_metadata_put import handle_metadata_put @@ -49,5 +48,5 @@ "handle_user_password_put", "handle_email_verify_token_post", "handle_sign_in_api", - "handle_signout" + "handle_signout", ] diff --git a/supertokens_python/recipe/dashboard/api/signin.py b/supertokens_python/recipe/dashboard/api/signin.py index db20f27e4..97e28f26f 100644 --- a/supertokens_python/recipe/dashboard/api/signin.py +++ b/supertokens_python/recipe/dashboard/api/signin.py @@ -16,9 +16,33 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: - from supertokens_python.recipe.dashboard.interfaces import (APIInterface, - APIOptions) + from supertokens_python.recipe.dashboard.interfaces import APIInterface, APIOptions +from supertokens_python.exceptions import raise_bad_input_exception +from supertokens_python.normalised_url_path import NormalisedURLPath +from supertokens_python.querier import Querier +from supertokens_python.utils import send_200_response + +# pylint: disable=unused-argument async def handle_sign_in_api(api_implementation: APIInterface, api_options: APIOptions): - pass \ No newline at end of file + body = await api_options.request.form_data() + + if not body["email"]: + raise_bad_input_exception("Missing required parameter 'email'") + if not body["password"]: + raise_bad_input_exception("Missing required parameter 'password'") + response = await Querier.get_instance().send_post_request( + NormalisedURLPath("/recipe/dashboard/signin"), + {"email": body["email"], "password": body["password"]}, + ) + + if "status" in response and response["status"] == "OK": + return send_200_response( + {"status": "OK", "sessionId": response["sessionId"]}, api_options.response + ) + if "status" in response and response["status"] == "INVALID_CREDENTIALS_ERROR": + return send_200_response( + {"status": "INVALID_CREDENTIALS_ERROR"}, api_options.response + ) + return send_200_response({"status": "USER_SUSPENDED_ERROR"}, api_options.response) diff --git a/supertokens_python/recipe/dashboard/api/signout.py b/supertokens_python/recipe/dashboard/api/signout.py index 1f5a55fff..47147e0a5 100644 --- a/supertokens_python/recipe/dashboard/api/signout.py +++ b/supertokens_python/recipe/dashboard/api/signout.py @@ -16,11 +16,32 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: - from supertokens_python.recipe.dashboard.interfaces import (APIInterface, - APIOptions) + from supertokens_python.recipe.dashboard.interfaces import APIInterface, APIOptions + +from supertokens_python.exceptions import raise_bad_input_exception +from supertokens_python.normalised_url_path import NormalisedURLPath +from supertokens_python.querier import Querier +from supertokens_python.utils import send_200_response from ..interfaces import SignOutOK -async def handle_signout(api_implementation: APIInterface, api_options: APIOptions) -> SignOutOK: - pass \ No newline at end of file +# pylint: disable=unused-argument +async def handle_signout( + api_implementation: APIInterface, api_options: APIOptions +) -> SignOutOK: + if api_options.config.api_key: + send_200_response({"status": "OK"}, api_options.response) + else: + sessionIdFormAuthHeader = api_options.request.get_header("authorization") + if not sessionIdFormAuthHeader: + return raise_bad_input_exception( + "Neither 'API Key' nor 'Authorization' header was found" + ) + sessionIdFormAuthHeader = sessionIdFormAuthHeader.split()[1] + response = await Querier.get_instance().send_delete_request( + NormalisedURLPath("/recipe/dashboard/session"), + {"sessionId": sessionIdFormAuthHeader}, + ) + send_200_response(response, api_options.response) + return SignOutOK() diff --git a/supertokens_python/recipe/dashboard/interfaces.py b/supertokens_python/recipe/dashboard/interfaces.py index 5035a45bb..0d4569a7e 100644 --- a/supertokens_python/recipe/dashboard/interfaces.py +++ b/supertokens_python/recipe/dashboard/interfaces.py @@ -14,11 +14,9 @@ from __future__ import annotations from abc import ABC, abstractmethod -from typing import (TYPE_CHECKING, Any, Awaitable, Callable, Dict, List, - Optional, Union) +from typing import TYPE_CHECKING, Any, Awaitable, Callable, Dict, List, Optional, Union -from supertokens_python.recipe.session.interfaces import \ - SessionInformationResult +from supertokens_python.recipe.session.interfaces import SessionInformationResult from supertokens_python.types import User from ...supertokens import AppInfo @@ -288,6 +286,7 @@ def __init__(self, error: str) -> None: def to_json(self) -> Dict[str, Any]: return {"status": self.status, "error": self.error} + class SignOutOK(APIResponse): def to_json(self): - return {"status": "OK"} \ No newline at end of file + return {"status": "OK"} diff --git a/supertokens_python/recipe/dashboard/recipe.py b/supertokens_python/recipe/dashboard/recipe.py index 02d0efb9c..1d90eabe6 100644 --- a/supertokens_python/recipe/dashboard/recipe.py +++ b/supertokens_python/recipe/dashboard/recipe.py @@ -19,14 +19,26 @@ from supertokens_python.normalised_url_path import NormalisedURLPath from supertokens_python.recipe_module import APIHandled, RecipeModule -from .api import (api_key_protector, handle_dashboard_api, - handle_email_verify_token_post, handle_metadata_get, - handle_metadata_put, handle_sessions_get, handle_sign_in_api, - handle_signout, handle_user_delete, - handle_user_email_verify_get, handle_user_email_verify_put, - handle_user_get, handle_user_password_put, handle_user_put, - handle_user_sessions_post, handle_users_count_get_api, - handle_users_get_api, handle_validate_key_api) +from .api import ( + api_key_protector, + handle_dashboard_api, + handle_email_verify_token_post, + handle_metadata_get, + handle_metadata_put, + handle_sessions_get, + handle_sign_in_api, + handle_signout, + handle_user_delete, + handle_user_email_verify_get, + handle_user_email_verify_put, + handle_user_get, + handle_user_password_put, + handle_user_put, + handle_user_sessions_post, + handle_users_count_get_api, + handle_users_get_api, + handle_validate_key_api, +) from .api.implementation import APIImplementation from .exceptions import SuperTokensDashboardError from .interfaces import APIInterface, APIOptions @@ -38,16 +50,28 @@ from supertokens_python.supertokens import AppInfo from supertokens_python.types import APIResponse -from supertokens_python.exceptions import (SuperTokensError, - raise_general_exception) - -from .constants import (DASHBOARD_API, EMAIL_PASSSWORD_SIGNOUT, - EMAIL_PASSWORD_SIGN_IN, USER_API, - USER_EMAIL_VERIFY_API, USER_EMAIL_VERIFY_TOKEN_API, - USER_METADATA_API, USER_PASSWORD_API, USER_SESSION_API, - USERS_COUNT_API, USERS_LIST_GET_API, VALIDATE_KEY_API) -from .utils import (InputOverrideConfig, get_api_if_matched, is_api_path, - validate_and_normalise_user_input) +from supertokens_python.exceptions import SuperTokensError, raise_general_exception + +from .constants import ( + DASHBOARD_API, + EMAIL_PASSSWORD_SIGNOUT, + EMAIL_PASSWORD_SIGN_IN, + USER_API, + USER_EMAIL_VERIFY_API, + USER_EMAIL_VERIFY_TOKEN_API, + USER_METADATA_API, + USER_PASSWORD_API, + USER_SESSION_API, + USERS_COUNT_API, + USERS_LIST_GET_API, + VALIDATE_KEY_API, +) +from .utils import ( + InputOverrideConfig, + get_api_if_matched, + is_api_path, + validate_and_normalise_user_input, +) class DashboardRecipe(RecipeModule): @@ -205,8 +229,7 @@ def reset(): if ("SUPERTOKENS_ENV" not in environ) or ( environ["SUPERTOKENS_ENV"] != "testing" ): - raise_general_exception( - "calling testing function in non testing env") + raise_general_exception("calling testing function in non testing env") DashboardRecipe.__instance = None def return_api_id_if_can_handle_request( diff --git a/supertokens_python/recipe/dashboard/recipe_implementation.py b/supertokens_python/recipe/dashboard/recipe_implementation.py index 1a2934162..a11d8bb3a 100644 --- a/supertokens_python/recipe/dashboard/recipe_implementation.py +++ b/supertokens_python/recipe/dashboard/recipe_implementation.py @@ -15,12 +15,13 @@ from typing import Any, Dict -from supertokens_python.framework import BaseRequest -from .interfaces import ( - RecipeInterface, -) from supertokens_python.constants import DASHBOARD_VERSION -from .utils import DashboardConfig +from supertokens_python.framework import BaseRequest +from supertokens_python.normalised_url_path import NormalisedURLPath +from supertokens_python.querier import Querier + +from .interfaces import RecipeInterface +from .utils import DashboardConfig, validate_APIKey class RecipeImplementation(RecipeInterface): @@ -33,12 +34,21 @@ async def should_allow_access( config: DashboardConfig, user_context: Dict[str, Any], ) -> bool: - api_key_header_value = request.get_header("authorization") - - # We receive the api key as `Bearer API_KEY`, this retrieves just the key - api_key = api_key_header_value.split(" ")[1] if api_key_header_value else None - - if api_key is None: - return False - - return api_key == config.api_key + if not config.api_key: + authHeaderValue = request.get_header("authorization") + + if not authHeaderValue: + return False + + authHeaderValue = authHeaderValue.split()[1] + sessionVerificationResponse = ( + await Querier.get_instance().send_post_request( + NormalisedURLPath("/recipe/dashboard/session/verify"), + {"sessionId": authHeaderValue}, + ) + ) + return ( + "status" in sessionVerificationResponse + and sessionVerificationResponse["status"] == "OK" + ) + return validate_APIKey(request, config) diff --git a/supertokens_python/recipe/dashboard/utils.py b/supertokens_python/recipe/dashboard/utils.py index 5b39f9b0a..a41f6dce3 100644 --- a/supertokens_python/recipe/dashboard/utils.py +++ b/supertokens_python/recipe/dashboard/utils.py @@ -19,32 +19,48 @@ from supertokens_python.framework.request import BaseRequest from supertokens_python.recipe.emailpassword import EmailPasswordRecipe -from supertokens_python.recipe.emailpassword.asyncio import \ - get_user_by_id as ep_get_user_by_id +from supertokens_python.recipe.emailpassword.asyncio import ( + get_user_by_id as ep_get_user_by_id, +) from supertokens_python.recipe.passwordless import PasswordlessRecipe -from supertokens_python.recipe.passwordless.asyncio import \ - get_user_by_id as pless_get_user_by_id +from supertokens_python.recipe.passwordless.asyncio import ( + get_user_by_id as pless_get_user_by_id, +) from supertokens_python.recipe.thirdparty import ThirdPartyRecipe -from supertokens_python.recipe.thirdparty.asyncio import \ - get_user_by_id as tp_get_user_by_idx -from supertokens_python.recipe.thirdpartyemailpassword import \ - ThirdPartyEmailPasswordRecipe -from supertokens_python.recipe.thirdpartyemailpassword.asyncio import \ - get_user_by_id as tpep_get_user_by_id -from supertokens_python.recipe.thirdpartypasswordless import \ - ThirdPartyPasswordlessRecipe -from supertokens_python.recipe.thirdpartypasswordless.asyncio import \ - get_user_by_id as tppless_get_user_by_id +from supertokens_python.recipe.thirdparty.asyncio import ( + get_user_by_id as tp_get_user_by_idx, +) +from supertokens_python.recipe.thirdpartyemailpassword import ( + ThirdPartyEmailPasswordRecipe, +) +from supertokens_python.recipe.thirdpartyemailpassword.asyncio import ( + get_user_by_id as tpep_get_user_by_id, +) +from supertokens_python.recipe.thirdpartypasswordless import ( + ThirdPartyPasswordlessRecipe, +) +from supertokens_python.recipe.thirdpartypasswordless.asyncio import ( + get_user_by_id as tppless_get_user_by_id, +) from supertokens_python.types import User from supertokens_python.utils import Awaitable from ...normalised_url_path import NormalisedURLPath from ...supertokens import AppInfo -from .constants import (DASHBOARD_API, EMAIL_PASSSWORD_SIGNOUT, - EMAIL_PASSWORD_SIGN_IN, USER_API, - USER_EMAIL_VERIFY_API, USER_EMAIL_VERIFY_TOKEN_API, - USER_METADATA_API, USER_PASSWORD_API, USER_SESSION_API, - USERS_COUNT_API, USERS_LIST_GET_API, VALIDATE_KEY_API) +from .constants import ( + DASHBOARD_API, + EMAIL_PASSSWORD_SIGNOUT, + EMAIL_PASSWORD_SIGN_IN, + USER_API, + USER_EMAIL_VERIFY_API, + USER_EMAIL_VERIFY_TOKEN_API, + USER_METADATA_API, + USER_PASSWORD_API, + USER_SESSION_API, + USERS_COUNT_API, + USERS_LIST_GET_API, + VALIDATE_KEY_API, +) if TYPE_CHECKING: from .interfaces import APIInterface, RecipeInterface @@ -132,8 +148,7 @@ def to_json(self) -> Dict[str, Any]: class InputOverrideConfig: def __init__( self, - functions: Union[Callable[[RecipeInterface], - RecipeInterface], None] = None, + functions: Union[Callable[[RecipeInterface], RecipeInterface], None] = None, apis: Union[Callable[[APIInterface], APIInterface], None] = None, ): self.functions = functions @@ -143,8 +158,7 @@ def __init__( class OverrideConfig: def __init__( self, - functions: Union[Callable[[RecipeInterface], - RecipeInterface], None] = None, + functions: Union[Callable[[RecipeInterface], RecipeInterface], None] = None, apis: Union[Callable[[APIInterface], APIInterface], None] = None, ): self.functions = functions @@ -152,12 +166,7 @@ def __init__( class DashboardConfig: - def __init__( - self, - api_key: str, - override: OverrideConfig, - auth_mode: str - ): + def __init__(self, api_key: str, override: OverrideConfig, auth_mode: str): self.api_key = api_key self.override = override self.auth_mode = auth_mode @@ -178,7 +187,7 @@ def validate_and_normalise_user_input( functions=override.functions, apis=override.apis, ), - "api-key" if api_key else "email-password" + "api-key" if api_key else "email-password", ) @@ -190,8 +199,7 @@ def is_api_path(path: NormalisedURLPath, app_info: AppInfo) -> bool: if not path.startswith(dashboard_recipe_base_path): return False - path_without_dashboard_path = path.get_as_string_dangerous().split(DASHBOARD_API)[ - 1] + path_without_dashboard_path = path.get_as_string_dangerous().split(DASHBOARD_API)[1] if len(path_without_dashboard_path) > 0 and path_without_dashboard_path[0] == "/": path_without_dashboard_path = path_without_dashboard_path[1:] @@ -242,16 +250,15 @@ def __init__(self, user: UserWithMetadata, recipe: str): if TYPE_CHECKING: - from supertokens_python.recipe.emailpassword.types import \ - User as EmailPasswordUser - from supertokens_python.recipe.passwordless.types import \ - User as PasswordlessUser - from supertokens_python.recipe.thirdparty.types import \ - User as ThirdPartyUser - from supertokens_python.recipe.thirdpartyemailpassword.types import \ - User as ThirdPartyEmailPasswordUser - from supertokens_python.recipe.thirdpartypasswordless.types import \ - User as ThirdPartyPasswordlessUser + from supertokens_python.recipe.emailpassword.types import User as EmailPasswordUser + from supertokens_python.recipe.passwordless.types import User as PasswordlessUser + from supertokens_python.recipe.thirdparty.types import User as ThirdPartyUser + from supertokens_python.recipe.thirdpartyemailpassword.types import ( + User as ThirdPartyEmailPasswordUser, + ) + from supertokens_python.recipe.thirdpartypasswordless.types import ( + User as ThirdPartyPasswordlessUser, + ) GetUserResult = Union[ EmailPasswordUser, From 351b585900d8468e779ba7d482e602c4727ee030 Mon Sep 17 00:00:00 2001 From: KShivendu Date: Mon, 27 Feb 2023 14:22:31 +0530 Subject: [PATCH 011/192] test: Add tests for thirdparty parsing --- tests/Django/settings.py | 2 +- tests/Django/test_django.py | 54 ++++++++++++++++++++++- tests/Flask/test_flask.py | 31 +++++++++++++- tests/thirdparty/test_thirdparty.py | 66 +++++++++++++++++++++++++++++ 4 files changed, 150 insertions(+), 3 deletions(-) create mode 100644 tests/thirdparty/test_thirdparty.py diff --git a/tests/Django/settings.py b/tests/Django/settings.py index a2311ab58..aa460378c 100644 --- a/tests/Django/settings.py +++ b/tests/Django/settings.py @@ -44,5 +44,5 @@ } MIDDLEWARE = [ - "supertokens-Fastapi.core.framework.django.django_middleware.middleware", + "supertokens_python.framework.django.django_middleware.middleware", ] diff --git a/tests/Django/test_django.py b/tests/Django/test_django.py index 99aa9efc8..9f14fceff 100644 --- a/tests/Django/test_django.py +++ b/tests/Django/test_django.py @@ -13,18 +13,20 @@ # under the License. import json +from urllib.parse import urlencode from datetime import datetime from inspect import isawaitable from typing import Any, Dict, Union from django.http import HttpRequest, HttpResponse, JsonResponse from django.test import RequestFactory, TestCase + from supertokens_python import InputAppInfo, SupertokensConfig, init from supertokens_python.framework.django import middleware from supertokens_python.framework.django.django_response import ( DjangoResponse as SuperTokensDjangoWrapper, ) -from supertokens_python.recipe import emailpassword, session +from supertokens_python.recipe import emailpassword, session, thirdparty from supertokens_python.recipe.emailpassword.interfaces import APIInterface, APIOptions from supertokens_python.recipe.session import SessionContainer from supertokens_python.recipe.session.asyncio import ( @@ -393,6 +395,56 @@ async def test_optional_session(self): dict_response = json.loads(response.content) assert dict_response["s"] == "empty session" + async def test_thirdparty_parsing_works(self): + init( + supertokens_config=SupertokensConfig("http://localhost:3567"), + app_info=InputAppInfo( + app_name="SuperTokens Demo", + api_domain="http://api.supertokens.io", + website_domain="http://supertokens.io", + api_base_path="/auth", + ), + framework="django", + mode="asgi", + recipe_list=[ + thirdparty.init( + sign_in_and_up_feature=thirdparty.SignInAndUpFeature( + providers=[ + thirdparty.Apple( + client_id="4398792-io.supertokens.example.service", + client_key_id="7M48Y4RYDL", + client_team_id="YWQCXGJRJL", + client_private_key="-----BEGIN PRIVATE KEY-----\nMIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgu8gXs+XYkqXD6Ala9Sf/iJXzhbwcoG5dMh1OonpdJUmgCgYIKoZIzj0DAQehRANCAASfrvlFbFCYqn3I2zeknYXLwtH30JuOKestDbSfZYxZNMqhF/OzdZFTV0zc5u5s3eN+oCWbnvl0hM+9IW0UlkdA\n-----END PRIVATE KEY-----", + ) + ] + ) + ), + ], + ) + + start_st() + + data = { + "state": "afc596274293e1587315c", + "code": "c7685e261f98e4b3b94e34b3a69ff9cf4.0.rvxt.eE8rO__6hGoqaX1B7ODPmA", + } + + request = self.factory.post( + "/auth/callback/apple", + urlencode(data).encode(), + content_type="application/x-www-form-urlencoded", + ) + temp = middleware(custom_response_view)(request) + if not isawaitable(temp): + raise Exception("Should never come here") + response = await temp + + self.assertEqual(response.status_code, 200) + self.assertEqual( + response.content, + b'', + ) + def test_remove_header_works(): response = HttpResponse() diff --git a/tests/Flask/test_flask.py b/tests/Flask/test_flask.py index bd108a19f..53ba2c585 100644 --- a/tests/Flask/test_flask.py +++ b/tests/Flask/test_flask.py @@ -19,7 +19,7 @@ from flask import Flask, g, jsonify, make_response, request from supertokens_python import InputAppInfo, SupertokensConfig, init from supertokens_python.framework.flask import Middleware -from supertokens_python.recipe import emailpassword, session +from supertokens_python.recipe import emailpassword, session, thirdparty from supertokens_python.recipe.emailpassword.interfaces import APIInterface, APIOptions from supertokens_python.recipe.session import SessionContainer from supertokens_python.recipe.session.framework.flask import verify_session @@ -108,6 +108,18 @@ async def email_exists_get( apis=override_email_password_apis ) ), + thirdparty.init( + sign_in_and_up_feature=thirdparty.SignInAndUpFeature( + providers=[ + thirdparty.Apple( + client_id="4398792-io.supertokens.example.service", + client_key_id="7M48Y4RYDL", + client_team_id="YWQCXGJRJL", + client_private_key="-----BEGIN PRIVATE KEY-----\nMIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgu8gXs+XYkqXD6Ala9Sf/iJXzhbwcoG5dMh1OonpdJUmgCgYIKoZIzj0DAQehRANCAASfrvlFbFCYqn3I2zeknYXLwtH30JuOKestDbSfZYxZNMqhF/OzdZFTV0zc5u5s3eN+oCWbnvl0hM+9IW0UlkdA\n-----END PRIVATE KEY-----", + ) + ] + ) + ), ], ) @@ -400,6 +412,23 @@ def test_optional_session(driver_config_app: Any): assert dict_response["s"] == "empty session" +def test_thirdparty_parsing_works(driver_config_app: Any): + start_st() + + test_client = driver_config_app.test_client() + data = { + "state": "afc596274293e1587315c", + "code": "c7685e261f98e4b3b94e34b3a69ff9cf4.0.rvxt.eE8rO__6hGoqaX1B7ODPmA", + } + response = test_client.post("/auth/callback/apple", data=data) + + assert response.status_code == 200 + assert ( + response.data + == b'' + ) + + from flask.wrappers import Response from supertokens_python.framework.flask.flask_response import ( FlaskResponse as SupertokensFlaskWrapper, diff --git a/tests/thirdparty/test_thirdparty.py b/tests/thirdparty/test_thirdparty.py new file mode 100644 index 000000000..a141e4437 --- /dev/null +++ b/tests/thirdparty/test_thirdparty.py @@ -0,0 +1,66 @@ +from pytest import fixture, mark +from fastapi import FastAPI +from supertokens_python.framework.fastapi import get_middleware +from starlette.testclient import TestClient + +from supertokens_python.recipe import session, thirdparty +from supertokens_python import init + +from tests.utils import ( + setup_function, + teardown_function, + start_st, + st_init_common_args, +) + + +_ = setup_function # type:ignore +_ = teardown_function # type:ignore +_ = start_st # type:ignore + + +pytestmark = mark.asyncio + + +@fixture(scope="function") +async def fastapi_client(): + app = FastAPI() + app.add_middleware(get_middleware()) + + return TestClient(app) + + +async def test_thirdpary_parsing_works(fastapi_client: TestClient): + st_init_args = { + **st_init_common_args, + "recipe_list": [ + session.init(), + thirdparty.init( + sign_in_and_up_feature=thirdparty.SignInAndUpFeature( + providers=[ + thirdparty.Apple( + client_id="4398792-io.supertokens.example.service", + client_key_id="7M48Y4RYDL", + client_team_id="YWQCXGJRJL", + client_private_key="-----BEGIN PRIVATE KEY-----\nMIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgu8gXs+XYkqXD6Ala9Sf/iJXzhbwcoG5dMh1OonpdJUmgCgYIKoZIzj0DAQehRANCAASfrvlFbFCYqn3I2zeknYXLwtH30JuOKestDbSfZYxZNMqhF/OzdZFTV0zc5u5s3eN+oCWbnvl0hM+9IW0UlkdA\n-----END PRIVATE KEY-----", + ) + ] + ) + ), + ], + } + init(**st_init_args) # type: ignore + start_st() + + data = { + "state": "afc596274293e1587315c", + "code": "c7685e261f98e4b3b94e34b3a69ff9cf4.0.rvxt.eE8rO__6hGoqaX1B7ODPmA", + } + + res = fastapi_client.post("/auth/callback/apple", data=data) + + assert res.status_code == 200 + assert ( + res.content + == b'' + ) From 6f991c30cc0804f54c8aa5d8f7e10e70bd89929a Mon Sep 17 00:00:00 2001 From: Iresh Sharma Date: Mon, 27 Feb 2023 14:34:02 +0530 Subject: [PATCH 012/192] signout auth_mode fix --- supertokens_python/recipe/dashboard/api/signout.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/supertokens_python/recipe/dashboard/api/signout.py b/supertokens_python/recipe/dashboard/api/signout.py index 47147e0a5..bdf912b98 100644 --- a/supertokens_python/recipe/dashboard/api/signout.py +++ b/supertokens_python/recipe/dashboard/api/signout.py @@ -30,7 +30,7 @@ async def handle_signout( api_implementation: APIInterface, api_options: APIOptions ) -> SignOutOK: - if api_options.config.api_key: + if api_options.config.auth_mode == "api-key": send_200_response({"status": "OK"}, api_options.response) else: sessionIdFormAuthHeader = api_options.request.get_header("authorization") From eeb537e0250c4dcba28677a19a204198702397cb Mon Sep 17 00:00:00 2001 From: Iresh Sharma Date: Mon, 27 Feb 2023 15:27:45 +0530 Subject: [PATCH 013/192] formatting fixes --- supertokens_python/constants.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/supertokens_python/constants.py b/supertokens_python/constants.py index dbfdc7b95..51171c879 100644 --- a/supertokens_python/constants.py +++ b/supertokens_python/constants.py @@ -11,7 +11,18 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. -SUPPORTED_CDI_VERSIONS = ["2.9", "2.10", "2.11", "2.12", "2.13", "2.14", "2.15", "2.16", "2.17", "2.18"] +SUPPORTED_CDI_VERSIONS = [ + "2.9", + "2.10", + "2.11", + "2.12", + "2.13", + "2.14", + "2.15", + "2.16", + "2.17", + "2.18", +] VERSION = "0.12.2" TELEMETRY = "/telemetry" USER_COUNT = "/users/count" From cc6c2c958a4ae0f8b004f252e52dfecbf22a46d5 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Mon, 27 Feb 2023 15:35:19 +0530 Subject: [PATCH 014/192] cicd change --- .circleci/config.yml | 2 +- .circleci/config_continue.yml | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 6b4000621..5c79bb1cf 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -46,7 +46,7 @@ workflows: tags: only: /dev-v[0-9]+(\.[0-9]+)*/ branches: - only: /test\/.*/ + only: /test-cicd\/.*/ - publish: context: - slack-notification diff --git a/.circleci/config_continue.yml b/.circleci/config_continue.yml index 29a1eb348..004004ce1 100644 --- a/.circleci/config_continue.yml +++ b/.circleci/config_continue.yml @@ -188,7 +188,7 @@ workflows: tags: only: /dev-v[0-9]+(\.[0-9]+)*/ branches: - only: /test\/.*/ + only: /test-cicd\/.*/ - test-unit: requires: - test-dev-tag-as-not-passed @@ -198,7 +198,7 @@ workflows: tags: only: /dev-v[0-9]+(\.[0-9]+)*/ branches: - only: /test\/.*/ + only: /test-cicd\/.*/ matrix: parameters: cdi-version: placeholder @@ -211,7 +211,7 @@ workflows: tags: only: /dev-v[0-9]+(\.[0-9]+)*/ branches: - only: /test\/.*/ + only: /test-cicd\/.*/ - test-website-flask: requires: - test-dev-tag-as-not-passed @@ -221,7 +221,7 @@ workflows: tags: only: /dev-v[0-9]+(\.[0-9]+)*/ branches: - only: /test\/.*/ + only: /test-cicd\/.*/ - test-website-django: requires: - test-dev-tag-as-not-passed @@ -231,7 +231,7 @@ workflows: tags: only: /dev-v[0-9]+(\.[0-9]+)*/ branches: - only: /test\/.*/ + only: /test-cicd\/.*/ - test-website-django2x: requires: - test-dev-tag-as-not-passed @@ -241,7 +241,7 @@ workflows: tags: only: /dev-v[0-9]+(\.[0-9]+)*/ branches: - only: /test\/.*/ + only: /test-cicd\/.*/ - test-authreact-fastapi: requires: - test-dev-tag-as-not-passed @@ -251,7 +251,7 @@ workflows: tags: only: /dev-v[0-9]+(\.[0-9]+)*/ branches: - only: /test\/.*/ + only: /test-cicd\/.*/ matrix: parameters: fdi-version: placeholder @@ -264,7 +264,7 @@ workflows: tags: only: /dev-v[0-9]+(\.[0-9]+)*/ branches: - only: /test\/.*/ + only: /test-cicd\/.*/ matrix: parameters: fdi-version: placeholder @@ -277,7 +277,7 @@ workflows: tags: only: /dev-v[0-9]+(\.[0-9]+)*/ branches: - only: /test\/.*/ + only: /test-cicd\/.*/ matrix: parameters: fdi-version: placeholder @@ -297,4 +297,4 @@ workflows: tags: only: /dev-v[0-9]+(\.[0-9]+)*/ branches: - only: /test\/.*/ + only: /test-cicd\/.*/ From 7d0ad5f88ba47296529ee80196f747730f86f888 Mon Sep 17 00:00:00 2001 From: Iresh Sharma Date: Mon, 27 Feb 2023 15:43:53 +0530 Subject: [PATCH 015/192] changelog edited --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 67f5acbc7..f3c6e813e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## unreleased ## [0.12.2] - 2023-02-27 -- Email passowrd login for dashboard recipe +- Adds APIs and logic to the dashboard recipe to enable email password based login ## [0.12.1] - 2023-02-06 From 38055c5bdfb6d68b57935d73ce786fbb331250c9 Mon Sep 17 00:00:00 2001 From: Iresh Sharma Date: Tue, 28 Feb 2023 12:32:49 +0530 Subject: [PATCH 016/192] add messages to signin and refactored signout --- .../recipe/dashboard/api/signin.py | 9 ++++++-- .../recipe/dashboard/api/signout.py | 23 ++++++++----------- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/supertokens_python/recipe/dashboard/api/signin.py b/supertokens_python/recipe/dashboard/api/signin.py index 97e28f26f..ed0ef434a 100644 --- a/supertokens_python/recipe/dashboard/api/signin.py +++ b/supertokens_python/recipe/dashboard/api/signin.py @@ -43,6 +43,11 @@ async def handle_sign_in_api(api_implementation: APIInterface, api_options: APIO ) if "status" in response and response["status"] == "INVALID_CREDENTIALS_ERROR": return send_200_response( - {"status": "INVALID_CREDENTIALS_ERROR"}, api_options.response + {"status": "INVALID_CREDENTIALS_ERROR", "message": response["message"]}, + api_options.response, + ) + if "status" in response and response["status"] == "USER_SUSPENDED_ERROR": + return send_200_response( + {"status": "USER_SUSPENDED_ERROR", "message": response["message"]}, + api_options.response, ) - return send_200_response({"status": "USER_SUSPENDED_ERROR"}, api_options.response) diff --git a/supertokens_python/recipe/dashboard/api/signout.py b/supertokens_python/recipe/dashboard/api/signout.py index bdf912b98..50b5fa795 100644 --- a/supertokens_python/recipe/dashboard/api/signout.py +++ b/supertokens_python/recipe/dashboard/api/signout.py @@ -21,7 +21,6 @@ from supertokens_python.exceptions import raise_bad_input_exception from supertokens_python.normalised_url_path import NormalisedURLPath from supertokens_python.querier import Querier -from supertokens_python.utils import send_200_response from ..interfaces import SignOutOK @@ -31,17 +30,15 @@ async def handle_signout( api_implementation: APIInterface, api_options: APIOptions ) -> SignOutOK: if api_options.config.auth_mode == "api-key": - send_200_response({"status": "OK"}, api_options.response) - else: - sessionIdFormAuthHeader = api_options.request.get_header("authorization") - if not sessionIdFormAuthHeader: - return raise_bad_input_exception( - "Neither 'API Key' nor 'Authorization' header was found" - ) - sessionIdFormAuthHeader = sessionIdFormAuthHeader.split()[1] - response = await Querier.get_instance().send_delete_request( - NormalisedURLPath("/recipe/dashboard/session"), - {"sessionId": sessionIdFormAuthHeader}, + return SignOutOK() + sessionIdFormAuthHeader = api_options.request.get_header("authorization") + if not sessionIdFormAuthHeader: + return raise_bad_input_exception( + "Neither 'API Key' nor 'Authorization' header was found" ) - send_200_response(response, api_options.response) + sessionIdFormAuthHeader = sessionIdFormAuthHeader.split()[1] + await Querier.get_instance().send_delete_request( + NormalisedURLPath("/recipe/dashboard/session"), + {"sessionId": sessionIdFormAuthHeader}, + ) return SignOutOK() From 13aa6a13bd14fd6df0cf1a23862f48cdacdde3a2 Mon Sep 17 00:00:00 2001 From: Iresh Sharma Date: Wed, 1 Mar 2023 15:54:51 +0530 Subject: [PATCH 017/192] good practice chamges --- supertokens_python/querier.py | 2 +- .../recipe/dashboard/api/__init__.py | 8 ++++---- .../recipe/dashboard/api/signin.py | 11 ++++++----- .../recipe/dashboard/api/signout.py | 13 ++++++------- .../recipe/dashboard/interfaces.py | 4 +++- supertokens_python/recipe/dashboard/recipe.py | 10 ++++++---- .../recipe/dashboard/recipe_implementation.py | 18 +++++++++--------- supertokens_python/recipe/dashboard/utils.py | 8 ++++---- 8 files changed, 39 insertions(+), 35 deletions(-) diff --git a/supertokens_python/querier.py b/supertokens_python/querier.py index 1eaeb30d5..b8eb8d827 100644 --- a/supertokens_python/querier.py +++ b/supertokens_python/querier.py @@ -169,7 +169,7 @@ async def f(url: str) -> Response: async def send_delete_request( self, path: NormalisedURLPath, params: Union[Dict[str, Any], None] = None ): - if not params: + if params is None: params = {} async def f(url: str) -> Response: diff --git a/supertokens_python/recipe/dashboard/api/__init__.py b/supertokens_python/recipe/dashboard/api/__init__.py index 204e3c6c9..fb9e8c6ef 100644 --- a/supertokens_python/recipe/dashboard/api/__init__.py +++ b/supertokens_python/recipe/dashboard/api/__init__.py @@ -13,8 +13,8 @@ # under the License. from .api_key_protector import api_key_protector from .dashboard import handle_dashboard_api -from .signin import handle_sign_in_api -from .signout import handle_signout +from .signin import handle_emailpassword_signin_api +from .signout import handle_emailpassword_signout_api from .userdetails.user_delete import handle_user_delete from .userdetails.user_email_verify_get import handle_user_email_verify_get from .userdetails.user_email_verify_put import handle_user_email_verify_put @@ -47,6 +47,6 @@ "handle_user_sessions_post", "handle_user_password_put", "handle_email_verify_token_post", - "handle_sign_in_api", - "handle_signout", + "handle_emailpassword_signin_api", + "handle_emailpassword_signout_api", ] diff --git a/supertokens_python/recipe/dashboard/api/signin.py b/supertokens_python/recipe/dashboard/api/signin.py index ed0ef434a..a70709c4e 100644 --- a/supertokens_python/recipe/dashboard/api/signin.py +++ b/supertokens_python/recipe/dashboard/api/signin.py @@ -24,17 +24,18 @@ from supertokens_python.utils import send_200_response -# pylint: disable=unused-argument -async def handle_sign_in_api(api_implementation: APIInterface, api_options: APIOptions): +async def handle_emailpassword_signin_api(_: APIInterface, api_options: APIOptions): body = await api_options.request.form_data() + email = body.get("email") + password = body.get("password") - if not body["email"]: + if email is None or not isinstance(email, str): raise_bad_input_exception("Missing required parameter 'email'") - if not body["password"]: + if password is None or not isinstance(password, str): raise_bad_input_exception("Missing required parameter 'password'") response = await Querier.get_instance().send_post_request( NormalisedURLPath("/recipe/dashboard/signin"), - {"email": body["email"], "password": body["password"]}, + {"email": email, "password": password}, ) if "status" in response and response["status"] == "OK": diff --git a/supertokens_python/recipe/dashboard/api/signout.py b/supertokens_python/recipe/dashboard/api/signout.py index 50b5fa795..cafb3cf98 100644 --- a/supertokens_python/recipe/dashboard/api/signout.py +++ b/supertokens_python/recipe/dashboard/api/signout.py @@ -25,20 +25,19 @@ from ..interfaces import SignOutOK -# pylint: disable=unused-argument -async def handle_signout( - api_implementation: APIInterface, api_options: APIOptions +async def handle_emailpassword_signout_api( + _: APIInterface, api_options: APIOptions ) -> SignOutOK: if api_options.config.auth_mode == "api-key": return SignOutOK() - sessionIdFormAuthHeader = api_options.request.get_header("authorization") - if not sessionIdFormAuthHeader: + session_id_form_auth_header = api_options.request.get_header("authorization") + if not session_id_form_auth_header: return raise_bad_input_exception( "Neither 'API Key' nor 'Authorization' header was found" ) - sessionIdFormAuthHeader = sessionIdFormAuthHeader.split()[1] + session_id_form_auth_header = session_id_form_auth_header.split()[1] await Querier.get_instance().send_delete_request( NormalisedURLPath("/recipe/dashboard/session"), - {"sessionId": sessionIdFormAuthHeader}, + {"sessionId": session_id_form_auth_header}, ) return SignOutOK() diff --git a/supertokens_python/recipe/dashboard/interfaces.py b/supertokens_python/recipe/dashboard/interfaces.py index 0d4569a7e..6c22bf220 100644 --- a/supertokens_python/recipe/dashboard/interfaces.py +++ b/supertokens_python/recipe/dashboard/interfaces.py @@ -288,5 +288,7 @@ def to_json(self) -> Dict[str, Any]: class SignOutOK(APIResponse): + status: str = "OK" + def to_json(self): - return {"status": "OK"} + return {"status": self.status} diff --git a/supertokens_python/recipe/dashboard/recipe.py b/supertokens_python/recipe/dashboard/recipe.py index fd8b15629..76ff3fb5b 100644 --- a/supertokens_python/recipe/dashboard/recipe.py +++ b/supertokens_python/recipe/dashboard/recipe.py @@ -23,11 +23,11 @@ api_key_protector, handle_dashboard_api, handle_email_verify_token_post, + handle_emailpassword_signin_api, + handle_emailpassword_signout_api, handle_metadata_get, handle_metadata_put, handle_sessions_get, - handle_sign_in_api, - handle_signout, handle_user_delete, handle_user_email_verify_get, handle_user_email_verify_put, @@ -141,7 +141,9 @@ async def handle_api_request( if request_id == VALIDATE_KEY_API: return await handle_validate_key_api(self.api_implementation, api_options) if request_id == EMAIL_PASSWORD_SIGN_IN: - return await handle_sign_in_api(self.api_implementation, api_options) + return await handle_emailpassword_signin_api( + self.api_implementation, api_options + ) # Do API key validation for the remaining APIs api_function: Optional[ @@ -178,7 +180,7 @@ async def handle_api_request( elif request_id == USER_EMAIL_VERIFY_TOKEN_API: api_function = handle_email_verify_token_post elif request_id == EMAIL_PASSSWORD_SIGNOUT: - api_function = handle_signout + api_function = handle_emailpassword_signout_api if api_function is not None: return await api_key_protector( diff --git a/supertokens_python/recipe/dashboard/recipe_implementation.py b/supertokens_python/recipe/dashboard/recipe_implementation.py index a11d8bb3a..0fb0980b7 100644 --- a/supertokens_python/recipe/dashboard/recipe_implementation.py +++ b/supertokens_python/recipe/dashboard/recipe_implementation.py @@ -21,7 +21,7 @@ from supertokens_python.querier import Querier from .interfaces import RecipeInterface -from .utils import DashboardConfig, validate_APIKey +from .utils import DashboardConfig, validate_api_key class RecipeImplementation(RecipeInterface): @@ -35,20 +35,20 @@ async def should_allow_access( user_context: Dict[str, Any], ) -> bool: if not config.api_key: - authHeaderValue = request.get_header("authorization") + auth_header_value = request.get_header("authorization") - if not authHeaderValue: + if not auth_header_value: return False - authHeaderValue = authHeaderValue.split()[1] - sessionVerificationResponse = ( + auth_header_value = auth_header_value.split()[1] + session_verification_response = ( await Querier.get_instance().send_post_request( NormalisedURLPath("/recipe/dashboard/session/verify"), - {"sessionId": authHeaderValue}, + {"sessionId": auth_header_value}, ) ) return ( - "status" in sessionVerificationResponse - and sessionVerificationResponse["status"] == "OK" + "status" in session_verification_response + and session_verification_response["status"] == "OK" ) - return validate_APIKey(request, config) + return validate_api_key(request, config) diff --git a/supertokens_python/recipe/dashboard/utils.py b/supertokens_python/recipe/dashboard/utils.py index 0769fabd5..029ce67ff 100644 --- a/supertokens_python/recipe/dashboard/utils.py +++ b/supertokens_python/recipe/dashboard/utils.py @@ -394,8 +394,8 @@ def is_recipe_initialised(recipeId: str) -> bool: return isRecipeInitialised -def validate_APIKey(req: BaseRequest, config: DashboardConfig) -> bool: - apiKeyHeaderValue = req.get_header("authorization") - if not apiKeyHeaderValue: +def validate_api_key(req: BaseRequest, config: DashboardConfig) -> bool: + api_key_header_value = req.get_header("authorization") + if not api_key_header_value: return False - return apiKeyHeaderValue == config.api_key + return api_key_header_value == config.api_key From f0788d22f1d471ed99e693c32eb8154f16a3d36a Mon Sep 17 00:00:00 2001 From: Iresh Sharma Date: Fri, 3 Mar 2023 16:38:34 +0530 Subject: [PATCH 018/192] api_key made optional at recipe/__init__.py --- supertokens_python/recipe/dashboard/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/supertokens_python/recipe/dashboard/__init__.py b/supertokens_python/recipe/dashboard/__init__.py index 17df015a2..bfb94637c 100644 --- a/supertokens_python/recipe/dashboard/__init__.py +++ b/supertokens_python/recipe/dashboard/__init__.py @@ -14,7 +14,7 @@ from __future__ import annotations -from typing import Optional, Callable +from typing import Callable, Optional, Union from supertokens_python import AppInfo, RecipeModule from supertokens_python.recipe.dashboard.utils import InputOverrideConfig @@ -23,7 +23,7 @@ def init( - api_key: str, + api_key: Union[str, None], override: Optional[InputOverrideConfig] = None, ) -> Callable[[AppInfo], RecipeModule]: return DashboardRecipe.init( From d8cc66b6605eaf7905c8a20a3275dd04d7db6710 Mon Sep 17 00:00:00 2001 From: Iresh Sharma Date: Fri, 3 Mar 2023 17:50:29 +0530 Subject: [PATCH 019/192] dashboard get edited to support authMode and self add dashboard recipe --- .../recipe/dashboard/api/implementation.py | 11 ++++++----- supertokens_python/supertokens.py | 9 ++++++++- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/supertokens_python/recipe/dashboard/api/implementation.py b/supertokens_python/recipe/dashboard/api/implementation.py index 345fb052b..f17c6a3bf 100644 --- a/supertokens_python/recipe/dashboard/api/implementation.py +++ b/supertokens_python/recipe/dashboard/api/implementation.py @@ -17,13 +17,12 @@ from textwrap import dedent from typing import TYPE_CHECKING, Any, Dict -from supertokens_python.normalised_url_domain import NormalisedURLDomain from supertokens_python import Supertokens +from supertokens_python.normalised_url_domain import NormalisedURLDomain from supertokens_python.normalised_url_path import NormalisedURLPath + from ..constants import DASHBOARD_API -from ..interfaces import ( - APIInterface, -) +from ..interfaces import APIInterface if TYPE_CHECKING: from ..interfaces import APIOptions @@ -48,7 +47,7 @@ async def dashboard_get( connection_uri = "" super_tokens_instance = Supertokens.get_instance() - + auth_mode = options.config.auth_mode connection_uri = super_tokens_instance.supertokens_config.connection_uri dashboard_path = options.app_info.api_base_path.append( @@ -65,6 +64,7 @@ async def dashboard_get( window.staticBasePath = "${bundleDomain}/static" window.dashboardAppPath = "${dashboardPath}" window.connectionURI = "${connectionURI}" + window.authMode = "${authMode}" @@ -81,6 +81,7 @@ async def dashboard_get( bundleDomain=bundle_domain, dashboardPath=dashboard_path, connectionURI=connection_uri, + authMode=auth_mode, ) self.dashboard_get = dashboard_get diff --git a/supertokens_python/supertokens.py b/supertokens_python/supertokens.py index 8b9791d4a..d69ef92fe 100644 --- a/supertokens_python/supertokens.py +++ b/supertokens_python/supertokens.py @@ -49,10 +49,10 @@ from .utils import ( execute_async, get_rid_from_header, + get_top_level_domain_for_same_site_resolution, is_version_gte, normalise_http_method, send_non_200_response_with_message, - get_top_level_domain_for_same_site_resolution, ) if TYPE_CHECKING: @@ -60,6 +60,7 @@ from supertokens_python.framework.request import BaseRequest from supertokens_python.framework.response import BaseResponse from supertokens_python.recipe.session import SessionContainer + from .recipe import dashboard import json from os import environ @@ -202,6 +203,12 @@ def __init__( map(lambda func: func(self.app_info), recipe_list) ) + filtered = list( + filter(lambda func: isinstance(func, type(dashboard.init)), recipe_list) + ) + if len(filtered) == 0: + self.recipe_modules.append(dashboard.init(None)(self.app_info)) + if telemetry is None: # If telemetry is not provided, enable it by default for production environment telemetry = ("SUPERTOKENS_ENV" not in environ) or ( From e45ecb9f204e754f7681f735a2ae5a68c2e5ee19 Mon Sep 17 00:00:00 2001 From: Iresh Sharma Date: Fri, 3 Mar 2023 18:05:18 +0530 Subject: [PATCH 020/192] import fixes --- supertokens_python/supertokens.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/supertokens_python/supertokens.py b/supertokens_python/supertokens.py index d69ef92fe..034d5ce64 100644 --- a/supertokens_python/supertokens.py +++ b/supertokens_python/supertokens.py @@ -60,7 +60,7 @@ from supertokens_python.framework.request import BaseRequest from supertokens_python.framework.response import BaseResponse from supertokens_python.recipe.session import SessionContainer - from .recipe import dashboard + from supertokens_python.recipe.dashboard import init as DashBoardInit import json from os import environ @@ -204,10 +204,10 @@ def __init__( ) filtered = list( - filter(lambda func: isinstance(func, type(dashboard.init)), recipe_list) + filter(lambda func: isinstance(func, type(DashBoardInit)), recipe_list) ) if len(filtered) == 0: - self.recipe_modules.append(dashboard.init(None)(self.app_info)) + self.recipe_modules.append(DashBoardInit(None)(self.app_info)) if telemetry is None: # If telemetry is not provided, enable it by default for production environment From 185eded8a1b1f302005796ba9f659d7ccfc7e3f1 Mon Sep 17 00:00:00 2001 From: Iresh Sharma Date: Fri, 3 Mar 2023 18:27:53 +0530 Subject: [PATCH 021/192] import errors --- supertokens_python/supertokens.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/supertokens_python/supertokens.py b/supertokens_python/supertokens.py index 034d5ce64..9363cac46 100644 --- a/supertokens_python/supertokens.py +++ b/supertokens_python/supertokens.py @@ -60,7 +60,7 @@ from supertokens_python.framework.request import BaseRequest from supertokens_python.framework.response import BaseResponse from supertokens_python.recipe.session import SessionContainer - from supertokens_python.recipe.dashboard import init as DashBoardInit + from supertokens_python.recipe import dashboard import json from os import environ @@ -204,10 +204,10 @@ def __init__( ) filtered = list( - filter(lambda func: isinstance(func, type(DashBoardInit)), recipe_list) + filter(lambda func: isinstance(func, type(dashboard.init)), recipe_list) ) if len(filtered) == 0: - self.recipe_modules.append(DashBoardInit(None)(self.app_info)) + self.recipe_modules.append(dashboard.init(None)(self.app_info)) if telemetry is None: # If telemetry is not provided, enable it by default for production environment From 7a7a03111b7ff6d0c50df9c852d40b98a4374d01 Mon Sep 17 00:00:00 2001 From: Iresh Sharma Date: Fri, 3 Mar 2023 18:33:01 +0530 Subject: [PATCH 022/192] commented auto add dashboard recipe --- supertokens_python/supertokens.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/supertokens_python/supertokens.py b/supertokens_python/supertokens.py index 9363cac46..649f1d7eb 100644 --- a/supertokens_python/supertokens.py +++ b/supertokens_python/supertokens.py @@ -60,7 +60,8 @@ from supertokens_python.framework.request import BaseRequest from supertokens_python.framework.response import BaseResponse from supertokens_python.recipe.session import SessionContainer - from supertokens_python.recipe import dashboard + + # from supertokens_python.recipe import dashboard import json from os import environ @@ -203,11 +204,11 @@ def __init__( map(lambda func: func(self.app_info), recipe_list) ) - filtered = list( - filter(lambda func: isinstance(func, type(dashboard.init)), recipe_list) - ) - if len(filtered) == 0: - self.recipe_modules.append(dashboard.init(None)(self.app_info)) + # filtered = list( + # filter(lambda func: isinstance(func, type(dashboard.init)), recipe_list) + # ) + # if len(filtered) == 0: + # self.recipe_modules.append(dashboard.init(None)(self.app_info)) if telemetry is None: # If telemetry is not provided, enable it by default for production environment From 5e66b183a467fc22cc3b71636e3b7ba7a6846d5f Mon Sep 17 00:00:00 2001 From: Iresh Sharma Date: Mon, 6 Mar 2023 14:34:09 +0530 Subject: [PATCH 023/192] email password functionality added, auto initialization of dashboard recipe removed --- .../with-flask/with-thirdpartyemailpassword/app.py | 3 ++- supertokens_python/recipe/dashboard/__init__.py | 14 ++++++++------ supertokens_python/recipe/dashboard/interfaces.py | 5 +++-- supertokens_python/recipe/dashboard/utils.py | 2 +- .../recipe/emailverification/interfaces.py | 5 +++-- .../recipe/passwordless/interfaces.py | 11 +++++++---- supertokens_python/supertokens.py | 14 ++++++++------ tests/auth-react/django3x/mysite/utils.py | 5 +++-- tests/auth-react/fastapi-server/app.py | 5 +++-- tests/auth-react/flask-server/app.py | 5 +++-- tests/utils.py | 7 +++---- 11 files changed, 44 insertions(+), 32 deletions(-) diff --git a/examples/with-flask/with-thirdpartyemailpassword/app.py b/examples/with-flask/with-thirdpartyemailpassword/app.py index 56e3163be..e54f4c964 100644 --- a/examples/with-flask/with-thirdpartyemailpassword/app.py +++ b/examples/with-flask/with-thirdpartyemailpassword/app.py @@ -3,6 +3,7 @@ from dotenv import load_dotenv from flask import Flask, abort, g, jsonify from flask_cors import CORS + from supertokens_python import ( InputAppInfo, SupertokensConfig, @@ -11,9 +12,9 @@ ) from supertokens_python.framework.flask import Middleware from supertokens_python.recipe import ( + emailverification, session, thirdpartyemailpassword, - emailverification, ) from supertokens_python.recipe.session.framework.flask import verify_session from supertokens_python.recipe.thirdpartyemailpassword import ( diff --git a/supertokens_python/recipe/dashboard/__init__.py b/supertokens_python/recipe/dashboard/__init__.py index bfb94637c..dbed7080a 100644 --- a/supertokens_python/recipe/dashboard/__init__.py +++ b/supertokens_python/recipe/dashboard/__init__.py @@ -14,18 +14,20 @@ from __future__ import annotations -from typing import Callable, Optional, Union +from typing import TYPE_CHECKING, Callable, Optional, Union -from supertokens_python import AppInfo, RecipeModule -from supertokens_python.recipe.dashboard.utils import InputOverrideConfig - -from .recipe import DashboardRecipe +if TYPE_CHECKING: + from supertokens_python import AppInfo, RecipeModule + from supertokens_python.recipe.dashboard.utils import InputOverrideConfig def init( - api_key: Union[str, None], + api_key: Union[str, None] = None, override: Optional[InputOverrideConfig] = None, ) -> Callable[[AppInfo], RecipeModule]: + # Global import for the following was avoided because of circular import errors + from .recipe import DashboardRecipe + return DashboardRecipe.init( api_key, override, diff --git a/supertokens_python/recipe/dashboard/interfaces.py b/supertokens_python/recipe/dashboard/interfaces.py index 6c22bf220..3a5353876 100644 --- a/supertokens_python/recipe/dashboard/interfaces.py +++ b/supertokens_python/recipe/dashboard/interfaces.py @@ -19,13 +19,14 @@ from supertokens_python.recipe.session.interfaces import SessionInformationResult from supertokens_python.types import User -from ...supertokens import AppInfo from ...types import APIResponse -from .utils import DashboardConfig, UserWithMetadata if TYPE_CHECKING: from supertokens_python.framework import BaseRequest, BaseResponse + from ...supertokens import AppInfo + from .utils import DashboardConfig, UserWithMetadata + class SessionInfo: def __init__(self, info: SessionInformationResult) -> None: diff --git a/supertokens_python/recipe/dashboard/utils.py b/supertokens_python/recipe/dashboard/utils.py index 029ce67ff..9c5d44ffd 100644 --- a/supertokens_python/recipe/dashboard/utils.py +++ b/supertokens_python/recipe/dashboard/utils.py @@ -17,6 +17,7 @@ if TYPE_CHECKING: from supertokens_python.framework.request import BaseRequest + from ...supertokens import AppInfo from supertokens_python.recipe.emailpassword import EmailPasswordRecipe from supertokens_python.recipe.emailpassword.asyncio import ( @@ -46,7 +47,6 @@ from supertokens_python.utils import Awaitable from ...normalised_url_path import NormalisedURLPath -from ...supertokens import AppInfo from .constants import ( DASHBOARD_API, EMAIL_PASSSWORD_SIGNOUT, diff --git a/supertokens_python/recipe/emailverification/interfaces.py b/supertokens_python/recipe/emailverification/interfaces.py index 1f508548c..e77af7bec 100644 --- a/supertokens_python/recipe/emailverification/interfaces.py +++ b/supertokens_python/recipe/emailverification/interfaces.py @@ -14,16 +14,17 @@ from __future__ import annotations from abc import ABC, abstractmethod -from typing import TYPE_CHECKING, Any, Dict, Union, Callable, Awaitable, Optional +from typing import TYPE_CHECKING, Any, Awaitable, Callable, Dict, Optional, Union from supertokens_python.ingredients.emaildelivery import EmailDeliveryIngredient from supertokens_python.types import APIResponse, GeneralErrorResponse + from ..session.interfaces import SessionContainer -from ...supertokens import AppInfo if TYPE_CHECKING: from supertokens_python.framework import BaseRequest, BaseResponse + from ...supertokens import AppInfo from .types import User, VerificationEmailTemplateVars from .utils import EmailVerificationConfig diff --git a/supertokens_python/recipe/passwordless/interfaces.py b/supertokens_python/recipe/passwordless/interfaces.py index bfc8dd844..05313e2b3 100644 --- a/supertokens_python/recipe/passwordless/interfaces.py +++ b/supertokens_python/recipe/passwordless/interfaces.py @@ -14,24 +14,27 @@ from __future__ import annotations from abc import ABC, abstractmethod -from typing import Any, Dict, List, Union +from typing import TYPE_CHECKING, Any, Dict, List, Union + +from typing_extensions import Literal from supertokens_python.framework import BaseRequest, BaseResponse from supertokens_python.ingredients.emaildelivery import EmailDeliveryIngredient from supertokens_python.recipe.session import SessionContainer from supertokens_python.types import APIResponse, GeneralErrorResponse -from typing_extensions import Literal # if TYPE_CHECKING: from .types import ( DeviceType, - SMSDeliveryIngredient, PasswordlessLoginEmailTemplateVars, PasswordlessLoginSMSTemplateVars, + SMSDeliveryIngredient, User, ) from .utils import PasswordlessConfig -from ...supertokens import AppInfo + +if TYPE_CHECKING: + from ...supertokens import AppInfo class CreateCodeOkResult: diff --git a/supertokens_python/supertokens.py b/supertokens_python/supertokens.py index 649f1d7eb..a916b701c 100644 --- a/supertokens_python/supertokens.py +++ b/supertokens_python/supertokens.py @@ -61,8 +61,6 @@ from supertokens_python.framework.response import BaseResponse from supertokens_python.recipe.session import SessionContainer - # from supertokens_python.recipe import dashboard - import json from os import environ @@ -203,12 +201,16 @@ def __init__( self.recipe_modules: List[RecipeModule] = list( map(lambda func: func(self.app_info), recipe_list) ) + # Global import for the following was avoided because of circular import errors + # ! below code for initializing dashboard by default, curretnly not done because of major refactoring required to make this viable + # from supertokens_python.recipe.dashboard import init as dashboard_init - # filtered = list( - # filter(lambda func: isinstance(func, type(dashboard.init)), recipe_list) + # is_dashboard_initialised = ( + # len([r for r in self.recipe_modules if r.get_recipe_id() == "dashboard"]) + # == 1 # ) - # if len(filtered) == 0: - # self.recipe_modules.append(dashboard.init(None)(self.app_info)) + # if not is_dashboard_initialised: + # self.recipe_modules.append(dashboard_init()(self.app_info)) if telemetry is None: # If telemetry is not provided, enable it by default for production environment diff --git a/tests/auth-react/django3x/mysite/utils.py b/tests/auth-react/django3x/mysite/utils.py index 32825e903..30a085d2c 100644 --- a/tests/auth-react/django3x/mysite/utils.py +++ b/tests/auth-react/django3x/mysite/utils.py @@ -2,6 +2,8 @@ from typing import Any, Dict, List, Optional, Union from dotenv import load_dotenv +from typing_extensions import Literal + from supertokens_python import InputAppInfo, Supertokens, SupertokensConfig, init from supertokens_python.framework.request import BaseRequest from supertokens_python.recipe import ( @@ -14,7 +16,7 @@ thirdpartypasswordless, userroles, ) -from supertokens_python.recipe.dashboard import DashboardRecipe +from supertokens_python.recipe.dashboard.recipe import DashboardRecipe from supertokens_python.recipe.emailpassword import EmailPasswordRecipe from supertokens_python.recipe.emailpassword.interfaces import ( APIInterface as EmailPasswordAPIInterface, @@ -85,7 +87,6 @@ ) from supertokens_python.recipe.userroles import UserRolesRecipe from supertokens_python.types import GeneralErrorResponse -from typing_extensions import Literal from .store import save_code, save_url_with_token diff --git a/tests/auth-react/fastapi-server/app.py b/tests/auth-react/fastapi-server/app.py index 2b2f94a95..9fa93a94d 100644 --- a/tests/auth-react/fastapi-server/app.py +++ b/tests/auth-react/fastapi-server/app.py @@ -25,6 +25,8 @@ from starlette.middleware.cors import CORSMiddleware from starlette.responses import Response from starlette.types import ASGIApp +from typing_extensions import Literal + from supertokens_python import ( InputAppInfo, Supertokens, @@ -44,7 +46,7 @@ thirdpartypasswordless, userroles, ) -from supertokens_python.recipe.dashboard import DashboardRecipe +from supertokens_python.recipe.dashboard.recipe import DashboardRecipe from supertokens_python.recipe.emailpassword import EmailPasswordRecipe from supertokens_python.recipe.emailpassword.interfaces import ( APIInterface as EmailPasswordAPIInterface, @@ -131,7 +133,6 @@ create_new_role_or_add_permissions, ) from supertokens_python.types import GeneralErrorResponse -from typing_extensions import Literal load_dotenv() diff --git a/tests/auth-react/flask-server/app.py b/tests/auth-react/flask-server/app.py index dc845dd8c..120206173 100644 --- a/tests/auth-react/flask-server/app.py +++ b/tests/auth-react/flask-server/app.py @@ -17,6 +17,8 @@ from dotenv import load_dotenv from flask import Flask, g, jsonify, make_response, request from flask_cors import CORS +from typing_extensions import Literal + from supertokens_python import ( InputAppInfo, Supertokens, @@ -36,7 +38,7 @@ thirdpartypasswordless, userroles, ) -from supertokens_python.recipe.dashboard import DashboardRecipe +from supertokens_python.recipe.dashboard.recipe import DashboardRecipe from supertokens_python.recipe.emailpassword import EmailPasswordRecipe from supertokens_python.recipe.emailpassword.interfaces import ( APIInterface as EmailPasswordAPIInterface, @@ -124,7 +126,6 @@ create_new_role_or_add_permissions, ) from supertokens_python.types import GeneralErrorResponse -from typing_extensions import Literal load_dotenv() diff --git a/tests/utils.py b/tests/utils.py index 3d1e0ec0b..dd5a564a6 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -13,7 +13,6 @@ # under the License. import asyncio from datetime import datetime, timezone -from urllib.parse import unquote from http.cookies import SimpleCookie from os import environ, kill, remove, scandir from shutil import rmtree @@ -21,14 +20,15 @@ from subprocess import DEVNULL, run from time import sleep from typing import Any, Dict, List, cast +from urllib.parse import unquote +from fastapi.testclient import TestClient from requests.models import Response from yaml import FullLoader, dump, load -from fastapi.testclient import TestClient from supertokens_python import InputAppInfo, Supertokens, SupertokensConfig from supertokens_python.process_state import ProcessState -from supertokens_python.recipe.dashboard import DashboardRecipe +from supertokens_python.recipe.dashboard.recipe import DashboardRecipe from supertokens_python.recipe.emailpassword import EmailPasswordRecipe from supertokens_python.recipe.emailverification import EmailVerificationRecipe from supertokens_python.recipe.jwt import JWTRecipe @@ -492,7 +492,6 @@ def wrapper(f: Any) -> Any: # Import AsyncMock import sys - from unittest.mock import MagicMock if sys.version_info >= (3, 8): From 5d9d5600b39da725e1d10e09ec12f996a9dc19de Mon Sep 17 00:00:00 2001 From: Iresh Sharma Date: Mon, 6 Mar 2023 17:46:58 +0530 Subject: [PATCH 024/192] validate_key api fixed --- .../recipe/dashboard/api/signin.py | 8 +++++--- .../recipe/dashboard/api/validate_key.py | 20 +++++++------------ 2 files changed, 12 insertions(+), 16 deletions(-) diff --git a/supertokens_python/recipe/dashboard/api/signin.py b/supertokens_python/recipe/dashboard/api/signin.py index a70709c4e..a5d7ef473 100644 --- a/supertokens_python/recipe/dashboard/api/signin.py +++ b/supertokens_python/recipe/dashboard/api/signin.py @@ -25,7 +25,9 @@ async def handle_emailpassword_signin_api(_: APIInterface, api_options: APIOptions): - body = await api_options.request.form_data() + body = await api_options.request.json() + if body is None: + raise_bad_input_exception("Please send body") email = body.get("email") password = body.get("password") @@ -44,11 +46,11 @@ async def handle_emailpassword_signin_api(_: APIInterface, api_options: APIOptio ) if "status" in response and response["status"] == "INVALID_CREDENTIALS_ERROR": return send_200_response( - {"status": "INVALID_CREDENTIALS_ERROR", "message": response["message"]}, + {"status": "INVALID_CREDENTIALS_ERROR"}, api_options.response, ) if "status" in response and response["status"] == "USER_SUSPENDED_ERROR": return send_200_response( - {"status": "USER_SUSPENDED_ERROR", "message": response["message"]}, + {"status": "USER_SUSPENDED_ERROR"}, api_options.response, ) diff --git a/supertokens_python/recipe/dashboard/api/validate_key.py b/supertokens_python/recipe/dashboard/api/validate_key.py index 652c31f09..932a0b76a 100644 --- a/supertokens_python/recipe/dashboard/api/validate_key.py +++ b/supertokens_python/recipe/dashboard/api/validate_key.py @@ -22,25 +22,19 @@ ) from supertokens_python.utils import ( - default_user_context, send_200_response, send_non_200_response_with_message, ) +from ..utils import validate_api_key + async def handle_validate_key_api( - api_implementation: APIInterface, api_options: APIOptions + _api_implementation: APIInterface, api_options: APIOptions ): - _ = api_implementation - should_allow_accesss = await api_options.recipe_implementation.should_allow_access( - api_options.request, - api_options.config, - default_user_context(api_options.request), - ) - if should_allow_accesss is False: - return send_non_200_response_with_message( - "Unauthorized access", 401, api_options.response - ) + is_valid_key = validate_api_key(api_options.request, api_options.config) - return send_200_response({"status": "OK"}, api_options.response) + if is_valid_key: + return send_200_response({"status": "OK"}, api_options.response) + return send_non_200_response_with_message("Unauthorised", 401, api_options.response) From d841199352dfa27d2f2e3fa218fc1351bed4ae9a Mon Sep 17 00:00:00 2001 From: Iresh Sharma Date: Mon, 6 Mar 2023 18:12:29 +0530 Subject: [PATCH 025/192] validate api check fixed --- supertokens_python/recipe/dashboard/utils.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/supertokens_python/recipe/dashboard/utils.py b/supertokens_python/recipe/dashboard/utils.py index 9c5d44ffd..f397c69f0 100644 --- a/supertokens_python/recipe/dashboard/utils.py +++ b/supertokens_python/recipe/dashboard/utils.py @@ -398,4 +398,7 @@ def validate_api_key(req: BaseRequest, config: DashboardConfig) -> bool: api_key_header_value = req.get_header("authorization") if not api_key_header_value: return False + # We receieve the api key as `Bearer API_KEY`, this retrieves just the key + api_key_header_value = api_key_header_value.split(" ")[1] + print(config.api_key) return api_key_header_value == config.api_key From 0fedebc5b60984ee70050ac019ee7814317fdf45 Mon Sep 17 00:00:00 2001 From: Iresh Sharma Date: Tue, 7 Mar 2023 11:53:02 +0530 Subject: [PATCH 026/192] removed unecessary print --- supertokens_python/recipe/dashboard/utils.py | 1 - 1 file changed, 1 deletion(-) diff --git a/supertokens_python/recipe/dashboard/utils.py b/supertokens_python/recipe/dashboard/utils.py index f397c69f0..2a2cdb523 100644 --- a/supertokens_python/recipe/dashboard/utils.py +++ b/supertokens_python/recipe/dashboard/utils.py @@ -400,5 +400,4 @@ def validate_api_key(req: BaseRequest, config: DashboardConfig) -> bool: return False # We receieve the api key as `Bearer API_KEY`, this retrieves just the key api_key_header_value = api_key_header_value.split(" ")[1] - print(config.api_key) return api_key_header_value == config.api_key From 63eddae47b5276373d1ded221dd9d8056bf4e1bf Mon Sep 17 00:00:00 2001 From: Iresh Sharma Date: Tue, 7 Mar 2023 13:15:05 +0530 Subject: [PATCH 027/192] added message to response for user_suspended --- supertokens_python/recipe/dashboard/api/signin.py | 2 +- supertokens_python/supertokens.py | 10 ---------- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/supertokens_python/recipe/dashboard/api/signin.py b/supertokens_python/recipe/dashboard/api/signin.py index a5d7ef473..f0bc46620 100644 --- a/supertokens_python/recipe/dashboard/api/signin.py +++ b/supertokens_python/recipe/dashboard/api/signin.py @@ -51,6 +51,6 @@ async def handle_emailpassword_signin_api(_: APIInterface, api_options: APIOptio ) if "status" in response and response["status"] == "USER_SUSPENDED_ERROR": return send_200_response( - {"status": "USER_SUSPENDED_ERROR"}, + {"status": "USER_SUSPENDED_ERROR", "message": response["message"]}, api_options.response, ) diff --git a/supertokens_python/supertokens.py b/supertokens_python/supertokens.py index a916b701c..2f1166591 100644 --- a/supertokens_python/supertokens.py +++ b/supertokens_python/supertokens.py @@ -201,16 +201,6 @@ def __init__( self.recipe_modules: List[RecipeModule] = list( map(lambda func: func(self.app_info), recipe_list) ) - # Global import for the following was avoided because of circular import errors - # ! below code for initializing dashboard by default, curretnly not done because of major refactoring required to make this viable - # from supertokens_python.recipe.dashboard import init as dashboard_init - - # is_dashboard_initialised = ( - # len([r for r in self.recipe_modules if r.get_recipe_id() == "dashboard"]) - # == 1 - # ) - # if not is_dashboard_initialised: - # self.recipe_modules.append(dashboard_init()(self.app_info)) if telemetry is None: # If telemetry is not provided, enable it by default for production environment From b698544192e9d05f62eabff153e52d3d16e5b165 Mon Sep 17 00:00:00 2001 From: Iresh Sharma <32684272+iresharma@users.noreply.github.com> Date: Tue, 7 Mar 2023 16:18:34 +0530 Subject: [PATCH 028/192] Update constants.py --- supertokens_python/constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/supertokens_python/constants.py b/supertokens_python/constants.py index 51171c879..a7af90956 100644 --- a/supertokens_python/constants.py +++ b/supertokens_python/constants.py @@ -23,7 +23,7 @@ "2.17", "2.18", ] -VERSION = "0.12.2" +VERSION = "0.12.3" TELEMETRY = "/telemetry" USER_COUNT = "/users/count" USER_DELETE = "/user/remove" From 94cc69383c66fc7b0b12f62d30b4937b05a9cade Mon Sep 17 00:00:00 2001 From: Iresh Sharma <32684272+iresharma@users.noreply.github.com> Date: Tue, 7 Mar 2023 16:31:31 +0530 Subject: [PATCH 029/192] Update constants.py --- supertokens_python/constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/supertokens_python/constants.py b/supertokens_python/constants.py index a7af90956..5f33750b4 100644 --- a/supertokens_python/constants.py +++ b/supertokens_python/constants.py @@ -36,4 +36,4 @@ FDI_KEY_HEADER = "fdi-version" API_VERSION = "/apiversion" API_VERSION_HEADER = "cdi-version" -DASHBOARD_VERSION = "0.3" +DASHBOARD_VERSION = "0.4" From 22b113c1482278d9d4f98c31631fc7ee0bb12f20 Mon Sep 17 00:00:00 2001 From: Iresh Sharma <32684272+iresharma@users.noreply.github.com> Date: Tue, 7 Mar 2023 16:46:23 +0530 Subject: [PATCH 030/192] Update supertokens.py --- supertokens_python/supertokens.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/supertokens_python/supertokens.py b/supertokens_python/supertokens.py index 2f1166591..8b9791d4a 100644 --- a/supertokens_python/supertokens.py +++ b/supertokens_python/supertokens.py @@ -49,10 +49,10 @@ from .utils import ( execute_async, get_rid_from_header, - get_top_level_domain_for_same_site_resolution, is_version_gte, normalise_http_method, send_non_200_response_with_message, + get_top_level_domain_for_same_site_resolution, ) if TYPE_CHECKING: From dc3fd43d6bf4ba28663d8349a9a426fff3485b21 Mon Sep 17 00:00:00 2001 From: Iresh Sharma <32684272+iresharma@users.noreply.github.com> Date: Tue, 7 Mar 2023 17:21:15 +0530 Subject: [PATCH 031/192] Update setup.py --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 41d7812f4..9d5305707 100644 --- a/setup.py +++ b/setup.py @@ -70,7 +70,7 @@ setup( name="supertokens_python", - version="0.12.2", + version="0.12.3", author="SuperTokens", license="Apache 2.0", author_email="team@supertokens.com", From 58a8ecab157c68ec83bc5559c4e485c9d29a5c56 Mon Sep 17 00:00:00 2001 From: Iresh Sharma Date: Wed, 8 Mar 2023 12:52:32 +0530 Subject: [PATCH 032/192] reset unwanted changes --- supertokens_python/recipe/dashboard/__init__.py | 5 ++--- .../recipe/dashboard/recipe_implementation.py | 2 +- supertokens_python/recipe/emailverification/interfaces.py | 2 +- supertokens_python/recipe/passwordless/interfaces.py | 7 +++---- tests/auth-react/django3x/mysite/utils.py | 2 +- tests/auth-react/fastapi-server/app.py | 2 +- tests/auth-react/flask-server/app.py | 2 +- tests/utils.py | 4 +--- 8 files changed, 11 insertions(+), 15 deletions(-) diff --git a/supertokens_python/recipe/dashboard/__init__.py b/supertokens_python/recipe/dashboard/__init__.py index dbed7080a..8476d9d7a 100644 --- a/supertokens_python/recipe/dashboard/__init__.py +++ b/supertokens_python/recipe/dashboard/__init__.py @@ -20,14 +20,13 @@ from supertokens_python import AppInfo, RecipeModule from supertokens_python.recipe.dashboard.utils import InputOverrideConfig +from .recipe import DashboardRecipe + def init( api_key: Union[str, None] = None, override: Optional[InputOverrideConfig] = None, ) -> Callable[[AppInfo], RecipeModule]: - # Global import for the following was avoided because of circular import errors - from .recipe import DashboardRecipe - return DashboardRecipe.init( api_key, override, diff --git a/supertokens_python/recipe/dashboard/recipe_implementation.py b/supertokens_python/recipe/dashboard/recipe_implementation.py index 0fb0980b7..9207e8f4d 100644 --- a/supertokens_python/recipe/dashboard/recipe_implementation.py +++ b/supertokens_python/recipe/dashboard/recipe_implementation.py @@ -34,7 +34,7 @@ async def should_allow_access( config: DashboardConfig, user_context: Dict[str, Any], ) -> bool: - if not config.api_key: + if config.auth_mode == "email-password": auth_header_value = request.get_header("authorization") if not auth_header_value: diff --git a/supertokens_python/recipe/emailverification/interfaces.py b/supertokens_python/recipe/emailverification/interfaces.py index e77af7bec..666c09ca5 100644 --- a/supertokens_python/recipe/emailverification/interfaces.py +++ b/supertokens_python/recipe/emailverification/interfaces.py @@ -19,12 +19,12 @@ from supertokens_python.ingredients.emaildelivery import EmailDeliveryIngredient from supertokens_python.types import APIResponse, GeneralErrorResponse +from ...supertokens import AppInfo from ..session.interfaces import SessionContainer if TYPE_CHECKING: from supertokens_python.framework import BaseRequest, BaseResponse - from ...supertokens import AppInfo from .types import User, VerificationEmailTemplateVars from .utils import EmailVerificationConfig diff --git a/supertokens_python/recipe/passwordless/interfaces.py b/supertokens_python/recipe/passwordless/interfaces.py index 05313e2b3..d861d2747 100644 --- a/supertokens_python/recipe/passwordless/interfaces.py +++ b/supertokens_python/recipe/passwordless/interfaces.py @@ -14,7 +14,7 @@ from __future__ import annotations from abc import ABC, abstractmethod -from typing import TYPE_CHECKING, Any, Dict, List, Union +from typing import Any, Dict, List, Union from typing_extensions import Literal @@ -23,6 +23,8 @@ from supertokens_python.recipe.session import SessionContainer from supertokens_python.types import APIResponse, GeneralErrorResponse +from ...supertokens import AppInfo + # if TYPE_CHECKING: from .types import ( DeviceType, @@ -33,9 +35,6 @@ ) from .utils import PasswordlessConfig -if TYPE_CHECKING: - from ...supertokens import AppInfo - class CreateCodeOkResult: def __init__( diff --git a/tests/auth-react/django3x/mysite/utils.py b/tests/auth-react/django3x/mysite/utils.py index 30a085d2c..b08cb9f0f 100644 --- a/tests/auth-react/django3x/mysite/utils.py +++ b/tests/auth-react/django3x/mysite/utils.py @@ -16,7 +16,7 @@ thirdpartypasswordless, userroles, ) -from supertokens_python.recipe.dashboard.recipe import DashboardRecipe +from supertokens_python.recipe.dashboard import DashboardRecipe from supertokens_python.recipe.emailpassword import EmailPasswordRecipe from supertokens_python.recipe.emailpassword.interfaces import ( APIInterface as EmailPasswordAPIInterface, diff --git a/tests/auth-react/fastapi-server/app.py b/tests/auth-react/fastapi-server/app.py index 9fa93a94d..eb928bdc2 100644 --- a/tests/auth-react/fastapi-server/app.py +++ b/tests/auth-react/fastapi-server/app.py @@ -46,7 +46,7 @@ thirdpartypasswordless, userroles, ) -from supertokens_python.recipe.dashboard.recipe import DashboardRecipe +from supertokens_python.recipe.dashboard import DashboardRecipe from supertokens_python.recipe.emailpassword import EmailPasswordRecipe from supertokens_python.recipe.emailpassword.interfaces import ( APIInterface as EmailPasswordAPIInterface, diff --git a/tests/auth-react/flask-server/app.py b/tests/auth-react/flask-server/app.py index 120206173..012c0b400 100644 --- a/tests/auth-react/flask-server/app.py +++ b/tests/auth-react/flask-server/app.py @@ -38,7 +38,7 @@ thirdpartypasswordless, userroles, ) -from supertokens_python.recipe.dashboard.recipe import DashboardRecipe +from supertokens_python.recipe.dashboard import DashboardRecipe from supertokens_python.recipe.emailpassword import EmailPasswordRecipe from supertokens_python.recipe.emailpassword.interfaces import ( APIInterface as EmailPasswordAPIInterface, diff --git a/tests/utils.py b/tests/utils.py index dd5a564a6..bbcf4f7a4 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -28,7 +28,7 @@ from supertokens_python import InputAppInfo, Supertokens, SupertokensConfig from supertokens_python.process_state import ProcessState -from supertokens_python.recipe.dashboard.recipe import DashboardRecipe +from supertokens_python.recipe.dashboard import DashboardRecipe from supertokens_python.recipe.emailpassword import EmailPasswordRecipe from supertokens_python.recipe.emailverification import EmailVerificationRecipe from supertokens_python.recipe.jwt import JWTRecipe @@ -476,7 +476,6 @@ def min_api_version(min_version: str) -> Any: """ Skips the test if the local ST core doesn't satisfy version requirements for the tests. - Fetches the core version only once throughout the testing session. """ @@ -528,7 +527,6 @@ def get_st_init_args(recipe_list: List[Any]) -> Dict[str, Any]: def is_subset(dict1: Any, dict2: Any) -> bool: """Check if dict2 is subset of dict1 in a nested manner - Iteratively compares list items with recursion if key's value is a list """ if isinstance(dict1, list): From 77e346383770d933138c382608df87378ba3e8b1 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Wed, 8 Mar 2023 13:18:58 +0530 Subject: [PATCH 033/192] adding dev-v0.12.3 tag to this commit to ensure building --- html/supertokens_python/constants.html | 17 +- html/supertokens_python/querier.html | 35 +++- .../recipe/dashboard/api/implementation.html | 15 +- .../recipe/dashboard/api/index.html | 103 +++++++++-- .../recipe/dashboard/api/signin.html | 163 ++++++++++++++++++ .../recipe/dashboard/api/signout.html | 136 +++++++++++++++ .../recipe/dashboard/api/validate_key.html | 39 ++--- .../recipe/dashboard/constants.html | 4 +- .../recipe/dashboard/index.html | 13 +- .../recipe/dashboard/interfaces.html | 66 ++++++- .../recipe/dashboard/recipe.html | 36 +++- .../dashboard/recipe_implementation.html | 80 ++++++--- .../recipe/dashboard/utils.html | 71 ++++++-- .../recipe/emailverification/interfaces.html | 5 +- .../recipe/passwordless/interfaces.html | 8 +- html/supertokens_python/types.html | 1 + 16 files changed, 672 insertions(+), 120 deletions(-) create mode 100644 html/supertokens_python/recipe/dashboard/api/signin.html create mode 100644 html/supertokens_python/recipe/dashboard/api/signout.html diff --git a/html/supertokens_python/constants.html b/html/supertokens_python/constants.html index 06d1f8628..c4d1e9543 100644 --- a/html/supertokens_python/constants.html +++ b/html/supertokens_python/constants.html @@ -39,8 +39,19 @@

Module supertokens_python.constants

# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. -SUPPORTED_CDI_VERSIONS = ["2.9", "2.10", "2.11", "2.12", "2.13", "2.14", "2.15"] -VERSION = "0.12.2" +SUPPORTED_CDI_VERSIONS = [ + "2.9", + "2.10", + "2.11", + "2.12", + "2.13", + "2.14", + "2.15", + "2.16", + "2.17", + "2.18", +] +VERSION = "0.12.3" TELEMETRY = "/telemetry" USER_COUNT = "/users/count" USER_DELETE = "/user/remove" @@ -53,7 +64,7 @@

Module supertokens_python.constants

FDI_KEY_HEADER = "fdi-version" API_VERSION = "/apiversion" API_VERSION_HEADER = "cdi-version" -DASHBOARD_VERSION = "0.3"
+DASHBOARD_VERSION = "0.4"
diff --git a/html/supertokens_python/querier.html b/html/supertokens_python/querier.html index 4d2987d4e..5a7f4b362 100644 --- a/html/supertokens_python/querier.html +++ b/html/supertokens_python/querier.html @@ -194,11 +194,18 @@

Module supertokens_python.querier

return await self.__send_request_helper(path, "POST", f, len(self.__hosts)) - async def send_delete_request(self, path: NormalisedURLPath): + async def send_delete_request( + self, path: NormalisedURLPath, params: Union[Dict[str, Any], None] = None + ): + if params is None: + params = {} + async def f(url: str) -> Response: async with AsyncClient() as client: return await client.delete( # type:ignore - url, headers=await self.__get_headers_with_api_version(path) + url, + params=params, + headers=await self.__get_headers_with_api_version(path), ) return await self.__send_request_helper(path, "DELETE", f, len(self.__hosts)) @@ -421,11 +428,18 @@

Classes

return await self.__send_request_helper(path, "POST", f, len(self.__hosts)) - async def send_delete_request(self, path: NormalisedURLPath): + async def send_delete_request( + self, path: NormalisedURLPath, params: Union[Dict[str, Any], None] = None + ): + if params is None: + params = {} + async def f(url: str) -> Response: async with AsyncClient() as client: return await client.delete( # type:ignore - url, headers=await self.__get_headers_with_api_version(path) + url, + params=params, + headers=await self.__get_headers_with_api_version(path), ) return await self.__send_request_helper(path, "DELETE", f, len(self.__hosts)) @@ -621,7 +635,7 @@

Methods

-async def send_delete_request(self, path: NormalisedURLPath) +async def send_delete_request(self, path: NormalisedURLPath, params: Union[Dict[str, Any], None] = None)
@@ -629,11 +643,18 @@

Methods

Expand source code -
async def send_delete_request(self, path: NormalisedURLPath):
+
async def send_delete_request(
+    self, path: NormalisedURLPath, params: Union[Dict[str, Any], None] = None
+):
+    if params is None:
+        params = {}
+
     async def f(url: str) -> Response:
         async with AsyncClient() as client:
             return await client.delete(  # type:ignore
-                url, headers=await self.__get_headers_with_api_version(path)
+                url,
+                params=params,
+                headers=await self.__get_headers_with_api_version(path),
             )
 
     return await self.__send_request_helper(path, "DELETE", f, len(self.__hosts))
diff --git a/html/supertokens_python/recipe/dashboard/api/implementation.html b/html/supertokens_python/recipe/dashboard/api/implementation.html index 32ea3f31f..976ed09aa 100644 --- a/html/supertokens_python/recipe/dashboard/api/implementation.html +++ b/html/supertokens_python/recipe/dashboard/api/implementation.html @@ -45,13 +45,12 @@

Module supertokens_python.recipe.dashboard.api.implement from textwrap import dedent from typing import TYPE_CHECKING, Any, Dict -from supertokens_python.normalised_url_domain import NormalisedURLDomain from supertokens_python import Supertokens +from supertokens_python.normalised_url_domain import NormalisedURLDomain from supertokens_python.normalised_url_path import NormalisedURLPath + from ..constants import DASHBOARD_API -from ..interfaces import ( - APIInterface, -) +from ..interfaces import APIInterface if TYPE_CHECKING: from ..interfaces import APIOptions @@ -76,7 +75,7 @@

Module supertokens_python.recipe.dashboard.api.implement connection_uri = "" super_tokens_instance = Supertokens.get_instance() - + auth_mode = options.config.auth_mode connection_uri = super_tokens_instance.supertokens_config.connection_uri dashboard_path = options.app_info.api_base_path.append( @@ -93,6 +92,7 @@

Module supertokens_python.recipe.dashboard.api.implement window.staticBasePath = "${bundleDomain}/static" window.dashboardAppPath = "${dashboardPath}" window.connectionURI = "${connectionURI}" + window.authMode = "${authMode}" </script> <script defer src="${bundleDomain}/static/js/bundle.js"></script></head> <link href="${bundleDomain}/static/css/main.css" rel="stylesheet" type="text/css"> @@ -109,6 +109,7 @@

Module supertokens_python.recipe.dashboard.api.implement bundleDomain=bundle_domain, dashboardPath=dashboard_path, connectionURI=connection_uri, + authMode=auth_mode, ) self.dashboard_get = dashboard_get

@@ -151,7 +152,7 @@

Classes

connection_uri = "" super_tokens_instance = Supertokens.get_instance() - + auth_mode = options.config.auth_mode connection_uri = super_tokens_instance.supertokens_config.connection_uri dashboard_path = options.app_info.api_base_path.append( @@ -168,6 +169,7 @@

Classes

window.staticBasePath = "${bundleDomain}/static" window.dashboardAppPath = "${dashboardPath}" window.connectionURI = "${connectionURI}" + window.authMode = "${authMode}" </script> <script defer src="${bundleDomain}/static/js/bundle.js"></script></head> <link href="${bundleDomain}/static/css/main.css" rel="stylesheet" type="text/css"> @@ -184,6 +186,7 @@

Classes

bundleDomain=bundle_domain, dashboardPath=dashboard_path, connectionURI=connection_uri, + authMode=auth_mode, ) self.dashboard_get = dashboard_get
diff --git a/html/supertokens_python/recipe/dashboard/api/index.html b/html/supertokens_python/recipe/dashboard/api/index.html index 1a7afd55e..a4a8b632c 100644 --- a/html/supertokens_python/recipe/dashboard/api/index.html +++ b/html/supertokens_python/recipe/dashboard/api/index.html @@ -41,6 +41,8 @@

Module supertokens_python.recipe.dashboard.apiModule supertokens_python.recipe.dashboard.api

@@ -87,6 +91,14 @@

Sub-modules

+
supertokens_python.recipe.dashboard.api.signin
+
+
+
+
supertokens_python.recipe.dashboard.api.signout
+
+
+
supertokens_python.recipe.dashboard.api.userdetails
@@ -214,6 +226,74 @@

Functions

return UserEmailVerifyTokenPostAPIOkResponse()
+
+async def handle_emailpassword_signin_api(_: APIInterface, api_options: APIOptions) +
+
+
+
+ +Expand source code + +
async def handle_emailpassword_signin_api(_: APIInterface, api_options: APIOptions):
+    body = await api_options.request.json()
+    if body is None:
+        raise_bad_input_exception("Please send body")
+    email = body.get("email")
+    password = body.get("password")
+
+    if email is None or not isinstance(email, str):
+        raise_bad_input_exception("Missing required parameter 'email'")
+    if password is None or not isinstance(password, str):
+        raise_bad_input_exception("Missing required parameter 'password'")
+    response = await Querier.get_instance().send_post_request(
+        NormalisedURLPath("/recipe/dashboard/signin"),
+        {"email": email, "password": password},
+    )
+
+    if "status" in response and response["status"] == "OK":
+        return send_200_response(
+            {"status": "OK", "sessionId": response["sessionId"]}, api_options.response
+        )
+    if "status" in response and response["status"] == "INVALID_CREDENTIALS_ERROR":
+        return send_200_response(
+            {"status": "INVALID_CREDENTIALS_ERROR"},
+            api_options.response,
+        )
+    if "status" in response and response["status"] == "USER_SUSPENDED_ERROR":
+        return send_200_response(
+            {"status": "USER_SUSPENDED_ERROR", "message": response["message"]},
+            api_options.response,
+        )
+
+
+
+async def handle_emailpassword_signout_api(_: APIInterface, api_options: APIOptions) ‑> SignOutOK +
+
+
+
+ +Expand source code + +
async def handle_emailpassword_signout_api(
+    _: APIInterface, api_options: APIOptions
+) -> SignOutOK:
+    if api_options.config.auth_mode == "api-key":
+        return SignOutOK()
+    session_id_form_auth_header = api_options.request.get_header("authorization")
+    if not session_id_form_auth_header:
+        return raise_bad_input_exception(
+            "Neither 'API Key' nor 'Authorization' header was found"
+        )
+    session_id_form_auth_header = session_id_form_auth_header.split()[1]
+    await Querier.get_instance().send_delete_request(
+        NormalisedURLPath("/recipe/dashboard/session"),
+        {"sessionId": session_id_form_auth_header},
+    )
+    return SignOutOK()
+
+
async def handle_metadata_get(_api_interface: APIInterface, api_options: APIOptions) ‑> Union[UserMetadataGetAPIOkResponseFeatureNotEnabledError]
@@ -824,7 +904,7 @@

Functions

-async def handle_validate_key_api(api_implementation: APIInterface, api_options: APIOptions) +async def handle_validate_key_api(_api_implementation: APIInterface, api_options: APIOptions)
@@ -833,21 +913,14 @@

Functions

Expand source code
async def handle_validate_key_api(
-    api_implementation: APIInterface, api_options: APIOptions
+    _api_implementation: APIInterface, api_options: APIOptions
 ):
-    _ = api_implementation
 
-    should_allow_accesss = await api_options.recipe_implementation.should_allow_access(
-        api_options.request,
-        api_options.config,
-        default_user_context(api_options.request),
-    )
-    if should_allow_accesss is False:
-        return send_non_200_response_with_message(
-            "Unauthorized access", 401, api_options.response
-        )
+    is_valid_key = validate_api_key(api_options.request, api_options.config)
 
-    return send_200_response({"status": "OK"}, api_options.response)
+ if is_valid_key: + return send_200_response({"status": "OK"}, api_options.response) + return send_non_200_response_with_message("Unauthorised", 401, api_options.response)
@@ -870,6 +943,8 @@

Index

  • supertokens_python.recipe.dashboard.api.dashboard
  • supertokens_python.recipe.dashboard.api.implementation
  • +
  • supertokens_python.recipe.dashboard.api.signin
  • +
  • supertokens_python.recipe.dashboard.api.signout
  • supertokens_python.recipe.dashboard.api.userdetails
  • supertokens_python.recipe.dashboard.api.users_count_get
  • supertokens_python.recipe.dashboard.api.users_get
  • @@ -881,6 +956,8 @@

    Index

  • api_key_protector
  • handle_dashboard_api
  • handle_email_verify_token_post
  • +
  • handle_emailpassword_signin_api
  • +
  • handle_emailpassword_signout_api
  • handle_metadata_get
  • handle_metadata_put
  • handle_sessions_get
  • diff --git a/html/supertokens_python/recipe/dashboard/api/signin.html b/html/supertokens_python/recipe/dashboard/api/signin.html new file mode 100644 index 000000000..5885d35a5 --- /dev/null +++ b/html/supertokens_python/recipe/dashboard/api/signin.html @@ -0,0 +1,163 @@ + + + + + + +supertokens_python.recipe.dashboard.api.signin API documentation + + + + + + + + + + + +
    +
    +
    +

    Module supertokens_python.recipe.dashboard.api.signin

    +
    +
    +
    + +Expand source code + +
    # Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved.
    +#
    +# This software is licensed under the Apache License, Version 2.0 (the
    +# "License") as published by the Apache Software Foundation.
    +#
    +# You may not use this file except in compliance with the License. You may
    +# obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
    +#
    +# Unless required by applicable law or agreed to in writing, software
    +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
    +# 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 __future__ import annotations
    +
    +from typing import TYPE_CHECKING
    +
    +if TYPE_CHECKING:
    +    from supertokens_python.recipe.dashboard.interfaces import APIInterface, APIOptions
    +
    +from supertokens_python.exceptions import raise_bad_input_exception
    +from supertokens_python.normalised_url_path import NormalisedURLPath
    +from supertokens_python.querier import Querier
    +from supertokens_python.utils import send_200_response
    +
    +
    +async def handle_emailpassword_signin_api(_: APIInterface, api_options: APIOptions):
    +    body = await api_options.request.json()
    +    if body is None:
    +        raise_bad_input_exception("Please send body")
    +    email = body.get("email")
    +    password = body.get("password")
    +
    +    if email is None or not isinstance(email, str):
    +        raise_bad_input_exception("Missing required parameter 'email'")
    +    if password is None or not isinstance(password, str):
    +        raise_bad_input_exception("Missing required parameter 'password'")
    +    response = await Querier.get_instance().send_post_request(
    +        NormalisedURLPath("/recipe/dashboard/signin"),
    +        {"email": email, "password": password},
    +    )
    +
    +    if "status" in response and response["status"] == "OK":
    +        return send_200_response(
    +            {"status": "OK", "sessionId": response["sessionId"]}, api_options.response
    +        )
    +    if "status" in response and response["status"] == "INVALID_CREDENTIALS_ERROR":
    +        return send_200_response(
    +            {"status": "INVALID_CREDENTIALS_ERROR"},
    +            api_options.response,
    +        )
    +    if "status" in response and response["status"] == "USER_SUSPENDED_ERROR":
    +        return send_200_response(
    +            {"status": "USER_SUSPENDED_ERROR", "message": response["message"]},
    +            api_options.response,
    +        )
    +
    +
    +
    +
    +
    +
    +
    +

    Functions

    +
    +
    +async def handle_emailpassword_signin_api(_: APIInterface, api_options: APIOptions) +
    +
    +
    +
    + +Expand source code + +
    async def handle_emailpassword_signin_api(_: APIInterface, api_options: APIOptions):
    +    body = await api_options.request.json()
    +    if body is None:
    +        raise_bad_input_exception("Please send body")
    +    email = body.get("email")
    +    password = body.get("password")
    +
    +    if email is None or not isinstance(email, str):
    +        raise_bad_input_exception("Missing required parameter 'email'")
    +    if password is None or not isinstance(password, str):
    +        raise_bad_input_exception("Missing required parameter 'password'")
    +    response = await Querier.get_instance().send_post_request(
    +        NormalisedURLPath("/recipe/dashboard/signin"),
    +        {"email": email, "password": password},
    +    )
    +
    +    if "status" in response and response["status"] == "OK":
    +        return send_200_response(
    +            {"status": "OK", "sessionId": response["sessionId"]}, api_options.response
    +        )
    +    if "status" in response and response["status"] == "INVALID_CREDENTIALS_ERROR":
    +        return send_200_response(
    +            {"status": "INVALID_CREDENTIALS_ERROR"},
    +            api_options.response,
    +        )
    +    if "status" in response and response["status"] == "USER_SUSPENDED_ERROR":
    +        return send_200_response(
    +            {"status": "USER_SUSPENDED_ERROR", "message": response["message"]},
    +            api_options.response,
    +        )
    +
    +
    +
    +
    +
    +
    +
    + +
    + + + \ No newline at end of file diff --git a/html/supertokens_python/recipe/dashboard/api/signout.html b/html/supertokens_python/recipe/dashboard/api/signout.html new file mode 100644 index 000000000..424568289 --- /dev/null +++ b/html/supertokens_python/recipe/dashboard/api/signout.html @@ -0,0 +1,136 @@ + + + + + + +supertokens_python.recipe.dashboard.api.signout API documentation + + + + + + + + + + + +
    +
    +
    +

    Module supertokens_python.recipe.dashboard.api.signout

    +
    +
    +
    + +Expand source code + +
    # Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved.
    +#
    +# This software is licensed under the Apache License, Version 2.0 (the
    +# "License") as published by the Apache Software Foundation.
    +#
    +# You may not use this file except in compliance with the License. You may
    +# obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
    +#
    +# Unless required by applicable law or agreed to in writing, software
    +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
    +# 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 __future__ import annotations
    +
    +from typing import TYPE_CHECKING
    +
    +if TYPE_CHECKING:
    +    from supertokens_python.recipe.dashboard.interfaces import APIInterface, APIOptions
    +
    +from supertokens_python.exceptions import raise_bad_input_exception
    +from supertokens_python.normalised_url_path import NormalisedURLPath
    +from supertokens_python.querier import Querier
    +
    +from ..interfaces import SignOutOK
    +
    +
    +async def handle_emailpassword_signout_api(
    +    _: APIInterface, api_options: APIOptions
    +) -> SignOutOK:
    +    if api_options.config.auth_mode == "api-key":
    +        return SignOutOK()
    +    session_id_form_auth_header = api_options.request.get_header("authorization")
    +    if not session_id_form_auth_header:
    +        return raise_bad_input_exception(
    +            "Neither 'API Key' nor 'Authorization' header was found"
    +        )
    +    session_id_form_auth_header = session_id_form_auth_header.split()[1]
    +    await Querier.get_instance().send_delete_request(
    +        NormalisedURLPath("/recipe/dashboard/session"),
    +        {"sessionId": session_id_form_auth_header},
    +    )
    +    return SignOutOK()
    +
    +
    +
    +
    +
    +
    +
    +

    Functions

    +
    +
    +async def handle_emailpassword_signout_api(_: APIInterface, api_options: APIOptions) ‑> SignOutOK +
    +
    +
    +
    + +Expand source code + +
    async def handle_emailpassword_signout_api(
    +    _: APIInterface, api_options: APIOptions
    +) -> SignOutOK:
    +    if api_options.config.auth_mode == "api-key":
    +        return SignOutOK()
    +    session_id_form_auth_header = api_options.request.get_header("authorization")
    +    if not session_id_form_auth_header:
    +        return raise_bad_input_exception(
    +            "Neither 'API Key' nor 'Authorization' header was found"
    +        )
    +    session_id_form_auth_header = session_id_form_auth_header.split()[1]
    +    await Querier.get_instance().send_delete_request(
    +        NormalisedURLPath("/recipe/dashboard/session"),
    +        {"sessionId": session_id_form_auth_header},
    +    )
    +    return SignOutOK()
    +
    +
    +
    +
    +
    +
    +
    + +
    + + + \ No newline at end of file diff --git a/html/supertokens_python/recipe/dashboard/api/validate_key.html b/html/supertokens_python/recipe/dashboard/api/validate_key.html index 938249248..ddd3a74d0 100644 --- a/html/supertokens_python/recipe/dashboard/api/validate_key.html +++ b/html/supertokens_python/recipe/dashboard/api/validate_key.html @@ -50,28 +50,22 @@

    Module supertokens_python.recipe.dashboard.api.validate_ ) from supertokens_python.utils import ( - default_user_context, send_200_response, send_non_200_response_with_message, ) +from ..utils import validate_api_key + async def handle_validate_key_api( - api_implementation: APIInterface, api_options: APIOptions + _api_implementation: APIInterface, api_options: APIOptions ): - _ = api_implementation - should_allow_accesss = await api_options.recipe_implementation.should_allow_access( - api_options.request, - api_options.config, - default_user_context(api_options.request), - ) - if should_allow_accesss is False: - return send_non_200_response_with_message( - "Unauthorized access", 401, api_options.response - ) + is_valid_key = validate_api_key(api_options.request, api_options.config) - return send_200_response({"status": "OK"}, api_options.response) + if is_valid_key: + return send_200_response({"status": "OK"}, api_options.response) + return send_non_200_response_with_message("Unauthorised", 401, api_options.response)
    @@ -82,7 +76,7 @@

    Module supertokens_python.recipe.dashboard.api.validate_

    Functions

    -async def handle_validate_key_api(api_implementation: APIInterface, api_options: APIOptions) +async def handle_validate_key_api(_api_implementation: APIInterface, api_options: APIOptions)
    @@ -91,21 +85,14 @@

    Functions

    Expand source code
    async def handle_validate_key_api(
    -    api_implementation: APIInterface, api_options: APIOptions
    +    _api_implementation: APIInterface, api_options: APIOptions
     ):
    -    _ = api_implementation
     
    -    should_allow_accesss = await api_options.recipe_implementation.should_allow_access(
    -        api_options.request,
    -        api_options.config,
    -        default_user_context(api_options.request),
    -    )
    -    if should_allow_accesss is False:
    -        return send_non_200_response_with_message(
    -            "Unauthorized access", 401, api_options.response
    -        )
    +    is_valid_key = validate_api_key(api_options.request, api_options.config)
     
    -    return send_200_response({"status": "OK"}, api_options.response)
    + if is_valid_key: + return send_200_response({"status": "OK"}, api_options.response) + return send_non_200_response_with_message("Unauthorised", 401, api_options.response)
    diff --git a/html/supertokens_python/recipe/dashboard/constants.html b/html/supertokens_python/recipe/dashboard/constants.html index 2f9063478..911487bbd 100644 --- a/html/supertokens_python/recipe/dashboard/constants.html +++ b/html/supertokens_python/recipe/dashboard/constants.html @@ -35,7 +35,9 @@

    Module supertokens_python.recipe.dashboard.constants +USER_EMAIL_VERIFY_TOKEN_API = "/api/user/email/verify/token" +EMAIL_PASSWORD_SIGN_IN = "/api/signin" +EMAIL_PASSSWORD_SIGNOUT = "/api/signout"

    diff --git a/html/supertokens_python/recipe/dashboard/index.html b/html/supertokens_python/recipe/dashboard/index.html index e2cf500d8..6e5125b18 100644 --- a/html/supertokens_python/recipe/dashboard/index.html +++ b/html/supertokens_python/recipe/dashboard/index.html @@ -42,16 +42,17 @@

    Module supertokens_python.recipe.dashboard

    from __future__ import annotations -from typing import Optional, Callable +from typing import TYPE_CHECKING, Callable, Optional, Union -from supertokens_python import AppInfo, RecipeModule -from supertokens_python.recipe.dashboard.utils import InputOverrideConfig +if TYPE_CHECKING: + from supertokens_python import AppInfo, RecipeModule + from supertokens_python.recipe.dashboard.utils import InputOverrideConfig from .recipe import DashboardRecipe def init( - api_key: str, + api_key: Union[str, None] = None, override: Optional[InputOverrideConfig] = None, ) -> Callable[[AppInfo], RecipeModule]: return DashboardRecipe.init( @@ -99,7 +100,7 @@

    Sub-modules

    Functions

    -def init(api_key: str, override: Optional[InputOverrideConfig] = None) ‑> Callable[[AppInfo], RecipeModule] +def init(api_key: Union[str, None] = None, override: Optional[InputOverrideConfig] = None) ‑> Callable[[AppInfo], RecipeModule]
    @@ -108,7 +109,7 @@

    Functions

    Expand source code
    def init(
    -    api_key: str,
    +    api_key: Union[str, None] = None,
         override: Optional[InputOverrideConfig] = None,
     ) -> Callable[[AppInfo], RecipeModule]:
         return DashboardRecipe.init(
    diff --git a/html/supertokens_python/recipe/dashboard/interfaces.html b/html/supertokens_python/recipe/dashboard/interfaces.html
    index f63e33c83..3ac2c4f86 100644
    --- a/html/supertokens_python/recipe/dashboard/interfaces.html
    +++ b/html/supertokens_python/recipe/dashboard/interfaces.html
    @@ -47,13 +47,14 @@ 

    Module supertokens_python.recipe.dashboard.interfacesModule supertokens_python.recipe.dashboard.interfaces

    + return {"status": self.status, "error": self.error} + + +class SignOutOK(APIResponse): + status: str = "OK" + + def to_json(self): + return {"status": self.status}
    @@ -572,6 +580,51 @@

    Methods

    self.time_created = info.time_created
    +
    +class SignOutOK +
    +
    +

    Helper class that provides a standard way to create an ABC using +inheritance.

    +
    + +Expand source code + +
    class SignOutOK(APIResponse):
    +    status: str = "OK"
    +
    +    def to_json(self):
    +        return {"status": self.status}
    +
    +

    Ancestors

    + +

    Class variables

    +
    +
    var status : str
    +
    +
    +
    +
    +

    Methods

    +
    +
    +def to_json(self) +
    +
    +
    +
    + +Expand source code + +
    def to_json(self):
    +    return {"status": self.status}
    +
    +
    +
    +
    class UserCountGetAPIResponse (count: int) @@ -1571,6 +1624,13 @@

    SessionInfo

  • +

    SignOutOK

    + +
  • +
  • UserCountGetAPIResponse

    • status
    • diff --git a/html/supertokens_python/recipe/dashboard/recipe.html b/html/supertokens_python/recipe/dashboard/recipe.html index e270b34e4..83176e0cf 100644 --- a/html/supertokens_python/recipe/dashboard/recipe.html +++ b/html/supertokens_python/recipe/dashboard/recipe.html @@ -51,6 +51,8 @@

      Module supertokens_python.recipe.dashboard.recipe api_key_protector, handle_dashboard_api, handle_email_verify_token_post, + handle_emailpassword_signin_api, + handle_emailpassword_signout_api, handle_metadata_get, handle_metadata_put, handle_sessions_get, @@ -80,6 +82,8 @@

      Module supertokens_python.recipe.dashboard.recipe from .constants import ( DASHBOARD_API, + EMAIL_PASSSWORD_SIGNOUT, + EMAIL_PASSWORD_SIGN_IN, USER_API, USER_EMAIL_VERIFY_API, USER_EMAIL_VERIFY_TOKEN_API, @@ -106,7 +110,7 @@

      Module supertokens_python.recipe.dashboard.recipe self, recipe_id: str, app_info: AppInfo, - api_key: str, + api_key: Union[str, None], override: Union[InputOverrideConfig, None] = None, ): super().__init__(recipe_id, app_info) @@ -164,6 +168,10 @@

      Module supertokens_python.recipe.dashboard.recipe return await handle_dashboard_api(self.api_implementation, api_options) if request_id == VALIDATE_KEY_API: return await handle_validate_key_api(self.api_implementation, api_options) + if request_id == EMAIL_PASSWORD_SIGN_IN: + return await handle_emailpassword_signin_api( + self.api_implementation, api_options + ) # Do API key validation for the remaining APIs api_function: Optional[ @@ -199,6 +207,8 @@

      Module supertokens_python.recipe.dashboard.recipe api_function = handle_user_password_put elif request_id == USER_EMAIL_VERIFY_TOKEN_API: api_function = handle_email_verify_token_post + elif request_id == EMAIL_PASSSWORD_SIGNOUT: + api_function = handle_emailpassword_signout_api if api_function is not None: return await api_key_protector( @@ -217,7 +227,7 @@

      Module supertokens_python.recipe.dashboard.recipe @staticmethod def init( - api_key: str, + api_key: Union[str, None], override: Union[InputOverrideConfig, None] = None, ): def func(app_info: AppInfo): @@ -279,7 +289,7 @@

      Classes

      class DashboardRecipe -(recipe_id: str, app_info: AppInfo, api_key: str, override: Union[InputOverrideConfig, None] = None) +(recipe_id: str, app_info: AppInfo, api_key: Union[str, None], override: Union[InputOverrideConfig, None] = None)

      Helper class that provides a standard way to create an ABC using @@ -296,7 +306,7 @@

      Classes

      self, recipe_id: str, app_info: AppInfo, - api_key: str, + api_key: Union[str, None], override: Union[InputOverrideConfig, None] = None, ): super().__init__(recipe_id, app_info) @@ -354,6 +364,10 @@

      Classes

      return await handle_dashboard_api(self.api_implementation, api_options) if request_id == VALIDATE_KEY_API: return await handle_validate_key_api(self.api_implementation, api_options) + if request_id == EMAIL_PASSWORD_SIGN_IN: + return await handle_emailpassword_signin_api( + self.api_implementation, api_options + ) # Do API key validation for the remaining APIs api_function: Optional[ @@ -389,6 +403,8 @@

      Classes

      api_function = handle_user_password_put elif request_id == USER_EMAIL_VERIFY_TOKEN_API: api_function = handle_email_verify_token_post + elif request_id == EMAIL_PASSSWORD_SIGNOUT: + api_function = handle_emailpassword_signout_api if api_function is not None: return await api_key_protector( @@ -407,7 +423,7 @@

      Classes

      @staticmethod def init( - api_key: str, + api_key: Union[str, None], override: Union[InputOverrideConfig, None] = None, ): def func(app_info: AppInfo): @@ -490,7 +506,7 @@

      Static methods

      -def init(api_key: str, override: Union[InputOverrideConfig, None] = None) +def init(api_key: Union[str, None], override: Union[InputOverrideConfig, None] = None)
      @@ -500,7 +516,7 @@

      Static methods

      @staticmethod
       def init(
      -    api_key: str,
      +    api_key: Union[str, None],
           override: Union[InputOverrideConfig, None] = None,
       ):
           def func(app_info: AppInfo):
      @@ -604,6 +620,10 @@ 

      Methods

      return await handle_dashboard_api(self.api_implementation, api_options) if request_id == VALIDATE_KEY_API: return await handle_validate_key_api(self.api_implementation, api_options) + if request_id == EMAIL_PASSWORD_SIGN_IN: + return await handle_emailpassword_signin_api( + self.api_implementation, api_options + ) # Do API key validation for the remaining APIs api_function: Optional[ @@ -639,6 +659,8 @@

      Methods

      api_function = handle_user_password_put elif request_id == USER_EMAIL_VERIFY_TOKEN_API: api_function = handle_email_verify_token_post + elif request_id == EMAIL_PASSSWORD_SIGNOUT: + api_function = handle_emailpassword_signout_api if api_function is not None: return await api_key_protector( diff --git a/html/supertokens_python/recipe/dashboard/recipe_implementation.html b/html/supertokens_python/recipe/dashboard/recipe_implementation.html index 9e8731779..e908d695a 100644 --- a/html/supertokens_python/recipe/dashboard/recipe_implementation.html +++ b/html/supertokens_python/recipe/dashboard/recipe_implementation.html @@ -43,12 +43,13 @@

      Module supertokens_python.recipe.dashboard.recipe_implem from typing import Any, Dict -from supertokens_python.framework import BaseRequest -from .interfaces import ( - RecipeInterface, -) from supertokens_python.constants import DASHBOARD_VERSION -from .utils import DashboardConfig +from supertokens_python.framework import BaseRequest +from supertokens_python.normalised_url_path import NormalisedURLPath +from supertokens_python.querier import Querier + +from .interfaces import RecipeInterface +from .utils import DashboardConfig, validate_api_key class RecipeImplementation(RecipeInterface): @@ -61,15 +62,24 @@

      Module supertokens_python.recipe.dashboard.recipe_implem config: DashboardConfig, user_context: Dict[str, Any], ) -> bool: - api_key_header_value = request.get_header("authorization") + if config.auth_mode == "email-password": + auth_header_value = request.get_header("authorization") - # We receive the api key as `Bearer API_KEY`, this retrieves just the key - api_key = api_key_header_value.split(" ")[1] if api_key_header_value else None - - if api_key is None: - return False + if not auth_header_value: + return False - return api_key == config.api_key

      + auth_header_value = auth_header_value.split()[1] + session_verification_response = ( + await Querier.get_instance().send_post_request( + NormalisedURLPath("/recipe/dashboard/session/verify"), + {"sessionId": auth_header_value}, + ) + ) + return ( + "status" in session_verification_response + and session_verification_response["status"] == "OK" + ) + return validate_api_key(request, config)
  • @@ -101,15 +111,24 @@

    Classes

    config: DashboardConfig, user_context: Dict[str, Any], ) -> bool: - api_key_header_value = request.get_header("authorization") + if config.auth_mode == "email-password": + auth_header_value = request.get_header("authorization") - # We receive the api key as `Bearer API_KEY`, this retrieves just the key - api_key = api_key_header_value.split(" ")[1] if api_key_header_value else None + if not auth_header_value: + return False - if api_key is None: - return False - - return api_key == config.api_key
    + auth_header_value = auth_header_value.split()[1] + session_verification_response = ( + await Querier.get_instance().send_post_request( + NormalisedURLPath("/recipe/dashboard/session/verify"), + {"sessionId": auth_header_value}, + ) + ) + return ( + "status" in session_verification_response + and session_verification_response["status"] == "OK" + ) + return validate_api_key(request, config)

    Ancestors

      @@ -146,15 +165,24 @@

      Methods

      config: DashboardConfig, user_context: Dict[str, Any], ) -> bool: - api_key_header_value = request.get_header("authorization") + if config.auth_mode == "email-password": + auth_header_value = request.get_header("authorization") - # We receive the api key as `Bearer API_KEY`, this retrieves just the key - api_key = api_key_header_value.split(" ")[1] if api_key_header_value else None - - if api_key is None: - return False + if not auth_header_value: + return False - return api_key == config.api_key
      + auth_header_value = auth_header_value.split()[1] + session_verification_response = ( + await Querier.get_instance().send_post_request( + NormalisedURLPath("/recipe/dashboard/session/verify"), + {"sessionId": auth_header_value}, + ) + ) + return ( + "status" in session_verification_response + and session_verification_response["status"] == "OK" + ) + return validate_api_key(request, config)
      diff --git a/html/supertokens_python/recipe/dashboard/utils.html b/html/supertokens_python/recipe/dashboard/utils.html index 9c4550b7e..898016852 100644 --- a/html/supertokens_python/recipe/dashboard/utils.html +++ b/html/supertokens_python/recipe/dashboard/utils.html @@ -43,6 +43,10 @@

      Module supertokens_python.recipe.dashboard.utils< from typing import TYPE_CHECKING, Any, Callable, Dict, Optional, Union +if TYPE_CHECKING: + from supertokens_python.framework.request import BaseRequest + from ...supertokens import AppInfo + from supertokens_python.recipe.emailpassword import EmailPasswordRecipe from supertokens_python.recipe.emailpassword.asyncio import ( get_user_by_id as ep_get_user_by_id, @@ -71,9 +75,10 @@

      Module supertokens_python.recipe.dashboard.utils< from supertokens_python.utils import Awaitable from ...normalised_url_path import NormalisedURLPath -from ...supertokens import AppInfo from .constants import ( DASHBOARD_API, + EMAIL_PASSSWORD_SIGNOUT, + EMAIL_PASSWORD_SIGN_IN, USER_API, USER_EMAIL_VERIFY_API, USER_EMAIL_VERIFY_TOKEN_API, @@ -190,21 +195,18 @@

      Module supertokens_python.recipe.dashboard.utils< class DashboardConfig: def __init__( - self, - api_key: str, - override: OverrideConfig, + self, api_key: Union[str, None], override: OverrideConfig, auth_mode: str ): self.api_key = api_key self.override = override + self.auth_mode = auth_mode def validate_and_normalise_user_input( # app_info: AppInfo, - api_key: str, + api_key: Union[str, None], override: Optional[InputOverrideConfig] = None, ) -> DashboardConfig: - if api_key.strip() == "": - raise Exception("apiKey provided to Dashboard recipe cannot be empty") if override is None: override = InputOverrideConfig() @@ -215,6 +217,7 @@

      Module supertokens_python.recipe.dashboard.utils< functions=override.functions, apis=override.apis, ), + "api-key" if api_key else "email-password", ) @@ -258,6 +261,10 @@

      Module supertokens_python.recipe.dashboard.utils< return USER_PASSWORD_API if path_str.endswith(USER_EMAIL_VERIFY_TOKEN_API) and method == "post": return USER_EMAIL_VERIFY_TOKEN_API + if path_str.endswith(EMAIL_PASSWORD_SIGN_IN) and method == "post": + return EMAIL_PASSWORD_SIGN_IN + if path_str.endswith(EMAIL_PASSSWORD_SIGNOUT) and method == "post": + return EMAIL_PASSSWORD_SIGNOUT return None @@ -412,7 +419,16 @@

      Module supertokens_python.recipe.dashboard.utils< except Exception: pass - return isRecipeInitialised + return isRecipeInitialised + + +def validate_api_key(req: BaseRequest, config: DashboardConfig) -> bool: + api_key_header_value = req.get_header("authorization") + if not api_key_header_value: + return False + # We receieve the api key as `Bearer API_KEY`, this retrieves just the key + api_key_header_value = api_key_header_value.split(" ")[1] + return api_key_header_value == config.api_key

    @@ -452,6 +468,10 @@

    Functions

    return USER_PASSWORD_API if path_str.endswith(USER_EMAIL_VERIFY_TOKEN_API) and method == "post": return USER_EMAIL_VERIFY_TOKEN_API + if path_str.endswith(EMAIL_PASSWORD_SIGN_IN) and method == "post": + return EMAIL_PASSWORD_SIGN_IN + if path_str.endswith(EMAIL_PASSSWORD_SIGNOUT) and method == "post": + return EMAIL_PASSSWORD_SIGNOUT return None @@ -638,7 +658,7 @@

    Functions

    -def validate_and_normalise_user_input(api_key: str, override: Optional[InputOverrideConfig] = None) ‑> DashboardConfig +def validate_and_normalise_user_input(api_key: Union[str, None], override: Optional[InputOverrideConfig] = None) ‑> DashboardConfig
    @@ -648,11 +668,9 @@

    Functions

    def validate_and_normalise_user_input(
         # app_info: AppInfo,
    -    api_key: str,
    +    api_key: Union[str, None],
         override: Optional[InputOverrideConfig] = None,
     ) -> DashboardConfig:
    -    if api_key.strip() == "":
    -        raise Exception("apiKey provided to Dashboard recipe cannot be empty")
     
         if override is None:
             override = InputOverrideConfig()
    @@ -663,9 +681,28 @@ 

    Functions

    functions=override.functions, apis=override.apis, ), + "api-key" if api_key else "email-password", )
    +
    +def validate_api_key(req: BaseRequest, config: DashboardConfig) ‑> bool +
    +
    +
    +
    + +Expand source code + +
    def validate_api_key(req: BaseRequest, config: DashboardConfig) -> bool:
    +    api_key_header_value = req.get_header("authorization")
    +    if not api_key_header_value:
    +        return False
    +    # We receieve the api key as `Bearer API_KEY`, this retrieves just the key
    +    api_key_header_value = api_key_header_value.split(" ")[1]
    +    return api_key_header_value == config.api_key
    +
    +
    @@ -673,7 +710,7 @@

    Classes

    class DashboardConfig -(api_key: str, override: OverrideConfig) +(api_key: Union[str, None], override: OverrideConfig, auth_mode: str)
    @@ -683,12 +720,11 @@

    Classes

    class DashboardConfig:
         def __init__(
    -        self,
    -        api_key: str,
    -        override: OverrideConfig,
    +        self, api_key: Union[str, None], override: OverrideConfig, auth_mode: str
         ):
             self.api_key = api_key
    -        self.override = override
    + self.override = override + self.auth_mode = auth_mode
    @@ -993,6 +1029,7 @@

    Index

  • is_recipe_initialised
  • is_valid_recipe_id
  • validate_and_normalise_user_input
  • +
  • validate_api_key
  • Classes

    diff --git a/html/supertokens_python/recipe/emailverification/interfaces.html b/html/supertokens_python/recipe/emailverification/interfaces.html index 134299c96..4a55f3906 100644 --- a/html/supertokens_python/recipe/emailverification/interfaces.html +++ b/html/supertokens_python/recipe/emailverification/interfaces.html @@ -42,12 +42,13 @@

    Module supertokens_python.recipe.emailverification.inter from __future__ import annotations from abc import ABC, abstractmethod -from typing import TYPE_CHECKING, Any, Dict, Union, Callable, Awaitable, Optional +from typing import TYPE_CHECKING, Any, Awaitable, Callable, Dict, Optional, Union from supertokens_python.ingredients.emaildelivery import EmailDeliveryIngredient from supertokens_python.types import APIResponse, GeneralErrorResponse -from ..session.interfaces import SessionContainer + from ...supertokens import AppInfo +from ..session.interfaces import SessionContainer if TYPE_CHECKING: from supertokens_python.framework import BaseRequest, BaseResponse diff --git a/html/supertokens_python/recipe/passwordless/interfaces.html b/html/supertokens_python/recipe/passwordless/interfaces.html index fcfbc4e5e..cad5bdbed 100644 --- a/html/supertokens_python/recipe/passwordless/interfaces.html +++ b/html/supertokens_python/recipe/passwordless/interfaces.html @@ -44,22 +44,24 @@

    Module supertokens_python.recipe.passwordless.interfaces from abc import ABC, abstractmethod from typing import Any, Dict, List, Union +from typing_extensions import Literal + from supertokens_python.framework import BaseRequest, BaseResponse from supertokens_python.ingredients.emaildelivery import EmailDeliveryIngredient from supertokens_python.recipe.session import SessionContainer from supertokens_python.types import APIResponse, GeneralErrorResponse -from typing_extensions import Literal + +from ...supertokens import AppInfo # if TYPE_CHECKING: from .types import ( DeviceType, - SMSDeliveryIngredient, PasswordlessLoginEmailTemplateVars, PasswordlessLoginSMSTemplateVars, + SMSDeliveryIngredient, User, ) from .utils import PasswordlessConfig -from ...supertokens import AppInfo class CreateCodeOkResult: diff --git a/html/supertokens_python/types.html b/html/supertokens_python/types.html index 01c386aac..e57adec62 100644 --- a/html/supertokens_python/types.html +++ b/html/supertokens_python/types.html @@ -139,6 +139,7 @@

    Subclasses

    • DashboardUsersGetResponse
    • FeatureNotEnabledError
    • +
    • SignOutOK
    • UserCountGetAPIResponse
    • UserDeleteAPIResponse
    • UserEmailVerifyGetAPIResponse
    • From d73d3030d095ca8e3e0038c4c52c6eea70c600f9 Mon Sep 17 00:00:00 2001 From: Iresh Sharma Date: Wed, 8 Mar 2023 14:10:34 +0530 Subject: [PATCH 034/192] broken tests fix for dashboard email password PR --- supertokens_python/recipe/dashboard/__init__.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/supertokens_python/recipe/dashboard/__init__.py b/supertokens_python/recipe/dashboard/__init__.py index 8476d9d7a..5b4ec8880 100644 --- a/supertokens_python/recipe/dashboard/__init__.py +++ b/supertokens_python/recipe/dashboard/__init__.py @@ -14,11 +14,10 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Callable, Optional, Union +from typing import Callable, Optional, Union -if TYPE_CHECKING: - from supertokens_python import AppInfo, RecipeModule - from supertokens_python.recipe.dashboard.utils import InputOverrideConfig +from supertokens_python import AppInfo, RecipeModule +from supertokens_python.recipe.dashboard.utils import InputOverrideConfig from .recipe import DashboardRecipe From eebbc0f1e3ec8095b6e1afdffe7a9881441a2cf8 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Wed, 8 Mar 2023 14:28:04 +0530 Subject: [PATCH 035/192] adding dev-v0.12.3 tag to this commit to ensure building --- html/supertokens_python/recipe/dashboard/index.html | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/html/supertokens_python/recipe/dashboard/index.html b/html/supertokens_python/recipe/dashboard/index.html index 6e5125b18..05055bf91 100644 --- a/html/supertokens_python/recipe/dashboard/index.html +++ b/html/supertokens_python/recipe/dashboard/index.html @@ -42,11 +42,10 @@

      Module supertokens_python.recipe.dashboard

      from __future__ import annotations -from typing import TYPE_CHECKING, Callable, Optional, Union +from typing import Callable, Optional, Union -if TYPE_CHECKING: - from supertokens_python import AppInfo, RecipeModule - from supertokens_python.recipe.dashboard.utils import InputOverrideConfig +from supertokens_python import AppInfo, RecipeModule +from supertokens_python.recipe.dashboard.utils import InputOverrideConfig from .recipe import DashboardRecipe @@ -100,7 +99,7 @@

      Sub-modules

      Functions

      -def init(api_key: Union[str, None] = None, override: Optional[InputOverrideConfig] = None) ‑> Callable[[AppInfo], RecipeModule] +def init(api_key: Union[str, None] = None, override: Optional[InputOverrideConfig] = None) ‑> Callable[[AppInfo], RecipeModule]
      From 52b82e079800f31194253d585fb9a858b8aa7bac Mon Sep 17 00:00:00 2001 From: Iresh Sharma <32684272+iresharma@users.noreply.github.com> Date: Wed, 22 Mar 2023 13:07:19 +0530 Subject: [PATCH 036/192] search apis for dashboard --- .../framework/django/django_request.py | 5 +++- .../framework/fastapi/fastapi_request.py | 5 +++- .../framework/flask/flask_request.py | 3 ++ supertokens_python/framework/request.py | 4 +++ .../recipe/dashboard/api/__init__.py | 2 ++ .../recipe/dashboard/api/search/__init__.py | 0 .../recipe/dashboard/api/search/getTags.py | 30 +++++++++++++++++++ .../recipe/dashboard/api/users_get.py | 1 + .../recipe/dashboard/constants.py | 1 + supertokens_python/recipe/dashboard/recipe.py | 4 +++ supertokens_python/recipe/dashboard/utils.py | 3 ++ supertokens_python/supertokens.py | 6 +++- 12 files changed, 61 insertions(+), 3 deletions(-) create mode 100644 supertokens_python/recipe/dashboard/api/search/__init__.py create mode 100644 supertokens_python/recipe/dashboard/api/search/getTags.py diff --git a/supertokens_python/framework/django/django_request.py b/supertokens_python/framework/django/django_request.py index abd9ed217..de8275bea 100644 --- a/supertokens_python/framework/django/django_request.py +++ b/supertokens_python/framework/django/django_request.py @@ -14,7 +14,7 @@ from __future__ import annotations import json -from typing import TYPE_CHECKING, Any, Union +from typing import TYPE_CHECKING, Any, Dict, Union from urllib.parse import parse_qsl from supertokens_python.framework.request import BaseRequest @@ -35,6 +35,9 @@ def get_query_param( ) -> Union[str, None]: return self.request.GET.get(key, default) + def get_query_params(self) -> Dict[str, Any]: + return self.request.GET.dict() + async def json(self) -> Union[Any, None]: try: body = json.loads(self.request.body) diff --git a/supertokens_python/framework/fastapi/fastapi_request.py b/supertokens_python/framework/fastapi/fastapi_request.py index 5d27c66b0..27da571dd 100644 --- a/supertokens_python/framework/fastapi/fastapi_request.py +++ b/supertokens_python/framework/fastapi/fastapi_request.py @@ -13,7 +13,7 @@ # under the License. from __future__ import annotations -from typing import TYPE_CHECKING, Any, Union +from typing import TYPE_CHECKING, Any, Dict, Union from urllib.parse import parse_qsl from supertokens_python.framework.request import BaseRequest @@ -35,6 +35,9 @@ def get_query_param( ) -> Union[str, None]: return self.request.query_params.get(key, default) + def get_query_params(self) -> Dict[str, Any]: + return dict(self.request.query_params.items()) # type: ignore + async def json(self) -> Union[Any, None]: try: return await self.request.json() diff --git a/supertokens_python/framework/flask/flask_request.py b/supertokens_python/framework/flask/flask_request.py index 1c010ab00..72f917b00 100644 --- a/supertokens_python/framework/flask/flask_request.py +++ b/supertokens_python/framework/flask/flask_request.py @@ -31,6 +31,9 @@ def __init__(self, req: Request): def get_query_param(self, key: str, default: Union[str, None] = None): return self.request.args.get(key, default) + def get_query_params(self) -> Dict[str, Any]: + return self.request.args.to_dict() + async def json(self) -> Union[Any, None]: try: return self.request.get_json() diff --git a/supertokens_python/framework/request.py b/supertokens_python/framework/request.py index 9a9ef50fb..fc8716f41 100644 --- a/supertokens_python/framework/request.py +++ b/supertokens_python/framework/request.py @@ -31,6 +31,10 @@ def get_query_param( ) -> Union[str, None]: pass + @abstractmethod + def get_query_params(self) -> Dict[str, Any]: + pass + @abstractmethod async def json(self) -> Union[Any, None]: pass diff --git a/supertokens_python/recipe/dashboard/api/__init__.py b/supertokens_python/recipe/dashboard/api/__init__.py index fb9e8c6ef..a06f5fac3 100644 --- a/supertokens_python/recipe/dashboard/api/__init__.py +++ b/supertokens_python/recipe/dashboard/api/__init__.py @@ -13,6 +13,7 @@ # under the License. from .api_key_protector import api_key_protector from .dashboard import handle_dashboard_api +from .search.getTags import handle_get_tags from .signin import handle_emailpassword_signin_api from .signout import handle_emailpassword_signout_api from .userdetails.user_delete import handle_user_delete @@ -49,4 +50,5 @@ "handle_email_verify_token_post", "handle_emailpassword_signin_api", "handle_emailpassword_signout_api", + "handle_get_tags", ] diff --git a/supertokens_python/recipe/dashboard/api/search/__init__.py b/supertokens_python/recipe/dashboard/api/search/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/supertokens_python/recipe/dashboard/api/search/getTags.py b/supertokens_python/recipe/dashboard/api/search/getTags.py new file mode 100644 index 000000000..7b54ff447 --- /dev/null +++ b/supertokens_python/recipe/dashboard/api/search/getTags.py @@ -0,0 +1,30 @@ +# Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. +# +# This software is licensed under the Apache License, Version 2.0 (the +# "License") as published by the Apache Software Foundation. +# +# You may not use this file except in compliance with the License. You may +# obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# 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 __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from supertokens_python.recipe.dashboard.interfaces import APIInterface, APIOptions + from supertokens_python.types import APIResponse + +from supertokens_python.normalised_url_path import NormalisedURLPath +from supertokens_python.querier import Querier + + +async def handle_get_tags(_: APIInterface, __: APIOptions) -> APIResponse: + response = await Querier.get_instance().send_get_request( + NormalisedURLPath("/user/search/tags") + ) + return response diff --git a/supertokens_python/recipe/dashboard/api/users_get.py b/supertokens_python/recipe/dashboard/api/users_get.py index 021724ee9..72a4ecc0d 100644 --- a/supertokens_python/recipe/dashboard/api/users_get.py +++ b/supertokens_python/recipe/dashboard/api/users_get.py @@ -55,6 +55,7 @@ async def handle_users_get_api( time_joined_order=time_joined_order, # type: ignore pagination_token=pagination_token, include_recipe_ids=None, + query=api_options.request.get_query_params(), ) # user metadata bulk fetch with batches: diff --git a/supertokens_python/recipe/dashboard/constants.py b/supertokens_python/recipe/dashboard/constants.py index 9505bf658..ee2385ad4 100644 --- a/supertokens_python/recipe/dashboard/constants.py +++ b/supertokens_python/recipe/dashboard/constants.py @@ -10,3 +10,4 @@ USER_EMAIL_VERIFY_TOKEN_API = "/api/user/email/verify/token" EMAIL_PASSWORD_SIGN_IN = "/api/signin" EMAIL_PASSSWORD_SIGNOUT = "/api/signout" +SEARCH_TAGS_API = "/api/search/tags" diff --git a/supertokens_python/recipe/dashboard/recipe.py b/supertokens_python/recipe/dashboard/recipe.py index 76ff3fb5b..aaa7410bb 100644 --- a/supertokens_python/recipe/dashboard/recipe.py +++ b/supertokens_python/recipe/dashboard/recipe.py @@ -25,6 +25,7 @@ handle_email_verify_token_post, handle_emailpassword_signin_api, handle_emailpassword_signout_api, + handle_get_tags, handle_metadata_get, handle_metadata_put, handle_sessions_get, @@ -56,6 +57,7 @@ DASHBOARD_API, EMAIL_PASSSWORD_SIGNOUT, EMAIL_PASSWORD_SIGN_IN, + SEARCH_TAGS_API, USER_API, USER_EMAIL_VERIFY_API, USER_EMAIL_VERIFY_TOKEN_API, @@ -181,6 +183,8 @@ async def handle_api_request( api_function = handle_email_verify_token_post elif request_id == EMAIL_PASSSWORD_SIGNOUT: api_function = handle_emailpassword_signout_api + elif request_id == SEARCH_TAGS_API: + api_function = handle_get_tags if api_function is not None: return await api_key_protector( diff --git a/supertokens_python/recipe/dashboard/utils.py b/supertokens_python/recipe/dashboard/utils.py index 2a2cdb523..2513d2fcd 100644 --- a/supertokens_python/recipe/dashboard/utils.py +++ b/supertokens_python/recipe/dashboard/utils.py @@ -51,6 +51,7 @@ DASHBOARD_API, EMAIL_PASSSWORD_SIGNOUT, EMAIL_PASSWORD_SIGN_IN, + SEARCH_TAGS_API, USER_API, USER_EMAIL_VERIFY_API, USER_EMAIL_VERIFY_TOKEN_API, @@ -237,6 +238,8 @@ def get_api_if_matched(path: NormalisedURLPath, method: str) -> Optional[str]: return EMAIL_PASSWORD_SIGN_IN if path_str.endswith(EMAIL_PASSSWORD_SIGNOUT) and method == "post": return EMAIL_PASSSWORD_SIGNOUT + if path_str.endswith(SEARCH_TAGS_API) and method == "post": + return SEARCH_TAGS_API return None diff --git a/supertokens_python/supertokens.py b/supertokens_python/supertokens.py index 8b9791d4a..8fc63533f 100644 --- a/supertokens_python/supertokens.py +++ b/supertokens_python/supertokens.py @@ -49,10 +49,10 @@ from .utils import ( execute_async, get_rid_from_header, + get_top_level_domain_for_same_site_resolution, is_version_gte, normalise_http_method, send_non_200_response_with_message, - get_top_level_domain_for_same_site_resolution, ) if TYPE_CHECKING: @@ -328,6 +328,7 @@ async def get_users( # pylint: disable=no-self-use limit: Union[int, None], pagination_token: Union[str, None], include_recipe_ids: Union[None, List[str]], + query: Union[Dict[str, str], None] = None, ) -> UsersResponse: querier = Querier.get_instance(None) params = {"timeJoinedOrder": time_joined_order} @@ -341,6 +342,9 @@ async def get_users( # pylint: disable=no-self-use include_recipe_ids_str = ",".join(include_recipe_ids) params = {"includeRecipeIds": include_recipe_ids_str, **params} + if query is not None: + params = {**params, **query} + response = await querier.send_get_request(NormalisedURLPath(USERS), params) next_pagination_token = None if "nextPaginationToken" in response: From 21400fcd4e36d863757412092a0005611575b673 Mon Sep 17 00:00:00 2001 From: Iresh Sharma <32684272+iresharma@users.noreply.github.com> Date: Wed, 22 Mar 2023 13:19:36 +0530 Subject: [PATCH 037/192] typo in route decleration fixed --- supertokens_python/recipe/dashboard/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/supertokens_python/recipe/dashboard/utils.py b/supertokens_python/recipe/dashboard/utils.py index 2513d2fcd..24b75a670 100644 --- a/supertokens_python/recipe/dashboard/utils.py +++ b/supertokens_python/recipe/dashboard/utils.py @@ -238,7 +238,7 @@ def get_api_if_matched(path: NormalisedURLPath, method: str) -> Optional[str]: return EMAIL_PASSWORD_SIGN_IN if path_str.endswith(EMAIL_PASSSWORD_SIGNOUT) and method == "post": return EMAIL_PASSSWORD_SIGNOUT - if path_str.endswith(SEARCH_TAGS_API) and method == "post": + if path_str.endswith(SEARCH_TAGS_API) and method == "get": return SEARCH_TAGS_API return None From 0597dabb259e254ebbe65cff7af0476831bb9007 Mon Sep 17 00:00:00 2001 From: Iresh Sharma <32684272+iresharma@users.noreply.github.com> Date: Wed, 22 Mar 2023 13:51:15 +0530 Subject: [PATCH 038/192] tested and works --- .../recipe/dashboard/api/search/getTags.py | 6 +++--- supertokens_python/recipe/dashboard/interfaces.py | 11 +++++++++++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/supertokens_python/recipe/dashboard/api/search/getTags.py b/supertokens_python/recipe/dashboard/api/search/getTags.py index 7b54ff447..b22b74693 100644 --- a/supertokens_python/recipe/dashboard/api/search/getTags.py +++ b/supertokens_python/recipe/dashboard/api/search/getTags.py @@ -17,14 +17,14 @@ if TYPE_CHECKING: from supertokens_python.recipe.dashboard.interfaces import APIInterface, APIOptions - from supertokens_python.types import APIResponse from supertokens_python.normalised_url_path import NormalisedURLPath from supertokens_python.querier import Querier +from supertokens_python.recipe.dashboard.interfaces import SearchTagsOK -async def handle_get_tags(_: APIInterface, __: APIOptions) -> APIResponse: +async def handle_get_tags(_: APIInterface, __: APIOptions) -> SearchTagsOK: response = await Querier.get_instance().send_get_request( NormalisedURLPath("/user/search/tags") ) - return response + return SearchTagsOK(tags=response["tags"]) diff --git a/supertokens_python/recipe/dashboard/interfaces.py b/supertokens_python/recipe/dashboard/interfaces.py index 3a5353876..db3aafc05 100644 --- a/supertokens_python/recipe/dashboard/interfaces.py +++ b/supertokens_python/recipe/dashboard/interfaces.py @@ -293,3 +293,14 @@ class SignOutOK(APIResponse): def to_json(self): return {"status": self.status} + + +class SearchTagsOK(APIResponse): + status: str = "OK" + tags: List[str] + + def __init__(self, tags: List[str]) -> None: + self.tags = tags + + def to_json(self): + return {"status": self.status, "tags": self.tags} From 61267ddd1aa1919d5eb4f4f9287f9a946222d6ac Mon Sep 17 00:00:00 2001 From: Nemi Shah Date: Thu, 23 Mar 2023 16:30:28 +0530 Subject: [PATCH 039/192] Add dashboard init to all example apps --- .../with-thirdpartyemailpassword/project/settings.py | 11 +++++++---- .../with-fastapi/with-thirdpartyemailpassword/main.py | 5 ++++- .../with-flask/with-thirdpartyemailpassword/app.py | 2 ++ 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/examples/with-django/with-thirdpartyemailpassword/project/settings.py b/examples/with-django/with-thirdpartyemailpassword/project/settings.py index df4d739a3..b83d49897 100644 --- a/examples/with-django/with-thirdpartyemailpassword/project/settings.py +++ b/examples/with-django/with-thirdpartyemailpassword/project/settings.py @@ -10,12 +10,13 @@ https://docs.djangoproject.com/en/4.0/ref/settings/ """ -from pathlib import Path import os - -from dotenv import load_dotenv +from pathlib import Path from typing import List + from corsheaders.defaults import default_headers +from dotenv import load_dotenv + from supertokens_python import ( InputAppInfo, SupertokensConfig, @@ -23,9 +24,10 @@ init, ) from supertokens_python.recipe import ( + dashboard, + emailverification, session, thirdpartyemailpassword, - emailverification, ) from supertokens_python.recipe.thirdpartyemailpassword import ( Apple, @@ -66,6 +68,7 @@ def get_website_domain(): mode="wsgi", recipe_list=[ session.init(), + dashboard.init(), emailverification.init("REQUIRED"), thirdpartyemailpassword.init( providers=[ diff --git a/examples/with-fastapi/with-thirdpartyemailpassword/main.py b/examples/with-fastapi/with-thirdpartyemailpassword/main.py index d3c9d3e8e..367fdb567 100644 --- a/examples/with-fastapi/with-thirdpartyemailpassword/main.py +++ b/examples/with-fastapi/with-thirdpartyemailpassword/main.py @@ -6,6 +6,7 @@ from fastapi.responses import JSONResponse, PlainTextResponse from starlette.exceptions import ExceptionMiddleware from starlette.middleware.cors import CORSMiddleware + from supertokens_python import ( InputAppInfo, SupertokensConfig, @@ -14,9 +15,10 @@ ) from supertokens_python.framework.fastapi import get_middleware from supertokens_python.recipe import ( + dashboard, + emailverification, session, thirdpartyemailpassword, - emailverification, ) from supertokens_python.recipe.session import SessionContainer from supertokens_python.recipe.session.framework.fastapi import verify_session @@ -56,6 +58,7 @@ def get_website_domain(): framework="fastapi", recipe_list=[ session.init(), + dashboard.init(), emailverification.init("REQUIRED"), thirdpartyemailpassword.init( providers=[ diff --git a/examples/with-flask/with-thirdpartyemailpassword/app.py b/examples/with-flask/with-thirdpartyemailpassword/app.py index e54f4c964..e035db3ca 100644 --- a/examples/with-flask/with-thirdpartyemailpassword/app.py +++ b/examples/with-flask/with-thirdpartyemailpassword/app.py @@ -12,6 +12,7 @@ ) from supertokens_python.framework.flask import Middleware from supertokens_python.recipe import ( + dashboard, emailverification, session, thirdpartyemailpassword, @@ -50,6 +51,7 @@ def get_website_domain(): framework="flask", recipe_list=[ session.init(), + dashboard.init(), emailverification.init("REQUIRED"), thirdpartyemailpassword.init( providers=[ From 1d0d87f3589c5eba5b2353fd38da5a8fdb161427 Mon Sep 17 00:00:00 2001 From: Nemi Shah Date: Thu, 23 Mar 2023 16:33:06 +0530 Subject: [PATCH 040/192] Update CHANGELOG --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 936c753da..2a599a3ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## unreleased +### Changed + +- Update all example apps to initialise dashboard recipe + ## [0.12.3] - 2023-02-27 - Adds APIs and logic to the dashboard recipe to enable email password based login ## [0.12.2] - 2023-02-23 From 81046dae07c586a5def1dbbd7d3cc4cd7aa0b97c Mon Sep 17 00:00:00 2001 From: Iresh Sharma <32684272+iresharma@users.noreply.github.com> Date: Mon, 27 Mar 2023 16:02:00 +0530 Subject: [PATCH 041/192] analytics api added --- supertokens_python/constants.py | 2 +- .../recipe/dashboard/api/__init__.py | 2 + .../recipe/dashboard/api/analytics.py | 99 +++++++++++++++++++ .../recipe/dashboard/constants.py | 1 + .../recipe/dashboard/interfaces.py | 7 ++ supertokens_python/recipe/dashboard/recipe.py | 5 + supertokens_python/recipe/dashboard/utils.py | 3 + supertokens_python/supertokens.py | 70 ++----------- 8 files changed, 125 insertions(+), 64 deletions(-) create mode 100644 supertokens_python/recipe/dashboard/api/analytics.py diff --git a/supertokens_python/constants.py b/supertokens_python/constants.py index 5f33750b4..7841e7061 100644 --- a/supertokens_python/constants.py +++ b/supertokens_python/constants.py @@ -29,7 +29,7 @@ USER_DELETE = "/user/remove" USERS = "/users" TELEMETRY_SUPERTOKENS_API_URL = "https://api.supertokens.com/0/st/telemetry" -TELEMETRY_SUPERTOKENS_API_VERSION = "2" +TELEMETRY_SUPERTOKENS_API_VERSION = "3" ERROR_MESSAGE_KEY = "message" API_KEY_HEADER = "api-key" RID_KEY_HEADER = "rid" diff --git a/supertokens_python/recipe/dashboard/api/__init__.py b/supertokens_python/recipe/dashboard/api/__init__.py index fb9e8c6ef..02d575427 100644 --- a/supertokens_python/recipe/dashboard/api/__init__.py +++ b/supertokens_python/recipe/dashboard/api/__init__.py @@ -11,6 +11,7 @@ # 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 .analytics import handle_analytics_post from .api_key_protector import api_key_protector from .dashboard import handle_dashboard_api from .signin import handle_emailpassword_signin_api @@ -49,4 +50,5 @@ "handle_email_verify_token_post", "handle_emailpassword_signin_api", "handle_emailpassword_signout_api", + "handle_analytics_post", ] diff --git a/supertokens_python/recipe/dashboard/api/analytics.py b/supertokens_python/recipe/dashboard/api/analytics.py new file mode 100644 index 000000000..3a62d36bc --- /dev/null +++ b/supertokens_python/recipe/dashboard/api/analytics.py @@ -0,0 +1,99 @@ +# Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. +# +# This software is licensed under the Apache License, Version 2.0 (the +# "License") as published by the Apache Software Foundation. +# +# You may not use this file except in compliance with the License. You may +# obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# 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 __future__ import annotations + +from typing import TYPE_CHECKING + +from httpx import AsyncClient + +from supertokens_python import Supertokens +from supertokens_python.constants import ( + TELEMETRY_SUPERTOKENS_API_URL, + TELEMETRY_SUPERTOKENS_API_VERSION, +) +from supertokens_python.constants import VERSION as SDKVersion +from supertokens_python.exceptions import raise_bad_input_exception +from supertokens_python.normalised_url_path import NormalisedURLPath +from supertokens_python.querier import Querier + +from ..interfaces import AnalyticsResponse + +if TYPE_CHECKING: + from supertokens_python.recipe.dashboard.interfaces import APIInterface, APIOptions + + +async def handle_analytics_post( + _: APIInterface, api_options: APIOptions +) -> AnalyticsResponse: + if not Supertokens.get_instance().telemetry: + return AnalyticsResponse() + body = await api_options.request.json() + if body is None: + raise_bad_input_exception("Please send body") + email = body.get("email") + dashboard_version = body.get("dashboardVersion") + + if email is None: + raise_bad_input_exception("Missing required property 'email'") + if dashboard_version is None: + raise_bad_input_exception("Missing required property 'dashboardVersion'") + + telemetry_id = None + + try: + response = await Querier.get_instance().send_get_request( + NormalisedURLPath("/telemetry") + ) + if response is not None: + telemetry_id = response["telemetryId"] + + number_of_users = await Supertokens.get_instance().get_user_count( + include_recipe_ids=None + ) + + except Exception as __: + # If either telemetry id API or user count fetch fails, no event should be sent + return AnalyticsResponse() + + apiDomain, websiteDomain, appName = ( + api_options.app_info.api_domain, + api_options.app_info.website_domain, + api_options.app_info.app_name, + ) + + data = { + "websiteDomain": websiteDomain, + "apiDomain": apiDomain, + "appName": appName, + "sdk": "supertokens-python", + "sdkVersion": SDKVersion, + "telemetryId": telemetry_id, + "numberOfUsers": number_of_users, + "email": email, + "dashboardVersion": dashboard_version, + } + + try: + async with AsyncClient() as client: + await client.post( # type: ignore + url=TELEMETRY_SUPERTOKENS_API_URL, + json=data, + headers={"api-version": TELEMETRY_SUPERTOKENS_API_VERSION}, + ) + except Exception as __: + # If telemetry event fails, no error should be thrown + pass + + return AnalyticsResponse() diff --git a/supertokens_python/recipe/dashboard/constants.py b/supertokens_python/recipe/dashboard/constants.py index 9505bf658..fc481e35a 100644 --- a/supertokens_python/recipe/dashboard/constants.py +++ b/supertokens_python/recipe/dashboard/constants.py @@ -10,3 +10,4 @@ USER_EMAIL_VERIFY_TOKEN_API = "/api/user/email/verify/token" EMAIL_PASSWORD_SIGN_IN = "/api/signin" EMAIL_PASSSWORD_SIGNOUT = "/api/signout" +DASHBOARD_ANALYTICS_API = "/api/analytics" diff --git a/supertokens_python/recipe/dashboard/interfaces.py b/supertokens_python/recipe/dashboard/interfaces.py index 3a5353876..83ccabd1c 100644 --- a/supertokens_python/recipe/dashboard/interfaces.py +++ b/supertokens_python/recipe/dashboard/interfaces.py @@ -293,3 +293,10 @@ class SignOutOK(APIResponse): def to_json(self): return {"status": self.status} + + +class AnalyticsResponse(APIResponse): + status: str = "OK" + + def to_json(self) -> Dict[str, Any]: + return {"status": self.status} diff --git a/supertokens_python/recipe/dashboard/recipe.py b/supertokens_python/recipe/dashboard/recipe.py index 76ff3fb5b..f52f6391b 100644 --- a/supertokens_python/recipe/dashboard/recipe.py +++ b/supertokens_python/recipe/dashboard/recipe.py @@ -21,6 +21,7 @@ from .api import ( api_key_protector, + handle_analytics_post, handle_dashboard_api, handle_email_verify_token_post, handle_emailpassword_signin_api, @@ -53,6 +54,7 @@ from supertokens_python.exceptions import SuperTokensError, raise_general_exception from .constants import ( + DASHBOARD_ANALYTICS_API, DASHBOARD_API, EMAIL_PASSSWORD_SIGNOUT, EMAIL_PASSWORD_SIGN_IN, @@ -181,6 +183,9 @@ async def handle_api_request( api_function = handle_email_verify_token_post elif request_id == EMAIL_PASSSWORD_SIGNOUT: api_function = handle_emailpassword_signout_api + elif request_id == DASHBOARD_ANALYTICS_API: + if method == "post": + api_function = handle_analytics_post if api_function is not None: return await api_key_protector( diff --git a/supertokens_python/recipe/dashboard/utils.py b/supertokens_python/recipe/dashboard/utils.py index 2a2cdb523..2b74be2bd 100644 --- a/supertokens_python/recipe/dashboard/utils.py +++ b/supertokens_python/recipe/dashboard/utils.py @@ -48,6 +48,7 @@ from ...normalised_url_path import NormalisedURLPath from .constants import ( + DASHBOARD_ANALYTICS_API, DASHBOARD_API, EMAIL_PASSSWORD_SIGNOUT, EMAIL_PASSWORD_SIGN_IN, @@ -237,6 +238,8 @@ def get_api_if_matched(path: NormalisedURLPath, method: str) -> Optional[str]: return EMAIL_PASSWORD_SIGN_IN if path_str.endswith(EMAIL_PASSSWORD_SIGNOUT) and method == "post": return EMAIL_PASSSWORD_SIGNOUT + if path_str.endswith(DASHBOARD_ANALYTICS_API) and method == "post": + return DASHBOARD_ANALYTICS_API return None diff --git a/supertokens_python/supertokens.py b/supertokens_python/supertokens.py index 8b9791d4a..d3919ad84 100644 --- a/supertokens_python/supertokens.py +++ b/supertokens_python/supertokens.py @@ -14,22 +14,14 @@ from __future__ import annotations +from os import environ from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Set, Union from typing_extensions import Literal from supertokens_python.logger import get_maybe_none_as_str, log_debug_message -from .constants import ( - FDI_KEY_HEADER, - RID_KEY_HEADER, - TELEMETRY, - TELEMETRY_SUPERTOKENS_API_URL, - TELEMETRY_SUPERTOKENS_API_VERSION, - USER_COUNT, - USER_DELETE, - USERS, -) +from .constants import FDI_KEY_HEADER, RID_KEY_HEADER, USER_COUNT, USER_DELETE, USERS from .exceptions import SuperTokensError from .interfaces import ( CreateUserIdMappingOkResult, @@ -47,12 +39,11 @@ from .querier import Querier from .types import ThirdPartyInfo, User, UsersResponse from .utils import ( - execute_async, get_rid_from_header, + get_top_level_domain_for_same_site_resolution, is_version_gte, normalise_http_method, send_non_200_response_with_message, - get_top_level_domain_for_same_site_resolution, ) if TYPE_CHECKING: @@ -62,9 +53,6 @@ from supertokens_python.recipe.session import SessionContainer import json -from os import environ - -from httpx import AsyncClient from .exceptions import BadInputError, GeneralError, raise_general_exception @@ -202,55 +190,11 @@ def __init__( map(lambda func: func(self.app_info), recipe_list) ) - if telemetry is None: - # If telemetry is not provided, enable it by default for production environment - telemetry = ("SUPERTOKENS_ENV" not in environ) or ( - environ["SUPERTOKENS_ENV"] != "testing" - ) - - if telemetry: - try: - execute_async(self.app_info.mode, self.send_telemetry) - except Exception: - pass # Do not stop app startup if telemetry fails - - async def send_telemetry(self): - # If telemetry is enabled manually and the app is running in testing mode, - # do not send the telemetry - skip_telemetry = ("SUPERTOKENS_ENV" in environ) and ( - environ["SUPERTOKENS_ENV"] == "testing" + self.telemetry = ( + telemetry + if telemetry is not None + else (environ.get("TEST_MODE") != "testing") ) - if skip_telemetry: - self._telemetry_status = "SKIPPED" - return - - try: - querier = Querier.get_instance(None) - response = await querier.send_get_request(NormalisedURLPath(TELEMETRY), {}) - telemetry_id = None - if ( - "exists" in response - and response["exists"] - and "telemetryId" in response - ): - telemetry_id = response["telemetryId"] - data = { - "appName": self.app_info.app_name, - "websiteDomain": self.app_info.website_domain.get_as_string_dangerous(), - "sdk": "python", - } - if telemetry_id is not None: - data = {**data, "telemetryId": telemetry_id} - async with AsyncClient() as client: - await client.post( # type: ignore - url=TELEMETRY_SUPERTOKENS_API_URL, - json=data, - headers={"api-version": TELEMETRY_SUPERTOKENS_API_VERSION}, - ) - - self._telemetry_status = "SUCCESS" - except Exception: - self._telemetry_status = "EXCEPTION" @staticmethod def init( From ae79c84c781ce6e44d6361d553c8fb13b83cd203 Mon Sep 17 00:00:00 2001 From: Nemi Shah Date: Mon, 27 Mar 2023 17:07:04 +0530 Subject: [PATCH 042/192] Use dev api URL --- supertokens_python/constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/supertokens_python/constants.py b/supertokens_python/constants.py index 7841e7061..f24e6b78d 100644 --- a/supertokens_python/constants.py +++ b/supertokens_python/constants.py @@ -28,7 +28,7 @@ USER_COUNT = "/users/count" USER_DELETE = "/user/remove" USERS = "/users" -TELEMETRY_SUPERTOKENS_API_URL = "https://api.supertokens.com/0/st/telemetry" +TELEMETRY_SUPERTOKENS_API_URL = "https://dev.api.supertokens.com/0/st/telemetry" TELEMETRY_SUPERTOKENS_API_VERSION = "3" ERROR_MESSAGE_KEY = "message" API_KEY_HEADER = "api-key" From bde4655bae4a057426a374b7d5880e09493ebcf1 Mon Sep 17 00:00:00 2001 From: Iresh Sharma <32684272+iresharma@users.noreply.github.com> Date: Mon, 27 Mar 2023 17:41:28 +0530 Subject: [PATCH 043/192] fixes to analytics api --- .../recipe/dashboard/api/analytics.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/supertokens_python/recipe/dashboard/api/analytics.py b/supertokens_python/recipe/dashboard/api/analytics.py index 3a62d36bc..545dd415f 100644 --- a/supertokens_python/recipe/dashboard/api/analytics.py +++ b/supertokens_python/recipe/dashboard/api/analytics.py @@ -57,7 +57,12 @@ async def handle_analytics_post( NormalisedURLPath("/telemetry") ) if response is not None: - telemetry_id = response["telemetryId"] + if ( + "exists" in response + and response["exists"] + and "telemetryId" in response + ): + telemetry_id = response["telemetryId"] number_of_users = await Supertokens.get_instance().get_user_count( include_recipe_ids=None @@ -74,17 +79,19 @@ async def handle_analytics_post( ) data = { - "websiteDomain": websiteDomain, - "apiDomain": apiDomain, + "websiteDomain": websiteDomain.get_as_string_dangerous(), + "apiDomain": apiDomain.get_as_string_dangerous(), "appName": appName, "sdk": "supertokens-python", "sdkVersion": SDKVersion, - "telemetryId": telemetry_id, "numberOfUsers": number_of_users, "email": email, "dashboardVersion": dashboard_version, } + if telemetry_id is not None: + data["telemetryId"] = telemetry_id + try: async with AsyncClient() as client: await client.post( # type: ignore From 6b7e18d32405886b5db452255a8f1a46bcafc47e Mon Sep 17 00:00:00 2001 From: Iresh Sharma <32684272+iresharma@users.noreply.github.com> Date: Tue, 28 Mar 2023 11:20:52 +0530 Subject: [PATCH 044/192] removed irrelevant tested and added new tests --- tests/telemetry/test_telemetry.py | 105 ++++-------------------------- 1 file changed, 13 insertions(+), 92 deletions(-) diff --git a/tests/telemetry/test_telemetry.py b/tests/telemetry/test_telemetry.py index d9cb2d176..57a8a442a 100644 --- a/tests/telemetry/test_telemetry.py +++ b/tests/telemetry/test_telemetry.py @@ -12,17 +12,20 @@ # License for the specific language governing permissions and limitations # under the License. +import asyncio +import os + import pytest + from supertokens_python import InputAppInfo, Supertokens, SupertokensConfig, init from supertokens_python.recipe import session -import asyncio from tests.utils import reset @pytest.mark.asyncio -async def test_asgi_telemetry(): +async def test_telemetry(): reset() - with pytest.warns(None) as record: + with pytest.warns(None) as _: init( supertokens_config=SupertokensConfig("http://localhost:3567"), app_info=InputAppInfo( @@ -44,20 +47,16 @@ async def test_asgi_telemetry(): ) await asyncio.sleep(1) - for warn in record: - if warn.category is RuntimeWarning: - if "telemetry" in str(warn.message) and "was never awaited" in str( - warn.message - ): - assert False, "Asyncio error" + assert Supertokens.get_instance().telemetry is not None - assert Supertokens.get_instance()._telemetry_status == "SKIPPED" # type: ignore pylint: disable=W0212 + assert Supertokens.get_instance().telemetry @pytest.mark.asyncio -async def test_asgi_telemetry_with_wrong_mode(): +async def test_read_from_env(): reset() - with pytest.warns(None) as record: + os.environ["TEST_MODE"] = "testing" + with pytest.warns(None) as _: init( supertokens_config=SupertokensConfig("http://localhost:3567"), app_info=InputAppInfo( @@ -67,75 +66,6 @@ async def test_asgi_telemetry_with_wrong_mode(): api_base_path="/auth", ), framework="fastapi", - mode="wsgi", - recipe_list=[ - session.init( - anti_csrf="VIA_TOKEN", - cookie_domain="supertokens.io", - override=session.InputOverrideConfig(), - ) - ], - telemetry=True, - ) - await asyncio.sleep(1) - - found_warning = False - for warn in record: - if warn.category is RuntimeWarning: - found_warning = found_warning or "Inconsistent mode detected" in str( - warn.message - ) - - assert found_warning, "Asyncio error" - - assert Supertokens.get_instance()._telemetry_status == "SKIPPED" # type: ignore pylint: disable=W0212 - - -def test_wsgi_telemetry(): - reset() - with pytest.warns(None) as record: - init( - supertokens_config=SupertokensConfig("http://localhost:3567"), - app_info=InputAppInfo( - app_name="SuperTokens Demo", - api_domain="http://api.supertokens.io", - website_domain="http://supertokens.io", - api_base_path="/auth", - ), - framework="flask", - mode="wsgi", - recipe_list=[ - session.init( - anti_csrf="VIA_TOKEN", - cookie_domain="supertokens.io", - override=session.InputOverrideConfig(), - ) - ], - telemetry=True, - ) - - for warn in record: - if warn.category is RuntimeWarning: - if "telemetry" in str(warn.message) and "was never awaited" in str( - warn.message - ): - assert False, "Asyncio error" - - assert Supertokens.get_instance()._telemetry_status == "SKIPPED" # type: ignore pylint: disable=W0212 - - -def test_wsgi_telemetry_with_wrong_mode(): - reset() - with pytest.warns(None) as record: - init( - supertokens_config=SupertokensConfig("http://localhost:3567"), - app_info=InputAppInfo( - app_name="SuperTokens Demo", - api_domain="http://api.supertokens.io", - website_domain="http://supertokens.io", - api_base_path="/auth", - ), - framework="flask", mode="asgi", recipe_list=[ session.init( @@ -144,16 +74,7 @@ def test_wsgi_telemetry_with_wrong_mode(): override=session.InputOverrideConfig(), ) ], - telemetry=True, ) + await asyncio.sleep(1) - found_warning = False - for warn in record: - if warn.category is RuntimeWarning: - found_warning = found_warning or "Inconsistent mode detected" in str( - warn.message - ) - - assert found_warning, "Asyncio error" - - assert Supertokens.get_instance()._telemetry_status == "SKIPPED" # type: ignore pylint: disable=W0212 + assert not Supertokens.get_instance().telemetry From 772f24ffe64d41e45debd6ba27dbe1337da3c11f Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Tue, 28 Mar 2023 18:52:35 +0530 Subject: [PATCH 045/192] removes unnecessary file --- src/supertokens-python | 1 - 1 file changed, 1 deletion(-) delete mode 160000 src/supertokens-python diff --git a/src/supertokens-python b/src/supertokens-python deleted file mode 160000 index e989cfd24..000000000 --- a/src/supertokens-python +++ /dev/null @@ -1 +0,0 @@ -Subproject commit e989cfd244dc5ed2437e49ab6b476f7e9bd16861 From f0b59992e3bcfaaa4c76f5e13280272b522cc309 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Wed, 29 Mar 2023 15:17:13 +0530 Subject: [PATCH 046/192] adds bitbucket provider --- .../recipe/thirdparty/__init__.py | 1 + .../recipe/thirdparty/providers/__init__.py | 1 + .../recipe/thirdparty/providers/bitbucket.py | 114 ++++++++++++++++++ .../thirdpartyemailpassword/__init__.py | 1 + .../recipe/thirdpartypasswordless/__init__.py | 1 + 5 files changed, 118 insertions(+) create mode 100644 supertokens_python/recipe/thirdparty/providers/bitbucket.py diff --git a/supertokens_python/recipe/thirdparty/__init__.py b/supertokens_python/recipe/thirdparty/__init__.py index 44312ecee..0d9d49808 100644 --- a/supertokens_python/recipe/thirdparty/__init__.py +++ b/supertokens_python/recipe/thirdparty/__init__.py @@ -29,6 +29,7 @@ Github = providers.Github Google = providers.Google GoogleWorkspaces = providers.GoogleWorkspaces +Bitbucket = providers.Bitbucket exceptions = ex if TYPE_CHECKING: diff --git a/supertokens_python/recipe/thirdparty/providers/__init__.py b/supertokens_python/recipe/thirdparty/providers/__init__.py index a97ff75c1..9abc3a34f 100644 --- a/supertokens_python/recipe/thirdparty/providers/__init__.py +++ b/supertokens_python/recipe/thirdparty/providers/__init__.py @@ -17,3 +17,4 @@ from .github import Github # type: ignore from .google import Google # type: ignore from .google_workspaces import GoogleWorkspaces # type: ignore +from .bitbucket import Bitbucket # type: ignore diff --git a/supertokens_python/recipe/thirdparty/providers/bitbucket.py b/supertokens_python/recipe/thirdparty/providers/bitbucket.py new file mode 100644 index 000000000..1e4958ace --- /dev/null +++ b/supertokens_python/recipe/thirdparty/providers/bitbucket.py @@ -0,0 +1,114 @@ +# Copyright (c) 2023, VRAI Labs and/or its affiliates. All rights reserved. +# +# This software is licensed under the Apache License, Version 2.0 (the +# "License") as published by the Apache Software Foundation. +# +# You may not use this file except in compliance with the License. You may +# obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# 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 __future__ import annotations + +from typing import TYPE_CHECKING, Any, Callable, Dict, List, Union + +from httpx import AsyncClient +from supertokens_python.recipe.thirdparty.provider import Provider +from supertokens_python.recipe.thirdparty.types import ( + AccessTokenAPI, + AuthorisationRedirectAPI, + UserInfo, + UserInfoEmail, +) + +if TYPE_CHECKING: + from supertokens_python.framework.request import BaseRequest + + +class Bitbucket(Provider): + def __init__( + self, + client_id: str, + client_secret: str, + scope: Union[None, List[str]] = None, + authorisation_redirect: Union[ + None, Dict[str, Union[str, Callable[[BaseRequest], str]]] + ] = None, + is_default: bool = False, + ): + super().__init__("bitbucket", is_default) + self.client_id = client_id + self.client_secret = client_secret + self.scopes = ["account", "email"] if scope is None else list(set(scope)) + self.access_token_api_url = "https://bitbucket.org/site/oauth2/access_token" + self.authorisation_redirect_url = "https://bitbucket.org/site/oauth2/authorize" + self.authorisation_redirect_params = {} + if authorisation_redirect is not None: + self.authorisation_redirect_params = authorisation_redirect + + async def get_profile_info( + self, auth_code_response: Dict[str, Any], user_context: Dict[str, Any] + ) -> UserInfo: + access_token: str = auth_code_response["access_token"] + headers = {"Authorization": f"Bearer {access_token}"} + async with AsyncClient() as client: + response = await client.get( # type: ignore + url="https://api.bitbucket.org/2.0/user", + headers=headers, + ) + user_info = response.json() + user_id = user_info["uuid"] + email_res = await client.get( # type: ignore + url="https://api.bitbucket.org/2.0/user/emails", + headers=headers, + ) + email_data = email_res.json() + email = None + is_verified = False + for email_info in email_data["values"]: + if email_info.get("is_primary"): + email = email_info["email"] + is_verified = email_info["is_confirmed"] + break + + if email is None: + return UserInfo(user_id) + return UserInfo(user_id, UserInfoEmail(email, is_verified)) + + def get_authorisation_redirect_api_info( + self, user_context: Dict[str, Any] + ) -> AuthorisationRedirectAPI: + params = { + "scope": " ".join(self.scopes), + "response_type": "code", + "client_id": self.client_id, + "access_type": "offline", + "include_granted_scopes": "true", + **self.authorisation_redirect_params, + } + return AuthorisationRedirectAPI(self.authorisation_redirect_url, params) + + def get_access_token_api_info( + self, + redirect_uri: str, + auth_code_from_request: str, + user_context: Dict[str, Any], + ) -> AccessTokenAPI: + params = { + "client_id": self.client_id, + "client_secret": self.client_secret, + "grant_type": "authorization_code", + "code": auth_code_from_request, + "redirect_uri": redirect_uri, + } + return AccessTokenAPI(self.access_token_api_url, params) + + def get_redirect_uri(self, user_context: Dict[str, Any]) -> Union[None, str]: + return None + + def get_client_id(self, user_context: Dict[str, Any]) -> str: + return self.client_id diff --git a/supertokens_python/recipe/thirdpartyemailpassword/__init__.py b/supertokens_python/recipe/thirdpartyemailpassword/__init__.py index 50deec0e0..b191272ab 100644 --- a/supertokens_python/recipe/thirdpartyemailpassword/__init__.py +++ b/supertokens_python/recipe/thirdpartyemailpassword/__init__.py @@ -35,6 +35,7 @@ Github = thirdparty.Github Google = thirdparty.Google GoogleWorkspaces = thirdparty.GoogleWorkspaces +Bitbucket = thirdparty.Bitbucket SMTPService = emaildelivery_services.SMTPService if TYPE_CHECKING: diff --git a/supertokens_python/recipe/thirdpartypasswordless/__init__.py b/supertokens_python/recipe/thirdpartypasswordless/__init__.py index ad7d2bb14..f830fa22a 100644 --- a/supertokens_python/recipe/thirdpartypasswordless/__init__.py +++ b/supertokens_python/recipe/thirdpartypasswordless/__init__.py @@ -38,6 +38,7 @@ Github = thirdparty.Github Google = thirdparty.Google GoogleWorkspaces = thirdparty.GoogleWorkspaces +Bitbucket = thirdparty.Bitbucket ContactPhoneOnlyConfig = passwordless.ContactPhoneOnlyConfig ContactEmailOnlyConfig = passwordless.ContactEmailOnlyConfig ContactEmailOrPhoneConfig = passwordless.ContactEmailOrPhoneConfig From b15723ee831ec33347c1b945273c372bde3f32e8 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Wed, 29 Mar 2023 15:58:34 +0530 Subject: [PATCH 047/192] adds gitlab --- CHANGELOG.md | 5 +- coreDriverInterfaceSupported.json | 5 +- setup.py | 2 +- supertokens_python/constants.py | 3 +- .../recipe/thirdparty/__init__.py | 1 + .../recipe/thirdparty/providers/__init__.py | 1 + .../recipe/thirdparty/providers/bitbucket.py | 1 - .../recipe/thirdparty/providers/gitlab.py | 106 ++++++++++++++++++ .../thirdpartyemailpassword/__init__.py | 1 + .../recipe/thirdpartypasswordless/__init__.py | 1 + 10 files changed, 120 insertions(+), 6 deletions(-) create mode 100644 supertokens_python/recipe/thirdparty/providers/gitlab.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a599a3ef..cf7a6ae51 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,10 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## unreleased +## [0.12.4] - 2023-03-29 ### Changed - - Update all example apps to initialise dashboard recipe +### Added +- Login with gitlab (single tenant only) and bitbucket + ## [0.12.3] - 2023-02-27 - Adds APIs and logic to the dashboard recipe to enable email password based login ## [0.12.2] - 2023-02-23 diff --git a/coreDriverInterfaceSupported.json b/coreDriverInterfaceSupported.json index 260ce4863..2ef1eea60 100644 --- a/coreDriverInterfaceSupported.json +++ b/coreDriverInterfaceSupported.json @@ -10,6 +10,7 @@ "2.15", "2.16", "2.17", - "2.18" + "2.18", + "2.19" ] -} +} \ No newline at end of file diff --git a/setup.py b/setup.py index 9d5305707..12f367479 100644 --- a/setup.py +++ b/setup.py @@ -70,7 +70,7 @@ setup( name="supertokens_python", - version="0.12.3", + version="0.12.4", author="SuperTokens", license="Apache 2.0", author_email="team@supertokens.com", diff --git a/supertokens_python/constants.py b/supertokens_python/constants.py index 5f33750b4..39f51637b 100644 --- a/supertokens_python/constants.py +++ b/supertokens_python/constants.py @@ -22,8 +22,9 @@ "2.16", "2.17", "2.18", + "2.19", ] -VERSION = "0.12.3" +VERSION = "0.12.4" TELEMETRY = "/telemetry" USER_COUNT = "/users/count" USER_DELETE = "/user/remove" diff --git a/supertokens_python/recipe/thirdparty/__init__.py b/supertokens_python/recipe/thirdparty/__init__.py index 0d9d49808..34afebc5a 100644 --- a/supertokens_python/recipe/thirdparty/__init__.py +++ b/supertokens_python/recipe/thirdparty/__init__.py @@ -30,6 +30,7 @@ Google = providers.Google GoogleWorkspaces = providers.GoogleWorkspaces Bitbucket = providers.Bitbucket +GitLab = providers.GitLab exceptions = ex if TYPE_CHECKING: diff --git a/supertokens_python/recipe/thirdparty/providers/__init__.py b/supertokens_python/recipe/thirdparty/providers/__init__.py index 9abc3a34f..b0a3e6264 100644 --- a/supertokens_python/recipe/thirdparty/providers/__init__.py +++ b/supertokens_python/recipe/thirdparty/providers/__init__.py @@ -18,3 +18,4 @@ from .google import Google # type: ignore from .google_workspaces import GoogleWorkspaces # type: ignore from .bitbucket import Bitbucket # type: ignore +from .gitlab import GitLab # type: ignore diff --git a/supertokens_python/recipe/thirdparty/providers/bitbucket.py b/supertokens_python/recipe/thirdparty/providers/bitbucket.py index 1e4958ace..f8c1144b1 100644 --- a/supertokens_python/recipe/thirdparty/providers/bitbucket.py +++ b/supertokens_python/recipe/thirdparty/providers/bitbucket.py @@ -87,7 +87,6 @@ def get_authorisation_redirect_api_info( "response_type": "code", "client_id": self.client_id, "access_type": "offline", - "include_granted_scopes": "true", **self.authorisation_redirect_params, } return AuthorisationRedirectAPI(self.authorisation_redirect_url, params) diff --git a/supertokens_python/recipe/thirdparty/providers/gitlab.py b/supertokens_python/recipe/thirdparty/providers/gitlab.py new file mode 100644 index 000000000..da8b526bc --- /dev/null +++ b/supertokens_python/recipe/thirdparty/providers/gitlab.py @@ -0,0 +1,106 @@ +# Copyright (c) 2023, VRAI Labs and/or its affiliates. All rights reserved. +# +# This software is licensed under the Apache License, Version 2.0 (the +# "License") as published by the Apache Software Foundation. +# +# You may not use this file except in compliance with the License. You may +# obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# 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 __future__ import annotations + +from typing import TYPE_CHECKING, Any, Callable, Dict, List, Union + +from supertokens_python.normalised_url_domain import NormalisedURLDomain + +from httpx import AsyncClient +from supertokens_python.recipe.thirdparty.provider import Provider +from supertokens_python.recipe.thirdparty.types import ( + AccessTokenAPI, + AuthorisationRedirectAPI, + UserInfo, + UserInfoEmail, +) + +if TYPE_CHECKING: + from supertokens_python.framework.request import BaseRequest + + +class GitLab(Provider): + def __init__( + self, + client_id: str, + client_secret: str, + scope: Union[None, List[str]] = None, + authorisation_redirect: Union[ + None, Dict[str, Union[str, Callable[[BaseRequest], str]]] + ] = None, + gitlab_base_url: str = "https://gitlab.com", + is_default: bool = False, + ): + super().__init__("gitlab", is_default) + default_scopes = ["read_user"] + if scope is None: + scope = default_scopes + self.client_id = client_id + self.client_secret = client_secret + self.scopes = list(set(scope)) + gitlab_base_url = NormalisedURLDomain(gitlab_base_url).get_as_string_dangerous() + self.gitlab_base_url = gitlab_base_url + self.access_token_api_url = f"{gitlab_base_url}/oauth/token" + self.authorisation_redirect_url = f"{gitlab_base_url}/oauth/authorize" + self.authorisation_redirect_params = {} + if authorisation_redirect is not None: + self.authorisation_redirect_params = authorisation_redirect + + async def get_profile_info( + self, auth_code_response: Dict[str, Any], user_context: Dict[str, Any] + ) -> UserInfo: + access_token: str = auth_code_response["access_token"] + headers = {"Authorization": f"Bearer {access_token}"} + async with AsyncClient() as client: + response = await client.get(f"{self.gitlab_base_url}/api/v4/user", headers=headers) # type: ignore + user_info = response.json() + user_id = str(user_info["id"]) + email = user_info.get("email") + if email is None: + return UserInfo(user_id) + is_email_verified = user_info.get("confirmed_at") is not None + return UserInfo(user_id, UserInfoEmail(email, is_email_verified)) + + def get_authorisation_redirect_api_info( + self, user_context: Dict[str, Any] + ) -> AuthorisationRedirectAPI: + params = { + "scope": " ".join(self.scopes), + "response_type": "code", + "client_id": self.client_id, + **self.authorisation_redirect_params, + } + return AuthorisationRedirectAPI(self.authorisation_redirect_url, params) + + def get_access_token_api_info( + self, + redirect_uri: str, + auth_code_from_request: str, + user_context: Dict[str, Any], + ) -> AccessTokenAPI: + params = { + "client_id": self.client_id, + "client_secret": self.client_secret, + "grant_type": "authorization_code", + "code": auth_code_from_request, + "redirect_uri": redirect_uri, + } + return AccessTokenAPI(self.access_token_api_url, params) + + def get_redirect_uri(self, user_context: Dict[str, Any]) -> Union[None, str]: + return None + + def get_client_id(self, user_context: Dict[str, Any]) -> str: + return self.client_id diff --git a/supertokens_python/recipe/thirdpartyemailpassword/__init__.py b/supertokens_python/recipe/thirdpartyemailpassword/__init__.py index b191272ab..0a6d28258 100644 --- a/supertokens_python/recipe/thirdpartyemailpassword/__init__.py +++ b/supertokens_python/recipe/thirdpartyemailpassword/__init__.py @@ -36,6 +36,7 @@ Google = thirdparty.Google GoogleWorkspaces = thirdparty.GoogleWorkspaces Bitbucket = thirdparty.Bitbucket +GitLab = thirdparty.GitLab SMTPService = emaildelivery_services.SMTPService if TYPE_CHECKING: diff --git a/supertokens_python/recipe/thirdpartypasswordless/__init__.py b/supertokens_python/recipe/thirdpartypasswordless/__init__.py index f830fa22a..bb4d766a1 100644 --- a/supertokens_python/recipe/thirdpartypasswordless/__init__.py +++ b/supertokens_python/recipe/thirdpartypasswordless/__init__.py @@ -39,6 +39,7 @@ Google = thirdparty.Google GoogleWorkspaces = thirdparty.GoogleWorkspaces Bitbucket = thirdparty.Bitbucket +GitLab = thirdparty.GitLab ContactPhoneOnlyConfig = passwordless.ContactPhoneOnlyConfig ContactEmailOnlyConfig = passwordless.ContactEmailOnlyConfig ContactEmailOrPhoneConfig = passwordless.ContactEmailOrPhoneConfig From 1a251e9899fa63767bba9b6b659c4454563ae62b Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Wed, 29 Mar 2023 16:02:27 +0530 Subject: [PATCH 048/192] adding dev-v0.12.4 tag to this commit to ensure building --- html/supertokens_python/constants.html | 3 +- .../recipe/thirdparty/index.html | 2 + .../recipe/thirdparty/provider.html | 2 + .../thirdparty/providers/bitbucket.html | 403 ++++++++++++++++++ .../recipe/thirdparty/providers/gitlab.html | 372 ++++++++++++++++ .../recipe/thirdparty/providers/index.html | 14 +- .../recipe/thirdpartyemailpassword/index.html | 2 + .../recipe/thirdpartypasswordless/index.html | 2 + 8 files changed, 798 insertions(+), 2 deletions(-) create mode 100644 html/supertokens_python/recipe/thirdparty/providers/bitbucket.html create mode 100644 html/supertokens_python/recipe/thirdparty/providers/gitlab.html diff --git a/html/supertokens_python/constants.html b/html/supertokens_python/constants.html index c4d1e9543..aaa01e934 100644 --- a/html/supertokens_python/constants.html +++ b/html/supertokens_python/constants.html @@ -50,8 +50,9 @@

      Module supertokens_python.constants

      "2.16", "2.17", "2.18", + "2.19", ] -VERSION = "0.12.3" +VERSION = "0.12.4" TELEMETRY = "/telemetry" USER_COUNT = "/users/count" USER_DELETE = "/user/remove" diff --git a/html/supertokens_python/recipe/thirdparty/index.html b/html/supertokens_python/recipe/thirdparty/index.html index 9225fd4b3..64544142a 100644 --- a/html/supertokens_python/recipe/thirdparty/index.html +++ b/html/supertokens_python/recipe/thirdparty/index.html @@ -57,6 +57,8 @@

      Module supertokens_python.recipe.thirdparty

      Github = providers.Github Google = providers.Google GoogleWorkspaces = providers.GoogleWorkspaces +Bitbucket = providers.Bitbucket +GitLab = providers.GitLab exceptions = ex if TYPE_CHECKING: diff --git a/html/supertokens_python/recipe/thirdparty/provider.html b/html/supertokens_python/recipe/thirdparty/provider.html index 32d6799f4..d56943002 100644 --- a/html/supertokens_python/recipe/thirdparty/provider.html +++ b/html/supertokens_python/recipe/thirdparty/provider.html @@ -145,9 +145,11 @@

      Ancestors

      Subclasses

      diff --git a/html/supertokens_python/recipe/thirdparty/providers/bitbucket.html b/html/supertokens_python/recipe/thirdparty/providers/bitbucket.html new file mode 100644 index 000000000..ca2abda76 --- /dev/null +++ b/html/supertokens_python/recipe/thirdparty/providers/bitbucket.html @@ -0,0 +1,403 @@ + + + + + + +supertokens_python.recipe.thirdparty.providers.bitbucket API documentation + + + + + + + + + + + +
      +
      +
      +

      Module supertokens_python.recipe.thirdparty.providers.bitbucket

      +
      +
      +
      + +Expand source code + +
      # Copyright (c) 2023, VRAI Labs and/or its affiliates. All rights reserved.
      +#
      +# This software is licensed under the Apache License, Version 2.0 (the
      +# "License") as published by the Apache Software Foundation.
      +#
      +# You may not use this file except in compliance with the License. You may
      +# obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
      +#
      +# Unless required by applicable law or agreed to in writing, software
      +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
      +# 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 __future__ import annotations
      +
      +from typing import TYPE_CHECKING, Any, Callable, Dict, List, Union
      +
      +from httpx import AsyncClient
      +from supertokens_python.recipe.thirdparty.provider import Provider
      +from supertokens_python.recipe.thirdparty.types import (
      +    AccessTokenAPI,
      +    AuthorisationRedirectAPI,
      +    UserInfo,
      +    UserInfoEmail,
      +)
      +
      +if TYPE_CHECKING:
      +    from supertokens_python.framework.request import BaseRequest
      +
      +
      +class Bitbucket(Provider):
      +    def __init__(
      +        self,
      +        client_id: str,
      +        client_secret: str,
      +        scope: Union[None, List[str]] = None,
      +        authorisation_redirect: Union[
      +            None, Dict[str, Union[str, Callable[[BaseRequest], str]]]
      +        ] = None,
      +        is_default: bool = False,
      +    ):
      +        super().__init__("bitbucket", is_default)
      +        self.client_id = client_id
      +        self.client_secret = client_secret
      +        self.scopes = ["account", "email"] if scope is None else list(set(scope))
      +        self.access_token_api_url = "https://bitbucket.org/site/oauth2/access_token"
      +        self.authorisation_redirect_url = "https://bitbucket.org/site/oauth2/authorize"
      +        self.authorisation_redirect_params = {}
      +        if authorisation_redirect is not None:
      +            self.authorisation_redirect_params = authorisation_redirect
      +
      +    async def get_profile_info(
      +        self, auth_code_response: Dict[str, Any], user_context: Dict[str, Any]
      +    ) -> UserInfo:
      +        access_token: str = auth_code_response["access_token"]
      +        headers = {"Authorization": f"Bearer {access_token}"}
      +        async with AsyncClient() as client:
      +            response = await client.get(  # type: ignore
      +                url="https://api.bitbucket.org/2.0/user",
      +                headers=headers,
      +            )
      +            user_info = response.json()
      +            user_id = user_info["uuid"]
      +            email_res = await client.get(  # type: ignore
      +                url="https://api.bitbucket.org/2.0/user/emails",
      +                headers=headers,
      +            )
      +            email_data = email_res.json()
      +            email = None
      +            is_verified = False
      +            for email_info in email_data["values"]:
      +                if email_info.get("is_primary"):
      +                    email = email_info["email"]
      +                    is_verified = email_info["is_confirmed"]
      +                    break
      +
      +            if email is None:
      +                return UserInfo(user_id)
      +            return UserInfo(user_id, UserInfoEmail(email, is_verified))
      +
      +    def get_authorisation_redirect_api_info(
      +        self, user_context: Dict[str, Any]
      +    ) -> AuthorisationRedirectAPI:
      +        params = {
      +            "scope": " ".join(self.scopes),
      +            "response_type": "code",
      +            "client_id": self.client_id,
      +            "access_type": "offline",
      +            **self.authorisation_redirect_params,
      +        }
      +        return AuthorisationRedirectAPI(self.authorisation_redirect_url, params)
      +
      +    def get_access_token_api_info(
      +        self,
      +        redirect_uri: str,
      +        auth_code_from_request: str,
      +        user_context: Dict[str, Any],
      +    ) -> AccessTokenAPI:
      +        params = {
      +            "client_id": self.client_id,
      +            "client_secret": self.client_secret,
      +            "grant_type": "authorization_code",
      +            "code": auth_code_from_request,
      +            "redirect_uri": redirect_uri,
      +        }
      +        return AccessTokenAPI(self.access_token_api_url, params)
      +
      +    def get_redirect_uri(self, user_context: Dict[str, Any]) -> Union[None, str]:
      +        return None
      +
      +    def get_client_id(self, user_context: Dict[str, Any]) -> str:
      +        return self.client_id
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +

      Classes

      +
      +
      +class Bitbucket +(client_id: str, client_secret: str, scope: Union[None, List[str]] = None, authorisation_redirect: Union[None, Dict[str, Union[str, Callable[[BaseRequest], str]]]] = None, is_default: bool = False) +
      +
      +

      Helper class that provides a standard way to create an ABC using +inheritance.

      +
      + +Expand source code + +
      class Bitbucket(Provider):
      +    def __init__(
      +        self,
      +        client_id: str,
      +        client_secret: str,
      +        scope: Union[None, List[str]] = None,
      +        authorisation_redirect: Union[
      +            None, Dict[str, Union[str, Callable[[BaseRequest], str]]]
      +        ] = None,
      +        is_default: bool = False,
      +    ):
      +        super().__init__("bitbucket", is_default)
      +        self.client_id = client_id
      +        self.client_secret = client_secret
      +        self.scopes = ["account", "email"] if scope is None else list(set(scope))
      +        self.access_token_api_url = "https://bitbucket.org/site/oauth2/access_token"
      +        self.authorisation_redirect_url = "https://bitbucket.org/site/oauth2/authorize"
      +        self.authorisation_redirect_params = {}
      +        if authorisation_redirect is not None:
      +            self.authorisation_redirect_params = authorisation_redirect
      +
      +    async def get_profile_info(
      +        self, auth_code_response: Dict[str, Any], user_context: Dict[str, Any]
      +    ) -> UserInfo:
      +        access_token: str = auth_code_response["access_token"]
      +        headers = {"Authorization": f"Bearer {access_token}"}
      +        async with AsyncClient() as client:
      +            response = await client.get(  # type: ignore
      +                url="https://api.bitbucket.org/2.0/user",
      +                headers=headers,
      +            )
      +            user_info = response.json()
      +            user_id = user_info["uuid"]
      +            email_res = await client.get(  # type: ignore
      +                url="https://api.bitbucket.org/2.0/user/emails",
      +                headers=headers,
      +            )
      +            email_data = email_res.json()
      +            email = None
      +            is_verified = False
      +            for email_info in email_data["values"]:
      +                if email_info.get("is_primary"):
      +                    email = email_info["email"]
      +                    is_verified = email_info["is_confirmed"]
      +                    break
      +
      +            if email is None:
      +                return UserInfo(user_id)
      +            return UserInfo(user_id, UserInfoEmail(email, is_verified))
      +
      +    def get_authorisation_redirect_api_info(
      +        self, user_context: Dict[str, Any]
      +    ) -> AuthorisationRedirectAPI:
      +        params = {
      +            "scope": " ".join(self.scopes),
      +            "response_type": "code",
      +            "client_id": self.client_id,
      +            "access_type": "offline",
      +            **self.authorisation_redirect_params,
      +        }
      +        return AuthorisationRedirectAPI(self.authorisation_redirect_url, params)
      +
      +    def get_access_token_api_info(
      +        self,
      +        redirect_uri: str,
      +        auth_code_from_request: str,
      +        user_context: Dict[str, Any],
      +    ) -> AccessTokenAPI:
      +        params = {
      +            "client_id": self.client_id,
      +            "client_secret": self.client_secret,
      +            "grant_type": "authorization_code",
      +            "code": auth_code_from_request,
      +            "redirect_uri": redirect_uri,
      +        }
      +        return AccessTokenAPI(self.access_token_api_url, params)
      +
      +    def get_redirect_uri(self, user_context: Dict[str, Any]) -> Union[None, str]:
      +        return None
      +
      +    def get_client_id(self, user_context: Dict[str, Any]) -> str:
      +        return self.client_id
      +
      +

      Ancestors

      + +

      Methods

      +
      +
      +def get_access_token_api_info(self, redirect_uri: str, auth_code_from_request: str, user_context: Dict[str, Any]) ‑> AccessTokenAPI +
      +
      +
      +
      + +Expand source code + +
      def get_access_token_api_info(
      +    self,
      +    redirect_uri: str,
      +    auth_code_from_request: str,
      +    user_context: Dict[str, Any],
      +) -> AccessTokenAPI:
      +    params = {
      +        "client_id": self.client_id,
      +        "client_secret": self.client_secret,
      +        "grant_type": "authorization_code",
      +        "code": auth_code_from_request,
      +        "redirect_uri": redirect_uri,
      +    }
      +    return AccessTokenAPI(self.access_token_api_url, params)
      +
      +
      +
      +def get_authorisation_redirect_api_info(self, user_context: Dict[str, Any]) ‑> AuthorisationRedirectAPI +
      +
      +
      +
      + +Expand source code + +
      def get_authorisation_redirect_api_info(
      +    self, user_context: Dict[str, Any]
      +) -> AuthorisationRedirectAPI:
      +    params = {
      +        "scope": " ".join(self.scopes),
      +        "response_type": "code",
      +        "client_id": self.client_id,
      +        "access_type": "offline",
      +        **self.authorisation_redirect_params,
      +    }
      +    return AuthorisationRedirectAPI(self.authorisation_redirect_url, params)
      +
      +
      +
      +def get_client_id(self, user_context: Dict[str, Any]) ‑> str +
      +
      +
      +
      + +Expand source code + +
      def get_client_id(self, user_context: Dict[str, Any]) -> str:
      +    return self.client_id
      +
      +
      +
      +async def get_profile_info(self, auth_code_response: Dict[str, Any], user_context: Dict[str, Any]) ‑> UserInfo +
      +
      +
      +
      + +Expand source code + +
      async def get_profile_info(
      +    self, auth_code_response: Dict[str, Any], user_context: Dict[str, Any]
      +) -> UserInfo:
      +    access_token: str = auth_code_response["access_token"]
      +    headers = {"Authorization": f"Bearer {access_token}"}
      +    async with AsyncClient() as client:
      +        response = await client.get(  # type: ignore
      +            url="https://api.bitbucket.org/2.0/user",
      +            headers=headers,
      +        )
      +        user_info = response.json()
      +        user_id = user_info["uuid"]
      +        email_res = await client.get(  # type: ignore
      +            url="https://api.bitbucket.org/2.0/user/emails",
      +            headers=headers,
      +        )
      +        email_data = email_res.json()
      +        email = None
      +        is_verified = False
      +        for email_info in email_data["values"]:
      +            if email_info.get("is_primary"):
      +                email = email_info["email"]
      +                is_verified = email_info["is_confirmed"]
      +                break
      +
      +        if email is None:
      +            return UserInfo(user_id)
      +        return UserInfo(user_id, UserInfoEmail(email, is_verified))
      +
      +
      +
      +def get_redirect_uri(self, user_context: Dict[str, Any]) ‑> Optional[None] +
      +
      +
      +
      + +Expand source code + +
      def get_redirect_uri(self, user_context: Dict[str, Any]) -> Union[None, str]:
      +    return None
      +
      +
      +
      +
      +
      +
      +
      + +
      + + + \ No newline at end of file diff --git a/html/supertokens_python/recipe/thirdparty/providers/gitlab.html b/html/supertokens_python/recipe/thirdparty/providers/gitlab.html new file mode 100644 index 000000000..72ec06ac8 --- /dev/null +++ b/html/supertokens_python/recipe/thirdparty/providers/gitlab.html @@ -0,0 +1,372 @@ + + + + + + +supertokens_python.recipe.thirdparty.providers.gitlab API documentation + + + + + + + + + + + +
      +
      +
      +

      Module supertokens_python.recipe.thirdparty.providers.gitlab

      +
      +
      +
      + +Expand source code + +
      # Copyright (c) 2023, VRAI Labs and/or its affiliates. All rights reserved.
      +#
      +# This software is licensed under the Apache License, Version 2.0 (the
      +# "License") as published by the Apache Software Foundation.
      +#
      +# You may not use this file except in compliance with the License. You may
      +# obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
      +#
      +# Unless required by applicable law or agreed to in writing, software
      +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
      +# 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 __future__ import annotations
      +
      +from typing import TYPE_CHECKING, Any, Callable, Dict, List, Union
      +
      +from supertokens_python.normalised_url_domain import NormalisedURLDomain
      +
      +from httpx import AsyncClient
      +from supertokens_python.recipe.thirdparty.provider import Provider
      +from supertokens_python.recipe.thirdparty.types import (
      +    AccessTokenAPI,
      +    AuthorisationRedirectAPI,
      +    UserInfo,
      +    UserInfoEmail,
      +)
      +
      +if TYPE_CHECKING:
      +    from supertokens_python.framework.request import BaseRequest
      +
      +
      +class GitLab(Provider):
      +    def __init__(
      +        self,
      +        client_id: str,
      +        client_secret: str,
      +        scope: Union[None, List[str]] = None,
      +        authorisation_redirect: Union[
      +            None, Dict[str, Union[str, Callable[[BaseRequest], str]]]
      +        ] = None,
      +        gitlab_base_url: str = "https://gitlab.com",
      +        is_default: bool = False,
      +    ):
      +        super().__init__("gitlab", is_default)
      +        default_scopes = ["read_user"]
      +        if scope is None:
      +            scope = default_scopes
      +        self.client_id = client_id
      +        self.client_secret = client_secret
      +        self.scopes = list(set(scope))
      +        gitlab_base_url = NormalisedURLDomain(gitlab_base_url).get_as_string_dangerous()
      +        self.gitlab_base_url = gitlab_base_url
      +        self.access_token_api_url = f"{gitlab_base_url}/oauth/token"
      +        self.authorisation_redirect_url = f"{gitlab_base_url}/oauth/authorize"
      +        self.authorisation_redirect_params = {}
      +        if authorisation_redirect is not None:
      +            self.authorisation_redirect_params = authorisation_redirect
      +
      +    async def get_profile_info(
      +        self, auth_code_response: Dict[str, Any], user_context: Dict[str, Any]
      +    ) -> UserInfo:
      +        access_token: str = auth_code_response["access_token"]
      +        headers = {"Authorization": f"Bearer {access_token}"}
      +        async with AsyncClient() as client:
      +            response = await client.get(f"{self.gitlab_base_url}/api/v4/user", headers=headers)  # type: ignore
      +            user_info = response.json()
      +            user_id = str(user_info["id"])
      +            email = user_info.get("email")
      +            if email is None:
      +                return UserInfo(user_id)
      +            is_email_verified = user_info.get("confirmed_at") is not None
      +            return UserInfo(user_id, UserInfoEmail(email, is_email_verified))
      +
      +    def get_authorisation_redirect_api_info(
      +        self, user_context: Dict[str, Any]
      +    ) -> AuthorisationRedirectAPI:
      +        params = {
      +            "scope": " ".join(self.scopes),
      +            "response_type": "code",
      +            "client_id": self.client_id,
      +            **self.authorisation_redirect_params,
      +        }
      +        return AuthorisationRedirectAPI(self.authorisation_redirect_url, params)
      +
      +    def get_access_token_api_info(
      +        self,
      +        redirect_uri: str,
      +        auth_code_from_request: str,
      +        user_context: Dict[str, Any],
      +    ) -> AccessTokenAPI:
      +        params = {
      +            "client_id": self.client_id,
      +            "client_secret": self.client_secret,
      +            "grant_type": "authorization_code",
      +            "code": auth_code_from_request,
      +            "redirect_uri": redirect_uri,
      +        }
      +        return AccessTokenAPI(self.access_token_api_url, params)
      +
      +    def get_redirect_uri(self, user_context: Dict[str, Any]) -> Union[None, str]:
      +        return None
      +
      +    def get_client_id(self, user_context: Dict[str, Any]) -> str:
      +        return self.client_id
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +

      Classes

      +
      +
      +class GitLab +(client_id: str, client_secret: str, scope: Union[None, List[str]] = None, authorisation_redirect: Union[None, Dict[str, Union[str, Callable[[BaseRequest], str]]]] = None, gitlab_base_url: str = 'https://gitlab.com', is_default: bool = False) +
      +
      +

      Helper class that provides a standard way to create an ABC using +inheritance.

      +
      + +Expand source code + +
      class GitLab(Provider):
      +    def __init__(
      +        self,
      +        client_id: str,
      +        client_secret: str,
      +        scope: Union[None, List[str]] = None,
      +        authorisation_redirect: Union[
      +            None, Dict[str, Union[str, Callable[[BaseRequest], str]]]
      +        ] = None,
      +        gitlab_base_url: str = "https://gitlab.com",
      +        is_default: bool = False,
      +    ):
      +        super().__init__("gitlab", is_default)
      +        default_scopes = ["read_user"]
      +        if scope is None:
      +            scope = default_scopes
      +        self.client_id = client_id
      +        self.client_secret = client_secret
      +        self.scopes = list(set(scope))
      +        gitlab_base_url = NormalisedURLDomain(gitlab_base_url).get_as_string_dangerous()
      +        self.gitlab_base_url = gitlab_base_url
      +        self.access_token_api_url = f"{gitlab_base_url}/oauth/token"
      +        self.authorisation_redirect_url = f"{gitlab_base_url}/oauth/authorize"
      +        self.authorisation_redirect_params = {}
      +        if authorisation_redirect is not None:
      +            self.authorisation_redirect_params = authorisation_redirect
      +
      +    async def get_profile_info(
      +        self, auth_code_response: Dict[str, Any], user_context: Dict[str, Any]
      +    ) -> UserInfo:
      +        access_token: str = auth_code_response["access_token"]
      +        headers = {"Authorization": f"Bearer {access_token}"}
      +        async with AsyncClient() as client:
      +            response = await client.get(f"{self.gitlab_base_url}/api/v4/user", headers=headers)  # type: ignore
      +            user_info = response.json()
      +            user_id = str(user_info["id"])
      +            email = user_info.get("email")
      +            if email is None:
      +                return UserInfo(user_id)
      +            is_email_verified = user_info.get("confirmed_at") is not None
      +            return UserInfo(user_id, UserInfoEmail(email, is_email_verified))
      +
      +    def get_authorisation_redirect_api_info(
      +        self, user_context: Dict[str, Any]
      +    ) -> AuthorisationRedirectAPI:
      +        params = {
      +            "scope": " ".join(self.scopes),
      +            "response_type": "code",
      +            "client_id": self.client_id,
      +            **self.authorisation_redirect_params,
      +        }
      +        return AuthorisationRedirectAPI(self.authorisation_redirect_url, params)
      +
      +    def get_access_token_api_info(
      +        self,
      +        redirect_uri: str,
      +        auth_code_from_request: str,
      +        user_context: Dict[str, Any],
      +    ) -> AccessTokenAPI:
      +        params = {
      +            "client_id": self.client_id,
      +            "client_secret": self.client_secret,
      +            "grant_type": "authorization_code",
      +            "code": auth_code_from_request,
      +            "redirect_uri": redirect_uri,
      +        }
      +        return AccessTokenAPI(self.access_token_api_url, params)
      +
      +    def get_redirect_uri(self, user_context: Dict[str, Any]) -> Union[None, str]:
      +        return None
      +
      +    def get_client_id(self, user_context: Dict[str, Any]) -> str:
      +        return self.client_id
      +
      +

      Ancestors

      + +

      Methods

      +
      +
      +def get_access_token_api_info(self, redirect_uri: str, auth_code_from_request: str, user_context: Dict[str, Any]) ‑> AccessTokenAPI +
      +
      +
      +
      + +Expand source code + +
      def get_access_token_api_info(
      +    self,
      +    redirect_uri: str,
      +    auth_code_from_request: str,
      +    user_context: Dict[str, Any],
      +) -> AccessTokenAPI:
      +    params = {
      +        "client_id": self.client_id,
      +        "client_secret": self.client_secret,
      +        "grant_type": "authorization_code",
      +        "code": auth_code_from_request,
      +        "redirect_uri": redirect_uri,
      +    }
      +    return AccessTokenAPI(self.access_token_api_url, params)
      +
      +
      +
      +def get_authorisation_redirect_api_info(self, user_context: Dict[str, Any]) ‑> AuthorisationRedirectAPI +
      +
      +
      +
      + +Expand source code + +
      def get_authorisation_redirect_api_info(
      +    self, user_context: Dict[str, Any]
      +) -> AuthorisationRedirectAPI:
      +    params = {
      +        "scope": " ".join(self.scopes),
      +        "response_type": "code",
      +        "client_id": self.client_id,
      +        **self.authorisation_redirect_params,
      +    }
      +    return AuthorisationRedirectAPI(self.authorisation_redirect_url, params)
      +
      +
      +
      +def get_client_id(self, user_context: Dict[str, Any]) ‑> str +
      +
      +
      +
      + +Expand source code + +
      def get_client_id(self, user_context: Dict[str, Any]) -> str:
      +    return self.client_id
      +
      +
      +
      +async def get_profile_info(self, auth_code_response: Dict[str, Any], user_context: Dict[str, Any]) ‑> UserInfo +
      +
      +
      +
      + +Expand source code + +
      async def get_profile_info(
      +    self, auth_code_response: Dict[str, Any], user_context: Dict[str, Any]
      +) -> UserInfo:
      +    access_token: str = auth_code_response["access_token"]
      +    headers = {"Authorization": f"Bearer {access_token}"}
      +    async with AsyncClient() as client:
      +        response = await client.get(f"{self.gitlab_base_url}/api/v4/user", headers=headers)  # type: ignore
      +        user_info = response.json()
      +        user_id = str(user_info["id"])
      +        email = user_info.get("email")
      +        if email is None:
      +            return UserInfo(user_id)
      +        is_email_verified = user_info.get("confirmed_at") is not None
      +        return UserInfo(user_id, UserInfoEmail(email, is_email_verified))
      +
      +
      +
      +def get_redirect_uri(self, user_context: Dict[str, Any]) ‑> Optional[None] +
      +
      +
      +
      + +Expand source code + +
      def get_redirect_uri(self, user_context: Dict[str, Any]) -> Union[None, str]:
      +    return None
      +
      +
      +
      +
      +
      +
      +
      + +
      + + + \ No newline at end of file diff --git a/html/supertokens_python/recipe/thirdparty/providers/index.html b/html/supertokens_python/recipe/thirdparty/providers/index.html index cb35a9e98..d16cd42b5 100644 --- a/html/supertokens_python/recipe/thirdparty/providers/index.html +++ b/html/supertokens_python/recipe/thirdparty/providers/index.html @@ -44,7 +44,9 @@

      Module supertokens_python.recipe.thirdparty.providers +from .google_workspaces import GoogleWorkspaces # type: ignore +from .bitbucket import Bitbucket # type: ignore +from .gitlab import GitLab # type: ignore
      @@ -54,6 +56,10 @@

      Sub-modules

      +
      supertokens_python.recipe.thirdparty.providers.bitbucket
      +
      +
      +
      supertokens_python.recipe.thirdparty.providers.discord
      @@ -66,6 +72,10 @@

      Sub-modules

      +
      supertokens_python.recipe.thirdparty.providers.gitlab
      +
      +
      +
      supertokens_python.recipe.thirdparty.providers.google
      @@ -101,9 +111,11 @@

      Index

    • Sub-modules

      • supertokens_python.recipe.thirdparty.providers.apple
      • +
      • supertokens_python.recipe.thirdparty.providers.bitbucket
      • supertokens_python.recipe.thirdparty.providers.discord
      • supertokens_python.recipe.thirdparty.providers.facebook
      • supertokens_python.recipe.thirdparty.providers.github
      • +
      • supertokens_python.recipe.thirdparty.providers.gitlab
      • supertokens_python.recipe.thirdparty.providers.google
      • supertokens_python.recipe.thirdparty.providers.google_workspaces
      • supertokens_python.recipe.thirdparty.providers.okta
      • diff --git a/html/supertokens_python/recipe/thirdpartyemailpassword/index.html b/html/supertokens_python/recipe/thirdpartyemailpassword/index.html index 89a176509..e6dd43c50 100644 --- a/html/supertokens_python/recipe/thirdpartyemailpassword/index.html +++ b/html/supertokens_python/recipe/thirdpartyemailpassword/index.html @@ -63,6 +63,8 @@

        Module supertokens_python.recipe.thirdpartyemailpassword Github = thirdparty.Github Google = thirdparty.Google GoogleWorkspaces = thirdparty.GoogleWorkspaces +Bitbucket = thirdparty.Bitbucket +GitLab = thirdparty.GitLab SMTPService = emaildelivery_services.SMTPService if TYPE_CHECKING: diff --git a/html/supertokens_python/recipe/thirdpartypasswordless/index.html b/html/supertokens_python/recipe/thirdpartypasswordless/index.html index 7f7932062..2f5982cbb 100644 --- a/html/supertokens_python/recipe/thirdpartypasswordless/index.html +++ b/html/supertokens_python/recipe/thirdpartypasswordless/index.html @@ -66,6 +66,8 @@

        Module supertokens_python.recipe.thirdpartypasswordless< Github = thirdparty.Github Google = thirdparty.Google GoogleWorkspaces = thirdparty.GoogleWorkspaces +Bitbucket = thirdparty.Bitbucket +GitLab = thirdparty.GitLab ContactPhoneOnlyConfig = passwordless.ContactPhoneOnlyConfig ContactEmailOnlyConfig = passwordless.ContactEmailOnlyConfig ContactEmailOrPhoneConfig = passwordless.ContactEmailOrPhoneConfig From 9d26b0266fd6c5db7703f5d13508745df904490d Mon Sep 17 00:00:00 2001 From: Nemi Shah Date: Thu, 30 Mar 2023 18:18:20 +0530 Subject: [PATCH 049/192] Refactor --- supertokens_python/constants.py | 2 +- supertokens_python/recipe/dashboard/api/analytics.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/supertokens_python/constants.py b/supertokens_python/constants.py index f24e6b78d..7841e7061 100644 --- a/supertokens_python/constants.py +++ b/supertokens_python/constants.py @@ -28,7 +28,7 @@ USER_COUNT = "/users/count" USER_DELETE = "/user/remove" USERS = "/users" -TELEMETRY_SUPERTOKENS_API_URL = "https://dev.api.supertokens.com/0/st/telemetry" +TELEMETRY_SUPERTOKENS_API_URL = "https://api.supertokens.com/0/st/telemetry" TELEMETRY_SUPERTOKENS_API_VERSION = "3" ERROR_MESSAGE_KEY = "message" API_KEY_HEADER = "api-key" diff --git a/supertokens_python/recipe/dashboard/api/analytics.py b/supertokens_python/recipe/dashboard/api/analytics.py index 545dd415f..a04c52411 100644 --- a/supertokens_python/recipe/dashboard/api/analytics.py +++ b/supertokens_python/recipe/dashboard/api/analytics.py @@ -82,7 +82,7 @@ async def handle_analytics_post( "websiteDomain": websiteDomain.get_as_string_dangerous(), "apiDomain": apiDomain.get_as_string_dangerous(), "appName": appName, - "sdk": "supertokens-python", + "sdk": "python", "sdkVersion": SDKVersion, "numberOfUsers": number_of_users, "email": email, From a397e7bba56243b640f16ac7ee92de314b365e6f Mon Sep 17 00:00:00 2001 From: Nemi Shah Date: Thu, 30 Mar 2023 18:25:20 +0530 Subject: [PATCH 050/192] Update package version, dashboard version and CHANGELOG --- CHANGELOG.md | 4 ++++ setup.py | 2 +- supertokens_python/constants.py | 4 ++-- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cf7a6ae51..6f7be7f09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## unreleased +## [0.12.5] - 2023-03-30 + +- Adds a telemetry API to the dashboard recipe + ## [0.12.4] - 2023-03-29 ### Changed - Update all example apps to initialise dashboard recipe diff --git a/setup.py b/setup.py index 12f367479..12aac3491 100644 --- a/setup.py +++ b/setup.py @@ -70,7 +70,7 @@ setup( name="supertokens_python", - version="0.12.4", + version="0.12.5", author="SuperTokens", license="Apache 2.0", author_email="team@supertokens.com", diff --git a/supertokens_python/constants.py b/supertokens_python/constants.py index d97622676..cbd75985b 100644 --- a/supertokens_python/constants.py +++ b/supertokens_python/constants.py @@ -24,7 +24,7 @@ "2.18", "2.19", ] -VERSION = "0.12.4" +VERSION = "0.12.5" TELEMETRY = "/telemetry" USER_COUNT = "/users/count" USER_DELETE = "/user/remove" @@ -37,4 +37,4 @@ FDI_KEY_HEADER = "fdi-version" API_VERSION = "/apiversion" API_VERSION_HEADER = "cdi-version" -DASHBOARD_VERSION = "0.4" +DASHBOARD_VERSION = "0.5" From 5c8da9ac3e1c9ef4d6554cca74bc3711d889e1df Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Thu, 30 Mar 2023 18:44:50 +0530 Subject: [PATCH 051/192] adding dev-v0.12.5 tag to this commit to ensure building --- html/supertokens_python/constants.html | 6 +- .../recipe/dashboard/api/analytics.html | 253 ++++++++++++++++++ .../recipe/dashboard/api/index.html | 89 ++++++ .../recipe/dashboard/constants.html | 3 +- .../recipe/dashboard/interfaces.html | 59 ++++ .../recipe/dashboard/recipe.html | 11 + .../recipe/dashboard/utils.html | 5 + html/supertokens_python/supertokens.html | 171 +----------- html/supertokens_python/types.html | 1 + 9 files changed, 434 insertions(+), 164 deletions(-) create mode 100644 html/supertokens_python/recipe/dashboard/api/analytics.html diff --git a/html/supertokens_python/constants.html b/html/supertokens_python/constants.html index aaa01e934..017cc0d20 100644 --- a/html/supertokens_python/constants.html +++ b/html/supertokens_python/constants.html @@ -52,20 +52,20 @@

        Module supertokens_python.constants

        "2.18", "2.19", ] -VERSION = "0.12.4" +VERSION = "0.12.5" TELEMETRY = "/telemetry" USER_COUNT = "/users/count" USER_DELETE = "/user/remove" USERS = "/users" TELEMETRY_SUPERTOKENS_API_URL = "https://api.supertokens.com/0/st/telemetry" -TELEMETRY_SUPERTOKENS_API_VERSION = "2" +TELEMETRY_SUPERTOKENS_API_VERSION = "3" ERROR_MESSAGE_KEY = "message" API_KEY_HEADER = "api-key" RID_KEY_HEADER = "rid" FDI_KEY_HEADER = "fdi-version" API_VERSION = "/apiversion" API_VERSION_HEADER = "cdi-version" -DASHBOARD_VERSION = "0.4"
        +DASHBOARD_VERSION = "0.5"

    • diff --git a/html/supertokens_python/recipe/dashboard/api/analytics.html b/html/supertokens_python/recipe/dashboard/api/analytics.html new file mode 100644 index 000000000..45d5c4d8b --- /dev/null +++ b/html/supertokens_python/recipe/dashboard/api/analytics.html @@ -0,0 +1,253 @@ + + + + + + +supertokens_python.recipe.dashboard.api.analytics API documentation + + + + + + + + + + + +
      +
      +
      +

      Module supertokens_python.recipe.dashboard.api.analytics

      +
      +
      +
      + +Expand source code + +
      # Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved.
      +#
      +# This software is licensed under the Apache License, Version 2.0 (the
      +# "License") as published by the Apache Software Foundation.
      +#
      +# You may not use this file except in compliance with the License. You may
      +# obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
      +#
      +# Unless required by applicable law or agreed to in writing, software
      +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
      +# 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 __future__ import annotations
      +
      +from typing import TYPE_CHECKING
      +
      +from httpx import AsyncClient
      +
      +from supertokens_python import Supertokens
      +from supertokens_python.constants import (
      +    TELEMETRY_SUPERTOKENS_API_URL,
      +    TELEMETRY_SUPERTOKENS_API_VERSION,
      +)
      +from supertokens_python.constants import VERSION as SDKVersion
      +from supertokens_python.exceptions import raise_bad_input_exception
      +from supertokens_python.normalised_url_path import NormalisedURLPath
      +from supertokens_python.querier import Querier
      +
      +from ..interfaces import AnalyticsResponse
      +
      +if TYPE_CHECKING:
      +    from supertokens_python.recipe.dashboard.interfaces import APIInterface, APIOptions
      +
      +
      +async def handle_analytics_post(
      +    _: APIInterface, api_options: APIOptions
      +) -> AnalyticsResponse:
      +    if not Supertokens.get_instance().telemetry:
      +        return AnalyticsResponse()
      +    body = await api_options.request.json()
      +    if body is None:
      +        raise_bad_input_exception("Please send body")
      +    email = body.get("email")
      +    dashboard_version = body.get("dashboardVersion")
      +
      +    if email is None:
      +        raise_bad_input_exception("Missing required property 'email'")
      +    if dashboard_version is None:
      +        raise_bad_input_exception("Missing required property 'dashboardVersion'")
      +
      +    telemetry_id = None
      +
      +    try:
      +        response = await Querier.get_instance().send_get_request(
      +            NormalisedURLPath("/telemetry")
      +        )
      +        if response is not None:
      +            if (
      +                "exists" in response
      +                and response["exists"]
      +                and "telemetryId" in response
      +            ):
      +                telemetry_id = response["telemetryId"]
      +
      +        number_of_users = await Supertokens.get_instance().get_user_count(
      +            include_recipe_ids=None
      +        )
      +
      +    except Exception as __:
      +        # If either telemetry id API or user count fetch fails, no event should be sent
      +        return AnalyticsResponse()
      +
      +    apiDomain, websiteDomain, appName = (
      +        api_options.app_info.api_domain,
      +        api_options.app_info.website_domain,
      +        api_options.app_info.app_name,
      +    )
      +
      +    data = {
      +        "websiteDomain": websiteDomain.get_as_string_dangerous(),
      +        "apiDomain": apiDomain.get_as_string_dangerous(),
      +        "appName": appName,
      +        "sdk": "python",
      +        "sdkVersion": SDKVersion,
      +        "numberOfUsers": number_of_users,
      +        "email": email,
      +        "dashboardVersion": dashboard_version,
      +    }
      +
      +    if telemetry_id is not None:
      +        data["telemetryId"] = telemetry_id
      +
      +    try:
      +        async with AsyncClient() as client:
      +            await client.post(  # type: ignore
      +                url=TELEMETRY_SUPERTOKENS_API_URL,
      +                json=data,
      +                headers={"api-version": TELEMETRY_SUPERTOKENS_API_VERSION},
      +            )
      +    except Exception as __:
      +        # If telemetry event fails, no error should be thrown
      +        pass
      +
      +    return AnalyticsResponse()
      +
      +
      +
      +
      +
      +
      +
      +

      Functions

      +
      +
      +async def handle_analytics_post(_: APIInterface, api_options: APIOptions) ‑> AnalyticsResponse +
      +
      +
      +
      + +Expand source code + +
      async def handle_analytics_post(
      +    _: APIInterface, api_options: APIOptions
      +) -> AnalyticsResponse:
      +    if not Supertokens.get_instance().telemetry:
      +        return AnalyticsResponse()
      +    body = await api_options.request.json()
      +    if body is None:
      +        raise_bad_input_exception("Please send body")
      +    email = body.get("email")
      +    dashboard_version = body.get("dashboardVersion")
      +
      +    if email is None:
      +        raise_bad_input_exception("Missing required property 'email'")
      +    if dashboard_version is None:
      +        raise_bad_input_exception("Missing required property 'dashboardVersion'")
      +
      +    telemetry_id = None
      +
      +    try:
      +        response = await Querier.get_instance().send_get_request(
      +            NormalisedURLPath("/telemetry")
      +        )
      +        if response is not None:
      +            if (
      +                "exists" in response
      +                and response["exists"]
      +                and "telemetryId" in response
      +            ):
      +                telemetry_id = response["telemetryId"]
      +
      +        number_of_users = await Supertokens.get_instance().get_user_count(
      +            include_recipe_ids=None
      +        )
      +
      +    except Exception as __:
      +        # If either telemetry id API or user count fetch fails, no event should be sent
      +        return AnalyticsResponse()
      +
      +    apiDomain, websiteDomain, appName = (
      +        api_options.app_info.api_domain,
      +        api_options.app_info.website_domain,
      +        api_options.app_info.app_name,
      +    )
      +
      +    data = {
      +        "websiteDomain": websiteDomain.get_as_string_dangerous(),
      +        "apiDomain": apiDomain.get_as_string_dangerous(),
      +        "appName": appName,
      +        "sdk": "python",
      +        "sdkVersion": SDKVersion,
      +        "numberOfUsers": number_of_users,
      +        "email": email,
      +        "dashboardVersion": dashboard_version,
      +    }
      +
      +    if telemetry_id is not None:
      +        data["telemetryId"] = telemetry_id
      +
      +    try:
      +        async with AsyncClient() as client:
      +            await client.post(  # type: ignore
      +                url=TELEMETRY_SUPERTOKENS_API_URL,
      +                json=data,
      +                headers={"api-version": TELEMETRY_SUPERTOKENS_API_VERSION},
      +            )
      +    except Exception as __:
      +        # If telemetry event fails, no error should be thrown
      +        pass
      +
      +    return AnalyticsResponse()
      +
      +
      +
      +
      +
      +
      +
      + +
      + + + \ No newline at end of file diff --git a/html/supertokens_python/recipe/dashboard/api/index.html b/html/supertokens_python/recipe/dashboard/api/index.html index a4a8b632c..da2f14eb7 100644 --- a/html/supertokens_python/recipe/dashboard/api/index.html +++ b/html/supertokens_python/recipe/dashboard/api/index.html @@ -39,6 +39,7 @@

      Module supertokens_python.recipe.dashboard.apiModule supertokens_python.recipe.dashboard.api

      Sub-modules

      +
      supertokens_python.recipe.dashboard.api.analytics
      +
      +
      +
      supertokens_python.recipe.dashboard.api.dashboard
      @@ -150,6 +156,87 @@

      Functions

      return send_200_response(response.to_json(), api_options.response)
      +
      +async def handle_analytics_post(_: APIInterface, api_options: APIOptions) ‑> AnalyticsResponse +
      +
      +
      +
      + +Expand source code + +
      async def handle_analytics_post(
      +    _: APIInterface, api_options: APIOptions
      +) -> AnalyticsResponse:
      +    if not Supertokens.get_instance().telemetry:
      +        return AnalyticsResponse()
      +    body = await api_options.request.json()
      +    if body is None:
      +        raise_bad_input_exception("Please send body")
      +    email = body.get("email")
      +    dashboard_version = body.get("dashboardVersion")
      +
      +    if email is None:
      +        raise_bad_input_exception("Missing required property 'email'")
      +    if dashboard_version is None:
      +        raise_bad_input_exception("Missing required property 'dashboardVersion'")
      +
      +    telemetry_id = None
      +
      +    try:
      +        response = await Querier.get_instance().send_get_request(
      +            NormalisedURLPath("/telemetry")
      +        )
      +        if response is not None:
      +            if (
      +                "exists" in response
      +                and response["exists"]
      +                and "telemetryId" in response
      +            ):
      +                telemetry_id = response["telemetryId"]
      +
      +        number_of_users = await Supertokens.get_instance().get_user_count(
      +            include_recipe_ids=None
      +        )
      +
      +    except Exception as __:
      +        # If either telemetry id API or user count fetch fails, no event should be sent
      +        return AnalyticsResponse()
      +
      +    apiDomain, websiteDomain, appName = (
      +        api_options.app_info.api_domain,
      +        api_options.app_info.website_domain,
      +        api_options.app_info.app_name,
      +    )
      +
      +    data = {
      +        "websiteDomain": websiteDomain.get_as_string_dangerous(),
      +        "apiDomain": apiDomain.get_as_string_dangerous(),
      +        "appName": appName,
      +        "sdk": "python",
      +        "sdkVersion": SDKVersion,
      +        "numberOfUsers": number_of_users,
      +        "email": email,
      +        "dashboardVersion": dashboard_version,
      +    }
      +
      +    if telemetry_id is not None:
      +        data["telemetryId"] = telemetry_id
      +
      +    try:
      +        async with AsyncClient() as client:
      +            await client.post(  # type: ignore
      +                url=TELEMETRY_SUPERTOKENS_API_URL,
      +                json=data,
      +                headers={"api-version": TELEMETRY_SUPERTOKENS_API_VERSION},
      +            )
      +    except Exception as __:
      +        # If telemetry event fails, no error should be thrown
      +        pass
      +
      +    return AnalyticsResponse()
      +
      +
      async def handle_dashboard_api(api_implementation: APIInterface, api_options: APIOptions) ‑> Optional[BaseResponse]
      @@ -941,6 +1028,7 @@

      Index

    • Sub-modules

    • diff --git a/html/supertokens_python/recipe/dashboard/interfaces.html b/html/supertokens_python/recipe/dashboard/interfaces.html index 3ac2c4f86..8bcdaf285 100644 --- a/html/supertokens_python/recipe/dashboard/interfaces.html +++ b/html/supertokens_python/recipe/dashboard/interfaces.html @@ -320,6 +320,13 @@

      Module supertokens_python.recipe.dashboard.interfaces

      @@ -381,6 +388,51 @@

      Subclasses

      self.app_info = app_info

      +
      +class AnalyticsResponse +
      +
      +

      Helper class that provides a standard way to create an ABC using +inheritance.

      +
      + +Expand source code + +
      class AnalyticsResponse(APIResponse):
      +    status: str = "OK"
      +
      +    def to_json(self) -> Dict[str, Any]:
      +        return {"status": self.status}
      +
      +

      Ancestors

      + +

      Class variables

      +
      +
      var status : str
      +
      +
      +
      +
      +

      Methods

      +
      +
      +def to_json(self) ‑> Dict[str, Any] +
      +
      +
      +
      + +Expand source code + +
      def to_json(self) -> Dict[str, Any]:
      +    return {"status": self.status}
      +
      +
      +
      +
      class DashboardUsersGetResponse (users: Union[List[User], List[UserWithMetadata]], next_pagination_token: Optional[str]) @@ -1600,6 +1652,13 @@

      APIOptions

    • +

      AnalyticsResponse

      + +
    • +
    • DashboardUsersGetResponse

      • status
      • diff --git a/html/supertokens_python/recipe/dashboard/recipe.html b/html/supertokens_python/recipe/dashboard/recipe.html index 83176e0cf..accb1a205 100644 --- a/html/supertokens_python/recipe/dashboard/recipe.html +++ b/html/supertokens_python/recipe/dashboard/recipe.html @@ -49,6 +49,7 @@

        Module supertokens_python.recipe.dashboard.recipe from .api import ( api_key_protector, + handle_analytics_post, handle_dashboard_api, handle_email_verify_token_post, handle_emailpassword_signin_api, @@ -81,6 +82,7 @@

        Module supertokens_python.recipe.dashboard.recipe from supertokens_python.exceptions import SuperTokensError, raise_general_exception from .constants import ( + DASHBOARD_ANALYTICS_API, DASHBOARD_API, EMAIL_PASSSWORD_SIGNOUT, EMAIL_PASSWORD_SIGN_IN, @@ -209,6 +211,9 @@

        Module supertokens_python.recipe.dashboard.recipe api_function = handle_email_verify_token_post elif request_id == EMAIL_PASSSWORD_SIGNOUT: api_function = handle_emailpassword_signout_api + elif request_id == DASHBOARD_ANALYTICS_API: + if method == "post": + api_function = handle_analytics_post if api_function is not None: return await api_key_protector( @@ -405,6 +410,9 @@

        Classes

        api_function = handle_email_verify_token_post elif request_id == EMAIL_PASSSWORD_SIGNOUT: api_function = handle_emailpassword_signout_api + elif request_id == DASHBOARD_ANALYTICS_API: + if method == "post": + api_function = handle_analytics_post if api_function is not None: return await api_key_protector( @@ -661,6 +669,9 @@

        Methods

        api_function = handle_email_verify_token_post elif request_id == EMAIL_PASSSWORD_SIGNOUT: api_function = handle_emailpassword_signout_api + elif request_id == DASHBOARD_ANALYTICS_API: + if method == "post": + api_function = handle_analytics_post if api_function is not None: return await api_key_protector( diff --git a/html/supertokens_python/recipe/dashboard/utils.html b/html/supertokens_python/recipe/dashboard/utils.html index 898016852..9b8e2f077 100644 --- a/html/supertokens_python/recipe/dashboard/utils.html +++ b/html/supertokens_python/recipe/dashboard/utils.html @@ -76,6 +76,7 @@

        Module supertokens_python.recipe.dashboard.utils< from ...normalised_url_path import NormalisedURLPath from .constants import ( + DASHBOARD_ANALYTICS_API, DASHBOARD_API, EMAIL_PASSSWORD_SIGNOUT, EMAIL_PASSWORD_SIGN_IN, @@ -265,6 +266,8 @@

        Module supertokens_python.recipe.dashboard.utils< return EMAIL_PASSWORD_SIGN_IN if path_str.endswith(EMAIL_PASSSWORD_SIGNOUT) and method == "post": return EMAIL_PASSSWORD_SIGNOUT + if path_str.endswith(DASHBOARD_ANALYTICS_API) and method == "post": + return DASHBOARD_ANALYTICS_API return None @@ -472,6 +475,8 @@

        Functions

        return EMAIL_PASSWORD_SIGN_IN if path_str.endswith(EMAIL_PASSSWORD_SIGNOUT) and method == "post": return EMAIL_PASSSWORD_SIGNOUT + if path_str.endswith(DASHBOARD_ANALYTICS_API) and method == "post": + return DASHBOARD_ANALYTICS_API return None
        diff --git a/html/supertokens_python/supertokens.html b/html/supertokens_python/supertokens.html index 13aadc8eb..9e4d49bfb 100644 --- a/html/supertokens_python/supertokens.html +++ b/html/supertokens_python/supertokens.html @@ -42,22 +42,14 @@

        Module supertokens_python.supertokens

        from __future__ import annotations +from os import environ from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Set, Union from typing_extensions import Literal from supertokens_python.logger import get_maybe_none_as_str, log_debug_message -from .constants import ( - FDI_KEY_HEADER, - RID_KEY_HEADER, - TELEMETRY, - TELEMETRY_SUPERTOKENS_API_URL, - TELEMETRY_SUPERTOKENS_API_VERSION, - USER_COUNT, - USER_DELETE, - USERS, -) +from .constants import FDI_KEY_HEADER, RID_KEY_HEADER, USER_COUNT, USER_DELETE, USERS from .exceptions import SuperTokensError from .interfaces import ( CreateUserIdMappingOkResult, @@ -75,12 +67,11 @@

        Module supertokens_python.supertokens

        from .querier import Querier from .types import ThirdPartyInfo, User, UsersResponse from .utils import ( - execute_async, get_rid_from_header, + get_top_level_domain_for_same_site_resolution, is_version_gte, normalise_http_method, send_non_200_response_with_message, - get_top_level_domain_for_same_site_resolution, ) if TYPE_CHECKING: @@ -90,9 +81,6 @@

        Module supertokens_python.supertokens

        from supertokens_python.recipe.session import SessionContainer import json -from os import environ - -from httpx import AsyncClient from .exceptions import BadInputError, GeneralError, raise_general_exception @@ -230,55 +218,11 @@

        Module supertokens_python.supertokens

        map(lambda func: func(self.app_info), recipe_list) ) - if telemetry is None: - # If telemetry is not provided, enable it by default for production environment - telemetry = ("SUPERTOKENS_ENV" not in environ) or ( - environ["SUPERTOKENS_ENV"] != "testing" - ) - - if telemetry: - try: - execute_async(self.app_info.mode, self.send_telemetry) - except Exception: - pass # Do not stop app startup if telemetry fails - - async def send_telemetry(self): - # If telemetry is enabled manually and the app is running in testing mode, - # do not send the telemetry - skip_telemetry = ("SUPERTOKENS_ENV" in environ) and ( - environ["SUPERTOKENS_ENV"] == "testing" + self.telemetry = ( + telemetry + if telemetry is not None + else (environ.get("TEST_MODE") != "testing") ) - if skip_telemetry: - self._telemetry_status = "SKIPPED" - return - - try: - querier = Querier.get_instance(None) - response = await querier.send_get_request(NormalisedURLPath(TELEMETRY), {}) - telemetry_id = None - if ( - "exists" in response - and response["exists"] - and "telemetryId" in response - ): - telemetry_id = response["telemetryId"] - data = { - "appName": self.app_info.app_name, - "websiteDomain": self.app_info.website_domain.get_as_string_dangerous(), - "sdk": "python", - } - if telemetry_id is not None: - data = {**data, "telemetryId": telemetry_id} - async with AsyncClient() as client: - await client.post( # type: ignore - url=TELEMETRY_SUPERTOKENS_API_URL, - json=data, - headers={"api-version": TELEMETRY_SUPERTOKENS_API_VERSION}, - ) - - self._telemetry_status = "SUCCESS" - except Exception: - self._telemetry_status = "EXCEPTION" @staticmethod def init( @@ -841,55 +785,11 @@

        Methods

        map(lambda func: func(self.app_info), recipe_list) ) - if telemetry is None: - # If telemetry is not provided, enable it by default for production environment - telemetry = ("SUPERTOKENS_ENV" not in environ) or ( - environ["SUPERTOKENS_ENV"] != "testing" - ) - - if telemetry: - try: - execute_async(self.app_info.mode, self.send_telemetry) - except Exception: - pass # Do not stop app startup if telemetry fails - - async def send_telemetry(self): - # If telemetry is enabled manually and the app is running in testing mode, - # do not send the telemetry - skip_telemetry = ("SUPERTOKENS_ENV" in environ) and ( - environ["SUPERTOKENS_ENV"] == "testing" + self.telemetry = ( + telemetry + if telemetry is not None + else (environ.get("TEST_MODE") != "testing") ) - if skip_telemetry: - self._telemetry_status = "SKIPPED" - return - - try: - querier = Querier.get_instance(None) - response = await querier.send_get_request(NormalisedURLPath(TELEMETRY), {}) - telemetry_id = None - if ( - "exists" in response - and response["exists"] - and "telemetryId" in response - ): - telemetry_id = response["telemetryId"] - data = { - "appName": self.app_info.app_name, - "websiteDomain": self.app_info.website_domain.get_as_string_dangerous(), - "sdk": "python", - } - if telemetry_id is not None: - data = {**data, "telemetryId": telemetry_id} - async with AsyncClient() as client: - await client.post( # type: ignore - url=TELEMETRY_SUPERTOKENS_API_URL, - json=data, - headers={"api-version": TELEMETRY_SUPERTOKENS_API_VERSION}, - ) - - self._telemetry_status = "SUCCESS" - except Exception: - self._telemetry_status = "EXCEPTION" @staticmethod def init( @@ -1694,54 +1594,6 @@

        Methods

        return None
        -
        -async def send_telemetry(self) -
        -
        -
        -
        - -Expand source code - -
        async def send_telemetry(self):
        -    # If telemetry is enabled manually and the app is running in testing mode,
        -    # do not send the telemetry
        -    skip_telemetry = ("SUPERTOKENS_ENV" in environ) and (
        -        environ["SUPERTOKENS_ENV"] == "testing"
        -    )
        -    if skip_telemetry:
        -        self._telemetry_status = "SKIPPED"
        -        return
        -
        -    try:
        -        querier = Querier.get_instance(None)
        -        response = await querier.send_get_request(NormalisedURLPath(TELEMETRY), {})
        -        telemetry_id = None
        -        if (
        -            "exists" in response
        -            and response["exists"]
        -            and "telemetryId" in response
        -        ):
        -            telemetry_id = response["telemetryId"]
        -        data = {
        -            "appName": self.app_info.app_name,
        -            "websiteDomain": self.app_info.website_domain.get_as_string_dangerous(),
        -            "sdk": "python",
        -        }
        -        if telemetry_id is not None:
        -            data = {**data, "telemetryId": telemetry_id}
        -        async with AsyncClient() as client:
        -            await client.post(  # type: ignore
        -                url=TELEMETRY_SUPERTOKENS_API_URL,
        -                json=data,
        -                headers={"api-version": TELEMETRY_SUPERTOKENS_API_VERSION},
        -            )
        -
        -        self._telemetry_status = "SUCCESS"
        -    except Exception:
        -        self._telemetry_status = "EXCEPTION"
        -
        -
        async def update_or_delete_user_id_mapping_info(self, user_id: str, user_id_type: Optional[UserIDTypes] = None, external_user_id_info: Optional[str] = None) ‑> Union[UpdateOrDeleteUserIdMappingInfoOkResultUnknownMappingError]
        @@ -1848,7 +1700,6 @@

        init
      • middleware
      • reset
      • -
      • send_telemetry
      • update_or_delete_user_id_mapping_info
    • diff --git a/html/supertokens_python/types.html b/html/supertokens_python/types.html index e57adec62..2edd5ee34 100644 --- a/html/supertokens_python/types.html +++ b/html/supertokens_python/types.html @@ -137,6 +137,7 @@

      Ancestors

    Subclasses

    diff --git a/html/supertokens_python/recipe/emailpassword/recipe.html b/html/supertokens_python/recipe/emailpassword/recipe.html index f333a596b..d9dbda285 100644 --- a/html/supertokens_python/recipe/emailpassword/recipe.html +++ b/html/supertokens_python/recipe/emailpassword/recipe.html @@ -92,6 +92,7 @@

    Module supertokens_python.recipe.emailpassword.recipeModule supertokens_python.recipe.emailpassword.recipeClasses

    override, email_delivery, ) - recipe_implementation = RecipeImplementation(Querier.get_instance(recipe_id)) + + def get_emailpassword_config() -> EmailPasswordConfig: + return self.config + + recipe_implementation = RecipeImplementation( + Querier.get_instance(recipe_id), get_emailpassword_config + ) self.recipe_implementation = ( recipe_implementation if self.config.override.functions is None diff --git a/html/supertokens_python/recipe/emailpassword/recipe_implementation.html b/html/supertokens_python/recipe/emailpassword/recipe_implementation.html index ad7a8e106..d111ba843 100644 --- a/html/supertokens_python/recipe/emailpassword/recipe_implementation.html +++ b/html/supertokens_python/recipe/emailpassword/recipe_implementation.html @@ -41,7 +41,7 @@

    Module supertokens_python.recipe.emailpassword.recipe_im # under the License. from __future__ import annotations -from typing import TYPE_CHECKING, Any, Dict, Union +from typing import TYPE_CHECKING, Any, Dict, Union, Callable from supertokens_python.normalised_url_path import NormalisedURLPath @@ -58,17 +58,25 @@

    Module supertokens_python.recipe.emailpassword.recipe_im UpdateEmailOrPasswordEmailAlreadyExistsError, UpdateEmailOrPasswordOkResult, UpdateEmailOrPasswordUnknownUserIdError, + UpdateEmailOrPasswordPasswordPolicyViolationError, ) from .types import User +from .utils import EmailPasswordConfig +from .constants import FORM_FIELD_PASSWORD_ID if TYPE_CHECKING: from supertokens_python.querier import Querier class RecipeImplementation(RecipeInterface): - def __init__(self, querier: Querier): + def __init__( + self, + querier: Querier, + get_emailpassword_config: Callable[[], EmailPasswordConfig], + ): super().__init__() self.querier = querier + self.get_emailpassword_config = get_emailpassword_config async def get_user_by_id( self, user_id: str, user_context: Dict[str, Any] @@ -166,16 +174,28 @@

    Module supertokens_python.recipe.emailpassword.recipe_im user_id: str, email: Union[str, None], password: Union[str, None], + apply_password_policy: Union[bool, None], user_context: Dict[str, Any], ) -> Union[ UpdateEmailOrPasswordOkResult, UpdateEmailOrPasswordEmailAlreadyExistsError, UpdateEmailOrPasswordUnknownUserIdError, + UpdateEmailOrPasswordPasswordPolicyViolationError, ]: data = {"userId": user_id} if email is not None: data = {"email": email, **data} if password is not None: + if apply_password_policy is None or apply_password_policy: + form_fields = ( + self.get_emailpassword_config().sign_up_feature.form_fields + ) + password_field = list( + filter(lambda x: x.id == FORM_FIELD_PASSWORD_ID, form_fields) + )[0] + error = await password_field.validate(password) + if error is not None: + return UpdateEmailOrPasswordPasswordPolicyViolationError(error) data = {"password": password, **data} response = await self.querier.send_put_request( NormalisedURLPath("/recipe/user"), data @@ -198,7 +218,7 @@

    Classes

    class RecipeImplementation -(querier: Querier) +(querier: Querier, get_emailpassword_config: Callable[[], EmailPasswordConfig])

    Helper class that provides a standard way to create an ABC using @@ -208,9 +228,14 @@

    Classes

    Expand source code
    class RecipeImplementation(RecipeInterface):
    -    def __init__(self, querier: Querier):
    +    def __init__(
    +        self,
    +        querier: Querier,
    +        get_emailpassword_config: Callable[[], EmailPasswordConfig],
    +    ):
             super().__init__()
             self.querier = querier
    +        self.get_emailpassword_config = get_emailpassword_config
     
         async def get_user_by_id(
             self, user_id: str, user_context: Dict[str, Any]
    @@ -308,16 +333,28 @@ 

    Classes

    user_id: str, email: Union[str, None], password: Union[str, None], + apply_password_policy: Union[bool, None], user_context: Dict[str, Any], ) -> Union[ UpdateEmailOrPasswordOkResult, UpdateEmailOrPasswordEmailAlreadyExistsError, UpdateEmailOrPasswordUnknownUserIdError, + UpdateEmailOrPasswordPasswordPolicyViolationError, ]: data = {"userId": user_id} if email is not None: data = {"email": email, **data} if password is not None: + if apply_password_policy is None or apply_password_policy: + form_fields = ( + self.get_emailpassword_config().sign_up_feature.form_fields + ) + password_field = list( + filter(lambda x: x.id == FORM_FIELD_PASSWORD_ID, form_fields) + )[0] + error = await password_field.validate(password) + if error is not None: + return UpdateEmailOrPasswordPasswordPolicyViolationError(error) data = {"password": password, **data} response = await self.querier.send_put_request( NormalisedURLPath("/recipe/user"), data @@ -487,7 +524,7 @@

    Methods

    -async def update_email_or_password(self, user_id: str, email: Union[str, None], password: Union[str, None], user_context: Dict[str, Any]) ‑> Union[UpdateEmailOrPasswordOkResultUpdateEmailOrPasswordEmailAlreadyExistsErrorUpdateEmailOrPasswordUnknownUserIdError] +async def update_email_or_password(self, user_id: str, email: Union[str, None], password: Union[str, None], apply_password_policy: Union[bool, None], user_context: Dict[str, Any]) ‑> Union[UpdateEmailOrPasswordOkResultUpdateEmailOrPasswordEmailAlreadyExistsErrorUpdateEmailOrPasswordUnknownUserIdErrorUpdateEmailOrPasswordPasswordPolicyViolationError]
    @@ -500,16 +537,28 @@

    Methods

    user_id: str, email: Union[str, None], password: Union[str, None], + apply_password_policy: Union[bool, None], user_context: Dict[str, Any], ) -> Union[ UpdateEmailOrPasswordOkResult, UpdateEmailOrPasswordEmailAlreadyExistsError, UpdateEmailOrPasswordUnknownUserIdError, + UpdateEmailOrPasswordPasswordPolicyViolationError, ]: data = {"userId": user_id} if email is not None: data = {"email": email, **data} if password is not None: + if apply_password_policy is None or apply_password_policy: + form_fields = ( + self.get_emailpassword_config().sign_up_feature.form_fields + ) + password_field = list( + filter(lambda x: x.id == FORM_FIELD_PASSWORD_ID, form_fields) + )[0] + error = await password_field.validate(password) + if error is not None: + return UpdateEmailOrPasswordPasswordPolicyViolationError(error) data = {"password": password, **data} response = await self.querier.send_put_request( NormalisedURLPath("/recipe/user"), data diff --git a/html/supertokens_python/recipe/emailpassword/syncio/index.html b/html/supertokens_python/recipe/emailpassword/syncio/index.html index 256bab0b4..b75c38137 100644 --- a/html/supertokens_python/recipe/emailpassword/syncio/index.html +++ b/html/supertokens_python/recipe/emailpassword/syncio/index.html @@ -51,11 +51,16 @@

    Module supertokens_python.recipe.emailpassword.syncioFunctions

    -def update_email_or_password(user_id: str, email: Optional[str] = None, password: Optional[str] = None, user_context: Optional[Dict[str, Any]] = None) +def update_email_or_password(user_id: str, email: Optional[str] = None, password: Optional[str] = None, apply_password_policy: Optional[bool] = None, user_context: Optional[Dict[str, Any]] = None)
    @@ -261,11 +266,16 @@

    Functions

    user_id: str, email: Union[str, None] = None, password: Union[str, None] = None, + apply_password_policy: Union[bool, None] = None, user_context: Union[None, Dict[str, Any]] = None, ): from supertokens_python.recipe.emailpassword.asyncio import update_email_or_password - return sync(update_email_or_password(user_id, email, password, user_context))
    + return sync( + update_email_or_password( + user_id, email, password, apply_password_policy, user_context + ) + )
    diff --git a/html/supertokens_python/recipe/jwt/api/jwks_get.html b/html/supertokens_python/recipe/jwt/api/jwks_get.html index 015fdd9ca..feb4afefe 100644 --- a/html/supertokens_python/recipe/jwt/api/jwks_get.html +++ b/html/supertokens_python/recipe/jwt/api/jwks_get.html @@ -51,8 +51,10 @@

    Module supertokens_python.recipe.jwt.api.jwks_get user_context = default_user_context(api_options.request) result = await api_implementation.jwks_get(api_options, user_context) + if isinstance(result, JWKSGetResponse): api_options.response.set_header("Access-Control-Allow-Origin", "*") + return send_200_response(result.to_json(), api_options.response) @@ -78,8 +80,10 @@

    Functions

    user_context = default_user_context(api_options.request) result = await api_implementation.jwks_get(api_options, user_context) + if isinstance(result, JWKSGetResponse): api_options.response.set_header("Access-Control-Allow-Origin", "*") + return send_200_response(result.to_json(), api_options.response)
    diff --git a/html/supertokens_python/recipe/jwt/asyncio/index.html b/html/supertokens_python/recipe/jwt/asyncio/index.html index 13b7f7684..c4cfb3c50 100644 --- a/html/supertokens_python/recipe/jwt/asyncio/index.html +++ b/html/supertokens_python/recipe/jwt/asyncio/index.html @@ -39,7 +39,7 @@

    Module supertokens_python.recipe.jwt.asyncio

    # 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, Union +from typing import Any, Dict, Union, Optional from supertokens_python.recipe.jwt.interfaces import ( CreateJwtOkResult, @@ -50,9 +50,10 @@

    Module supertokens_python.recipe.jwt.asyncio

    async def create_jwt( - payload: Union[None, Dict[str, Any]] = None, - validity_seconds: Union[None, int] = None, - user_context: Union[Dict[str, Any], None] = None, + payload: Optional[Dict[str, Any]] = None, + validity_seconds: Optional[int] = None, + use_static_signing_key: Optional[bool] = None, + user_context: Optional[Dict[str, Any]] = None, ) -> Union[CreateJwtOkResult, CreateJwtResultUnsupportedAlgorithm]: if user_context is None: user_context = {} @@ -60,11 +61,11 @@

    Module supertokens_python.recipe.jwt.asyncio

    payload = {} return await JWTRecipe.get_instance().recipe_implementation.create_jwt( - payload, validity_seconds, user_context + payload, validity_seconds, use_static_signing_key, user_context ) -async def get_jwks(user_context: Union[Dict[str, Any], None] = None) -> GetJWKSResult: +async def get_jwks(user_context: Optional[Dict[str, Any]] = None) -> GetJWKSResult: if user_context is None: user_context = {} return await JWTRecipe.get_instance().recipe_implementation.get_jwks(user_context)
    @@ -78,7 +79,7 @@

    Module supertokens_python.recipe.jwt.asyncio

    Functions

    -async def create_jwt(payload: Optional[Dict[str, Any]] = None, validity_seconds: Optional[None] = None, user_context: Optional[Dict[str, Any]] = None) ‑> Union[CreateJwtOkResultCreateJwtResultUnsupportedAlgorithm] +async def create_jwt(payload: Optional[Dict[str, Any]] = None, validity_seconds: Optional[int] = None, use_static_signing_key: Optional[bool] = None, user_context: Optional[Dict[str, Any]] = None) ‑> Union[CreateJwtOkResultCreateJwtResultUnsupportedAlgorithm]
    @@ -87,9 +88,10 @@

    Functions

    Expand source code
    async def create_jwt(
    -    payload: Union[None, Dict[str, Any]] = None,
    -    validity_seconds: Union[None, int] = None,
    -    user_context: Union[Dict[str, Any], None] = None,
    +    payload: Optional[Dict[str, Any]] = None,
    +    validity_seconds: Optional[int] = None,
    +    use_static_signing_key: Optional[bool] = None,
    +    user_context: Optional[Dict[str, Any]] = None,
     ) -> Union[CreateJwtOkResult, CreateJwtResultUnsupportedAlgorithm]:
         if user_context is None:
             user_context = {}
    @@ -97,7 +99,7 @@ 

    Functions

    payload = {} return await JWTRecipe.get_instance().recipe_implementation.create_jwt( - payload, validity_seconds, user_context + payload, validity_seconds, use_static_signing_key, user_context )
    @@ -110,7 +112,7 @@

    Functions

    Expand source code -
    async def get_jwks(user_context: Union[Dict[str, Any], None] = None) -> GetJWKSResult:
    +
    async def get_jwks(user_context: Optional[Dict[str, Any]] = None) -> GetJWKSResult:
         if user_context is None:
             user_context = {}
         return await JWTRecipe.get_instance().recipe_implementation.get_jwks(user_context)
    diff --git a/html/supertokens_python/recipe/jwt/interfaces.html b/html/supertokens_python/recipe/jwt/interfaces.html index cf871106f..6f0514c12 100644 --- a/html/supertokens_python/recipe/jwt/interfaces.html +++ b/html/supertokens_python/recipe/jwt/interfaces.html @@ -40,7 +40,7 @@

    Module supertokens_python.recipe.jwt.interfacesModule supertokens_python.recipe.jwt.interfacesModule supertokens_python.recipe.jwt.interfacesModule supertokens_python.recipe.jwt.interfacesMethods

    Expand source code
    class JWKSGetResponse(APIResponse):
    -    status: str = "OK"
    -
         def __init__(self, keys: List[JsonWebKey]):
             self.keys = keys
     
    @@ -292,20 +289,13 @@ 

    Methods

    } ) - return {"status": self.status, "keys": keys}
    + return {"keys": keys}

    Ancestors

    -

    Class variables

    -
    -
    var status : str
    -
    -
    -
    -

    Methods

    @@ -331,7 +321,7 @@

    Methods

    } ) - return {"status": self.status, "keys": keys}
    + return {"keys": keys}
    @@ -374,7 +364,8 @@

    Methods

    async def create_jwt( self, payload: Dict[str, Any], - validity_seconds: Union[int, None], + validity_seconds: Optional[int], + use_static_signing_key: Optional[bool], user_context: Dict[str, Any], ) -> Union[CreateJwtOkResult, CreateJwtResultUnsupportedAlgorithm]: pass @@ -394,7 +385,7 @@

    Subclasses

    Methods

    -async def create_jwt(self, payload: Dict[str, Any], validity_seconds: Optional[None], user_context: Dict[str, Any]) ‑> Union[CreateJwtOkResultCreateJwtResultUnsupportedAlgorithm] +async def create_jwt(self, payload: Dict[str, Any], validity_seconds: Optional[int], use_static_signing_key: Optional[bool], user_context: Dict[str, Any]) ‑> Union[CreateJwtOkResultCreateJwtResultUnsupportedAlgorithm]
    @@ -406,7 +397,8 @@

    Methods

    async def create_jwt( self, payload: Dict[str, Any], - validity_seconds: Union[int, None], + validity_seconds: Optional[int], + use_static_signing_key: Optional[bool], user_context: Dict[str, Any], ) -> Union[CreateJwtOkResult, CreateJwtResultUnsupportedAlgorithm]: pass @@ -465,7 +457,6 @@

    JWKSGetResponse

  • diff --git a/html/supertokens_python/recipe/jwt/recipe.html b/html/supertokens_python/recipe/jwt/recipe.html index a3e9115c5..23fae915e 100644 --- a/html/supertokens_python/recipe/jwt/recipe.html +++ b/html/supertokens_python/recipe/jwt/recipe.html @@ -121,7 +121,10 @@

    Module supertokens_python.recipe.jwt.recipe

    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 @@ -246,7 +249,10 @@

    Classes

    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 @@ -434,7 +440,10 @@

    Methods

    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
    diff --git a/html/supertokens_python/recipe/jwt/recipe_implementation.html b/html/supertokens_python/recipe/jwt/recipe_implementation.html index fb4093c49..1acc479cf 100644 --- a/html/supertokens_python/recipe/jwt/recipe_implementation.html +++ b/html/supertokens_python/recipe/jwt/recipe_implementation.html @@ -41,7 +41,7 @@

    Module supertokens_python.recipe.jwt.recipe_implementati # under the License. from __future__ import annotations -from typing import TYPE_CHECKING, Any, Dict, List, Union +from typing import TYPE_CHECKING, Any, Dict, List, Union, Optional from supertokens_python.normalised_url_path import NormalisedURLPath from supertokens_python.querier import Querier @@ -70,7 +70,8 @@

    Module supertokens_python.recipe.jwt.recipe_implementati async def create_jwt( self, payload: Dict[str, Any], - validity_seconds: Union[int, None], + validity_seconds: Optional[int], + use_static_signing_key: Optional[bool], user_context: Dict[str, Any], ) -> Union[CreateJwtOkResult, CreateJwtResultUnsupportedAlgorithm]: if validity_seconds is None: @@ -79,6 +80,7 @@

    Module supertokens_python.recipe.jwt.recipe_implementati data = { "payload": payload, "validity": validity_seconds, + "use_static_signing_key": use_static_signing_key is not False, "algorithm": "RS256", "jwksDomain": self.app_info.api_domain.get_as_string_dangerous(), } @@ -92,7 +94,7 @@

    Module supertokens_python.recipe.jwt.recipe_implementati async def get_jwks(self, user_context: Dict[str, Any]) -> GetJWKSResult: response = await self.querier.send_get_request( - NormalisedURLPath("/recipe/jwt/jwks"), {} + NormalisedURLPath("/.well-known/jwks.json"), {} ) keys: List[JsonWebKey] = [] @@ -135,7 +137,8 @@

    Classes

    async def create_jwt( self, payload: Dict[str, Any], - validity_seconds: Union[int, None], + validity_seconds: Optional[int], + use_static_signing_key: Optional[bool], user_context: Dict[str, Any], ) -> Union[CreateJwtOkResult, CreateJwtResultUnsupportedAlgorithm]: if validity_seconds is None: @@ -144,6 +147,7 @@

    Classes

    data = { "payload": payload, "validity": validity_seconds, + "use_static_signing_key": use_static_signing_key is not False, "algorithm": "RS256", "jwksDomain": self.app_info.api_domain.get_as_string_dangerous(), } @@ -157,7 +161,7 @@

    Classes

    async def get_jwks(self, user_context: Dict[str, Any]) -> GetJWKSResult: response = await self.querier.send_get_request( - NormalisedURLPath("/recipe/jwt/jwks"), {} + NormalisedURLPath("/.well-known/jwks.json"), {} ) keys: List[JsonWebKey] = [] @@ -177,7 +181,7 @@

    Ancestors

    Methods

    -async def create_jwt(self, payload: Dict[str, Any], validity_seconds: Union[int, None], user_context: Dict[str, Any]) ‑> Union[CreateJwtOkResultCreateJwtResultUnsupportedAlgorithm] +async def create_jwt(self, payload: Dict[str, Any], validity_seconds: Optional[int], use_static_signing_key: Optional[bool], user_context: Dict[str, Any]) ‑> Union[CreateJwtOkResultCreateJwtResultUnsupportedAlgorithm]
    @@ -188,7 +192,8 @@

    Methods

    async def create_jwt(
         self,
         payload: Dict[str, Any],
    -    validity_seconds: Union[int, None],
    +    validity_seconds: Optional[int],
    +    use_static_signing_key: Optional[bool],
         user_context: Dict[str, Any],
     ) -> Union[CreateJwtOkResult, CreateJwtResultUnsupportedAlgorithm]:
         if validity_seconds is None:
    @@ -197,6 +202,7 @@ 

    Methods

    data = { "payload": payload, "validity": validity_seconds, + "use_static_signing_key": use_static_signing_key is not False, "algorithm": "RS256", "jwksDomain": self.app_info.api_domain.get_as_string_dangerous(), } @@ -220,7 +226,7 @@

    Methods

    async def get_jwks(self, user_context: Dict[str, Any]) -> GetJWKSResult:
         response = await self.querier.send_get_request(
    -        NormalisedURLPath("/recipe/jwt/jwks"), {}
    +        NormalisedURLPath("/.well-known/jwks.json"), {}
         )
     
         keys: List[JsonWebKey] = []
    diff --git a/html/supertokens_python/recipe/jwt/syncio/index.html b/html/supertokens_python/recipe/jwt/syncio/index.html
    index c23b28f54..d759d6af4 100644
    --- a/html/supertokens_python/recipe/jwt/syncio/index.html
    +++ b/html/supertokens_python/recipe/jwt/syncio/index.html
    @@ -39,7 +39,7 @@ 

    Module supertokens_python.recipe.jwt.syncio

    # 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, Union +from typing import Any, Dict, Union, Optional from supertokens_python.async_to_sync_wrapper import sync from supertokens_python.recipe.jwt import asyncio @@ -51,14 +51,19 @@

    Module supertokens_python.recipe.jwt.syncio

    def create_jwt( - payload: Union[None, Dict[str, Any]] = None, - validity_seconds: Union[None, int] = None, - user_context: Union[Dict[str, Any], None] = None, + payload: Optional[Dict[str, Any]] = None, + validity_seconds: Optional[int] = None, + use_static_signing_key: Optional[bool] = None, + user_context: Optional[Dict[str, Any]] = None, ) -> Union[CreateJwtOkResult, CreateJwtResultUnsupportedAlgorithm]: - return sync(asyncio.create_jwt(payload, validity_seconds, user_context)) + return sync( + asyncio.create_jwt( + payload, validity_seconds, use_static_signing_key, user_context + ) + ) -def get_jwks(user_context: Union[Dict[str, Any], None] = None) -> GetJWKSResult: +def get_jwks(user_context: Optional[Dict[str, Any]] = None) -> GetJWKSResult: return sync(asyncio.get_jwks(user_context))
    @@ -70,7 +75,7 @@

    Module supertokens_python.recipe.jwt.syncio

    Functions

    -def create_jwt(payload: Optional[Dict[str, Any]] = None, validity_seconds: Optional[None] = None, user_context: Optional[Dict[str, Any]] = None) ‑> Union[CreateJwtOkResultCreateJwtResultUnsupportedAlgorithm] +def create_jwt(payload: Optional[Dict[str, Any]] = None, validity_seconds: Optional[int] = None, use_static_signing_key: Optional[bool] = None, user_context: Optional[Dict[str, Any]] = None) ‑> Union[CreateJwtOkResultCreateJwtResultUnsupportedAlgorithm]
    @@ -79,11 +84,16 @@

    Functions

    Expand source code
    def create_jwt(
    -    payload: Union[None, Dict[str, Any]] = None,
    -    validity_seconds: Union[None, int] = None,
    -    user_context: Union[Dict[str, Any], None] = None,
    +    payload: Optional[Dict[str, Any]] = None,
    +    validity_seconds: Optional[int] = None,
    +    use_static_signing_key: Optional[bool] = None,
    +    user_context: Optional[Dict[str, Any]] = None,
     ) -> Union[CreateJwtOkResult, CreateJwtResultUnsupportedAlgorithm]:
    -    return sync(asyncio.create_jwt(payload, validity_seconds, user_context))
    + return sync( + asyncio.create_jwt( + payload, validity_seconds, use_static_signing_key, user_context + ) + )
    @@ -95,7 +105,7 @@

    Functions

    Expand source code -
    def get_jwks(user_context: Union[Dict[str, Any], None] = None) -> GetJWKSResult:
    +
    def get_jwks(user_context: Optional[Dict[str, Any]] = None) -> GetJWKSResult:
         return sync(asyncio.get_jwks(user_context))
    diff --git a/html/supertokens_python/recipe/openid/asyncio/index.html b/html/supertokens_python/recipe/openid/asyncio/index.html index 878c67ec0..5e595dfd0 100644 --- a/html/supertokens_python/recipe/openid/asyncio/index.html +++ b/html/supertokens_python/recipe/openid/asyncio/index.html @@ -39,7 +39,7 @@

    Module supertokens_python.recipe.openid.asyncioModule supertokens_python.recipe.openid.asyncioModule supertokens_python.recipe.openid.asyncioModule supertokens_python.recipe.openid.asyncioModule supertokens_python.recipe.openid.asyncioFunctions

    -async def create_jwt(payload: Optional[Dict[str, Any]] = None, validity_seconds: Optional[None] = None, user_context: Optional[Dict[str, Any]] = None) ‑> Union[CreateJwtOkResultCreateJwtResultUnsupportedAlgorithm] +async def create_jwt(payload: Optional[Dict[str, Any]] = None, validity_seconds: Optional[int] = None, use_static_signing_key: Optional[bool] = None, user_context: Optional[Dict[str, Any]] = None) ‑> Union[CreateJwtOkResultCreateJwtResultUnsupportedAlgorithm]
    @@ -103,9 +104,10 @@

    Functions

    Expand source code
    async def create_jwt(
    -    payload: Union[None, Dict[str, Any]] = None,
    -    validity_seconds: Union[None, int] = None,
    -    user_context: Union[Dict[str, Any], None] = None,
    +    payload: Optional[Dict[str, Any]] = None,
    +    validity_seconds: Optional[int] = None,
    +    use_static_signing_key: Optional[bool] = None,
    +    user_context: Optional[Dict[str, Any]] = None,
     ) -> Union[CreateJwtOkResult, CreateJwtResultUnsupportedAlgorithm]:
         if user_context is None:
             user_context = {}
    @@ -113,7 +115,7 @@ 

    Functions

    payload = {} return await OpenIdRecipe.get_instance().recipe_implementation.create_jwt( - payload, validity_seconds, user_context + payload, validity_seconds, use_static_signing_key, user_context )
    @@ -126,7 +128,7 @@

    Functions

    Expand source code -
    async def get_jwks(user_context: Union[Dict[str, Any], None] = None) -> GetJWKSResult:
    +
    async def get_jwks(user_context: Optional[Dict[str, Any]] = None) -> GetJWKSResult:
         if user_context is None:
             user_context = {}
         return await OpenIdRecipe.get_instance().recipe_implementation.get_jwks(
    @@ -144,7 +146,7 @@ 

    Functions

    Expand source code
    async def get_open_id_discovery_configuration(
    -    user_context: Union[Dict[str, Any], None] = None
    +    user_context: Optional[Dict[str, Any]] = None
     ) -> GetOpenIdDiscoveryConfigurationResult:
         if user_context is None:
             user_context = {}
    diff --git a/html/supertokens_python/recipe/openid/interfaces.html b/html/supertokens_python/recipe/openid/interfaces.html
    index 87f97a3a1..dfa934768 100644
    --- a/html/supertokens_python/recipe/openid/interfaces.html
    +++ b/html/supertokens_python/recipe/openid/interfaces.html
    @@ -40,7 +40,7 @@ 

    Module supertokens_python.recipe.openid.interfacesModule supertokens_python.recipe.openid.interfacesMethods

    async def create_jwt( self, payload: Dict[str, Any], - validity_seconds: Union[int, None], + validity_seconds: Optional[int], + use_static_signing_key: Optional[bool], user_context: Dict[str, Any], ) -> Union[CreateJwtOkResult, CreateJwtResultUnsupportedAlgorithm]: pass @@ -309,7 +311,7 @@

    Subclasses

    Methods

    -async def create_jwt(self, payload: Dict[str, Any], validity_seconds: Optional[None], user_context: Dict[str, Any]) ‑> Union[CreateJwtOkResultCreateJwtResultUnsupportedAlgorithm] +async def create_jwt(self, payload: Dict[str, Any], validity_seconds: Optional[int], use_static_signing_key: Optional[bool], user_context: Dict[str, Any]) ‑> Union[CreateJwtOkResultCreateJwtResultUnsupportedAlgorithm]
    @@ -321,7 +323,8 @@

    Methods

    async def create_jwt( self, payload: Dict[str, Any], - validity_seconds: Union[int, None], + validity_seconds: Optional[int], + use_static_signing_key: Optional[bool], user_context: Dict[str, Any], ) -> Union[CreateJwtOkResult, CreateJwtResultUnsupportedAlgorithm]: pass
    diff --git a/html/supertokens_python/recipe/openid/recipe_implementation.html b/html/supertokens_python/recipe/openid/recipe_implementation.html index e69212ca2..614da5d80 100644 --- a/html/supertokens_python/recipe/openid/recipe_implementation.html +++ b/html/supertokens_python/recipe/openid/recipe_implementation.html @@ -41,7 +41,7 @@

    Module supertokens_python.recipe.openid.recipe_implement # under the License. from __future__ import annotations -from typing import TYPE_CHECKING, Any, Dict, Union +from typing import TYPE_CHECKING, Any, Dict, Union, Optional from supertokens_python.querier import Querier @@ -97,7 +97,8 @@

    Module supertokens_python.recipe.openid.recipe_implement async def create_jwt( self, payload: Dict[str, Any], - validity_seconds: Union[int, None], + validity_seconds: Optional[int], + use_static_signing_key: Optional[bool], user_context: Dict[str, Any], ) -> Union[CreateJwtOkResult, CreateJwtResultUnsupportedAlgorithm]: issuer = ( @@ -106,7 +107,7 @@

    Module supertokens_python.recipe.openid.recipe_implement ) payload = {"iss": issuer, **payload} return await self.jwt_recipe_implementation.create_jwt( - payload, validity_seconds, user_context + payload, validity_seconds, use_static_signing_key, user_context ) async def get_jwks(self, user_context: Dict[str, Any]) -> GetJWKSResult: @@ -167,7 +168,8 @@

    Classes

    async def create_jwt( self, payload: Dict[str, Any], - validity_seconds: Union[int, None], + validity_seconds: Optional[int], + use_static_signing_key: Optional[bool], user_context: Dict[str, Any], ) -> Union[CreateJwtOkResult, CreateJwtResultUnsupportedAlgorithm]: issuer = ( @@ -176,7 +178,7 @@

    Classes

    ) payload = {"iss": issuer, **payload} return await self.jwt_recipe_implementation.create_jwt( - payload, validity_seconds, user_context + payload, validity_seconds, use_static_signing_key, user_context ) async def get_jwks(self, user_context: Dict[str, Any]) -> GetJWKSResult: @@ -190,7 +192,7 @@

    Ancestors

    Methods

    -async def create_jwt(self, payload: Dict[str, Any], validity_seconds: Union[int, None], user_context: Dict[str, Any]) ‑> Union[CreateJwtOkResult, CreateJwtResultUnsupportedAlgorithm] +async def create_jwt(self, payload: Dict[str, Any], validity_seconds: Optional[int], use_static_signing_key: Optional[bool], user_context: Dict[str, Any]) ‑> Union[CreateJwtOkResult, CreateJwtResultUnsupportedAlgorithm]
    @@ -201,7 +203,8 @@

    Methods

    async def create_jwt(
         self,
         payload: Dict[str, Any],
    -    validity_seconds: Union[int, None],
    +    validity_seconds: Optional[int],
    +    use_static_signing_key: Optional[bool],
         user_context: Dict[str, Any],
     ) -> Union[CreateJwtOkResult, CreateJwtResultUnsupportedAlgorithm]:
         issuer = (
    @@ -210,7 +213,7 @@ 

    Methods

    ) payload = {"iss": issuer, **payload} return await self.jwt_recipe_implementation.create_jwt( - payload, validity_seconds, user_context + payload, validity_seconds, use_static_signing_key, user_context )
    diff --git a/html/supertokens_python/recipe/openid/syncio/index.html b/html/supertokens_python/recipe/openid/syncio/index.html index f152bc9e0..d35d2a0da 100644 --- a/html/supertokens_python/recipe/openid/syncio/index.html +++ b/html/supertokens_python/recipe/openid/syncio/index.html @@ -39,7 +39,7 @@

    Module supertokens_python.recipe.openid.syncioModule supertokens_python.recipe.openid.syncio

    @@ -80,7 +85,7 @@

    Module supertokens_python.recipe.openid.syncioFunctions

    -def create_jwt(payload: Optional[Dict[str, Any]] = None, validity_seconds: Optional[None] = None, user_context: Optional[Dict[str, Any]] = None) ‑> Union[CreateJwtOkResultCreateJwtResultUnsupportedAlgorithm] +def create_jwt(payload: Optional[Dict[str, Any]] = None, validity_seconds: Optional[int] = None, use_static_signing_key: Optional[bool] = None, user_context: Optional[Dict[str, Any]] = None) ‑> Union[CreateJwtOkResultCreateJwtResultUnsupportedAlgorithm]
    @@ -89,11 +94,16 @@

    Functions

    Expand source code
    def create_jwt(
    -    payload: Union[None, Dict[str, Any]] = None,
    -    validity_seconds: Union[None, int] = None,
    -    user_context: Union[Dict[str, Any], None] = None,
    +    payload: Optional[Dict[str, Any]] = None,
    +    validity_seconds: Optional[int] = None,
    +    use_static_signing_key: Optional[bool] = None,
    +    user_context: Optional[Dict[str, Any]] = None,
     ) -> Union[CreateJwtOkResult, CreateJwtResultUnsupportedAlgorithm]:
    -    return sync(asyncio.create_jwt(payload, validity_seconds, user_context))
    + return sync( + asyncio.create_jwt( + payload, validity_seconds, use_static_signing_key, user_context + ) + )
    @@ -105,7 +115,7 @@

    Functions

    Expand source code -
    def get_jwks(user_context: Union[Dict[str, Any], None] = None) -> GetJWKSResult:
    +
    def get_jwks(user_context: Optional[Dict[str, Any]] = None) -> GetJWKSResult:
         return sync(asyncio.get_jwks(user_context))
    @@ -119,7 +129,7 @@

    Functions

    Expand source code
    def get_open_id_discovery_configuration(
    -    user_context: Union[Dict[str, Any], None] = None
    +    user_context: Optional[Dict[str, Any]] = None
     ) -> GetOpenIdDiscoveryConfigurationResult:
         return sync(asyncio.get_open_id_discovery_configuration(user_context))
    diff --git a/html/supertokens_python/recipe/session/access_token.html b/html/supertokens_python/recipe/session/access_token.html index a7432f854..bd49f6846 100644 --- a/html/supertokens_python/recipe/session/access_token.html +++ b/html/supertokens_python/recipe/session/access_token.html @@ -41,13 +41,17 @@

    Module supertokens_python.recipe.session.access_tokenModule supertokens_python.recipe.session.access_tokenModule supertokens_python.recipe.session.access_tokenModule supertokens_python.recipe.session.access_tokenFunctions

    -def get_info_from_access_token(jwt_info: ParsedJWTInfo, jwt_signing_public_key: str, do_anti_csrf_check: bool) +def get_info_from_access_token(jwt_info: ParsedJWTInfo, jwk_clients: List[JWKClient], do_anti_csrf_check: bool)
    @@ -143,24 +201,68 @@

    Functions

    Expand source code
    def get_info_from_access_token(
    -    jwt_info: ParsedJWTInfo, jwt_signing_public_key: str, do_anti_csrf_check: bool
    +    jwt_info: ParsedJWTInfo,
    +    jwk_clients: List[JWKClient],
    +    do_anti_csrf_check: bool,
     ):
         try:
    -        verify_jwt(jwt_info, jwt_signing_public_key)
    -        payload = jwt_info.payload
    +        payload: Optional[Dict[str, Any]] = None
    +        client: Optional[JWKClient] = None
    +        keys: Optional[List[PyJWK]] = None
    +
    +        # Get the keys from the first available client
    +        for c in jwk_clients:
    +            try:
    +                client = c
    +                keys = c.get_latest_keys()
    +                break
    +            except JWKSRequestError:
    +                continue
    +
    +        if keys is None or client is None:
    +            raise PyJWKClientError("No key found")
    +
    +        if jwt_info.version < 3:
    +            # It won't have kid. So we'll have to try the token against all the keys from all the jwk_clients
    +            # If any of them work, we'll use that payload
    +            for k in keys:
    +                try:
    +                    payload = jwt.decode(jwt_info.raw_token_string, k.key, algorithms=["RS256"])  # type: ignore
    +                    break
    +                except DecodeError:
    +                    pass
    +
    +        elif jwt_info.version >= 3:
    +            matching_key = client.get_matching_key_from_jwt(jwt_info.raw_token_string)
    +            payload = jwt.decode(  # type: ignore
    +                jwt_info.raw_token_string,
    +                matching_key.key,  # type: ignore
    +                algorithms=["RS256"],
    +                options={"verify_signature": True, "verify_exp": True},
    +            )
    +
    +        if payload is None:
    +            raise DecodeError("Could not decode the token")
    +
    +        validate_access_token_structure(payload, jwt_info.version)
     
    -        validate_access_token_structure(payload)
    +        if jwt_info.version == 2:
    +            user_id = sanitize_string(payload.get("userId"))
    +            expiry_time = sanitize_number(payload.get("expiryTime"))
    +            time_created = sanitize_number(payload.get("timeCreated"))
    +            user_data = payload.get("userData")
    +        else:
    +            user_id = sanitize_string(payload.get("sub"))
    +            expiry_time = sanitize_number(payload.get("exp", 0) * 1000)
    +            time_created = sanitize_number(payload.get("iat", 0) * 1000)
    +            user_data = payload
     
             session_handle = sanitize_string(payload.get("sessionHandle"))
    -        user_id = sanitize_string(payload.get("userId"))
             refresh_token_hash_1 = sanitize_string(payload.get("refreshTokenHash1"))
             parent_refresh_token_hash_1 = sanitize_string(
                 payload.get("parentRefreshTokenHash1")
             )
    -        user_data = payload.get("userData")
             anti_csrf_token = sanitize_string(payload.get("antiCsrfToken"))
    -        expiry_time = sanitize_number(payload.get("expiryTime"))
    -        time_created = sanitize_number(payload.get("timeCreated"))
     
             if anti_csrf_token is None and do_anti_csrf_check:
                 raise Exception("Access token does not contain the anti-csrf token")
    @@ -197,8 +299,7 @@ 

    Functions

    Expand source code
    def sanitize_number(n: Any) -> Union[Union[int, float], None]:
    -    _type = type(n)
    -    if _type == int or _type == float:  # pylint: disable=consider-using-in
    +    if isinstance(n, (int, float)):
             return n
     
         return None
    @@ -224,7 +325,7 @@

    Functions

    -def validate_access_token_structure(payload: Dict[str, Any]) ‑> None +def validate_access_token_structure(payload: Dict[str, Any], version: int) ‑> None
    @@ -232,8 +333,19 @@

    Functions

    Expand source code -
    def validate_access_token_structure(payload: Dict[str, Any]) -> None:
    -    if (
    +
    def validate_access_token_structure(payload: Dict[str, Any], version: int) -> None:
    +    if version >= 3:
    +        if (
    +            not isinstance(payload.get("sub"), str)
    +            or not isinstance(payload.get("exp"), int)
    +            or not isinstance(payload.get("iat"), int)
    +            or not isinstance(payload.get("sessionHandle"), str)
    +            or not isinstance(payload.get("refreshTokenHash1"), str)
    +        ):
    +            raise Exception(
    +                "Access token does not contain all the information. Maybe the structure has changed?"
    +            )
    +    elif (
             not isinstance(payload.get("sessionHandle"), str)
             or payload.get("userData") is None
             or not isinstance(payload.get("refreshTokenHash1"), str)
    diff --git a/html/supertokens_python/recipe/session/api/implementation.html b/html/supertokens_python/recipe/session/api/implementation.html
    index a3780af88..8ce30237f 100644
    --- a/html/supertokens_python/recipe/session/api/implementation.html
    +++ b/html/supertokens_python/recipe/session/api/implementation.html
    @@ -52,21 +52,27 @@ 

    Module supertokens_python.recipe.session.api.implementat from supertokens_python.types import MaybeAwaitable from supertokens_python.utils import normalise_http_method -from ..utils import get_required_claim_validators - if TYPE_CHECKING: from supertokens_python.recipe.session.interfaces import APIOptions from ..interfaces import SessionContainer from typing import Any, Dict +from ..session_request_functions import ( + get_session_from_request, + refresh_session_in_request, +) + class APIImplementation(APIInterface): async def refresh_post( self, api_options: APIOptions, user_context: Dict[str, Any] ) -> SessionContainer: - return await api_options.recipe_implementation.refresh_session( - api_options.request, user_context + return await refresh_session_in_request( + api_options.request, + user_context, + api_options.config, + api_options.recipe_implementation, ) async def signout_post( @@ -97,26 +103,24 @@

    Module supertokens_python.recipe.session.api.implementat return None incoming_path = NormalisedURLPath(api_options.request.get_path()) refresh_token_path = api_options.config.refresh_token_path - if incoming_path.equals(refresh_token_path) and method == "post": - return await api_options.recipe_implementation.refresh_session( - api_options.request, user_context - ) - session = await api_options.recipe_implementation.get_session( - api_options.request, - anti_csrf_check, - session_required, - user_context, - ) - if session is not None: - claim_validators = await get_required_claim_validators( - session, - override_global_claim_validators, + if incoming_path.equals(refresh_token_path) and method == "post": + return await refresh_session_in_request( + api_options.request, user_context, + api_options.config, + api_options.recipe_implementation, ) - await session.assert_claims(claim_validators, user_context) - return session

    + return await get_session_from_request( + api_options.request, + api_options.config, + api_options.recipe_implementation, + session_required=session_required, + anti_csrf_check=anti_csrf_check, + override_global_claim_validators=override_global_claim_validators, + user_context=user_context, + )
    @@ -142,8 +146,11 @@

    Classes

    async def refresh_post( self, api_options: APIOptions, user_context: Dict[str, Any] ) -> SessionContainer: - return await api_options.recipe_implementation.refresh_session( - api_options.request, user_context + return await refresh_session_in_request( + api_options.request, + user_context, + api_options.config, + api_options.recipe_implementation, ) async def signout_post( @@ -174,26 +181,24 @@

    Classes

    return None incoming_path = NormalisedURLPath(api_options.request.get_path()) refresh_token_path = api_options.config.refresh_token_path - if incoming_path.equals(refresh_token_path) and method == "post": - return await api_options.recipe_implementation.refresh_session( - api_options.request, user_context - ) - session = await api_options.recipe_implementation.get_session( - api_options.request, - anti_csrf_check, - session_required, - user_context, - ) - if session is not None: - claim_validators = await get_required_claim_validators( - session, - override_global_claim_validators, + if incoming_path.equals(refresh_token_path) and method == "post": + return await refresh_session_in_request( + api_options.request, user_context, + api_options.config, + api_options.recipe_implementation, ) - await session.assert_claims(claim_validators, user_context) - return session
    + return await get_session_from_request( + api_options.request, + api_options.config, + api_options.recipe_implementation, + session_required=session_required, + anti_csrf_check=anti_csrf_check, + override_global_claim_validators=override_global_claim_validators, + user_context=user_context, + )

    Ancestors

      @@ -214,8 +219,11 @@

      Methods

      async def refresh_post(
           self, api_options: APIOptions, user_context: Dict[str, Any]
       ) -> SessionContainer:
      -    return await api_options.recipe_implementation.refresh_session(
      -        api_options.request, user_context
      +    return await refresh_session_in_request(
      +        api_options.request,
      +        user_context,
      +        api_options.config,
      +        api_options.recipe_implementation,
           )
      @@ -266,26 +274,24 @@

      Methods

      return None incoming_path = NormalisedURLPath(api_options.request.get_path()) refresh_token_path = api_options.config.refresh_token_path - if incoming_path.equals(refresh_token_path) and method == "post": - return await api_options.recipe_implementation.refresh_session( - api_options.request, user_context - ) - session = await api_options.recipe_implementation.get_session( - api_options.request, - anti_csrf_check, - session_required, - user_context, - ) - if session is not None: - claim_validators = await get_required_claim_validators( - session, - override_global_claim_validators, + if incoming_path.equals(refresh_token_path) and method == "post": + return await refresh_session_in_request( + api_options.request, user_context, + api_options.config, + api_options.recipe_implementation, ) - await session.assert_claims(claim_validators, user_context) - return session
      + return await get_session_from_request( + api_options.request, + api_options.config, + api_options.recipe_implementation, + session_required=session_required, + anti_csrf_check=anti_csrf_check, + override_global_claim_validators=override_global_claim_validators, + user_context=user_context, + )
    diff --git a/html/supertokens_python/recipe/session/api/signout.html b/html/supertokens_python/recipe/session/api/signout.html index 90ffc1b5f..0d27fe3f7 100644 --- a/html/supertokens_python/recipe/session/api/signout.html +++ b/html/supertokens_python/recipe/session/api/signout.html @@ -43,8 +43,15 @@

    Module supertokens_python.recipe.session.api.signoutModule supertokens_python.recipe.session.api.signoutFunctions

    or api_implementation.signout_post is None ): return None + user_context = default_user_context(api_options.request) - session = await api_options.recipe_implementation.get_session( - request=api_options.request, - anti_csrf_check=None, + session = await get_session_from_request( + api_options.request, + api_options.config, + api_options.recipe_implementation, session_required=False, + override_global_claim_validators=lambda _, __, ___: [], user_context=user_context, ) diff --git a/html/supertokens_python/recipe/session/asyncio/index.html b/html/supertokens_python/recipe/session/asyncio/index.html index d6296d30f..b144ecb40 100644 --- a/html/supertokens_python/recipe/session/asyncio/index.html +++ b/html/supertokens_python/recipe/session/asyncio/index.html @@ -39,31 +39,37 @@

    Module supertokens_python.recipe.session.asyncio< # 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.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, + SessionInformationResult, ) from supertokens_python.recipe.session.recipe import SessionRecipe from supertokens_python.types import MaybeAwaitable -from supertokens_python.utils import FRAMEWORKS, resolve, deprecated_warn -from ..utils import get_required_claim_validators +from supertokens_python.utils import FRAMEWORKS, resolve + 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") @@ -72,13 +78,43 @@

    Module supertokens_python.recipe.session.asyncio< request: Any, user_id: str, access_token_payload: Union[Dict[str, Any], None] = None, - session_data: Union[Dict[str, Any], None] = None, + session_data_in_database: Union[Dict[str, Any], None] = None, + user_context: Union[None, Dict[str, Any]] = None, +) -> SessionContainer: + if user_context is None: + user_context = {} + if session_data_in_database is None: + session_data_in_database = {} + if access_token_payload is None: + access_token_payload = {} + + recipe_instance = SessionRecipe.get_instance() + config = recipe_instance.config + app_info = recipe_instance.app_info + + return await create_new_session_in_request( + request, + user_context, + recipe_instance, + access_token_payload, + user_id, + config, + app_info, + session_data_in_database, + ) + + +async def create_new_session_without_request_response( + user_id: str, + access_token_payload: Union[Dict[str, Any], None] = None, + session_data_in_database: Union[Dict[str, Any], None] = None, + disable_anti_csrf: bool = False, user_context: Union[None, Dict[str, Any]] = None, ) -> SessionContainer: if user_context is None: user_context = {} - if session_data is None: - session_data = {} + if session_data_in_database is None: + session_data_in_database = {} if access_token_payload is None: access_token_payload = {} @@ -91,16 +127,11 @@

    Module supertokens_python.recipe.session.asyncio< update = await claim.build(user_id, user_context) final_access_token_payload = {**final_access_token_payload, **update} - if not hasattr(request, "wrapper_used") or not request.wrapper_used: - request = FRAMEWORKS[ - SessionRecipe.get_instance().app_info.framework - ].wrap_request(request) - return await SessionRecipe.get_instance().recipe_implementation.create_new_session( - request, user_id, final_access_token_payload, - session_data, + session_data_in_database, + disable_anti_csrf, user_context=user_context, ) @@ -152,7 +183,7 @@

    Module supertokens_python.recipe.session.asyncio< claim_validation_res = await recipe_impl.validate_claims( session_info.user_id, - session_info.access_token_payload, + session_info.custom_claims_in_access_token_payload, claim_validators, user_context, ) @@ -265,8 +296,9 @@

    Module supertokens_python.recipe.session.asyncio< async def get_session( request: Any, - anti_csrf_check: Union[bool, None] = None, - session_required: bool = True, + session_required: Optional[bool] = None, + anti_csrf_check: Optional[bool] = None, + check_database: Optional[bool] = None, override_global_claim_validators: Optional[ Callable[ [List[SessionClaimValidator], SessionContainer, Dict[str, Any]], @@ -277,16 +309,78 @@

    Module supertokens_python.recipe.session.asyncio< ) -> Union[SessionContainer, None]: if user_context is None: user_context = {} - if not hasattr(request, "wrapper_used") or not request.wrapper_used: - request = FRAMEWORKS[ - SessionRecipe.get_instance().app_info.framework - ].wrap_request(request) - session_recipe_impl = SessionRecipe.get_instance().recipe_implementation - session = await session_recipe_impl.get_session( + if session_required is None: + session_required = True + + recipe_instance = SessionRecipe.get_instance() + recipe_interface_impl = recipe_instance.recipe_implementation + config = recipe_instance.config + + return await get_session_from_request( request, + config, + recipe_interface_impl, + session_required=session_required, + anti_csrf_check=anti_csrf_check, + check_database=check_database, + override_global_claim_validators=override_global_claim_validators, + user_context=user_context, + ) + + +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[ + [List[SessionClaimValidator], SessionContainer, Dict[str, Any]], + MaybeAwaitable[List[SessionClaimValidator]], + ] + ] = None, + user_context: Union[None, Dict[str, Any]] = None, +) -> Optional[SessionContainer]: + """Tries to validate an access token and build a Session object from it. + + Notes about anti-csrf checking: + - if the `antiCsrf` is set to VIA_HEADER in the Session recipe config you have to handle anti-csrf checking before calling this function and set antiCsrfCheck to false in the options. + - you can disable anti-csrf checks by setting antiCsrf to NONE in the Session recipe config. We only recommend this if you are always getting the access-token from the Authorization header. + - if the antiCsrf check fails the returned status will be TRY_REFRESH_TOKEN_ERROR + + 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. 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 + + 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 + + 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, ) @@ -300,17 +394,40 @@

    Module supertokens_python.recipe.session.asyncio< async def refresh_session( - request: Any, user_context: Union[None, Dict[str, Any]] = None + request: Any, + user_context: Union[None, Dict[str, Any]] = None, ) -> SessionContainer: if user_context is None: user_context = {} + if not hasattr(request, "wrapper_used") or not request.wrapper_used: request = FRAMEWORKS[ SessionRecipe.get_instance().app_info.framework ].wrap_request(request) + recipe_instance = SessionRecipe.get_instance() + config = recipe_instance.config + recipe_interface_impl = recipe_instance.recipe_implementation + + return await refresh_session_in_request( + request, + user_context, + config, + recipe_interface_impl, + ) + + +async def refresh_session_without_request_response( + refresh_token: str, + disable_anti_csrf: bool = False, + anti_csrf_token: Optional[str] = None, + user_context: Optional[Dict[str, Any]] = None, +) -> SessionContainer: + if user_context is None: + user_context = {} + return await SessionRecipe.get_instance().recipe_implementation.refresh_session( - request, user_context + refresh_token, anti_csrf_token, disable_anti_csrf, user_context ) @@ -364,35 +481,18 @@

    Module supertokens_python.recipe.session.asyncio< ) -async def update_session_data( +async def update_session_data_in_database( session_handle: str, new_session_data: Dict[str, Any], user_context: Union[None, Dict[str, Any]] = None, ) -> bool: if user_context is None: user_context = {} - return await SessionRecipe.get_instance().recipe_implementation.update_session_data( + return await SessionRecipe.get_instance().recipe_implementation.update_session_data_in_database( session_handle, new_session_data, user_context ) -async def update_access_token_payload( - session_handle: str, - new_access_token_payload: Dict[str, Any], - user_context: Union[None, Dict[str, Any]] = None, -) -> bool: - if user_context is None: - user_context = {} - - deprecated_warn( - "update_access_token_payload is deprecated. Use merge_into_access_token_payload instead" - ) - - return await SessionRecipe.get_instance().recipe_implementation.update_access_token_payload( - session_handle, new_access_token_payload, user_context - ) - - async def merge_into_access_token_payload( session_handle: str, new_access_token_payload: Dict[str, Any], @@ -408,20 +508,16 @@

    Module supertokens_python.recipe.session.asyncio< async def create_jwt( payload: Dict[str, Any], - validity_seconds: Union[None, int] = None, + validity_seconds: Optional[int] = None, + use_static_signing_key: Optional[bool] = None, user_context: Union[None, Dict[str, Any]] = None, ) -> Union[CreateJwtOkResult, CreateJwtResultUnsupportedAlgorithm]: if user_context is None: user_context = {} openid_recipe = SessionRecipe.get_instance().openid_recipe - if openid_recipe is not None: - return await openid_recipe.recipe_implementation.create_jwt( - payload, validity_seconds, user_context=user_context - ) - - raise Exception( - "create_jwt cannot be used without enabling the JWT feature. Please set 'enable: True' for jwt config when initialising the Session recipe" + return await openid_recipe.recipe_implementation.create_jwt( + payload, validity_seconds, use_static_signing_key, user_context ) @@ -429,12 +525,7 @@

    Module supertokens_python.recipe.session.asyncio< if user_context is None: user_context = {} openid_recipe = SessionRecipe.get_instance().openid_recipe - if openid_recipe is not None: - return await openid_recipe.recipe_implementation.get_jwks(user_context) - - raise Exception( - "get_jwks cannot be used without enabling the JWT feature. Please set 'enable: True' for jwt config when initialising the Session recipe" - ) + return await openid_recipe.recipe_implementation.get_jwks(user_context) async def get_open_id_discovery_configuration( @@ -444,13 +535,10 @@

    Module supertokens_python.recipe.session.asyncio< user_context = {} openid_recipe = SessionRecipe.get_instance().openid_recipe - if openid_recipe is not None: - return await openid_recipe.recipe_implementation.get_open_id_discovery_configuration( + return ( + await openid_recipe.recipe_implementation.get_open_id_discovery_configuration( user_context ) - - raise Exception( - "get_open_id_discovery_configuration cannot be used without enabling the JWT feature. Please set 'enable: True' for jwt config when initialising the Session recipe" ) @@ -474,7 +562,7 @@

    Module supertokens_python.recipe.session.asyncio<

    Functions

    -async def create_jwt(payload: Dict[str, Any], validity_seconds: Optional[None] = None, user_context: Optional[Dict[str, Any]] = None) ‑> Union[CreateJwtOkResultCreateJwtResultUnsupportedAlgorithm] +async def create_jwt(payload: Dict[str, Any], validity_seconds: Optional[int] = None, use_static_signing_key: Optional[bool] = None, user_context: Optional[Dict[str, Any]] = None) ‑> Union[CreateJwtOkResultCreateJwtResultUnsupportedAlgorithm]
    @@ -484,25 +572,21 @@

    Functions

    async def create_jwt(
         payload: Dict[str, Any],
    -    validity_seconds: Union[None, int] = None,
    +    validity_seconds: Optional[int] = None,
    +    use_static_signing_key: Optional[bool] = None,
         user_context: Union[None, Dict[str, Any]] = None,
     ) -> Union[CreateJwtOkResult, CreateJwtResultUnsupportedAlgorithm]:
         if user_context is None:
             user_context = {}
         openid_recipe = SessionRecipe.get_instance().openid_recipe
     
    -    if openid_recipe is not None:
    -        return await openid_recipe.recipe_implementation.create_jwt(
    -            payload, validity_seconds, user_context=user_context
    -        )
    -
    -    raise Exception(
    -        "create_jwt cannot be used without enabling the JWT feature. Please set 'enable: True' for jwt config when initialising the Session recipe"
    +    return await openid_recipe.recipe_implementation.create_jwt(
    +        payload, validity_seconds, use_static_signing_key, user_context
         )
    -async def create_new_session(request: Any, user_id: str, access_token_payload: Optional[Dict[str, Any]] = None, session_data: Optional[Dict[str, Any]] = None, user_context: Optional[Dict[str, Any]] = None) ‑> SessionContainer +async def create_new_session(request: Any, user_id: str, access_token_payload: Optional[Dict[str, Any]] = None, session_data_in_database: Optional[Dict[str, Any]] = None, user_context: Optional[Dict[str, Any]] = None) ‑> SessionContainer
    @@ -514,13 +598,52 @@

    Functions

    request: Any, user_id: str, access_token_payload: Union[Dict[str, Any], None] = None, - session_data: Union[Dict[str, Any], None] = None, + session_data_in_database: Union[Dict[str, Any], None] = None, user_context: Union[None, Dict[str, Any]] = None, ) -> SessionContainer: if user_context is None: user_context = {} - if session_data is None: - session_data = {} + if session_data_in_database is None: + session_data_in_database = {} + if access_token_payload is None: + access_token_payload = {} + + recipe_instance = SessionRecipe.get_instance() + config = recipe_instance.config + app_info = recipe_instance.app_info + + return await create_new_session_in_request( + request, + user_context, + recipe_instance, + access_token_payload, + user_id, + config, + app_info, + session_data_in_database, + )
    + +
    +
    +async def create_new_session_without_request_response(user_id: str, access_token_payload: Optional[Dict[str, Any]] = None, session_data_in_database: Optional[Dict[str, Any]] = None, disable_anti_csrf: bool = False, user_context: Optional[Dict[str, Any]] = None) ‑> SessionContainer +
    +
    +
    +
    + +Expand source code + +
    async def create_new_session_without_request_response(
    +    user_id: str,
    +    access_token_payload: Union[Dict[str, Any], None] = None,
    +    session_data_in_database: Union[Dict[str, Any], None] = None,
    +    disable_anti_csrf: bool = False,
    +    user_context: Union[None, Dict[str, Any]] = None,
    +) -> SessionContainer:
    +    if user_context is None:
    +        user_context = {}
    +    if session_data_in_database is None:
    +        session_data_in_database = {}
         if access_token_payload is None:
             access_token_payload = {}
     
    @@ -533,16 +656,11 @@ 

    Functions

    update = await claim.build(user_id, user_context) final_access_token_payload = {**final_access_token_payload, **update} - if not hasattr(request, "wrapper_used") or not request.wrapper_used: - request = FRAMEWORKS[ - SessionRecipe.get_instance().app_info.framework - ].wrap_request(request) - return await SessionRecipe.get_instance().recipe_implementation.create_new_session( - request, user_id, final_access_token_payload, - session_data, + session_data_in_database, + disable_anti_csrf, user_context=user_context, )
    @@ -621,12 +739,7 @@

    Functions

    if user_context is None: user_context = {} openid_recipe = SessionRecipe.get_instance().openid_recipe - if openid_recipe is not None: - return await openid_recipe.recipe_implementation.get_jwks(user_context) - - raise Exception( - "get_jwks cannot be used without enabling the JWT feature. Please set 'enable: True' for jwt config when initialising the Session recipe" - )
    + return await openid_recipe.recipe_implementation.get_jwks(user_context)
    @@ -645,18 +758,15 @@

    Functions

    user_context = {} openid_recipe = SessionRecipe.get_instance().openid_recipe - if openid_recipe is not None: - return await openid_recipe.recipe_implementation.get_open_id_discovery_configuration( + return ( + await openid_recipe.recipe_implementation.get_open_id_discovery_configuration( user_context ) - - raise Exception( - "get_open_id_discovery_configuration cannot be used without enabling the JWT feature. Please set 'enable: True' for jwt config when initialising the Session recipe" )
    -async def get_session(request: Any, anti_csrf_check: Optional[bool] = None, session_required: bool = True, override_global_claim_validators: Optional[Callable[[List[SessionClaimValidator], SessionContainer, Dict[str, Any]], Union[Awaitable[List[SessionClaimValidator]], List[SessionClaimValidator]]]] = None, user_context: Optional[Dict[str, Any]] = None) ‑> Optional[SessionContainer] +async def get_session(request: Any, session_required: Optional[bool] = None, anti_csrf_check: Optional[bool] = None, check_database: Optional[bool] = None, override_global_claim_validators: Optional[Callable[[List[SessionClaimValidator], SessionContainer, Dict[str, Any]], Union[Awaitable[List[SessionClaimValidator]], List[SessionClaimValidator]]]] = None, user_context: Optional[Dict[str, Any]] = None) ‑> Optional[SessionContainer]
    @@ -666,8 +776,9 @@

    Functions

    async def get_session(
         request: Any,
    -    anti_csrf_check: Union[bool, None] = None,
    -    session_required: bool = True,
    +    session_required: Optional[bool] = None,
    +    anti_csrf_check: Optional[bool] = None,
    +    check_database: Optional[bool] = None,
         override_global_claim_validators: Optional[
             Callable[
                 [List[SessionClaimValidator], SessionContainer, Dict[str, Any]],
    @@ -678,26 +789,24 @@ 

    Functions

    ) -> Union[SessionContainer, None]: if user_context is None: user_context = {} - if not hasattr(request, "wrapper_used") or not request.wrapper_used: - request = FRAMEWORKS[ - SessionRecipe.get_instance().app_info.framework - ].wrap_request(request) - session_recipe_impl = SessionRecipe.get_instance().recipe_implementation - session = await session_recipe_impl.get_session( - request, - anti_csrf_check, - session_required, - user_context, - ) + if session_required is None: + session_required = True - if session is not None: - claim_validators = await get_required_claim_validators( - session, override_global_claim_validators, user_context - ) - await session.assert_claims(claim_validators, user_context) + recipe_instance = SessionRecipe.get_instance() + recipe_interface_impl = recipe_instance.recipe_implementation + config = recipe_instance.config - return session
    + return await get_session_from_request( + request, + config, + recipe_interface_impl, + session_required=session_required, + anti_csrf_check=anti_csrf_check, + check_database=check_database, + override_global_claim_validators=override_global_claim_validators, + user_context=user_context, + )
    @@ -719,6 +828,97 @@

    Functions

    )
    +
    +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[[List[SessionClaimValidator], SessionContainer, Dict[str, Any]], Union[Awaitable[List[SessionClaimValidator]], List[SessionClaimValidator]]]] = None, user_context: Optional[Dict[str, Any]] = None) ‑> Optional[SessionContainer] +
    +
    +

    Tries to validate an access token and build a Session object from it.

    +

    Notes about anti-csrf checking: +- if the antiCsrf is set to VIA_HEADER in the Session recipe config you have to handle anti-csrf checking before calling this function and set antiCsrfCheck to false in the options. +- you can disable anti-csrf checks by setting antiCsrf to NONE in the Session recipe config. We only recommend this if you are always getting the access-token from the Authorization header. +- if the antiCsrf check fails the returned status will be TRY_REFRESH_TOKEN_ERROR

    +

    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. 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

    +

    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.

    +
    + +Expand source code + +
    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[
    +            [List[SessionClaimValidator], SessionContainer, Dict[str, Any]],
    +            MaybeAwaitable[List[SessionClaimValidator]],
    +        ]
    +    ] = None,
    +    user_context: Union[None, Dict[str, Any]] = None,
    +) -> Optional[SessionContainer]:
    +    """Tries to validate an access token and build a Session object from it.
    +
    +    Notes about anti-csrf checking:
    +    - if the `antiCsrf` is set to VIA_HEADER in the Session recipe config you have to handle anti-csrf checking before calling this function and set antiCsrfCheck to false in the options.
    +    - you can disable anti-csrf checks by setting antiCsrf to NONE in the Session recipe config. We only recommend this if you are always getting the access-token from the Authorization header.
    +    - if the antiCsrf check fails the returned status will be TRY_REFRESH_TOKEN_ERROR
    +
    +    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. 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
    +
    +    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
    +
    +    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 session is not None:
    +        claim_validators = await get_required_claim_validators(
    +            session, override_global_claim_validators, user_context
    +        )
    +        await session.assert_claims(claim_validators, user_context)
    +
    +    return session
    +
    +
    async def merge_into_access_token_payload(session_handle: str, new_access_token_payload: Dict[str, Any], user_context: Optional[Dict[str, Any]] = None) ‑> bool
    @@ -751,17 +951,49 @@

    Functions

    Expand source code
    async def refresh_session(
    -    request: Any, user_context: Union[None, Dict[str, Any]] = None
    +    request: Any,
    +    user_context: Union[None, Dict[str, Any]] = None,
     ) -> SessionContainer:
         if user_context is None:
             user_context = {}
    +
         if not hasattr(request, "wrapper_used") or not request.wrapper_used:
             request = FRAMEWORKS[
                 SessionRecipe.get_instance().app_info.framework
             ].wrap_request(request)
     
    +    recipe_instance = SessionRecipe.get_instance()
    +    config = recipe_instance.config
    +    recipe_interface_impl = recipe_instance.recipe_implementation
    +
    +    return await refresh_session_in_request(
    +        request,
    +        user_context,
    +        config,
    +        recipe_interface_impl,
    +    )
    + + +
    +async def refresh_session_without_request_response(refresh_token: str, disable_anti_csrf: bool = False, anti_csrf_token: Optional[str] = None, user_context: Optional[Dict[str, Any]] = None) ‑> SessionContainer +
    +
    +
    +
    + +Expand source code + +
    async def refresh_session_without_request_response(
    +    refresh_token: str,
    +    disable_anti_csrf: bool = False,
    +    anti_csrf_token: Optional[str] = None,
    +    user_context: Optional[Dict[str, Any]] = None,
    +) -> SessionContainer:
    +    if user_context is None:
    +        user_context = {}
    +
         return await SessionRecipe.get_instance().recipe_implementation.refresh_session(
    -        request, user_context
    +        refresh_token, anti_csrf_token, disable_anti_csrf, user_context
         )
    @@ -886,34 +1118,8 @@

    Functions

    ) -
    -async def update_access_token_payload(session_handle: str, new_access_token_payload: Dict[str, Any], user_context: Optional[Dict[str, Any]] = None) ‑> bool -
    -
    -
    -
    - -Expand source code - -
    async def update_access_token_payload(
    -    session_handle: str,
    -    new_access_token_payload: Dict[str, Any],
    -    user_context: Union[None, Dict[str, Any]] = None,
    -) -> bool:
    -    if user_context is None:
    -        user_context = {}
    -
    -    deprecated_warn(
    -        "update_access_token_payload is deprecated. Use merge_into_access_token_payload instead"
    -    )
    -
    -    return await SessionRecipe.get_instance().recipe_implementation.update_access_token_payload(
    -        session_handle, new_access_token_payload, user_context
    -    )
    -
    -
    -
    -async def update_session_data(session_handle: str, new_session_data: Dict[str, Any], user_context: Optional[Dict[str, Any]] = None) ‑> bool +
    +async def update_session_data_in_database(session_handle: str, new_session_data: Dict[str, Any], user_context: Optional[Dict[str, Any]] = None) ‑> bool
    @@ -921,14 +1127,14 @@

    Functions

    Expand source code -
    async def update_session_data(
    +
    async def update_session_data_in_database(
         session_handle: str,
         new_session_data: Dict[str, Any],
         user_context: Union[None, Dict[str, Any]] = None,
     ) -> bool:
         if user_context is None:
             user_context = {}
    -    return await SessionRecipe.get_instance().recipe_implementation.update_session_data(
    +    return await SessionRecipe.get_instance().recipe_implementation.update_session_data_in_database(
             session_handle, new_session_data, user_context
         )
    @@ -989,7 +1195,7 @@

    Functions

    claim_validation_res = await recipe_impl.validate_claims( session_info.user_id, - session_info.access_token_payload, + session_info.custom_claims_in_access_token_payload, claim_validators, user_context, ) @@ -1080,6 +1286,7 @@

    Index

    diff --git a/html/supertokens_python/recipe/session/claim_base_classes/boolean_claim.html b/html/supertokens_python/recipe/session/claim_base_classes/boolean_claim.html index 4f7820903..debbf0580 100644 --- a/html/supertokens_python/recipe/session/claim_base_classes/boolean_claim.html +++ b/html/supertokens_python/recipe/session/claim_base_classes/boolean_claim.html @@ -81,7 +81,7 @@

    Classes

    class BooleanClaim -(key: str, fetch_value: Callable[[str, Dict[str, Any]], Union[Awaitable[Optional[bool]], bool, ForwardRef(None)]], default_max_age_in_sec: Optional[None] = None) +(key: str, fetch_value: Callable[[str, Dict[str, Any]], Union[Awaitable[Optional[bool]], bool, ForwardRef(None)]], default_max_age_in_sec: Optional[int] = None)

    Helper class that provides a standard way to create an ABC using @@ -140,7 +140,7 @@

    Inherited members

    class BooleanClaimValidators -(claim: SessionClaim[~Primitive], default_max_age_in_sec: Optional[None]) +(claim: SessionClaim[~Primitive], default_max_age_in_sec: Optional[int])

    Abstract base class for generic types.

    @@ -180,7 +180,7 @@

    Subclasses

    Methods

    -def is_false(self, max_age: Optional[None], id_: Optional[str] = None) +def is_false(self, max_age: Optional[int], id_: Optional[str] = None)
    @@ -193,7 +193,7 @@

    Methods

    -def is_true(self, max_age: Optional[None], id_: Optional[str] = None) +def is_true(self, max_age: Optional[int], id_: Optional[str] = None)
    diff --git a/html/supertokens_python/recipe/session/claim_base_classes/primitive_array_claim.html b/html/supertokens_python/recipe/session/claim_base_classes/primitive_array_claim.html index 3030cd34f..45a2f5108 100644 --- a/html/supertokens_python/recipe/session/claim_base_classes/primitive_array_claim.html +++ b/html/supertokens_python/recipe/session/claim_base_classes/primitive_array_claim.html @@ -315,7 +315,7 @@

    Classes

    class ExcludesAllSCV -(id_: str, claim: SessionClaim[~PrimitiveList], val: ~_T, max_age_in_sec: Optional[None] = None) +(id_: str, claim: SessionClaim[~PrimitiveList], val: ~_T, max_age_in_sec: Optional[int] = None)

    Helper class that provides a standard way to create an ABC using @@ -362,7 +362,7 @@

    Methods

    class ExcludesSCV -(id_: str, claim: SessionClaim[~PrimitiveList], val: ~_T, max_age_in_sec: Optional[None] = None) +(id_: str, claim: SessionClaim[~PrimitiveList], val: ~_T, max_age_in_sec: Optional[int] = None)

    Helper class that provides a standard way to create an ABC using @@ -409,7 +409,7 @@

    Methods

    class IncludesAllSCV -(id_: str, claim: SessionClaim[~PrimitiveList], val: ~_T, max_age_in_sec: Optional[None] = None) +(id_: str, claim: SessionClaim[~PrimitiveList], val: ~_T, max_age_in_sec: Optional[int] = None)

    Helper class that provides a standard way to create an ABC using @@ -456,7 +456,7 @@

    Methods

    class IncludesSCV -(id_: str, claim: SessionClaim[~PrimitiveList], val: ~_T, max_age_in_sec: Optional[None] = None) +(id_: str, claim: SessionClaim[~PrimitiveList], val: ~_T, max_age_in_sec: Optional[int] = None)

    Helper class that provides a standard way to create an ABC using @@ -503,7 +503,7 @@

    Methods

    class PrimitiveArrayClaim -(key: str, fetch_value: Callable[[str, Dict[str, Any]], Union[Awaitable[Optional[~PrimitiveList]], ~PrimitiveList, ForwardRef(None)]], default_max_age_in_sec: Optional[None] = None) +(key: str, fetch_value: Callable[[str, Dict[str, Any]], Union[Awaitable[Optional[~PrimitiveList]], ~PrimitiveList, ForwardRef(None)]], default_max_age_in_sec: Optional[int] = None)

    Helper class that provides a standard way to create an ABC using @@ -588,7 +588,7 @@

    Subclasses

    Methods

    -def get_last_refetch_time(self, payload: Dict[str, Any], user_context: Optional[Dict[str, Any]] = None) ‑> Optional[None] +def get_last_refetch_time(self, payload: Dict[str, Any], user_context: Optional[Dict[str, Any]] = None) ‑> Optional[int]
    @@ -619,7 +619,7 @@

    Inherited members

    class PrimitiveArrayClaimValidators -(claim: SessionClaim[~PrimitiveList], default_max_age_in_sec: Optional[None] = None) +(claim: SessionClaim[~PrimitiveList], default_max_age_in_sec: Optional[int] = None)

    Abstract base class for generic types.

    @@ -700,7 +700,7 @@

    Ancestors

    Methods

    -def excludes(self, val: ~Primitive, max_age_in_seconds: Optional[None] = None, id_: Optional[str] = None) ‑> SessionClaimValidator +def excludes(self, val: ~Primitive, max_age_in_seconds: Optional[int] = None, id_: Optional[str] = None) ‑> SessionClaimValidator
    @@ -721,7 +721,7 @@

    Methods

    -def excludes_all(self, val: ~PrimitiveList, max_age_in_seconds: Optional[None] = None, id_: Optional[str] = None) ‑> SessionClaimValidator +def excludes_all(self, val: ~PrimitiveList, max_age_in_seconds: Optional[int] = None, id_: Optional[str] = None) ‑> SessionClaimValidator
    @@ -742,7 +742,7 @@

    Methods

    -def includes(self, val: ~Primitive, max_age_in_seconds: Optional[None] = None, id_: Optional[str] = None) ‑> SessionClaimValidator +def includes(self, val: ~Primitive, max_age_in_seconds: Optional[int] = None, id_: Optional[str] = None) ‑> SessionClaimValidator
    @@ -763,7 +763,7 @@

    Methods

    -def includes_all(self, val: ~PrimitiveList, max_age_in_seconds: Optional[None] = None, id_: Optional[str] = None) ‑> SessionClaimValidator +def includes_all(self, val: ~PrimitiveList, max_age_in_seconds: Optional[int] = None, id_: Optional[str] = None) ‑> SessionClaimValidator
    @@ -787,7 +787,7 @@

    Methods

    class SCVMixin -(id_: str, claim: SessionClaim[~PrimitiveList], val: ~_T, max_age_in_sec: Optional[None] = None) +(id_: str, claim: SessionClaim[~PrimitiveList], val: ~_T, max_age_in_sec: Optional[int] = None)

    Helper class that provides a standard way to create an ABC using diff --git a/html/supertokens_python/recipe/session/claim_base_classes/primitive_claim.html b/html/supertokens_python/recipe/session/claim_base_classes/primitive_claim.html index b330d2d40..6b1ce56a3 100644 --- a/html/supertokens_python/recipe/session/claim_base_classes/primitive_claim.html +++ b/html/supertokens_python/recipe/session/claim_base_classes/primitive_claim.html @@ -219,7 +219,7 @@

    Classes

    class HasValueSCV -(id_: str, claim: SessionClaim[~Primitive], val: ~Primitive, max_age_in_sec: Optional[None] = None) +(id_: str, claim: SessionClaim[~Primitive], val: ~Primitive, max_age_in_sec: Optional[int] = None)

    Helper class that provides a standard way to create an ABC using @@ -400,7 +400,7 @@

    Methods

    class PrimitiveClaim -(key: str, fetch_value: Callable[[str, Dict[str, Any]], Union[Awaitable[Optional[~Primitive]], ~Primitive, ForwardRef(None)]], default_max_age_in_sec: Optional[None] = None) +(key: str, fetch_value: Callable[[str, Dict[str, Any]], Union[Awaitable[Optional[~Primitive]], ~Primitive, ForwardRef(None)]], default_max_age_in_sec: Optional[int] = None)

    Helper class that provides a standard way to create an ABC using @@ -484,7 +484,7 @@

    Subclasses

    Methods

    -def get_last_refetch_time(self, payload: Dict[str, Any], user_context: Optional[Dict[str, Any]] = None) ‑> Optional[None] +def get_last_refetch_time(self, payload: Dict[str, Any], user_context: Optional[Dict[str, Any]] = None) ‑> Optional[int]
    @@ -515,7 +515,7 @@

    Inherited members

    class PrimitiveClaimValidators -(claim: SessionClaim[~Primitive], default_max_age_in_sec: Optional[None]) +(claim: SessionClaim[~Primitive], default_max_age_in_sec: Optional[int])

    Abstract base class for generic types.

    @@ -567,7 +567,7 @@

    Subclasses

    Methods

    -def has_value(self, val: ~Primitive, max_age_in_sec: Optional[None] = None, id_: Optional[str] = None) ‑> SessionClaimValidator +def has_value(self, val: ~Primitive, max_age_in_sec: Optional[int] = None, id_: Optional[str] = None) ‑> SessionClaimValidator
    diff --git a/html/supertokens_python/recipe/session/constants.html b/html/supertokens_python/recipe/session/constants.html index 5a6b14070..884604dfb 100644 --- a/html/supertokens_python/recipe/session/constants.html +++ b/html/supertokens_python/recipe/session/constants.html @@ -40,6 +40,7 @@

    Module supertokens_python.recipe.session.constantsModule supertokens_python.recipe.session.constants

    +available_token_transfer_methods: List[TokenTransferMethod] = ["cookie", "header"] + +JWKCacheMaxAgeInMs = 60 * 1000 # 1min +JWKRequestCooldownInMs = 500 # 0.5s +protected_props = [ + "sub", + "iat", + "exp", + "sessionHandle", + "parentRefreshTokenHash1", + "refreshTokenHash1", + "antiCsrfToken", +]
    diff --git a/html/supertokens_python/recipe/session/cookie_and_header.html b/html/supertokens_python/recipe/session/cookie_and_header.html index a818263a1..0d3801a5b 100644 --- a/html/supertokens_python/recipe/session/cookie_and_header.html +++ b/html/supertokens_python/recipe/session/cookie_and_header.html @@ -59,52 +59,46 @@

    Module supertokens_python.recipe.session.cookie_and_head RID_HEADER_KEY, available_token_transfer_methods, ) +from ...logger import log_debug_message +from supertokens_python.constants import HUNDRED_YEARS_IN_MS if TYPE_CHECKING: from supertokens_python.framework.request import BaseRequest from supertokens_python.framework.response import BaseResponse from .recipe import SessionRecipe - from .utils import TokenTransferMethod, TokenType, SessionConfig + from .utils import ( + TokenTransferMethod, + TokenType, + SessionConfig, + ) from json import dumps -from typing import Any, Dict, Union +from typing import Any, Dict + +from supertokens_python.utils import get_header, utf_base64encode, get_timestamp_ms + -from supertokens_python.utils import get_header, utf_base64encode +def build_front_token( + user_id: str, at_expiry: int, access_token_payload: Optional[Dict[str, Any]] = None +): + if access_token_payload is None: + access_token_payload = {} + token_info = {"uid": user_id, "ate": at_expiry, "up": access_token_payload} + return utf_base64encode( + dumps(token_info, separators=(",", ":"), sort_keys=True), urlsafe=False + ) def _set_front_token_in_headers( response: BaseResponse, - user_id: str, - expires: int, - jwt_payload: Union[None, Dict[str, Any]] = None, + front_token: str, ): - if jwt_payload is None: - jwt_payload = {} - token_info = {"uid": user_id, "ate": expires, "up": jwt_payload} - set_header( - response, - FRONT_TOKEN_HEADER_SET_KEY, - utf_base64encode(dumps(token_info, separators=(",", ":"), sort_keys=True)), - False, - ) + set_header(response, FRONT_TOKEN_HEADER_SET_KEY, front_token, False) set_header( response, ACCESS_CONTROL_EXPOSE_HEADERS, FRONT_TOKEN_HEADER_SET_KEY, True ) -def front_token_response_mutator( - user_id: str, - expires: int, - jwt_payload: Union[None, Dict[str, Any]] = None, -): - def mutator( - response: BaseResponse, - ): - return _set_front_token_in_headers(response, user_id, expires, jwt_payload) - - return mutator - - def get_cors_allowed_headers(): return [ ANTI_CSRF_HEADER_KEY, @@ -216,6 +210,15 @@

    Module supertokens_python.recipe.session.cookie_and_head _clear_session(response, recipe.config, transfer_method) +def clear_session_mutator(config: SessionConfig, transfer_method: TokenTransferMethod): + def mutator( + response: BaseResponse, + ): + return _clear_session(response, config, transfer_method) + + return mutator + + def _clear_session( response: BaseResponse, config: SessionConfig, @@ -289,6 +292,7 @@

    Module supertokens_python.recipe.session.cookie_and_head expires: int, transfer_method: TokenTransferMethod, ): + log_debug_message("Setting %s token as %s", token_type, transfer_method) if transfer_method == "cookie": _set_cookie( response, @@ -328,7 +332,58 @@

    Module supertokens_python.recipe.session.cookie_and_head def set_token_in_header(response: BaseResponse, name: str, value: str): set_header(response, name, value, allow_duplicate=False) - set_header(response, ACCESS_CONTROL_EXPOSE_HEADERS, name, allow_duplicate=True) + set_header(response, ACCESS_CONTROL_EXPOSE_HEADERS, name, allow_duplicate=True) + + +def access_token_mutator( + access_token: str, + front_token: str, + config: SessionConfig, + transfer_method: TokenTransferMethod, +): + def mutator( + response: BaseResponse, + ): + _set_access_token_in_response( + response, access_token, front_token, config, transfer_method + ) + + return mutator + + +def _set_access_token_in_response( + res: BaseResponse, + access_token: str, + front_token: str, + config: SessionConfig, + transfer_method: TokenTransferMethod, +): + _set_front_token_in_headers(res, front_token) + _set_token( + res, + config, + "access", + access_token, + # We set the expiration to 100 years, because we can't really access the expiration of the refresh token everywhere we are setting it. + # 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. + # Even if the token is expired the presence of the token indicates that the user could have a valid refresh + # Setting them to infinity would require special case handling on the frontend and just adding 10 years seems enough. + get_timestamp_ms() + HUNDRED_YEARS_IN_MS, + transfer_method, + ) + + if ( + config.expose_access_token_to_frontend_in_cookie_based_auth + and transfer_method == "cookie" + ): + _set_token( + res, + config, + "access", + access_token, + get_timestamp_ms() + HUNDRED_YEARS_IN_MS, + "header", + )

    @@ -338,6 +393,31 @@

    Module supertokens_python.recipe.session.cookie_and_head

    Functions

    +
    +def access_token_mutator(access_token: str, front_token: str, config: SessionConfig, transfer_method: TokenTransferMethod) +
    +
    +
    +
    + +Expand source code + +
    def access_token_mutator(
    +    access_token: str,
    +    front_token: str,
    +    config: SessionConfig,
    +    transfer_method: TokenTransferMethod,
    +):
    +    def mutator(
    +        response: BaseResponse,
    +    ):
    +        _set_access_token_in_response(
    +            response, access_token, front_token, config, transfer_method
    +        )
    +
    +    return mutator
    +
    +
    def anti_csrf_response_mutator(value: str)
    @@ -356,6 +436,26 @@

    Functions

    return mutator

    +
    +def build_front_token(user_id: str, at_expiry: int, access_token_payload: Optional[Dict[str, Any]] = None) +
    +
    +
    +
    + +Expand source code + +
    def build_front_token(
    +    user_id: str, at_expiry: int, access_token_payload: Optional[Dict[str, Any]] = None
    +):
    +    if access_token_payload is None:
    +        access_token_payload = {}
    +    token_info = {"uid": user_id, "ate": at_expiry, "up": access_token_payload}
    +    return utf_base64encode(
    +        dumps(token_info, separators=(",", ":"), sort_keys=True), urlsafe=False
    +    )
    +
    +
    def clear_session_from_all_token_transfer_methods(response: BaseResponse, recipe: SessionRecipe)
    @@ -378,8 +478,8 @@

    Functions

    _clear_session(response, recipe.config, transfer_method)
    -
    -def clear_session_response_mutator(config: SessionConfig, transfer_method: TokenTransferMethod) +
    +def clear_session_mutator(config: SessionConfig, transfer_method: TokenTransferMethod)
    @@ -387,10 +487,7 @@

    Functions

    Expand source code -
    def clear_session_response_mutator(
    -    config: SessionConfig,
    -    transfer_method: TokenTransferMethod,
    -):
    +
    def clear_session_mutator(config: SessionConfig, transfer_method: TokenTransferMethod):
         def mutator(
             response: BaseResponse,
         ):
    @@ -399,8 +496,8 @@ 

    Functions

    return mutator
    -
    -def front_token_response_mutator(user_id: str, expires: int, jwt_payload: Union[None, Dict[str, Any]] = None) +
    +def clear_session_response_mutator(config: SessionConfig, transfer_method: TokenTransferMethod)
    @@ -408,15 +505,14 @@

    Functions

    Expand source code -
    def front_token_response_mutator(
    -    user_id: str,
    -    expires: int,
    -    jwt_payload: Union[None, Dict[str, Any]] = None,
    +
    def clear_session_response_mutator(
    +    config: SessionConfig,
    +    transfer_method: TokenTransferMethod,
     ):
         def mutator(
             response: BaseResponse,
         ):
    -        return _set_front_token_in_headers(response, user_id, expires, jwt_payload)
    +        return _clear_session(response, config, transfer_method)
     
         return mutator
    @@ -661,10 +757,12 @@

    Index

  • Functions

  • -def raise_unauthorised_exception(msg: str, clear_tokens: bool = True) ‑> NoReturn +def raise_unauthorised_exception(msg: str, clear_tokens: bool = True, response_mutators: Optional[List[ResponseMutator]] = None) ‑> NoReturn
    @@ -164,8 +173,17 @@

    Functions

    Expand source code -
    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
    diff --git a/html/supertokens_python/recipe/session/index.html b/html/supertokens_python/recipe/session/index.html index 6c9e88f13..1a9db28c7 100644 --- a/html/supertokens_python/recipe/session/index.html +++ b/html/supertokens_python/recipe/session/index.html @@ -56,7 +56,6 @@

    Module supertokens_python.recipe.session

    InputErrorHandlers = utils.InputErrorHandlers InputOverrideConfig = utils.InputOverrideConfig -JWTConfig = utils.JWTConfig SessionContainer = interfaces.SessionContainer exceptions = ex @@ -76,8 +75,9 @@

    Module supertokens_python.recipe.session

    ] = None, error_handlers: Union[InputErrorHandlers, None] = None, override: Union[InputOverrideConfig, None] = None, - jwt: Union[JWTConfig, None] = None, invalid_claim_status_code: Union[int, None] = None, + use_dynamic_access_token_signing_key: Union[bool, None] = None, + expose_access_token_to_frontend_in_cookie_based_auth: Union[bool, None] = None, ) -> Callable[[AppInfo], RecipeModule]: return SessionRecipe.init( cookie_domain, @@ -88,8 +88,9 @@

    Module supertokens_python.recipe.session

    get_token_transfer_method, error_handlers, override, - jwt, invalid_claim_status_code, + use_dynamic_access_token_signing_key, + expose_access_token_to_frontend_in_cookie_based_auth, )
    @@ -136,6 +137,10 @@

    Sub-modules

    +
    supertokens_python.recipe.session.jwks
    +
    +
    +
    supertokens_python.recipe.session.jwt
    @@ -156,15 +161,15 @@

    Sub-modules

    -
    supertokens_python.recipe.session.syncio
    +
    supertokens_python.recipe.session.session_request_functions
    -
    supertokens_python.recipe.session.utils
    +
    supertokens_python.recipe.session.syncio
    -
    supertokens_python.recipe.session.with_jwt
    +
    supertokens_python.recipe.session.utils
    @@ -176,7 +181,7 @@

    Sub-modules

    Functions

    -def init(cookie_domain: Union[str, None] = None, cookie_secure: Union[bool, None] = None, cookie_same_site: "Union[Literal['lax', 'none', 'strict'], None]" = None, session_expired_status_code: Union[int, None] = None, anti_csrf: "Union[Literal['VIA_TOKEN', 'VIA_CUSTOM_HEADER', 'NONE'], None]" = None, get_token_transfer_method: "Union[Callable[[BaseRequest, bool, Dict[str, Any]], Union[TokenTransferMethod, Literal['any']]], None]" = None, error_handlers: Union[InputErrorHandlers, None] = None, override: Union[InputOverrideConfig, None] = None, jwt: Union[JWTConfig, None] = None, invalid_claim_status_code: Union[int, None] = None) ‑> Callable[[AppInfo], RecipeModule] +def init(cookie_domain: Union[str, None] = None, cookie_secure: Union[bool, None] = None, cookie_same_site: "Union[Literal['lax', 'none', 'strict'], None]" = None, session_expired_status_code: Union[int, None] = None, anti_csrf: "Union[Literal['VIA_TOKEN', 'VIA_CUSTOM_HEADER', 'NONE'], None]" = None, get_token_transfer_method: "Union[Callable[[BaseRequest, bool, Dict[str, Any]], Union[TokenTransferMethod, Literal['any']]], None]" = None, error_handlers: Union[InputErrorHandlers, None] = None, override: Union[InputOverrideConfig, None] = None, invalid_claim_status_code: Union[int, None] = None, use_dynamic_access_token_signing_key: Union[bool, None] = None, expose_access_token_to_frontend_in_cookie_based_auth: Union[bool, None] = None) ‑> Callable[[AppInfo], RecipeModule]
    @@ -199,8 +204,9 @@

    Functions

    ] = None, error_handlers: Union[InputErrorHandlers, None] = None, override: Union[InputOverrideConfig, None] = None, - jwt: Union[JWTConfig, None] = None, invalid_claim_status_code: Union[int, None] = None, + use_dynamic_access_token_signing_key: Union[bool, None] = None, + expose_access_token_to_frontend_in_cookie_based_auth: Union[bool, None] = None, ) -> Callable[[AppInfo], RecipeModule]: return SessionRecipe.init( cookie_domain, @@ -211,8 +217,9 @@

    Functions

    get_token_transfer_method, error_handlers, override, - jwt, invalid_claim_status_code, + use_dynamic_access_token_signing_key, + expose_access_token_to_frontend_in_cookie_based_auth, )
    @@ -244,14 +251,15 @@

    Index

  • supertokens_python.recipe.session.exceptions
  • supertokens_python.recipe.session.framework
  • supertokens_python.recipe.session.interfaces
  • +
  • supertokens_python.recipe.session.jwks
  • supertokens_python.recipe.session.jwt
  • supertokens_python.recipe.session.recipe
  • supertokens_python.recipe.session.recipe_implementation
  • supertokens_python.recipe.session.session_class
  • supertokens_python.recipe.session.session_functions
  • +
  • supertokens_python.recipe.session.session_request_functions
  • supertokens_python.recipe.session.syncio
  • supertokens_python.recipe.session.utils
  • -
  • supertokens_python.recipe.session.with_jwt
  • Functions

    diff --git a/html/supertokens_python/recipe/session/interfaces.html b/html/supertokens_python/recipe/session/interfaces.html index a15e3d1c9..1a3f77e3f 100644 --- a/html/supertokens_python/recipe/session/interfaces.html +++ b/html/supertokens_python/recipe/session/interfaces.html @@ -53,6 +53,7 @@

    Module supertokens_python.recipe.session.interfacesModule supertokens_python.recipe.session.interfacesModule supertokens_python.recipe.session.interfacesModule supertokens_python.recipe.session.interfacesModule supertokens_python.recipe.session.interfacesModule supertokens_python.recipe.session.interfacesModule supertokens_python.recipe.session.interfacesModule supertokens_python.recipe.session.interfacesModule supertokens_python.recipe.session.interfacesModule supertokens_python.recipe.session.interfacesModule supertokens_python.recipe.session.interfacesModule supertokens_python.recipe.session.interfacesModule supertokens_python.recipe.session.interfacesModule supertokens_python.recipe.session.interfacesModule supertokens_python.recipe.session.interfacesModule supertokens_python.recipe.session.interfacesAncestors

  • typing.Generic
  • +
    +class GetSessionTokensDangerouslyDict +(*args, **kwargs) +
    +
    +

    dict() -> new empty dictionary +dict(mapping) -> new dictionary initialized from a mapping object's +(key, value) pairs +dict(iterable) -> new dictionary initialized as if via: +d = {} +for k, v in iterable: +d[k] = v +dict(**kwargs) -> new dictionary initialized with the name=value pairs +in the keyword argument list. +For example: +dict(one=1, two=2)

    +
    + +Expand source code + +
    class GetSessionTokensDangerouslyDict(TypedDict):
    +    accessToken: str
    +    accessAndFrontTokenUpdated: bool
    +    refreshToken: Optional[str]
    +    frontToken: str
    +    antiCsrfToken: Optional[str]
    +
    +

    Ancestors

    +
      +
    • builtins.dict
    • +
    +

    Class variables

    +
    +
    var accessAndFrontTokenUpdated : bool
    +
    +
    +
    +
    var accessToken : str
    +
    +
    +
    +
    var antiCsrfToken : Optional[str]
    +
    +
    +
    +
    var frontToken : str
    +
    +
    +
    +
    var refreshToken : Optional[str]
    +
    +
    +
    +
    +
    class RecipeInterface
    @@ -916,16 +1005,18 @@

    Ancestors

    Expand source code
    class RecipeInterface(ABC):  # pylint: disable=too-many-public-methods
    +    JWK_clients: List[JWKClient] = []
    +
         def __init__(self):
             pass
     
         @abstractmethod
         async def create_new_session(
             self,
    -        request: BaseRequest,
             user_id: str,
    -        access_token_payload: Union[None, Dict[str, Any]],
    -        session_data: Union[None, Dict[str, Any]],
    +        access_token_payload: Optional[Dict[str, Any]],
    +        session_data_in_database: Optional[Dict[str, Any]],
    +        disable_anti_csrf: Optional[bool],
             user_context: Dict[str, Any],
         ) -> SessionContainer:
             pass
    @@ -942,11 +1033,19 @@ 

    Ancestors

    @abstractmethod async def get_session( self, - request: BaseRequest, - anti_csrf_check: Union[bool, None], - session_required: bool, - user_context: Dict[str, Any], - ) -> Union[SessionContainer, None]: + access_token: str, + anti_csrf_token: Optional[str], + anti_csrf_check: Optional[bool] = None, + session_required: Optional[bool] = None, + check_database: Optional[bool] = None, + override_global_claim_validators: Optional[ + Callable[ + [List[SessionClaimValidator], SessionContainer, Dict[str, Any]], + MaybeAwaitable[List[SessionClaimValidator]], + ] + ] = None, + user_context: Optional[Dict[str, Any]] = None, + ) -> Optional[SessionContainer]: pass @abstractmethod @@ -971,7 +1070,11 @@

    Ancestors

    @abstractmethod async def refresh_session( - self, request: BaseRequest, user_context: Dict[str, Any] + self, + refresh_token: str, + anti_csrf_token: Optional[str], + disable_anti_csrf: bool, + user_context: Dict[str, Any], ) -> SessionContainer: pass @@ -1006,7 +1109,7 @@

    Ancestors

    pass @abstractmethod - async def update_session_data( + async def update_session_data_in_database( self, session_handle: str, new_session_data: Dict[str, Any], @@ -1014,15 +1117,6 @@

    Ancestors

    ) -> bool: pass - @abstractmethod - async def update_access_token_payload( - self, - session_handle: str, - new_access_token_payload: Dict[str, Any], - user_context: Dict[str, Any], - ) -> bool: - """DEPRECATED: Use merge_into_access_token_payload instead""" - @abstractmethod async def merge_into_access_token_payload( self, @@ -1032,14 +1126,6 @@

    Ancestors

    ) -> bool: pass - @abstractmethod - async def get_access_token_lifetime_ms(self, user_context: Dict[str, Any]) -> int: - pass - - @abstractmethod - async def get_refresh_token_lifetime_ms(self, user_context: Dict[str, Any]) -> int: - pass - @abstractmethod async def fetch_and_set_claim( self, @@ -1094,10 +1180,17 @@

    Subclasses

    +

    Class variables

    +
    +
    var JWK_clients : List[JWKClient]
    +
    +
    +
    +

    Methods

    -async def create_new_session(self, request: BaseRequest, user_id: str, access_token_payload: Union[None, Dict[str, Any]], session_data: Union[None, Dict[str, Any]], user_context: Dict[str, Any]) ‑> SessionContainer +async def create_new_session(self, user_id: str, access_token_payload: Optional[Dict[str, Any]], session_data_in_database: Optional[Dict[str, Any]], disable_anti_csrf: Optional[bool], user_context: Dict[str, Any]) ‑> SessionContainer
    @@ -1108,10 +1201,10 @@

    Methods

    @abstractmethod
     async def create_new_session(
         self,
    -    request: BaseRequest,
         user_id: str,
    -    access_token_payload: Union[None, Dict[str, Any]],
    -    session_data: Union[None, Dict[str, Any]],
    +    access_token_payload: Optional[Dict[str, Any]],
    +    session_data_in_database: Optional[Dict[str, Any]],
    +    disable_anti_csrf: Optional[bool],
         user_context: Dict[str, Any],
     ) -> SessionContainer:
         pass
    @@ -1136,20 +1229,6 @@

    Methods

    pass
    -
    -async def get_access_token_lifetime_ms(self, user_context: Dict[str, Any]) ‑> int -
    -
    -
    -
    - -Expand source code - -
    @abstractmethod
    -async def get_access_token_lifetime_ms(self, user_context: Dict[str, Any]) -> int:
    -    pass
    -
    -
    async def get_all_session_handles_for_user(self, user_id: str, user_context: Dict[str, Any]) ‑> List[str]
    @@ -1204,22 +1283,8 @@

    Methods

    pass
    -
    -async def get_refresh_token_lifetime_ms(self, user_context: Dict[str, Any]) ‑> int -
    -
    -
    -
    - -Expand source code - -
    @abstractmethod
    -async def get_refresh_token_lifetime_ms(self, user_context: Dict[str, Any]) -> int:
    -    pass
    -
    -
    -async def get_session(self, request: BaseRequest, anti_csrf_check: Union[bool, None], session_required: bool, user_context: Dict[str, Any]) ‑> Union[SessionContainer, None] +async def get_session(self, access_token: str, anti_csrf_token: Optional[str], anti_csrf_check: Optional[bool] = None, session_required: Optional[bool] = None, check_database: Optional[bool] = None, override_global_claim_validators: Optional[Callable[[List[SessionClaimValidator], SessionContainer, Dict[str, Any]], MaybeAwaitable[List[SessionClaimValidator]]]] = None, user_context: Optional[Dict[str, Any]] = None) ‑> Optional[SessionContainer]
    @@ -1230,11 +1295,19 @@

    Methods

    @abstractmethod
     async def get_session(
         self,
    -    request: BaseRequest,
    -    anti_csrf_check: Union[bool, None],
    -    session_required: bool,
    -    user_context: Dict[str, Any],
    -) -> Union[SessionContainer, None]:
    +    access_token: str,
    +    anti_csrf_token: Optional[str],
    +    anti_csrf_check: Optional[bool] = None,
    +    session_required: Optional[bool] = None,
    +    check_database: Optional[bool] = None,
    +    override_global_claim_validators: Optional[
    +        Callable[
    +            [List[SessionClaimValidator], SessionContainer, Dict[str, Any]],
    +            MaybeAwaitable[List[SessionClaimValidator]],
    +        ]
    +    ] = None,
    +    user_context: Optional[Dict[str, Any]] = None,
    +) -> Optional[SessionContainer]:
         pass
    @@ -1274,7 +1347,7 @@

    Methods

    -async def refresh_session(self, request: BaseRequest, user_context: Dict[str, Any]) ‑> SessionContainer +async def refresh_session(self, refresh_token: str, anti_csrf_token: Optional[str], disable_anti_csrf: bool, user_context: Dict[str, Any]) ‑> SessionContainer
    @@ -1284,7 +1357,11 @@

    Methods

    @abstractmethod
     async def refresh_session(
    -    self, request: BaseRequest, user_context: Dict[str, Any]
    +    self,
    +    refresh_token: str,
    +    anti_csrf_token: Optional[str],
    +    disable_anti_csrf: bool,
    +    user_context: Dict[str, Any],
     ) -> SessionContainer:
         pass
    @@ -1395,27 +1472,8 @@

    Methods

    pass
    -
    -async def update_access_token_payload(self, session_handle: str, new_access_token_payload: Dict[str, Any], user_context: Dict[str, Any]) ‑> bool -
    -
    -

    DEPRECATED: Use merge_into_access_token_payload instead

    -
    - -Expand source code - -
    @abstractmethod
    -async def update_access_token_payload(
    -    self,
    -    session_handle: str,
    -    new_access_token_payload: Dict[str, Any],
    -    user_context: Dict[str, Any],
    -) -> bool:
    -    """DEPRECATED: Use merge_into_access_token_payload instead"""
    -
    -
    -
    -async def update_session_data(self, session_handle: str, new_session_data: Dict[str, Any], user_context: Dict[str, Any]) ‑> bool +
    +async def update_session_data_in_database(self, session_handle: str, new_session_data: Dict[str, Any], user_context: Dict[str, Any]) ‑> bool
    @@ -1424,7 +1482,7 @@

    Methods

    Expand source code
    @abstractmethod
    -async def update_session_data(
    +async def update_session_data_in_database(
         self,
         session_handle: str,
         new_session_data: Dict[str, Any],
    @@ -1491,6 +1549,26 @@ 

    Methods

    self.access_token = access_token
    +
    +class ReqResInfo +(request: BaseRequest, transfer_method: TokenTransferMethod) +
    +
    +
    +
    + +Expand source code + +
    class ReqResInfo:
    +    def __init__(
    +        self,
    +        request: BaseRequest,
    +        transfer_method: TokenTransferMethod,
    +    ):
    +        self.request = request
    +        self.transfer_method = transfer_method
    +
    +
    class SessionClaim (key: str, fetch_value: Callable[[str, Dict[str, Any]], MaybeAwaitable[Optional[_T]]]) @@ -1751,7 +1829,7 @@

    Methods

    class SessionContainer -(recipe_implementation: RecipeInterface, config: SessionConfig, access_token: str, session_handle: str, user_id: str, access_token_payload: Dict[str, Any], transfer_method: TokenTransferMethod) +(recipe_implementation: RecipeInterface, config: SessionConfig, access_token: str, front_token: str, refresh_token: Optional[TokenInfo], anti_csrf_token: Optional[str], session_handle: str, user_id: str, user_data_in_access_token: Optional[Dict[str, Any]], req_res_info: Optional[ReqResInfo], access_token_updated: bool)

    Helper class that provides a standard way to create an ABC using @@ -1766,18 +1844,26 @@

    Methods

    recipe_implementation: RecipeInterface, config: SessionConfig, access_token: str, + front_token: str, + refresh_token: Optional[TokenInfo], + anti_csrf_token: Optional[str], session_handle: str, user_id: str, - access_token_payload: Dict[str, Any], - transfer_method: TokenTransferMethod, + user_data_in_access_token: Optional[Dict[str, Any]], + req_res_info: Optional[ReqResInfo], + access_token_updated: bool, ): self.recipe_implementation = recipe_implementation self.config = config self.access_token = access_token + self.front_token = front_token + self.refresh_token = refresh_token + self.anti_csrf_token = anti_csrf_token self.session_handle = session_handle - self.access_token_payload = access_token_payload self.user_id = user_id - self.transfer_method: TokenTransferMethod = transfer_method + self.user_data_in_access_token = user_data_in_access_token + self.req_res_info: Optional[ReqResInfo] = req_res_info + self.access_token_updated = access_token_updated self.response_mutators: List[ResponseMutator] = [] @@ -1788,13 +1874,13 @@

    Methods

    pass @abstractmethod - async def get_session_data( + async def get_session_data_from_database( self, user_context: Optional[Dict[str, Any]] = None ) -> Dict[str, Any]: pass @abstractmethod - async def update_session_data( + async def update_session_data_in_database( self, new_session_data: Dict[str, Any], user_context: Optional[Dict[str, Any]] = None, @@ -1802,12 +1888,10 @@

    Methods

    pass @abstractmethod - async def update_access_token_payload( - self, - new_access_token_payload: Dict[str, Any], - user_context: Optional[Dict[str, Any]] = None, - ) -> None: - """DEPRECATED: Use merge_into_access_token_payload instead""" + async def attach_to_request_response( + self, request: BaseRequest, transfer_method: TokenTransferMethod + ): + pass @abstractmethod async def merge_into_access_token_payload( @@ -1831,6 +1915,10 @@

    Methods

    def get_handle(self, user_context: Optional[Dict[str, Any]] = None) -> str: pass + @abstractmethod + def get_all_session_tokens_dangerously(self) -> GetSessionTokensDangerouslyDict: + pass + @abstractmethod def get_access_token(self, user_context: Optional[Dict[str, Any]] = None) -> str: pass @@ -1890,10 +1978,10 @@

    Methods

    ) -> None: return sync(self.revoke_session(user_context=user_context)) - def sync_get_session_data( + def sync_get_session_data_from_database( self, user_context: Union[Dict[str, Any], None] = None ) -> Dict[str, Any]: - return sync(self.get_session_data(user_context)) + return sync(self.get_session_data_from_database(user_context)) def sync_get_time_created( self, user_context: Optional[Dict[str, Any]] = None @@ -1911,22 +1999,15 @@

    Methods

    ) ) - def sync_update_access_token_payload( + def sync_update_session_data_in_database( self, - new_access_token_payload: Dict[str, Any], + new_session_data: Dict[str, Any], user_context: Optional[Dict[str, Any]] = None, ) -> None: return sync( - self.update_access_token_payload(new_access_token_payload, user_context) + self.update_session_data_in_database(new_session_data, user_context) ) - def sync_update_session_data( - self, - new_session_data: Dict[str, Any], - user_context: Optional[Dict[str, Any]] = None, - ) -> None: - return sync(self.update_session_data(new_session_data, user_context)) - # Session claims sync functions: def sync_assert_claims( self, @@ -1958,6 +2039,11 @@

    Methods

    ) -> None: return sync(self.remove_claim(claim, user_context)) + def sync_attach_to_request_response( + self, request: BaseRequest, token_transfer: TokenTransferMethod + ) -> None: + return sync(self.attach_to_request_response(request, token_transfer)) + # This is there so that we can do session["..."] to access some of the members of this class def __getitem__(self, item: str): return getattr(self, item)
    @@ -1990,6 +2076,22 @@

    Methods

    pass
    +
    +async def attach_to_request_response(self, request: BaseRequest, transfer_method: TokenTransferMethod) +
    +
    +
    +
    + +Expand source code + +
    @abstractmethod
    +async def attach_to_request_response(
    +    self, request: BaseRequest, transfer_method: TokenTransferMethod
    +):
    +    pass
    +
    +
    async def fetch_and_set_claim(self, claim: SessionClaim[Any], user_context: Optional[Dict[str, Any]] = None) ‑> None
    @@ -2036,6 +2138,20 @@

    Methods

    pass
    +
    +def get_all_session_tokens_dangerously(self) ‑> GetSessionTokensDangerouslyDict +
    +
    +
    +
    + +Expand source code + +
    @abstractmethod
    +def get_all_session_tokens_dangerously(self) -> GetSessionTokensDangerouslyDict:
    +    pass
    +
    +
    async def get_claim_value(self, claim: SessionClaim[_T], user_context: Optional[Dict[str, Any]] = None) ‑> Optional[~_T]
    @@ -2080,8 +2196,8 @@

    Methods

    pass
    -
    -async def get_session_data(self, user_context: Optional[Dict[str, Any]] = None) ‑> Dict[str, Any] +
    +async def get_session_data_from_database(self, user_context: Optional[Dict[str, Any]] = None) ‑> Dict[str, Any]
    @@ -2090,7 +2206,7 @@

    Methods

    Expand source code
    @abstractmethod
    -async def get_session_data(
    +async def get_session_data_from_database(
         self, user_context: Optional[Dict[str, Any]] = None
     ) -> Dict[str, Any]:
         pass
    @@ -2214,6 +2330,21 @@

    Methods

    return sync(self.assert_claims(claim_validators, user_context))
    +
    +def sync_attach_to_request_response(self, request: BaseRequest, token_transfer: TokenTransferMethod) ‑> None +
    +
    +
    +
    + +Expand source code + +
    def sync_attach_to_request_response(
    +    self, request: BaseRequest, token_transfer: TokenTransferMethod
    +) -> None:
    +    return sync(self.attach_to_request_response(request, token_transfer))
    +
    +
    def sync_fetch_and_set_claim(self, claim: SessionClaim[Any], user_context: Optional[Dict[str, Any]] = None) ‑> None
    @@ -2257,8 +2388,8 @@

    Methods

    return sync(self.get_expiry(user_context))
    -
    -def sync_get_session_data(self, user_context: Union[Dict[str, Any], None] = None) ‑> Dict[str, Any] +
    +def sync_get_session_data_from_database(self, user_context: Union[Dict[str, Any], None] = None) ‑> Dict[str, Any]
    @@ -2266,10 +2397,10 @@

    Methods

    Expand source code -
    def sync_get_session_data(
    +
    def sync_get_session_data_from_database(
         self, user_context: Union[Dict[str, Any], None] = None
     ) -> Dict[str, Any]:
    -    return sync(self.get_session_data(user_context))
    + return sync(self.get_session_data_from_database(user_context))
    @@ -2356,8 +2487,8 @@

    Methods

    return sync(self.set_claim_value(claim, value, user_context))
    -
    -def sync_update_access_token_payload(self, new_access_token_payload: Dict[str, Any], user_context: Optional[Dict[str, Any]] = None) ‑> None +
    +def sync_update_session_data_in_database(self, new_session_data: Dict[str, Any], user_context: Optional[Dict[str, Any]] = None) ‑> None
    @@ -2365,18 +2496,18 @@

    Methods

    Expand source code -
    def sync_update_access_token_payload(
    +
    def sync_update_session_data_in_database(
         self,
    -    new_access_token_payload: Dict[str, Any],
    +    new_session_data: Dict[str, Any],
         user_context: Optional[Dict[str, Any]] = None,
     ) -> None:
         return sync(
    -        self.update_access_token_payload(new_access_token_payload, user_context)
    +        self.update_session_data_in_database(new_session_data, user_context)
         )
    -
    -def sync_update_session_data(self, new_session_data: Dict[str, Any], user_context: Optional[Dict[str, Any]] = None) ‑> None +
    +async def update_session_data_in_database(self, new_session_data: Dict[str, Any], user_context: Optional[Dict[str, Any]] = None) ‑> None
    @@ -2384,43 +2515,8 @@

    Methods

    Expand source code -
    def sync_update_session_data(
    -    self,
    -    new_session_data: Dict[str, Any],
    -    user_context: Optional[Dict[str, Any]] = None,
    -) -> None:
    -    return sync(self.update_session_data(new_session_data, user_context))
    - -
    -
    -async def update_access_token_payload(self, new_access_token_payload: Dict[str, Any], user_context: Optional[Dict[str, Any]] = None) ‑> None -
    -
    -

    DEPRECATED: Use merge_into_access_token_payload instead

    -
    - -Expand source code -
    @abstractmethod
    -async def update_access_token_payload(
    -    self,
    -    new_access_token_payload: Dict[str, Any],
    -    user_context: Optional[Dict[str, Any]] = None,
    -) -> None:
    -    """DEPRECATED: Use merge_into_access_token_payload instead"""
    -
    -
    -
    -async def update_session_data(self, new_session_data: Dict[str, Any], user_context: Optional[Dict[str, Any]] = None) ‑> None -
    -
    -
    -
    - -Expand source code - -
    @abstractmethod
    -async def update_session_data(
    +async def update_session_data_in_database(
         self,
         new_session_data: Dict[str, Any],
         user_context: Optional[Dict[str, Any]] = None,
    @@ -2445,7 +2541,7 @@ 

    Methods

    class SessionInformationResult -(session_handle: str, user_id: str, session_data: Dict[str, Any], expiry: int, access_token_payload: Dict[str, Any], time_created: int) +(session_handle: str, user_id: str, session_data_in_database: Dict[str, Any], expiry: int, custom_claims_in_access_token_payload: Dict[str, Any], time_created: int)
    @@ -2458,16 +2554,18 @@

    Methods

    self, session_handle: str, user_id: str, - session_data: Dict[str, Any], + session_data_in_database: Dict[str, Any], expiry: int, - access_token_payload: Dict[str, Any], + custom_claims_in_access_token_payload: Dict[str, Any], time_created: int, ): self.session_handle: str = session_handle self.user_id: str = user_id - self.session_data: Dict[str, Any] = session_data + self.session_data_in_database: Dict[str, Any] = session_data_in_database self.expiry: int = expiry - self.access_token_payload: Dict[str, Any] = access_token_payload + self.custom_claims_in_access_token_payload: Dict[ + str, Any + ] = custom_claims_in_access_token_payload self.time_created: int = time_created
    @@ -2527,6 +2625,23 @@

    Methods

    +
    +class TokenInfo +(token: str, expiry: int, created_time: int) +
    +
    +
    +
    + +Expand source code + +
    class TokenInfo:
    +    def __init__(self, token: str, expiry: int, created_time: int):
    +        self.token = token
    +        self.expiry = expiry
    +        self.created_time = created_time
    +
    +
    @@ -2567,15 +2682,24 @@

    GetClaimValueOkResult

  • +

    GetSessionTokensDangerouslyDict

    + +
  • +
  • RecipeInterface

    @@ -2596,6 +2719,9 @@

    RegenerateAccessTokenOkResult

  • +

    ReqResInfo

    +
  • +
  • SessionClaim

  • +
  • +

    TokenInfo

    +
  • diff --git a/html/supertokens_python/recipe/session/jwks.html b/html/supertokens_python/recipe/session/jwks.html new file mode 100644 index 000000000..7c61dccfc --- /dev/null +++ b/html/supertokens_python/recipe/session/jwks.html @@ -0,0 +1,435 @@ + + + + + + +supertokens_python.recipe.session.jwks API documentation + + + + + + + + + + + +
    +
    +
    +

    Module supertokens_python.recipe.session.jwks

    +
    +
    +
    + +Expand source code + +
    import json
    +import urllib.request
    +from typing import List, Optional
    +from urllib.error import URLError
    +
    +from jwt import PyJWK, PyJWKSet
    +from jwt.api_jwt import decode_complete as decode_token  # type: ignore
    +
    +from supertokens_python.utils import get_timestamp_ms
    +
    +from .constants import JWKCacheMaxAgeInMs, JWKRequestCooldownInMs
    +
    +
    +class JWKClient:
    +    def __init__(
    +        self,
    +        uri: str,
    +        cooldown_duration: int = JWKRequestCooldownInMs,
    +        cache_max_age: int = JWKCacheMaxAgeInMs,
    +    ):
    +        """A client for retrieving JSON Web Key Sets (JWKS) from a given URI.
    +
    +        Args:
    +            uri (str): The URI of the JWKS.
    +            cooldown_duration (int, optional): The cooldown duration in ms. Defaults to 500 seconds.
    +            cache_max_age (int, optional): The cache max age in ms. Defaults to 5 minutes.
    +
    +        Note: The JSON Web Key Set is fetched when no key matches the selection
    +        process but only as frequently as the `self.cooldown_duration` option
    +        allows to prevent abuse. The `self.cache_max_age` option is used to
    +        determine how long the JWKS is cached for.
    +
    +        Whenever you make a call to `get_signing_key_from_jwt`, the JWKS
    +        is fetched if it is older than `self.cache_max_age` ms unless
    +        cooldown is active.
    +        """
    +        self.uri = uri
    +        self.cooldown_duration = cooldown_duration
    +        self.cache_max_age = cache_max_age
    +        self.timeout_sec = 5
    +        self.last_fetch_time: int = 0
    +        self.jwk_set: Optional[PyJWKSet] = None
    +
    +    def reload(self):
    +        try:
    +            with urllib.request.urlopen(self.uri, timeout=self.timeout_sec) as response:
    +                self.jwk_set = PyJWKSet.from_dict(json.load(response))  # type: ignore
    +                self.last_fetch_time = get_timestamp_ms()
    +        except URLError as e:
    +            raise JWKSRequestError(f'Failed to fetch data from the url, err: "{e}"')
    +
    +    def is_cooling_down(self) -> bool:
    +        return (self.last_fetch_time > 0) and (
    +            get_timestamp_ms() - self.last_fetch_time < self.cooldown_duration
    +        )
    +
    +    def is_fresh(self) -> bool:
    +        return (self.last_fetch_time > 0) and (
    +            get_timestamp_ms() - self.last_fetch_time < self.cache_max_age
    +        )
    +
    +    def get_latest_keys(self) -> List[PyJWK]:
    +        if self.jwk_set is None or not self.is_fresh():
    +            self.reload()
    +
    +        if self.jwk_set is None:
    +            raise JWKSRequestError("Failed to fetch the latest keys")
    +
    +        all_keys: List[PyJWK] = self.jwk_set.keys  # type: ignore
    +
    +        return all_keys
    +
    +    def get_matching_key_from_jwt(self, token: str) -> PyJWK:
    +        header = decode_token(token, options={"verify_signature": False})["header"]
    +        kid: str = header["kid"]  # type: ignore
    +
    +        if self.jwk_set is None or not self.is_fresh():
    +            self.reload()
    +
    +        assert self.jwk_set is not None
    +
    +        try:
    +            return self.jwk_set[kid]  # type: ignore
    +        except IndexError:
    +            if not self.is_cooling_down():
    +                # One more attempt to fetch the latest keys
    +                # and then try to find the key again.
    +                self.reload()
    +                try:
    +                    return self.jwk_set[kid]  # type: ignore
    +                except IndexError:
    +                    pass
    +
    +        raise JWKSKeyNotFoundError("No key found for the given kid")
    +
    +
    +class JWKSKeyNotFoundError(Exception):
    +    pass
    +
    +
    +class JWKSRequestError(Exception):
    +    pass
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +

    Classes

    +
    +
    +class JWKClient +(uri: str, cooldown_duration: int = 500, cache_max_age: int = 60000) +
    +
    +

    A client for retrieving JSON Web Key Sets (JWKS) from a given URI.

    +

    Args

    +
    +
    uri : str
    +
    The URI of the JWKS.
    +
    cooldown_duration : int, optional
    +
    The cooldown duration in ms. Defaults to 500 seconds.
    +
    cache_max_age : int, optional
    +
    The cache max age in ms. Defaults to 5 minutes.
    +
    +

    Note: The JSON Web Key Set is fetched when no key matches the selection +process but only as frequently as the self.cooldown_duration option +allows to prevent abuse. The self.cache_max_age option is used to +determine how long the JWKS is cached for.

    +

    Whenever you make a call to get_signing_key_from_jwt, the JWKS +is fetched if it is older than self.cache_max_age ms unless +cooldown is active.

    +
    + +Expand source code + +
    class JWKClient:
    +    def __init__(
    +        self,
    +        uri: str,
    +        cooldown_duration: int = JWKRequestCooldownInMs,
    +        cache_max_age: int = JWKCacheMaxAgeInMs,
    +    ):
    +        """A client for retrieving JSON Web Key Sets (JWKS) from a given URI.
    +
    +        Args:
    +            uri (str): The URI of the JWKS.
    +            cooldown_duration (int, optional): The cooldown duration in ms. Defaults to 500 seconds.
    +            cache_max_age (int, optional): The cache max age in ms. Defaults to 5 minutes.
    +
    +        Note: The JSON Web Key Set is fetched when no key matches the selection
    +        process but only as frequently as the `self.cooldown_duration` option
    +        allows to prevent abuse. The `self.cache_max_age` option is used to
    +        determine how long the JWKS is cached for.
    +
    +        Whenever you make a call to `get_signing_key_from_jwt`, the JWKS
    +        is fetched if it is older than `self.cache_max_age` ms unless
    +        cooldown is active.
    +        """
    +        self.uri = uri
    +        self.cooldown_duration = cooldown_duration
    +        self.cache_max_age = cache_max_age
    +        self.timeout_sec = 5
    +        self.last_fetch_time: int = 0
    +        self.jwk_set: Optional[PyJWKSet] = None
    +
    +    def reload(self):
    +        try:
    +            with urllib.request.urlopen(self.uri, timeout=self.timeout_sec) as response:
    +                self.jwk_set = PyJWKSet.from_dict(json.load(response))  # type: ignore
    +                self.last_fetch_time = get_timestamp_ms()
    +        except URLError as e:
    +            raise JWKSRequestError(f'Failed to fetch data from the url, err: "{e}"')
    +
    +    def is_cooling_down(self) -> bool:
    +        return (self.last_fetch_time > 0) and (
    +            get_timestamp_ms() - self.last_fetch_time < self.cooldown_duration
    +        )
    +
    +    def is_fresh(self) -> bool:
    +        return (self.last_fetch_time > 0) and (
    +            get_timestamp_ms() - self.last_fetch_time < self.cache_max_age
    +        )
    +
    +    def get_latest_keys(self) -> List[PyJWK]:
    +        if self.jwk_set is None or not self.is_fresh():
    +            self.reload()
    +
    +        if self.jwk_set is None:
    +            raise JWKSRequestError("Failed to fetch the latest keys")
    +
    +        all_keys: List[PyJWK] = self.jwk_set.keys  # type: ignore
    +
    +        return all_keys
    +
    +    def get_matching_key_from_jwt(self, token: str) -> PyJWK:
    +        header = decode_token(token, options={"verify_signature": False})["header"]
    +        kid: str = header["kid"]  # type: ignore
    +
    +        if self.jwk_set is None or not self.is_fresh():
    +            self.reload()
    +
    +        assert self.jwk_set is not None
    +
    +        try:
    +            return self.jwk_set[kid]  # type: ignore
    +        except IndexError:
    +            if not self.is_cooling_down():
    +                # One more attempt to fetch the latest keys
    +                # and then try to find the key again.
    +                self.reload()
    +                try:
    +                    return self.jwk_set[kid]  # type: ignore
    +                except IndexError:
    +                    pass
    +
    +        raise JWKSKeyNotFoundError("No key found for the given kid")
    +
    +

    Methods

    +
    +
    +def get_latest_keys(self) ‑> List[jwt.api_jwk.PyJWK] +
    +
    +
    +
    + +Expand source code + +
    def get_latest_keys(self) -> List[PyJWK]:
    +    if self.jwk_set is None or not self.is_fresh():
    +        self.reload()
    +
    +    if self.jwk_set is None:
    +        raise JWKSRequestError("Failed to fetch the latest keys")
    +
    +    all_keys: List[PyJWK] = self.jwk_set.keys  # type: ignore
    +
    +    return all_keys
    +
    +
    +
    +def get_matching_key_from_jwt(self, token: str) ‑> jwt.api_jwk.PyJWK +
    +
    +
    +
    + +Expand source code + +
    def get_matching_key_from_jwt(self, token: str) -> PyJWK:
    +    header = decode_token(token, options={"verify_signature": False})["header"]
    +    kid: str = header["kid"]  # type: ignore
    +
    +    if self.jwk_set is None or not self.is_fresh():
    +        self.reload()
    +
    +    assert self.jwk_set is not None
    +
    +    try:
    +        return self.jwk_set[kid]  # type: ignore
    +    except IndexError:
    +        if not self.is_cooling_down():
    +            # One more attempt to fetch the latest keys
    +            # and then try to find the key again.
    +            self.reload()
    +            try:
    +                return self.jwk_set[kid]  # type: ignore
    +            except IndexError:
    +                pass
    +
    +    raise JWKSKeyNotFoundError("No key found for the given kid")
    +
    +
    +
    +def is_cooling_down(self) ‑> bool +
    +
    +
    +
    + +Expand source code + +
    def is_cooling_down(self) -> bool:
    +    return (self.last_fetch_time > 0) and (
    +        get_timestamp_ms() - self.last_fetch_time < self.cooldown_duration
    +    )
    +
    +
    +
    +def is_fresh(self) ‑> bool +
    +
    +
    +
    + +Expand source code + +
    def is_fresh(self) -> bool:
    +    return (self.last_fetch_time > 0) and (
    +        get_timestamp_ms() - self.last_fetch_time < self.cache_max_age
    +    )
    +
    +
    +
    +def reload(self) +
    +
    +
    +
    + +Expand source code + +
    def reload(self):
    +    try:
    +        with urllib.request.urlopen(self.uri, timeout=self.timeout_sec) as response:
    +            self.jwk_set = PyJWKSet.from_dict(json.load(response))  # type: ignore
    +            self.last_fetch_time = get_timestamp_ms()
    +    except URLError as e:
    +        raise JWKSRequestError(f'Failed to fetch data from the url, err: "{e}"')
    +
    +
    +
    +
    +
    +class JWKSKeyNotFoundError +(*args, **kwargs) +
    +
    +

    Common base class for all non-exit exceptions.

    +
    + +Expand source code + +
    class JWKSKeyNotFoundError(Exception):
    +    pass
    +
    +

    Ancestors

    +
      +
    • builtins.Exception
    • +
    • builtins.BaseException
    • +
    +
    +
    +class JWKSRequestError +(*args, **kwargs) +
    +
    +

    Common base class for all non-exit exceptions.

    +
    + +Expand source code + +
    class JWKSRequestError(Exception):
    +    pass
    +
    +

    Ancestors

    +
      +
    • builtins.Exception
    • +
    • builtins.BaseException
    • +
    +
    +
    +
    +
    + +
    + + + \ No newline at end of file diff --git a/html/supertokens_python/recipe/session/jwt.html b/html/supertokens_python/recipe/session/jwt.html index 1ec8359de..97f908a6b 100644 --- a/html/supertokens_python/recipe/session/jwt.html +++ b/html/supertokens_python/recipe/session/jwt.html @@ -39,34 +39,23 @@

    Module supertokens_python.recipe.session.jwt

    # 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 base64 import b64decode from json import dumps, loads -from textwrap import wrap -from typing import Any, Dict +from typing import Any, Dict, Optional -from Crypto.Hash import SHA256 -from Crypto.PublicKey import RSA -from Crypto.Signature.pkcs1_15 import PKCS115_SigScheme from supertokens_python.utils import utf_base64decode, utf_base64encode -_key_start = "-----BEGIN PUBLIC KEY-----\n" -_key_end = "\n-----END PUBLIC KEY-----" - -""" -why separators is used in dumps: -- without it's use, output of dumps is: '{"alg": "RS256", "typ": "JWT", "version": "1"}' -- with it's use, output of dumps is: '{"alg":"RS256","typ":"JWT","version":"1"}' - -we require the non-spaced version, else the base64 encoding string will end up different than required -""" +# why separators is used in dumps: +# - without it's use, output of dumps is: '{"alg": "RS256", "typ": "JWT", "version": "1"}' +# - with it's use, output of dumps is: '{"alg":"RS256","typ":"JWT","version":"1"}' +# we require the non-spaced version, else the base64 encoding string will end up different than required _allowed_headers = [ utf_base64encode( dumps( {"alg": "RS256", "typ": "JWT", "version": "2"}, separators=(",", ":"), sort_keys=True, - ) + ), + urlsafe=False, ) ] @@ -74,17 +63,21 @@

    Module supertokens_python.recipe.session.jwt

    class ParsedJWTInfo: def __init__( self, + version: int, raw_token_string: str, raw_payload: str, header: str, payload: Dict[str, Any], signature: str, + kid: Optional[str], ) -> None: + self.version = version self.raw_token_string = raw_token_string self.raw_payload = raw_payload self.header = header self.payload = payload self.signature = signature + self.kid = kid def parse_jwt_without_signature_verification(jwt: str) -> ParsedJWTInfo: @@ -92,31 +85,47 @@

    Module supertokens_python.recipe.session.jwt

    if len(splitted_input) != 3: raise Exception("invalid jwt") + # V1 and V2 are functionally identical, plus all legacy tokens should be V2 now. + # So we can assume these defaults: + version = 2 + kid = None + # V2 or older tokens didn't save the key id header, payload, signature = splitted_input + # checking the header if header not in _allowed_headers: - raise Exception("jwt header mismatch") + parsed_header = loads(utf_base64decode(header, True)) + header_version = parsed_header.get("version") + + # We have to ensure version is a string, otherwise Number.parseInt can have unexpected results + if not isinstance(header_version, str): + raise Exception("JWT header mismatch") + + try: + version = int(header_version) + except ValueError: + version = None + + kid = parsed_header.get("kid") + # Number.isInteger returns false for Number.NaN (if it fails to parse the version) + if ( + parsed_header["typ"] != "JWT" + or not isinstance(version, int) + or version < 3 + or kid is None + ): + raise Exception("JWT header mismatch") return ParsedJWTInfo( + version=version, raw_token_string=jwt, raw_payload=payload, header=header, # Ideally we would only parse this after the signature verification is done # We do this at the start, since we want to check if a token can be a supertokens access token or not. - payload=loads(utf_base64decode(payload)), + payload=loads(utf_base64decode(payload, True)), signature=signature, - ) - - -def verify_jwt(info: ParsedJWTInfo, jwt_signing_public_key: str): - public_key = RSA.import_key( - _key_start + "\n".join(wrap(jwt_signing_public_key, width=64)) + _key_end - ) - verifier = PKCS115_SigScheme(public_key) - to_verify = SHA256.new((info.header + "." + info.raw_payload).encode("utf-8")) - try: - verifier.verify(to_verify, b64decode(info.signature.encode("utf-8"))) - except BaseException: - raise Exception("jwt verification failed")
    + kid=kid, + )
    @@ -140,42 +149,49 @@

    Functions

    if len(splitted_input) != 3: raise Exception("invalid jwt") + # V1 and V2 are functionally identical, plus all legacy tokens should be V2 now. + # So we can assume these defaults: + version = 2 + kid = None + # V2 or older tokens didn't save the key id header, payload, signature = splitted_input + # checking the header if header not in _allowed_headers: - raise Exception("jwt header mismatch") + parsed_header = loads(utf_base64decode(header, True)) + header_version = parsed_header.get("version") + + # We have to ensure version is a string, otherwise Number.parseInt can have unexpected results + if not isinstance(header_version, str): + raise Exception("JWT header mismatch") + + try: + version = int(header_version) + except ValueError: + version = None + + kid = parsed_header.get("kid") + # Number.isInteger returns false for Number.NaN (if it fails to parse the version) + if ( + parsed_header["typ"] != "JWT" + or not isinstance(version, int) + or version < 3 + or kid is None + ): + raise Exception("JWT header mismatch") return ParsedJWTInfo( + version=version, raw_token_string=jwt, raw_payload=payload, header=header, # Ideally we would only parse this after the signature verification is done # We do this at the start, since we want to check if a token can be a supertokens access token or not. - payload=loads(utf_base64decode(payload)), + payload=loads(utf_base64decode(payload, True)), signature=signature, + kid=kid, )
    -
    -def verify_jwt(info: ParsedJWTInfo, jwt_signing_public_key: str) -
    -
    -
    -
    - -Expand source code - -
    def verify_jwt(info: ParsedJWTInfo, jwt_signing_public_key: str):
    -    public_key = RSA.import_key(
    -        _key_start + "\n".join(wrap(jwt_signing_public_key, width=64)) + _key_end
    -    )
    -    verifier = PKCS115_SigScheme(public_key)
    -    to_verify = SHA256.new((info.header + "." + info.raw_payload).encode("utf-8"))
    -    try:
    -        verifier.verify(to_verify, b64decode(info.signature.encode("utf-8")))
    -    except BaseException:
    -        raise Exception("jwt verification failed")
    -
    -
    @@ -183,7 +199,7 @@

    Classes

    class ParsedJWTInfo -(raw_token_string: str, raw_payload: str, header: str, payload: Dict[str, Any], signature: str) +(version: int, raw_token_string: str, raw_payload: str, header: str, payload: Dict[str, Any], signature: str, kid: Optional[str])
    @@ -194,17 +210,21 @@

    Classes

    class ParsedJWTInfo:
         def __init__(
             self,
    +        version: int,
             raw_token_string: str,
             raw_payload: str,
             header: str,
             payload: Dict[str, Any],
             signature: str,
    +        kid: Optional[str],
         ) -> None:
    +        self.version = version
             self.raw_token_string = raw_token_string
             self.raw_payload = raw_payload
             self.header = header
             self.payload = payload
    -        self.signature = signature
    + self.signature = signature + self.kid = kid
    @@ -224,7 +244,6 @@

    Index

  • Functions

  • Classes

    diff --git a/html/supertokens_python/recipe/session/recipe.html b/html/supertokens_python/recipe/session/recipe.html index 463bc2b35..68b07b4a4 100644 --- a/html/supertokens_python/recipe/session/recipe.html +++ b/html/supertokens_python/recipe/session/recipe.html @@ -47,16 +47,20 @@

    Module supertokens_python.recipe.session.recipeModule supertokens_python.recipe.session.recipeModule supertokens_python.recipe.session.recipeModule supertokens_python.recipe.session.recipeModule supertokens_python.recipe.session.recipeModule supertokens_python.recipe.session.recipeModule supertokens_python.recipe.session.recipeModule supertokens_python.recipe.session.recipeModule supertokens_python.recipe.session.recipeModule supertokens_python.recipe.session.recipeModule supertokens_python.recipe.session.recipeModule supertokens_python.recipe.session.recipeClasses

    class SessionRecipe -(recipe_id: str, app_info: AppInfo, cookie_domain: Union[str, None] = None, cookie_secure: Union[bool, None] = None, cookie_same_site: "Union[Literal['lax', 'none', 'strict'], None]" = None, session_expired_status_code: Union[int, None] = None, anti_csrf: "Union[Literal['VIA_TOKEN', 'VIA_CUSTOM_HEADER', 'NONE'], None]" = None, get_token_transfer_method: "Union[Callable[[BaseRequest, bool, Dict[str, Any]], Union[TokenTransferMethod, Literal['any']]], None]" = None, error_handlers: Union[InputErrorHandlers, None] = None, override: Union[InputOverrideConfig, None] = None, jwt: Union[JWTConfig, None] = None, invalid_claim_status_code: Union[int, None] = None) +(recipe_id: str, app_info: AppInfo, cookie_domain: Union[str, None] = None, cookie_secure: Union[bool, None] = None, cookie_same_site: "Union[Literal['lax', 'none', 'strict'], None]" = None, session_expired_status_code: Union[int, None] = None, anti_csrf: "Union[Literal['VIA_TOKEN', 'VIA_CUSTOM_HEADER', 'NONE'], None]" = None, get_token_transfer_method: "Union[Callable[[BaseRequest, bool, Dict[str, Any]], Union[TokenTransferMethod, Literal['any']]], None]" = None, error_handlers: Union[InputErrorHandlers, None] = None, override: Union[InputOverrideConfig, None] = None, invalid_claim_status_code: Union[int, None] = None, use_dynamic_access_token_signing_key: Union[bool, None] = None, expose_access_token_to_frontend_in_cookie_based_auth: Union[bool, None] = None)

    Helper class that provides a standard way to create an ABC using @@ -453,11 +438,18 @@

    Classes

    ] = None, error_handlers: Union[InputErrorHandlers, None] = None, override: Union[InputOverrideConfig, None] = None, - jwt: Union[JWTConfig, None] = None, invalid_claim_status_code: Union[int, None] = None, + use_dynamic_access_token_signing_key: Union[bool, None] = None, + expose_access_token_to_frontend_in_cookie_based_auth: Union[bool, None] = None, ): super().__init__(recipe_id, app_info) - self.openid_recipe: Union[None, OpenIdRecipe] = None + self.openid_recipe = OpenIdRecipe( + recipe_id, + app_info, + None, + None, + override.openid_feature if override is not None else None, + ) self.config = validate_and_normalise_user_input( app_info, cookie_domain, @@ -468,8 +460,9 @@

    Classes

    get_token_transfer_method, error_handlers, override, - jwt, invalid_claim_status_code, + use_dynamic_access_token_signing_key, + expose_access_token_to_frontend_in_cookie_based_auth, ) log_debug_message("session init: anti_csrf: %s", self.config.anti_csrf) if self.config.cookie_domain is not None: @@ -492,35 +485,17 @@

    Classes

    "session init: session_expired_status_code: %s", str(self.config.session_expired_status_code), ) - - if self.config.jwt.enable: - openid_feature_override = None - if override is not None: - openid_feature_override = override.openid_feature - self.openid_recipe = OpenIdRecipe( - recipe_id, - app_info, - None, - self.config.jwt.issuer, - openid_feature_override, - ) - recipe_implementation = RecipeImplementation( - Querier.get_instance(recipe_id), self.config, self.app_info - ) - recipe_implementation = get_recipe_implementation_with_jwt( - recipe_implementation, - self.config, - self.openid_recipe.recipe_implementation, - ) - else: - recipe_implementation = RecipeImplementation( - Querier.get_instance(recipe_id), self.config, self.app_info - ) + recipe_implementation = RecipeImplementation( + Querier.get_instance(recipe_id), self.config, self.app_info + ) self.recipe_implementation: RecipeInterface = ( recipe_implementation if self.config.override.functions is None else self.config.override.functions(recipe_implementation) ) + + from .api.implementation import APIImplementation + api_implementation = APIImplementation() self.api_implementation: APIInterface = ( api_implementation @@ -534,10 +509,7 @@

    Classes

    def is_error_from_this_recipe_based_on_instance(self, err: Exception) -> bool: return isinstance(err, SuperTokensError) and ( isinstance(err, SuperTokensSessionError) - or ( - self.openid_recipe is not None - and self.openid_recipe.is_error_from_this_recipe_based_on_instance(err) - ) + or self.openid_recipe.is_error_from_this_recipe_based_on_instance(err) ) def get_apis_handled(self) -> List[APIHandled]: @@ -555,8 +527,7 @@

    Classes

    self.api_implementation.disable_signout_post, ), ] - if self.openid_recipe is not None: - apis_handled = apis_handled + self.openid_recipe.get_apis_handled() + apis_handled.extend(self.openid_recipe.get_apis_handled()) return apis_handled @@ -590,11 +561,9 @@

    Classes

    self.recipe_implementation, ), ) - if self.openid_recipe is not None: - return await self.openid_recipe.handle_api_request( - request_id, request, path, method, response - ) - return None + return await self.openid_recipe.handle_api_request( + request_id, request, path, method, response + ) async def handle_error( self, request: BaseRequest, err: SuperTokensError, response: BaseResponse @@ -626,8 +595,7 @@

    Classes

    def get_all_cors_headers(self) -> List[str]: cors_headers = get_cors_allowed_headers() - if self.openid_recipe is not None: - cors_headers = cors_headers + self.openid_recipe.get_all_cors_headers() + cors_headers.extend(self.openid_recipe.get_all_cors_headers()) return cors_headers @@ -649,8 +617,9 @@

    Classes

    ] = None, error_handlers: Union[InputErrorHandlers, None] = None, override: Union[InputOverrideConfig, None] = None, - jwt: Union[JWTConfig, None] = None, invalid_claim_status_code: Union[int, None] = None, + use_dynamic_access_token_signing_key: Union[bool, None] = None, + expose_access_token_to_frontend_in_cookie_based_auth: Union[bool, None] = None, ): def func(app_info: AppInfo): if SessionRecipe.__instance is None: @@ -665,8 +634,9 @@

    Classes

    get_token_transfer_method, error_handlers, override, - jwt, invalid_claim_status_code, + use_dynamic_access_token_signing_key, + expose_access_token_to_frontend_in_cookie_based_auth, ) return SessionRecipe.__instance raise_general_exception( @@ -775,7 +745,7 @@

    Static methods

    -def init(cookie_domain: Union[str, None] = None, cookie_secure: Union[bool, None] = None, cookie_same_site: "Union[Literal['lax', 'none', 'strict'], None]" = None, session_expired_status_code: Union[int, None] = None, anti_csrf: "Union[Literal['VIA_TOKEN', 'VIA_CUSTOM_HEADER', 'NONE'], None]" = None, get_token_transfer_method: "Union[Callable[[BaseRequest, bool, Dict[str, Any]], Union[TokenTransferMethod, Literal['any']]], None]" = None, error_handlers: Union[InputErrorHandlers, None] = None, override: Union[InputOverrideConfig, None] = None, jwt: Union[JWTConfig, None] = None, invalid_claim_status_code: Union[int, None] = None) +def init(cookie_domain: Union[str, None] = None, cookie_secure: Union[bool, None] = None, cookie_same_site: "Union[Literal['lax', 'none', 'strict'], None]" = None, session_expired_status_code: Union[int, None] = None, anti_csrf: "Union[Literal['VIA_TOKEN', 'VIA_CUSTOM_HEADER', 'NONE'], None]" = None, get_token_transfer_method: "Union[Callable[[BaseRequest, bool, Dict[str, Any]], Union[TokenTransferMethod, Literal['any']]], None]" = None, error_handlers: Union[InputErrorHandlers, None] = None, override: Union[InputOverrideConfig, None] = None, invalid_claim_status_code: Union[int, None] = None, use_dynamic_access_token_signing_key: Union[bool, None] = None, expose_access_token_to_frontend_in_cookie_based_auth: Union[bool, None] = None)
    @@ -801,8 +771,9 @@

    Static methods

    ] = None, error_handlers: Union[InputErrorHandlers, None] = None, override: Union[InputOverrideConfig, None] = None, - jwt: Union[JWTConfig, None] = None, invalid_claim_status_code: Union[int, None] = None, + use_dynamic_access_token_signing_key: Union[bool, None] = None, + expose_access_token_to_frontend_in_cookie_based_auth: Union[bool, None] = None, ): def func(app_info: AppInfo): if SessionRecipe.__instance is None: @@ -817,8 +788,9 @@

    Static methods

    get_token_transfer_method, error_handlers, override, - jwt, invalid_claim_status_code, + use_dynamic_access_token_signing_key, + expose_access_token_to_frontend_in_cookie_based_auth, ) return SessionRecipe.__instance raise_general_exception( @@ -894,8 +866,7 @@

    Methods

    def get_all_cors_headers(self) -> List[str]:
         cors_headers = get_cors_allowed_headers()
    -    if self.openid_recipe is not None:
    -        cors_headers = cors_headers + self.openid_recipe.get_all_cors_headers()
    +    cors_headers.extend(self.openid_recipe.get_all_cors_headers())
     
         return cors_headers
    @@ -924,8 +895,7 @@

    Methods

    self.api_implementation.disable_signout_post, ), ] - if self.openid_recipe is not None: - apis_handled = apis_handled + self.openid_recipe.get_apis_handled() + apis_handled.extend(self.openid_recipe.get_apis_handled()) return apis_handled @@ -997,11 +967,9 @@

    Methods

    self.recipe_implementation, ), ) - if self.openid_recipe is not None: - return await self.openid_recipe.handle_api_request( - request_id, request, path, method, response - ) - return None + return await self.openid_recipe.handle_api_request( + request_id, request, path, method, response + )
    @@ -1054,10 +1022,7 @@

    Methods

    def is_error_from_this_recipe_based_on_instance(self, err: Exception) -> bool:
         return isinstance(err, SuperTokensError) and (
             isinstance(err, SuperTokensSessionError)
    -        or (
    -            self.openid_recipe is not None
    -            and self.openid_recipe.is_error_from_this_recipe_based_on_instance(err)
    -        )
    +        or self.openid_recipe.is_error_from_this_recipe_based_on_instance(err)
         )
    diff --git a/html/supertokens_python/recipe/session/recipe_implementation.html b/html/supertokens_python/recipe/session/recipe_implementation.html index 4d8a68549..434ef3077 100644 --- a/html/supertokens_python/recipe/session/recipe_implementation.html +++ b/html/supertokens_python/recipe/session/recipe_implementation.html @@ -44,37 +44,15 @@

    Module supertokens_python.recipe.session.recipe_implemen import json from typing import TYPE_CHECKING, Any, Callable, Dict, Optional -from supertokens_python.framework import BaseRequest from supertokens_python.logger import log_debug_message from supertokens_python.normalised_url_path import NormalisedURLPath -from supertokens_python.process_state import AllowedProcessStates, ProcessState -from supertokens_python.utils import ( - execute_async, - get_timestamp_ms, - is_an_ip_address, - normalise_http_method, - resolve, -) +from supertokens_python.utils import resolve from ...types import MaybeAwaitable from . import session_functions from .access_token import validate_access_token_structure -from .cookie_and_header import ( - anti_csrf_response_mutator, - clear_session_response_mutator, - front_token_response_mutator, - get_anti_csrf_header, - get_rid_header, - get_token, - set_cookie_response_mutator, - token_response_mutator, -) -from .exceptions import ( - TokenTheftError, - UnauthorisedError, - raise_try_refresh_token_exception, - raise_unauthorised_exception, -) +from .cookie_and_header import build_front_token +from .exceptions import UnauthorisedError from .interfaces import ( AccessTokenObj, ClaimsValidationResult, @@ -82,233 +60,83 @@

    Module supertokens_python.recipe.session.recipe_implemen JSONObject, RecipeInterface, RegenerateAccessTokenOkResult, - ResponseMutator, SessionClaim, SessionClaimValidator, SessionDoesNotExistError, SessionInformationResult, SessionObj, ) +from .jwks import JWKClient from .jwt import ParsedJWTInfo, parse_jwt_without_signature_verification from .session_class import Session -from .utils import ( - HUNDRED_YEARS_IN_MS, - SessionConfig, - TokenTransferMethod, - validate_claims_in_payload, -) +from .utils import SessionConfig, validate_claims_in_payload if TYPE_CHECKING: from typing import List, Union from supertokens_python import AppInfo from supertokens_python.querier import Querier -from .constants import available_token_transfer_methods +from .constants import JWKCacheMaxAgeInMs, JWKRequestCooldownInMs from .interfaces import SessionContainer -class HandshakeInfo: - def __init__(self, info: Dict[str, Any]): - self.access_token_blacklisting_enabled = info["accessTokenBlacklistingEnabled"] - self.raw_jwt_signing_public_key_list: List[Dict[str, Any]] = [] - self.anti_csrf = info["antiCsrf"] - self.access_token_validity = info["accessTokenValidity"] - self.refresh_token_validity = info["refreshTokenValidity"] - - def set_jwt_signing_public_key_list(self, updated_list: List[Dict[str, Any]]): - self.raw_jwt_signing_public_key_list = updated_list - - def get_jwt_signing_public_key_list(self) -> List[Dict[str, Any]]: - time_now = get_timestamp_ms() - return [ - key - for key in self.raw_jwt_signing_public_key_list - if key["expiryTime"] > time_now - ] - - -LEGACY_ID_REFRESH_TOKEN_COOKIE_NAME = "sIdRefreshToken" - - class RecipeImplementation(RecipeInterface): # pylint: disable=too-many-public-methods def __init__(self, querier: Querier, config: SessionConfig, app_info: AppInfo): super().__init__() self.querier = querier self.config = config self.app_info = app_info - self.handshake_info: Union[HandshakeInfo, None] = None - - async def call_get_handshake_info(): - try: - await self.get_handshake_info() - except Exception: - pass - try: - execute_async(config.mode, call_get_handshake_info) - except Exception: - pass - - async def get_handshake_info(self, force_refetch: bool = False) -> HandshakeInfo: - if ( - self.handshake_info is None - or len(self.handshake_info.get_jwt_signing_public_key_list()) == 0 - or force_refetch - ): - ProcessState.get_instance().add_state( - AllowedProcessStates.CALLING_SERVICE_IN_GET_HANDSHAKE_INFO - ) - response = await self.querier.send_post_request( - NormalisedURLPath("/recipe/handshake"), {} + self.JWK_clients = [ + JWKClient( + uri, + cooldown_duration=JWKRequestCooldownInMs, + cache_max_age=JWKCacheMaxAgeInMs, ) - self.handshake_info = HandshakeInfo( - {**response, "antiCsrf": self.config.anti_csrf} - ) - - self.update_jwt_signing_public_key_info( - response["jwtSigningPublicKeyList"], - response["jwtSigningPublicKey"], - response["jwtSigningPublicKeyExpiryTime"], + for uri in self.querier.get_all_core_urls_for_path( + "./.well-known/jwks.json" ) - - return self.handshake_info - - def update_jwt_signing_public_key_info( - self, - key_list: Union[List[Dict[str, Any]], None], - public_key: str, - expiry_time: int, - ): - if key_list is None: - key_list = [ - { - "publicKey": public_key, - "expiryTime": expiry_time, - "createdAt": get_timestamp_ms(), - } - ] - - if self.handshake_info is not None: - self.handshake_info.set_jwt_signing_public_key_list(key_list) + ] async def create_new_session( self, - request: BaseRequest, user_id: str, - access_token_payload: Union[None, Dict[str, Any]], - session_data: Union[None, Dict[str, Any]], + access_token_payload: Optional[Dict[str, Any]], + session_data_in_database: Optional[Dict[str, Any]], + disable_anti_csrf: Optional[bool], user_context: Dict[str, Any], ) -> SessionContainer: log_debug_message("createNewSession: Started") - output_transfer_method = self.config.get_token_transfer_method( - request, True, user_context - ) - if output_transfer_method == "any": - output_transfer_method = "header" - - log_debug_message( - "createNewSession: using transfer method %s", output_transfer_method - ) - - if ( - (output_transfer_method == "cookie") - and self.config.cookie_same_site == "none" - and not self.config.cookie_secure - and not ( - ( - self.app_info.top_level_api_domain == "localhost" - or is_an_ip_address(self.app_info.top_level_api_domain) - ) - and ( - self.app_info.top_level_website_domain == "localhost" - or is_an_ip_address(self.app_info.top_level_website_domain) - ) - ) - ): - # We can allow insecure cookie when both website & API domain are localhost or an IP - # When either of them is a different domain, API domain needs to have https and a secure cookie to work - raise Exception( - "Since your API and website domain are different, for sessions to work, please use " - "https on your apiDomain and don't set cookieSecure to false." - ) - - disable_anti_csrf = output_transfer_method == "header" result = await session_functions.create_new_session( self, user_id, - disable_anti_csrf, + disable_anti_csrf is True, access_token_payload, - session_data, + session_data_in_database, ) + log_debug_message("createNewSession: Finished") - response_mutators: List[ResponseMutator] = [] - - for transfer_method in available_token_transfer_methods: - request_access_token = get_token(request, "access", transfer_method) - - if ( - transfer_method != output_transfer_method - and request_access_token is not None - ): - response_mutators.append( - clear_session_response_mutator( - self.config, - transfer_method, - ) - ) + payload = parse_jwt_without_signature_verification( + result.accessToken.token + ).payload new_session = Session( self, self.config, - result["accessToken"]["token"], - result["session"]["handle"], - result["session"]["userId"], - result["session"]["userDataInJWT"], - output_transfer_method, + result.accessToken.token, + build_front_token( + result.session.userId, result.accessToken.expiry, payload + ), + result.refreshToken, + result.antiCsrfToken, + result.session.handle, + result.session.userId, + payload, + None, + True, ) - new_access_token_info: Dict[str, Any] = result["accessToken"] - new_refresh_token_info: Dict[str, Any] = result["refreshToken"] - anti_csrf_token: Optional[str] = result.get("antiCsrfToken") - - response_mutators.append( - front_token_response_mutator( - new_session.user_id, - new_access_token_info["expiry"], - new_session.access_token_payload, - ) - ) - # We set the expiration to 100 years, because we can't really access the expiration of the refresh token everywhere we are setting it. - # 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. - # Even if the token is expired the presence of the token indicates that the user could have a valid refresh - # Setting them to infinity would require special case handling on the frontend and just adding 100 years seems enough. - response_mutators.append( - token_response_mutator( - self.config, - "access", - new_access_token_info["token"], - get_timestamp_ms() + HUNDRED_YEARS_IN_MS, - new_session.transfer_method, - ) - ) - response_mutators.append( - token_response_mutator( - self.config, - "refresh", - new_refresh_token_info["token"], - new_refresh_token_info[ - "expiry" - ], # This comes from the core and is 100 days - new_session.transfer_method, - ) - ) - if anti_csrf_token is not None: - response_mutators.append(anti_csrf_response_mutator(anti_csrf_token)) - - new_session.response_mutators.extend(response_mutators) - - request.set_session(new_session) return new_session async def validate_claims( @@ -369,337 +197,140 @@

    Module supertokens_python.recipe.session.recipe_implemen return ClaimsValidationResult(invalid_claims) - # In all cases if sIdRefreshToken token exists (so it's a legacy session) we return TRY_REFRESH_TOKEN. The refresh - # endpoint will clear this cookie and try to upgrade the session. - # Check https://supertokens.com/docs/contribute/decisions/session/0007 for further details and a table of expected - # behaviours async def get_session( self, - request: BaseRequest, - anti_csrf_check: Union[bool, None], - session_required: bool, - user_context: Dict[str, Any], + access_token: str, + anti_csrf_token: Optional[str], + anti_csrf_check: Optional[bool] = None, + session_required: Optional[bool] = None, + check_database: Optional[bool] = None, + override_global_claim_validators: Optional[ + Callable[ + [List[SessionClaimValidator], SessionContainer, Dict[str, Any]], + MaybeAwaitable[List[SessionClaimValidator]], + ] + ] = None, + user_context: Optional[Dict[str, Any]] = None, ) -> Optional[SessionContainer]: - log_debug_message("getSession: Started") - - # This token isn't handled by getToken to limit the scope of this legacy/migration code - if request.get_cookie(LEGACY_ID_REFRESH_TOKEN_COOKIE_NAME) is not None: - # This could create a spike on refresh calls during the update of the backend SDK - return raise_try_refresh_token_exception( - "using legacy session, please call the refresh API" + if ( + anti_csrf_check is not False + and self.config.anti_csrf == "VIA_CUSTOM_HEADER" + ): + raise Exception( + "Since the anti-csrf mode is VIA_CUSTOM_HEADER getSession can't check the CSRF token. Please either use VIA_TOKEN or set anti_csrf_check to false" ) - session_optional = not session_required - log_debug_message("getSession: optional validation: %s", session_optional) - - access_tokens: Dict[TokenTransferMethod, ParsedJWTInfo] = {} - - # We check all token transfer methods for available access tokens - for transfer_method in available_token_transfer_methods: - token_string = get_token(request, "access", transfer_method) - - if token_string is not None: - try: - info = parse_jwt_without_signature_verification(token_string) - validate_access_token_structure(info.payload) - log_debug_message( - "getSession: got access token from %s", transfer_method - ) - access_tokens[transfer_method] = info - except Exception: - log_debug_message( - "getSession: ignoring token in %s, because it doesn't match our access token structure", - transfer_method, - ) + log_debug_message("getSession: Started") - allowed_transfer_method = self.config.get_token_transfer_method( - request, False, user_context - ) - request_transfer_method: TokenTransferMethod - request_access_token: Union[ParsedJWTInfo, None] - - if (allowed_transfer_method in ("any", "header")) and access_tokens.get( - "header" - ) is not None: - log_debug_message("getSession: using header transfer method") - request_transfer_method = "header" - request_access_token = access_tokens["header"] - elif (allowed_transfer_method in ("any", "cookie")) and access_tokens.get( - "cookie" - ) is not None: - log_debug_message("getSession: using cookie transfer method") - request_transfer_method = "cookie" - request_access_token = access_tokens["cookie"] - else: - if session_optional: + access_token_obj: Optional[ParsedJWTInfo] = None + try: + access_token_obj = parse_jwt_without_signature_verification(access_token) + validate_access_token_structure( + access_token_obj.payload, access_token_obj.version + ) + except Exception as _: + if session_required is False: log_debug_message( - "getSession: returning None because accessToken is undefined and sessionRequired is false" + "getSession: Returning undefined because parsing failed and session_required is False" ) - # there is no session that exists here, and the user wants session verification - # to be optional. So we return None return None - log_debug_message( - "getSession: UNAUTHORISED because access_token in request is None" - ) - # we do not clear the session here because of a race condition mentioned in: - # https://github.com/supertokens/supertokens-node/issues/17 - raise_unauthorised_exception( - "Session does not exist. Are you sending the session tokens in the " - "request with the appropriate token transfer method?", - clear_tokens=False, - ) + raise UnauthorisedError("Token parsing failed", clear_tokens=False) - anti_csrf_token = get_anti_csrf_header(request) - do_anti_csrf_check = anti_csrf_check - - if do_anti_csrf_check is None: - do_anti_csrf_check = normalise_http_method(request.method()) != "get" - if request_transfer_method == "header": - do_anti_csrf_check = False - log_debug_message( - "getSession: Value of doAntiCsrfCheck is: %s", do_anti_csrf_check - ) - - result = await session_functions.get_session( + response = await session_functions.get_session( self, - request_access_token, + access_token_obj, anti_csrf_token, - do_anti_csrf_check, - get_rid_header(request) is not None, + (anti_csrf_check is not False), + (check_database is True), ) - # Default is to respond with the access token obtained from the request - access_token_string = request_access_token.raw_token_string + log_debug_message("getSession: Success!") + + if access_token_obj.version >= 3: + if response.accessToken is not None: + payload = parse_jwt_without_signature_verification( + response.accessToken.token + ).payload + else: + payload = access_token_obj.payload + else: + payload = response.session.userDataInJWT + + if response.accessToken is not None: + access_token_str = response.accessToken.token + expiry_time = response.accessToken.expiry + access_token_updated = True + else: + access_token_str = access_token + expiry_time = response.session.expiryTime + access_token_updated = False session = Session( self, self.config, - access_token_string, - result["session"]["handle"], - result["session"]["userId"], - result["session"]["userDataInJWT"], - request_transfer_method, + access_token_str, + build_front_token(response.session.userId, expiry_time, payload), + None, # refresh_token + anti_csrf_token, + response.session.handle, + response.session.userId, + payload, + None, + access_token_updated, ) - if "accessToken" in result: - session.access_token = result["accessToken"]["token"] - new_access_token_info = result["accessToken"] - - session.response_mutators.append( - front_token_response_mutator( - session.user_id, - new_access_token_info["expiry"], - session.access_token_payload, - ) - ) - # We set the expiration to 100 years, because we can't really access the expiration of the refresh token everywhere we are setting it. - # 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. - # Even if the token is expired the presence of the token indicates that the user could have a valid refresh - # Setting them to infinity would require special case handling on the frontend and just adding 100 years seems enough. - session.response_mutators.append( - token_response_mutator( - self.config, - "access", - session.access_token, - get_timestamp_ms() + HUNDRED_YEARS_IN_MS, - session.transfer_method, - ) - ) - - log_debug_message("getSession: Success!") - request.set_session(session) return session - # In all cases: if sIdRefreshToken token exists (it's a legacy session) we clear it - # Check http://localhost:3002/docs/contribute/decisions/session/0008 for further details and - # a table of expected behaviours async def refresh_session( - self, request: BaseRequest, user_context: Dict[str, Any] + self, + refresh_token: str, + anti_csrf_token: Optional[str], + disable_anti_csrf: bool, + user_context: Dict[str, Any], ) -> SessionContainer: - log_debug_message("refreshSession: Started") - - response_mutators: List[Callable[[Any], None]] = [] - refresh_tokens: Dict[TokenTransferMethod, Optional[str]] = {} - - # We check all token transfer methods for available refresh tokens - # We do this so that we can later clear all we are not overwriting - for transfer_method in available_token_transfer_methods: - refresh_token = get_token( - request, - "refresh", - transfer_method, - ) - if refresh_token is not None: - log_debug_message( - "refreshSession: got refresh token from %s", transfer_method - ) - - refresh_tokens[transfer_method] = refresh_token - - allowed_transfer_method = self.config.get_token_transfer_method( - request, False, user_context - ) - log_debug_message( - "refreshSession: getTokenTransferMethod returned: %s", - allowed_transfer_method, - ) - - request_transfer_method: TokenTransferMethod - refresh_token: Optional[str] - - if (allowed_transfer_method in ("any", "header")) and ( - refresh_tokens.get("header") - ): - log_debug_message("refreshSession: using header transfer method") - request_transfer_method = "header" - refresh_token = refresh_tokens["header"] - elif (allowed_transfer_method in ("any", "cookie")) and ( - refresh_tokens.get("cookie") + if ( + disable_anti_csrf is not True + and self.config.anti_csrf == "VIA_CUSTOM_HEADER" ): - log_debug_message("refreshSession: using cookie transfer method") - request_transfer_method = "cookie" - refresh_token = refresh_tokens["cookie"] - else: - # This token isn't handled by getToken/setToken to limit the scope of this legacy/migration code - if request.get_cookie(LEGACY_ID_REFRESH_TOKEN_COOKIE_NAME) is not None: - log_debug_message( - "refreshSession: cleared legacy id refresh token because refresh token was not found" - ) - response_mutators.append( - set_cookie_response_mutator( - self.config, - LEGACY_ID_REFRESH_TOKEN_COOKIE_NAME, - "", - 0, - "access_token_path", - ) - ) - - log_debug_message( - "refreshSession: UNAUTHORISED because refresh_token in request is None" - ) - return raise_unauthorised_exception( - "Refresh token not found. Are you sending the refresh token in the request?", - clear_tokens=False, + raise Exception( + "Since the anti-csrf mode is VIA_CUSTOM_HEADER getSession can't check the CSRF token. Please either use VIA_TOKEN or set antiCsrfCheck to false" ) - assert refresh_token is not None - - try: - anti_csrf_token = get_anti_csrf_header(request) - result = await session_functions.refresh_session( - self, - refresh_token, - anti_csrf_token, - get_rid_header(request) is not None, - request_transfer_method, - ) - log_debug_message( - "refreshSession: Attaching refresh session info as %s", - request_transfer_method, - ) + log_debug_message("refreshSession: Started") - for transfer_method in available_token_transfer_methods: - if ( - transfer_method != request_transfer_method - and refresh_tokens.get(transfer_method) is not None - ): - response_mutators.append( - clear_session_response_mutator(self.config, transfer_method) - ) + response = await session_functions.refresh_session( + self, + refresh_token, + anti_csrf_token, + disable_anti_csrf, + ) - if request.get_cookie(LEGACY_ID_REFRESH_TOKEN_COOKIE_NAME) is not None: - log_debug_message( - "refreshSession: cleared legacy id refresh token after successful refresh" - ) - response_mutators.append( - set_cookie_response_mutator( - self.config, - LEGACY_ID_REFRESH_TOKEN_COOKIE_NAME, - "", - 0, - "access_token_path", - ) - ) + log_debug_message("refreshSession: Success!") - session = Session( - self, - self.config, - result["accessToken"]["token"], - result["session"]["handle"], - result["session"]["userId"], - result["session"]["userDataInJWT"], - request_transfer_method, - ) - new_access_token_info = result["accessToken"] - new_refresh_token_info = result["refreshToken"] - new_anti_csrf_token = result.get("antiCsrfToken") - - if new_access_token_info is not None: - response_mutators.append( - front_token_response_mutator( - session.user_id, - new_access_token_info["expiry"], - session.access_token_payload, - ) - ) - # We set the expiration to 100 years, because we can't really access the expiration of the refresh token everywhere we are setting it. - # 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. - # Even if the token is expired the presence of the token indicates that the user could have a valid refresh - # Setting them to infinity would require special case handling on the frontend and just adding 100 years seems enough. - response_mutators.append( - token_response_mutator( - self.config, - "access", - new_access_token_info["token"], - get_timestamp_ms() + HUNDRED_YEARS_IN_MS, # 100 years - session.transfer_method, - ) - ) - if new_refresh_token_info is not None: - response_mutators.append( - token_response_mutator( - self.config, - "refresh", - new_refresh_token_info["token"], - new_refresh_token_info[ - "expiry" - ], # This comes from the core and is 100 days - session.transfer_method, - ) - ) + payload = parse_jwt_without_signature_verification( + response.accessToken.token, + ).payload - anti_csrf_token = new_anti_csrf_token - if anti_csrf_token is not None: - response_mutators.append(anti_csrf_response_mutator(anti_csrf_token)) - - session.response_mutators.extend(response_mutators) - - log_debug_message("refreshSession: Success!") - request.set_session(session) - return session - except (TokenTheftError, UnauthorisedError) as e: - if ( - isinstance(e, UnauthorisedError) and e.clear_tokens is True - ) or isinstance(e, TokenTheftError): - # This token isn't handled by getToken/setToken to limit the scope of this legacy/migration code - if request.get_cookie(LEGACY_ID_REFRESH_TOKEN_COOKIE_NAME) is not None: - log_debug_message( - "refreshSession: cleared legacy id refresh token because refresh token was not found" - ) - response_mutators.append( - set_cookie_response_mutator( - self.config, - LEGACY_ID_REFRESH_TOKEN_COOKIE_NAME, - "", - 0, - "access_token_path", - ) - ) - e.response_mutators.extend(response_mutators) + session = Session( + self, + self.config, + response.accessToken.token, + build_front_token( + response.session.userId, + response.accessToken.expiry, + payload, + ), + response.refreshToken, + response.antiCsrfToken, + response.session.handle, + response.session.userId, + user_data_in_access_token=payload, + req_res_info=None, + access_token_updated=True, + ) - raise e + return session async def revoke_session( self, session_handle: str, user_context: Dict[str, Any] @@ -726,33 +357,16 @@

    Module supertokens_python.recipe.session.recipe_implemen ) -> Union[SessionInformationResult, None]: return await session_functions.get_session_information(self, session_handle) - async def update_session_data( + async def update_session_data_in_database( self, session_handle: str, new_session_data: Dict[str, Any], user_context: Dict[str, Any], ) -> bool: - return await session_functions.update_session_data( + return await session_functions.update_session_data_in_database( self, session_handle, new_session_data ) - async def update_access_token_payload( - self, - session_handle: str, - new_access_token_payload: Dict[str, Any], - user_context: Dict[str, Any], - ) -> bool: - - return await session_functions.update_access_token_payload( - self, session_handle, new_access_token_payload - ) - - async def get_access_token_lifetime_ms(self, user_context: Dict[str, Any]) -> int: - return (await self.get_handshake_info()).access_token_validity - - async def get_refresh_token_lifetime_ms(self, user_context: Dict[str, Any]) -> int: - return (await self.get_handshake_info()).refresh_token_validity - async def merge_into_access_token_payload( self, session_handle: str, @@ -764,15 +378,15 @@

    Module supertokens_python.recipe.session.recipe_implemen return False new_access_token_payload = { - **session_info.access_token_payload, + **session_info.custom_claims_in_access_token_payload, **access_token_payload_update, } for k in access_token_payload_update.keys(): if new_access_token_payload[k] is None: del new_access_token_payload[k] - return await self.update_access_token_payload( - session_handle, new_access_token_payload, user_context + return await session_functions.update_access_token_payload( + self, session_handle, new_access_token_payload ) async def fetch_and_set_claim( @@ -816,7 +430,7 @@

    Module supertokens_python.recipe.session.recipe_implemen return GetClaimValueOkResult( value=claim.get_value_from_payload( - session_info.access_token_payload, user_context + session_info.custom_claims_in_access_token_payload, user_context ) ) @@ -877,70 +491,6 @@

    Module supertokens_python.recipe.session.recipe_implemen

    Classes

    -
    -class HandshakeInfo -(info: Dict[str, Any]) -
    -
    -
    -
    - -Expand source code - -
    class HandshakeInfo:
    -    def __init__(self, info: Dict[str, Any]):
    -        self.access_token_blacklisting_enabled = info["accessTokenBlacklistingEnabled"]
    -        self.raw_jwt_signing_public_key_list: List[Dict[str, Any]] = []
    -        self.anti_csrf = info["antiCsrf"]
    -        self.access_token_validity = info["accessTokenValidity"]
    -        self.refresh_token_validity = info["refreshTokenValidity"]
    -
    -    def set_jwt_signing_public_key_list(self, updated_list: List[Dict[str, Any]]):
    -        self.raw_jwt_signing_public_key_list = updated_list
    -
    -    def get_jwt_signing_public_key_list(self) -> List[Dict[str, Any]]:
    -        time_now = get_timestamp_ms()
    -        return [
    -            key
    -            for key in self.raw_jwt_signing_public_key_list
    -            if key["expiryTime"] > time_now
    -        ]
    -
    -

    Methods

    -
    -
    -def get_jwt_signing_public_key_list(self) ‑> List[Dict[str, Any]] -
    -
    -
    -
    - -Expand source code - -
    def get_jwt_signing_public_key_list(self) -> List[Dict[str, Any]]:
    -    time_now = get_timestamp_ms()
    -    return [
    -        key
    -        for key in self.raw_jwt_signing_public_key_list
    -        if key["expiryTime"] > time_now
    -    ]
    -
    -
    -
    -def set_jwt_signing_public_key_list(self, updated_list: List[Dict[str, Any]]) -
    -
    -
    -
    - -Expand source code - -
    def set_jwt_signing_public_key_list(self, updated_list: List[Dict[str, Any]]):
    -    self.raw_jwt_signing_public_key_list = updated_list
    -
    -
    -
    -
    class RecipeImplementation (querier: Querier, config: SessionConfig, app_info: AppInfo) @@ -958,179 +508,57 @@

    Methods

    self.querier = querier self.config = config self.app_info = app_info - self.handshake_info: Union[HandshakeInfo, None] = None - - async def call_get_handshake_info(): - try: - await self.get_handshake_info() - except Exception: - pass - try: - execute_async(config.mode, call_get_handshake_info) - except Exception: - pass - - async def get_handshake_info(self, force_refetch: bool = False) -> HandshakeInfo: - if ( - self.handshake_info is None - or len(self.handshake_info.get_jwt_signing_public_key_list()) == 0 - or force_refetch - ): - ProcessState.get_instance().add_state( - AllowedProcessStates.CALLING_SERVICE_IN_GET_HANDSHAKE_INFO - ) - response = await self.querier.send_post_request( - NormalisedURLPath("/recipe/handshake"), {} + self.JWK_clients = [ + JWKClient( + uri, + cooldown_duration=JWKRequestCooldownInMs, + cache_max_age=JWKCacheMaxAgeInMs, ) - self.handshake_info = HandshakeInfo( - {**response, "antiCsrf": self.config.anti_csrf} + for uri in self.querier.get_all_core_urls_for_path( + "./.well-known/jwks.json" ) - - self.update_jwt_signing_public_key_info( - response["jwtSigningPublicKeyList"], - response["jwtSigningPublicKey"], - response["jwtSigningPublicKeyExpiryTime"], - ) - - return self.handshake_info - - def update_jwt_signing_public_key_info( - self, - key_list: Union[List[Dict[str, Any]], None], - public_key: str, - expiry_time: int, - ): - if key_list is None: - key_list = [ - { - "publicKey": public_key, - "expiryTime": expiry_time, - "createdAt": get_timestamp_ms(), - } - ] - - if self.handshake_info is not None: - self.handshake_info.set_jwt_signing_public_key_list(key_list) + ] async def create_new_session( self, - request: BaseRequest, user_id: str, - access_token_payload: Union[None, Dict[str, Any]], - session_data: Union[None, Dict[str, Any]], + access_token_payload: Optional[Dict[str, Any]], + session_data_in_database: Optional[Dict[str, Any]], + disable_anti_csrf: Optional[bool], user_context: Dict[str, Any], ) -> SessionContainer: log_debug_message("createNewSession: Started") - output_transfer_method = self.config.get_token_transfer_method( - request, True, user_context - ) - if output_transfer_method == "any": - output_transfer_method = "header" - - log_debug_message( - "createNewSession: using transfer method %s", output_transfer_method - ) - - if ( - (output_transfer_method == "cookie") - and self.config.cookie_same_site == "none" - and not self.config.cookie_secure - and not ( - ( - self.app_info.top_level_api_domain == "localhost" - or is_an_ip_address(self.app_info.top_level_api_domain) - ) - and ( - self.app_info.top_level_website_domain == "localhost" - or is_an_ip_address(self.app_info.top_level_website_domain) - ) - ) - ): - # We can allow insecure cookie when both website & API domain are localhost or an IP - # When either of them is a different domain, API domain needs to have https and a secure cookie to work - raise Exception( - "Since your API and website domain are different, for sessions to work, please use " - "https on your apiDomain and don't set cookieSecure to false." - ) - - disable_anti_csrf = output_transfer_method == "header" result = await session_functions.create_new_session( self, user_id, - disable_anti_csrf, + disable_anti_csrf is True, access_token_payload, - session_data, + session_data_in_database, ) + log_debug_message("createNewSession: Finished") - response_mutators: List[ResponseMutator] = [] - - for transfer_method in available_token_transfer_methods: - request_access_token = get_token(request, "access", transfer_method) - - if ( - transfer_method != output_transfer_method - and request_access_token is not None - ): - response_mutators.append( - clear_session_response_mutator( - self.config, - transfer_method, - ) - ) + payload = parse_jwt_without_signature_verification( + result.accessToken.token + ).payload new_session = Session( self, self.config, - result["accessToken"]["token"], - result["session"]["handle"], - result["session"]["userId"], - result["session"]["userDataInJWT"], - output_transfer_method, + result.accessToken.token, + build_front_token( + result.session.userId, result.accessToken.expiry, payload + ), + result.refreshToken, + result.antiCsrfToken, + result.session.handle, + result.session.userId, + payload, + None, + True, ) - new_access_token_info: Dict[str, Any] = result["accessToken"] - new_refresh_token_info: Dict[str, Any] = result["refreshToken"] - anti_csrf_token: Optional[str] = result.get("antiCsrfToken") - - response_mutators.append( - front_token_response_mutator( - new_session.user_id, - new_access_token_info["expiry"], - new_session.access_token_payload, - ) - ) - # We set the expiration to 100 years, because we can't really access the expiration of the refresh token everywhere we are setting it. - # 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. - # Even if the token is expired the presence of the token indicates that the user could have a valid refresh - # Setting them to infinity would require special case handling on the frontend and just adding 100 years seems enough. - response_mutators.append( - token_response_mutator( - self.config, - "access", - new_access_token_info["token"], - get_timestamp_ms() + HUNDRED_YEARS_IN_MS, - new_session.transfer_method, - ) - ) - response_mutators.append( - token_response_mutator( - self.config, - "refresh", - new_refresh_token_info["token"], - new_refresh_token_info[ - "expiry" - ], # This comes from the core and is 100 days - new_session.transfer_method, - ) - ) - if anti_csrf_token is not None: - response_mutators.append(anti_csrf_response_mutator(anti_csrf_token)) - - new_session.response_mutators.extend(response_mutators) - - request.set_session(new_session) return new_session async def validate_claims( @@ -1191,337 +619,140 @@

    Methods

    return ClaimsValidationResult(invalid_claims) - # In all cases if sIdRefreshToken token exists (so it's a legacy session) we return TRY_REFRESH_TOKEN. The refresh - # endpoint will clear this cookie and try to upgrade the session. - # Check https://supertokens.com/docs/contribute/decisions/session/0007 for further details and a table of expected - # behaviours async def get_session( self, - request: BaseRequest, - anti_csrf_check: Union[bool, None], - session_required: bool, - user_context: Dict[str, Any], + access_token: str, + anti_csrf_token: Optional[str], + anti_csrf_check: Optional[bool] = None, + session_required: Optional[bool] = None, + check_database: Optional[bool] = None, + override_global_claim_validators: Optional[ + Callable[ + [List[SessionClaimValidator], SessionContainer, Dict[str, Any]], + MaybeAwaitable[List[SessionClaimValidator]], + ] + ] = None, + user_context: Optional[Dict[str, Any]] = None, ) -> Optional[SessionContainer]: - log_debug_message("getSession: Started") - - # This token isn't handled by getToken to limit the scope of this legacy/migration code - if request.get_cookie(LEGACY_ID_REFRESH_TOKEN_COOKIE_NAME) is not None: - # This could create a spike on refresh calls during the update of the backend SDK - return raise_try_refresh_token_exception( - "using legacy session, please call the refresh API" + if ( + anti_csrf_check is not False + and self.config.anti_csrf == "VIA_CUSTOM_HEADER" + ): + raise Exception( + "Since the anti-csrf mode is VIA_CUSTOM_HEADER getSession can't check the CSRF token. Please either use VIA_TOKEN or set anti_csrf_check to false" ) - session_optional = not session_required - log_debug_message("getSession: optional validation: %s", session_optional) - - access_tokens: Dict[TokenTransferMethod, ParsedJWTInfo] = {} - - # We check all token transfer methods for available access tokens - for transfer_method in available_token_transfer_methods: - token_string = get_token(request, "access", transfer_method) - - if token_string is not None: - try: - info = parse_jwt_without_signature_verification(token_string) - validate_access_token_structure(info.payload) - log_debug_message( - "getSession: got access token from %s", transfer_method - ) - access_tokens[transfer_method] = info - except Exception: - log_debug_message( - "getSession: ignoring token in %s, because it doesn't match our access token structure", - transfer_method, - ) + log_debug_message("getSession: Started") - allowed_transfer_method = self.config.get_token_transfer_method( - request, False, user_context - ) - request_transfer_method: TokenTransferMethod - request_access_token: Union[ParsedJWTInfo, None] - - if (allowed_transfer_method in ("any", "header")) and access_tokens.get( - "header" - ) is not None: - log_debug_message("getSession: using header transfer method") - request_transfer_method = "header" - request_access_token = access_tokens["header"] - elif (allowed_transfer_method in ("any", "cookie")) and access_tokens.get( - "cookie" - ) is not None: - log_debug_message("getSession: using cookie transfer method") - request_transfer_method = "cookie" - request_access_token = access_tokens["cookie"] - else: - if session_optional: + access_token_obj: Optional[ParsedJWTInfo] = None + try: + access_token_obj = parse_jwt_without_signature_verification(access_token) + validate_access_token_structure( + access_token_obj.payload, access_token_obj.version + ) + except Exception as _: + if session_required is False: log_debug_message( - "getSession: returning None because accessToken is undefined and sessionRequired is false" + "getSession: Returning undefined because parsing failed and session_required is False" ) - # there is no session that exists here, and the user wants session verification - # to be optional. So we return None return None - log_debug_message( - "getSession: UNAUTHORISED because access_token in request is None" - ) - # we do not clear the session here because of a race condition mentioned in: - # https://github.com/supertokens/supertokens-node/issues/17 - raise_unauthorised_exception( - "Session does not exist. Are you sending the session tokens in the " - "request with the appropriate token transfer method?", - clear_tokens=False, - ) - - anti_csrf_token = get_anti_csrf_header(request) - do_anti_csrf_check = anti_csrf_check - - if do_anti_csrf_check is None: - do_anti_csrf_check = normalise_http_method(request.method()) != "get" - if request_transfer_method == "header": - do_anti_csrf_check = False - log_debug_message( - "getSession: Value of doAntiCsrfCheck is: %s", do_anti_csrf_check - ) + raise UnauthorisedError("Token parsing failed", clear_tokens=False) - result = await session_functions.get_session( + response = await session_functions.get_session( self, - request_access_token, + access_token_obj, anti_csrf_token, - do_anti_csrf_check, - get_rid_header(request) is not None, + (anti_csrf_check is not False), + (check_database is True), ) - # Default is to respond with the access token obtained from the request - access_token_string = request_access_token.raw_token_string - - session = Session( - self, - self.config, - access_token_string, - result["session"]["handle"], - result["session"]["userId"], - result["session"]["userDataInJWT"], - request_transfer_method, - ) - - if "accessToken" in result: - session.access_token = result["accessToken"]["token"] - new_access_token_info = result["accessToken"] - - session.response_mutators.append( - front_token_response_mutator( - session.user_id, - new_access_token_info["expiry"], - session.access_token_payload, - ) - ) - # We set the expiration to 100 years, because we can't really access the expiration of the refresh token everywhere we are setting it. - # 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. - # Even if the token is expired the presence of the token indicates that the user could have a valid refresh - # Setting them to infinity would require special case handling on the frontend and just adding 100 years seems enough. - session.response_mutators.append( - token_response_mutator( - self.config, - "access", - session.access_token, - get_timestamp_ms() + HUNDRED_YEARS_IN_MS, - session.transfer_method, - ) - ) - log_debug_message("getSession: Success!") - request.set_session(session) - return session - # In all cases: if sIdRefreshToken token exists (it's a legacy session) we clear it - # Check http://localhost:3002/docs/contribute/decisions/session/0008 for further details and - # a table of expected behaviours - async def refresh_session( - self, request: BaseRequest, user_context: Dict[str, Any] - ) -> SessionContainer: - log_debug_message("refreshSession: Started") - - response_mutators: List[Callable[[Any], None]] = [] - refresh_tokens: Dict[TokenTransferMethod, Optional[str]] = {} - - # We check all token transfer methods for available refresh tokens - # We do this so that we can later clear all we are not overwriting - for transfer_method in available_token_transfer_methods: - refresh_token = get_token( - request, - "refresh", - transfer_method, - ) - if refresh_token is not None: - log_debug_message( - "refreshSession: got refresh token from %s", transfer_method - ) - - refresh_tokens[transfer_method] = refresh_token - - allowed_transfer_method = self.config.get_token_transfer_method( - request, False, user_context - ) - log_debug_message( - "refreshSession: getTokenTransferMethod returned: %s", - allowed_transfer_method, - ) - - request_transfer_method: TokenTransferMethod - refresh_token: Optional[str] + if access_token_obj.version >= 3: + if response.accessToken is not None: + payload = parse_jwt_without_signature_verification( + response.accessToken.token + ).payload + else: + payload = access_token_obj.payload + else: + payload = response.session.userDataInJWT - if (allowed_transfer_method in ("any", "header")) and ( - refresh_tokens.get("header") - ): - log_debug_message("refreshSession: using header transfer method") - request_transfer_method = "header" - refresh_token = refresh_tokens["header"] - elif (allowed_transfer_method in ("any", "cookie")) and ( - refresh_tokens.get("cookie") - ): - log_debug_message("refreshSession: using cookie transfer method") - request_transfer_method = "cookie" - refresh_token = refresh_tokens["cookie"] + if response.accessToken is not None: + access_token_str = response.accessToken.token + expiry_time = response.accessToken.expiry + access_token_updated = True else: - # This token isn't handled by getToken/setToken to limit the scope of this legacy/migration code - if request.get_cookie(LEGACY_ID_REFRESH_TOKEN_COOKIE_NAME) is not None: - log_debug_message( - "refreshSession: cleared legacy id refresh token because refresh token was not found" - ) - response_mutators.append( - set_cookie_response_mutator( - self.config, - LEGACY_ID_REFRESH_TOKEN_COOKIE_NAME, - "", - 0, - "access_token_path", - ) - ) + access_token_str = access_token + expiry_time = response.session.expiryTime + access_token_updated = False - log_debug_message( - "refreshSession: UNAUTHORISED because refresh_token in request is None" - ) - return raise_unauthorised_exception( - "Refresh token not found. Are you sending the refresh token in the request?", - clear_tokens=False, - ) + session = Session( + self, + self.config, + access_token_str, + build_front_token(response.session.userId, expiry_time, payload), + None, # refresh_token + anti_csrf_token, + response.session.handle, + response.session.userId, + payload, + None, + access_token_updated, + ) - assert refresh_token is not None + return session - try: - anti_csrf_token = get_anti_csrf_header(request) - result = await session_functions.refresh_session( - self, - refresh_token, - anti_csrf_token, - get_rid_header(request) is not None, - request_transfer_method, - ) - log_debug_message( - "refreshSession: Attaching refresh session info as %s", - request_transfer_method, + async def refresh_session( + self, + refresh_token: str, + anti_csrf_token: Optional[str], + disable_anti_csrf: bool, + user_context: Dict[str, Any], + ) -> SessionContainer: + if ( + disable_anti_csrf is not True + and self.config.anti_csrf == "VIA_CUSTOM_HEADER" + ): + raise Exception( + "Since the anti-csrf mode is VIA_CUSTOM_HEADER getSession can't check the CSRF token. Please either use VIA_TOKEN or set antiCsrfCheck to false" ) - for transfer_method in available_token_transfer_methods: - if ( - transfer_method != request_transfer_method - and refresh_tokens.get(transfer_method) is not None - ): - response_mutators.append( - clear_session_response_mutator(self.config, transfer_method) - ) + log_debug_message("refreshSession: Started") - if request.get_cookie(LEGACY_ID_REFRESH_TOKEN_COOKIE_NAME) is not None: - log_debug_message( - "refreshSession: cleared legacy id refresh token after successful refresh" - ) - response_mutators.append( - set_cookie_response_mutator( - self.config, - LEGACY_ID_REFRESH_TOKEN_COOKIE_NAME, - "", - 0, - "access_token_path", - ) - ) + response = await session_functions.refresh_session( + self, + refresh_token, + anti_csrf_token, + disable_anti_csrf, + ) - session = Session( - self, - self.config, - result["accessToken"]["token"], - result["session"]["handle"], - result["session"]["userId"], - result["session"]["userDataInJWT"], - request_transfer_method, - ) - new_access_token_info = result["accessToken"] - new_refresh_token_info = result["refreshToken"] - new_anti_csrf_token = result.get("antiCsrfToken") - - if new_access_token_info is not None: - response_mutators.append( - front_token_response_mutator( - session.user_id, - new_access_token_info["expiry"], - session.access_token_payload, - ) - ) - # We set the expiration to 100 years, because we can't really access the expiration of the refresh token everywhere we are setting it. - # 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. - # Even if the token is expired the presence of the token indicates that the user could have a valid refresh - # Setting them to infinity would require special case handling on the frontend and just adding 100 years seems enough. - response_mutators.append( - token_response_mutator( - self.config, - "access", - new_access_token_info["token"], - get_timestamp_ms() + HUNDRED_YEARS_IN_MS, # 100 years - session.transfer_method, - ) - ) - if new_refresh_token_info is not None: - response_mutators.append( - token_response_mutator( - self.config, - "refresh", - new_refresh_token_info["token"], - new_refresh_token_info[ - "expiry" - ], # This comes from the core and is 100 days - session.transfer_method, - ) - ) + log_debug_message("refreshSession: Success!") - anti_csrf_token = new_anti_csrf_token - if anti_csrf_token is not None: - response_mutators.append(anti_csrf_response_mutator(anti_csrf_token)) - - session.response_mutators.extend(response_mutators) - - log_debug_message("refreshSession: Success!") - request.set_session(session) - return session - except (TokenTheftError, UnauthorisedError) as e: - if ( - isinstance(e, UnauthorisedError) and e.clear_tokens is True - ) or isinstance(e, TokenTheftError): - # This token isn't handled by getToken/setToken to limit the scope of this legacy/migration code - if request.get_cookie(LEGACY_ID_REFRESH_TOKEN_COOKIE_NAME) is not None: - log_debug_message( - "refreshSession: cleared legacy id refresh token because refresh token was not found" - ) - response_mutators.append( - set_cookie_response_mutator( - self.config, - LEGACY_ID_REFRESH_TOKEN_COOKIE_NAME, - "", - 0, - "access_token_path", - ) - ) - e.response_mutators.extend(response_mutators) + payload = parse_jwt_without_signature_verification( + response.accessToken.token, + ).payload + + session = Session( + self, + self.config, + response.accessToken.token, + build_front_token( + response.session.userId, + response.accessToken.expiry, + payload, + ), + response.refreshToken, + response.antiCsrfToken, + response.session.handle, + response.session.userId, + user_data_in_access_token=payload, + req_res_info=None, + access_token_updated=True, + ) - raise e + return session async def revoke_session( self, session_handle: str, user_context: Dict[str, Any] @@ -1548,33 +779,16 @@

    Methods

    ) -> Union[SessionInformationResult, None]: return await session_functions.get_session_information(self, session_handle) - async def update_session_data( + async def update_session_data_in_database( self, session_handle: str, new_session_data: Dict[str, Any], user_context: Dict[str, Any], ) -> bool: - return await session_functions.update_session_data( + return await session_functions.update_session_data_in_database( self, session_handle, new_session_data ) - async def update_access_token_payload( - self, - session_handle: str, - new_access_token_payload: Dict[str, Any], - user_context: Dict[str, Any], - ) -> bool: - - return await session_functions.update_access_token_payload( - self, session_handle, new_access_token_payload - ) - - async def get_access_token_lifetime_ms(self, user_context: Dict[str, Any]) -> int: - return (await self.get_handshake_info()).access_token_validity - - async def get_refresh_token_lifetime_ms(self, user_context: Dict[str, Any]) -> int: - return (await self.get_handshake_info()).refresh_token_validity - async def merge_into_access_token_payload( self, session_handle: str, @@ -1586,15 +800,15 @@

    Methods

    return False new_access_token_payload = { - **session_info.access_token_payload, + **session_info.custom_claims_in_access_token_payload, **access_token_payload_update, } for k in access_token_payload_update.keys(): if new_access_token_payload[k] is None: del new_access_token_payload[k] - return await self.update_access_token_payload( - session_handle, new_access_token_payload, user_context + return await session_functions.update_access_token_payload( + self, session_handle, new_access_token_payload ) async def fetch_and_set_claim( @@ -1638,7 +852,7 @@

    Methods

    return GetClaimValueOkResult( value=claim.get_value_from_payload( - session_info.access_token_payload, user_context + session_info.custom_claims_in_access_token_payload, user_context ) ) @@ -1694,10 +908,17 @@

    Ancestors

  • RecipeInterface
  • abc.ABC
  • +

    Class variables

    +
    +
    var JWK_clients : List[JWKClient]
    +
    +
    +
    +

    Methods

    -async def create_new_session(self, request: BaseRequest, user_id: str, access_token_payload: Union[None, Dict[str, Any]], session_data: Union[None, Dict[str, Any]], user_context: Dict[str, Any]) ‑> SessionContainer +async def create_new_session(self, user_id: str, access_token_payload: Optional[Dict[str, Any]], session_data_in_database: Optional[Dict[str, Any]], disable_anti_csrf: Optional[bool], user_context: Dict[str, Any]) ‑> SessionContainer
    @@ -1707,122 +928,43 @@

    Methods

    async def create_new_session(
         self,
    -    request: BaseRequest,
         user_id: str,
    -    access_token_payload: Union[None, Dict[str, Any]],
    -    session_data: Union[None, Dict[str, Any]],
    +    access_token_payload: Optional[Dict[str, Any]],
    +    session_data_in_database: Optional[Dict[str, Any]],
    +    disable_anti_csrf: Optional[bool],
         user_context: Dict[str, Any],
     ) -> SessionContainer:
         log_debug_message("createNewSession: Started")
    -    output_transfer_method = self.config.get_token_transfer_method(
    -        request, True, user_context
    -    )
    -    if output_transfer_method == "any":
    -        output_transfer_method = "header"
    -
    -    log_debug_message(
    -        "createNewSession: using transfer method %s", output_transfer_method
    -    )
    -
    -    if (
    -        (output_transfer_method == "cookie")
    -        and self.config.cookie_same_site == "none"
    -        and not self.config.cookie_secure
    -        and not (
    -            (
    -                self.app_info.top_level_api_domain == "localhost"
    -                or is_an_ip_address(self.app_info.top_level_api_domain)
    -            )
    -            and (
    -                self.app_info.top_level_website_domain == "localhost"
    -                or is_an_ip_address(self.app_info.top_level_website_domain)
    -            )
    -        )
    -    ):
    -        # We can allow insecure cookie when both website & API domain are localhost or an IP
    -        # When either of them is a different domain, API domain needs to have https and a secure cookie to work
    -        raise Exception(
    -            "Since your API and website domain are different, for sessions to work, please use "
    -            "https on your apiDomain and don't set cookieSecure to false."
    -        )
    -
    -    disable_anti_csrf = output_transfer_method == "header"
     
         result = await session_functions.create_new_session(
             self,
             user_id,
    -        disable_anti_csrf,
    +        disable_anti_csrf is True,
             access_token_payload,
    -        session_data,
    +        session_data_in_database,
         )
    +    log_debug_message("createNewSession: Finished")
     
    -    response_mutators: List[ResponseMutator] = []
    -
    -    for transfer_method in available_token_transfer_methods:
    -        request_access_token = get_token(request, "access", transfer_method)
    -
    -        if (
    -            transfer_method != output_transfer_method
    -            and request_access_token is not None
    -        ):
    -            response_mutators.append(
    -                clear_session_response_mutator(
    -                    self.config,
    -                    transfer_method,
    -                )
    -            )
    +    payload = parse_jwt_without_signature_verification(
    +        result.accessToken.token
    +    ).payload
     
         new_session = Session(
             self,
             self.config,
    -        result["accessToken"]["token"],
    -        result["session"]["handle"],
    -        result["session"]["userId"],
    -        result["session"]["userDataInJWT"],
    -        output_transfer_method,
    +        result.accessToken.token,
    +        build_front_token(
    +            result.session.userId, result.accessToken.expiry, payload
    +        ),
    +        result.refreshToken,
    +        result.antiCsrfToken,
    +        result.session.handle,
    +        result.session.userId,
    +        payload,
    +        None,
    +        True,
         )
     
    -    new_access_token_info: Dict[str, Any] = result["accessToken"]
    -    new_refresh_token_info: Dict[str, Any] = result["refreshToken"]
    -    anti_csrf_token: Optional[str] = result.get("antiCsrfToken")
    -
    -    response_mutators.append(
    -        front_token_response_mutator(
    -            new_session.user_id,
    -            new_access_token_info["expiry"],
    -            new_session.access_token_payload,
    -        )
    -    )
    -    # We set the expiration to 100 years, because we can't really access the expiration of the refresh token everywhere we are setting it.
    -    # 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.
    -    # Even if the token is expired the presence of the token indicates that the user could have a valid refresh
    -    # Setting them to infinity would require special case handling on the frontend and just adding 100 years seems enough.
    -    response_mutators.append(
    -        token_response_mutator(
    -            self.config,
    -            "access",
    -            new_access_token_info["token"],
    -            get_timestamp_ms() + HUNDRED_YEARS_IN_MS,
    -            new_session.transfer_method,
    -        )
    -    )
    -    response_mutators.append(
    -        token_response_mutator(
    -            self.config,
    -            "refresh",
    -            new_refresh_token_info["token"],
    -            new_refresh_token_info[
    -                "expiry"
    -            ],  # This comes from the core and is 100 days
    -            new_session.transfer_method,
    -        )
    -    )
    -    if anti_csrf_token is not None:
    -        response_mutators.append(anti_csrf_response_mutator(anti_csrf_token))
    -
    -    new_session.response_mutators.extend(response_mutators)
    -
    -    request.set_session(new_session)
         return new_session
    @@ -1853,19 +995,6 @@

    Methods

    )
    -
    -async def get_access_token_lifetime_ms(self, user_context: Dict[str, Any]) ‑> int -
    -
    -
    -
    - -Expand source code - -
    async def get_access_token_lifetime_ms(self, user_context: Dict[str, Any]) -> int:
    -    return (await self.get_handshake_info()).access_token_validity
    -
    -
    async def get_all_session_handles_for_user(self, user_id: str, user_context: Dict[str, Any]) ‑> List[str]
    @@ -1902,7 +1031,7 @@

    Methods

    return GetClaimValueOkResult( value=claim.get_value_from_payload( - session_info.access_token_payload, user_context + session_info.custom_claims_in_access_token_payload, user_context ) )
    @@ -1925,55 +1054,8 @@

    Methods

    return claim_validators_added_by_other_recipes
    -
    -async def get_handshake_info(self, force_refetch: bool = False) ‑> HandshakeInfo -
    -
    -
    -
    - -Expand source code - -
    async def get_handshake_info(self, force_refetch: bool = False) -> HandshakeInfo:
    -    if (
    -        self.handshake_info is None
    -        or len(self.handshake_info.get_jwt_signing_public_key_list()) == 0
    -        or force_refetch
    -    ):
    -        ProcessState.get_instance().add_state(
    -            AllowedProcessStates.CALLING_SERVICE_IN_GET_HANDSHAKE_INFO
    -        )
    -        response = await self.querier.send_post_request(
    -            NormalisedURLPath("/recipe/handshake"), {}
    -        )
    -        self.handshake_info = HandshakeInfo(
    -            {**response, "antiCsrf": self.config.anti_csrf}
    -        )
    -
    -        self.update_jwt_signing_public_key_info(
    -            response["jwtSigningPublicKeyList"],
    -            response["jwtSigningPublicKey"],
    -            response["jwtSigningPublicKeyExpiryTime"],
    -        )
    -
    -    return self.handshake_info
    -
    -
    -
    -async def get_refresh_token_lifetime_ms(self, user_context: Dict[str, Any]) ‑> int -
    -
    -
    -
    - -Expand source code - -
    async def get_refresh_token_lifetime_ms(self, user_context: Dict[str, Any]) -> int:
    -    return (await self.get_handshake_info()).refresh_token_validity
    -
    -
    -async def get_session(self, request: BaseRequest, anti_csrf_check: Union[bool, None], session_required: bool, user_context: Dict[str, Any]) ‑> Optional[SessionContainer] +async def get_session(self, access_token: str, anti_csrf_token: Optional[str], anti_csrf_check: Optional[bool] = None, session_required: Optional[bool] = None, check_database: Optional[bool] = None, override_global_claim_validators: Optional[Callable[[List[SessionClaimValidator], SessionContainer, Dict[str, Any]], MaybeAwaitable[List[SessionClaimValidator]]]] = None, user_context: Optional[Dict[str, Any]] = None) ‑> Optional[SessionContainer]
    @@ -1983,140 +1065,87 @@

    Methods

    async def get_session(
         self,
    -    request: BaseRequest,
    -    anti_csrf_check: Union[bool, None],
    -    session_required: bool,
    -    user_context: Dict[str, Any],
    +    access_token: str,
    +    anti_csrf_token: Optional[str],
    +    anti_csrf_check: Optional[bool] = None,
    +    session_required: Optional[bool] = None,
    +    check_database: Optional[bool] = None,
    +    override_global_claim_validators: Optional[
    +        Callable[
    +            [List[SessionClaimValidator], SessionContainer, Dict[str, Any]],
    +            MaybeAwaitable[List[SessionClaimValidator]],
    +        ]
    +    ] = None,
    +    user_context: Optional[Dict[str, Any]] = None,
     ) -> Optional[SessionContainer]:
    -    log_debug_message("getSession: Started")
    -
    -    # This token isn't handled by getToken to limit the scope of this legacy/migration code
    -    if request.get_cookie(LEGACY_ID_REFRESH_TOKEN_COOKIE_NAME) is not None:
    -        # This could create a spike on refresh calls during the update of the backend SDK
    -        return raise_try_refresh_token_exception(
    -            "using legacy session, please call the refresh API"
    +    if (
    +        anti_csrf_check is not False
    +        and self.config.anti_csrf == "VIA_CUSTOM_HEADER"
    +    ):
    +        raise Exception(
    +            "Since the anti-csrf mode is VIA_CUSTOM_HEADER getSession can't check the CSRF token. Please either use VIA_TOKEN or set anti_csrf_check to false"
             )
     
    -    session_optional = not session_required
    -    log_debug_message("getSession: optional validation: %s", session_optional)
    -
    -    access_tokens: Dict[TokenTransferMethod, ParsedJWTInfo] = {}
    -
    -    # We check all token transfer methods for available access tokens
    -    for transfer_method in available_token_transfer_methods:
    -        token_string = get_token(request, "access", transfer_method)
    -
    -        if token_string is not None:
    -            try:
    -                info = parse_jwt_without_signature_verification(token_string)
    -                validate_access_token_structure(info.payload)
    -                log_debug_message(
    -                    "getSession: got access token from %s", transfer_method
    -                )
    -                access_tokens[transfer_method] = info
    -            except Exception:
    -                log_debug_message(
    -                    "getSession: ignoring token in %s, because it doesn't match our access token structure",
    -                    transfer_method,
    -                )
    +    log_debug_message("getSession: Started")
     
    -    allowed_transfer_method = self.config.get_token_transfer_method(
    -        request, False, user_context
    -    )
    -    request_transfer_method: TokenTransferMethod
    -    request_access_token: Union[ParsedJWTInfo, None]
    -
    -    if (allowed_transfer_method in ("any", "header")) and access_tokens.get(
    -        "header"
    -    ) is not None:
    -        log_debug_message("getSession: using header transfer method")
    -        request_transfer_method = "header"
    -        request_access_token = access_tokens["header"]
    -    elif (allowed_transfer_method in ("any", "cookie")) and access_tokens.get(
    -        "cookie"
    -    ) is not None:
    -        log_debug_message("getSession: using cookie transfer method")
    -        request_transfer_method = "cookie"
    -        request_access_token = access_tokens["cookie"]
    -    else:
    -        if session_optional:
    +    access_token_obj: Optional[ParsedJWTInfo] = None
    +    try:
    +        access_token_obj = parse_jwt_without_signature_verification(access_token)
    +        validate_access_token_structure(
    +            access_token_obj.payload, access_token_obj.version
    +        )
    +    except Exception as _:
    +        if session_required is False:
                 log_debug_message(
    -                "getSession: returning None because accessToken is undefined and sessionRequired is false"
    +                "getSession: Returning undefined because parsing failed and session_required is False"
                 )
    -            # there is no session that exists here, and the user wants session verification
    -            # to be optional. So we return None
                 return None
     
    -        log_debug_message(
    -            "getSession: UNAUTHORISED because access_token in request is None"
    -        )
    -        # we do not clear the session here because of a race condition mentioned in:
    -        # https://github.com/supertokens/supertokens-node/issues/17
    -        raise_unauthorised_exception(
    -            "Session does not exist. Are you sending the session tokens in the "
    -            "request with the appropriate token transfer method?",
    -            clear_tokens=False,
    -        )
    -
    -    anti_csrf_token = get_anti_csrf_header(request)
    -    do_anti_csrf_check = anti_csrf_check
    -
    -    if do_anti_csrf_check is None:
    -        do_anti_csrf_check = normalise_http_method(request.method()) != "get"
    -    if request_transfer_method == "header":
    -        do_anti_csrf_check = False
    -    log_debug_message(
    -        "getSession: Value of doAntiCsrfCheck is: %s", do_anti_csrf_check
    -    )
    +        raise UnauthorisedError("Token parsing failed", clear_tokens=False)
     
    -    result = await session_functions.get_session(
    +    response = await session_functions.get_session(
             self,
    -        request_access_token,
    +        access_token_obj,
             anti_csrf_token,
    -        do_anti_csrf_check,
    -        get_rid_header(request) is not None,
    +        (anti_csrf_check is not False),
    +        (check_database is True),
         )
     
    -    # Default is to respond with the access token obtained from the request
    -    access_token_string = request_access_token.raw_token_string
    +    log_debug_message("getSession: Success!")
    +
    +    if access_token_obj.version >= 3:
    +        if response.accessToken is not None:
    +            payload = parse_jwt_without_signature_verification(
    +                response.accessToken.token
    +            ).payload
    +        else:
    +            payload = access_token_obj.payload
    +    else:
    +        payload = response.session.userDataInJWT
    +
    +    if response.accessToken is not None:
    +        access_token_str = response.accessToken.token
    +        expiry_time = response.accessToken.expiry
    +        access_token_updated = True
    +    else:
    +        access_token_str = access_token
    +        expiry_time = response.session.expiryTime
    +        access_token_updated = False
     
         session = Session(
             self,
             self.config,
    -        access_token_string,
    -        result["session"]["handle"],
    -        result["session"]["userId"],
    -        result["session"]["userDataInJWT"],
    -        request_transfer_method,
    +        access_token_str,
    +        build_front_token(response.session.userId, expiry_time, payload),
    +        None,  # refresh_token
    +        anti_csrf_token,
    +        response.session.handle,
    +        response.session.userId,
    +        payload,
    +        None,
    +        access_token_updated,
         )
     
    -    if "accessToken" in result:
    -        session.access_token = result["accessToken"]["token"]
    -        new_access_token_info = result["accessToken"]
    -
    -        session.response_mutators.append(
    -            front_token_response_mutator(
    -                session.user_id,
    -                new_access_token_info["expiry"],
    -                session.access_token_payload,
    -            )
    -        )
    -        # We set the expiration to 100 years, because we can't really access the expiration of the refresh token everywhere we are setting it.
    -        # 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.
    -        # Even if the token is expired the presence of the token indicates that the user could have a valid refresh
    -        # Setting them to infinity would require special case handling on the frontend and just adding 100 years seems enough.
    -        session.response_mutators.append(
    -            token_response_mutator(
    -                self.config,
    -                "access",
    -                session.access_token,
    -                get_timestamp_ms() + HUNDRED_YEARS_IN_MS,
    -                session.transfer_method,
    -            )
    -        )
    -
    -    log_debug_message("getSession: Success!")
    -    request.set_session(session)
         return session
    @@ -2155,20 +1184,20 @@

    Methods

    return False new_access_token_payload = { - **session_info.access_token_payload, + **session_info.custom_claims_in_access_token_payload, **access_token_payload_update, } for k in access_token_payload_update.keys(): if new_access_token_payload[k] is None: del new_access_token_payload[k] - return await self.update_access_token_payload( - session_handle, new_access_token_payload, user_context + return await session_functions.update_access_token_payload( + self, session_handle, new_access_token_payload )
    -async def refresh_session(self, request: BaseRequest, user_context: Dict[str, Any]) ‑> SessionContainer +async def refresh_session(self, refresh_token: str, anti_csrf_token: Optional[str], disable_anti_csrf: bool, user_context: Dict[str, Any]) ‑> SessionContainer
    @@ -2177,191 +1206,54 @@

    Methods

    Expand source code
    async def refresh_session(
    -    self, request: BaseRequest, user_context: Dict[str, Any]
    +    self,
    +    refresh_token: str,
    +    anti_csrf_token: Optional[str],
    +    disable_anti_csrf: bool,
    +    user_context: Dict[str, Any],
     ) -> SessionContainer:
    -    log_debug_message("refreshSession: Started")
    -
    -    response_mutators: List[Callable[[Any], None]] = []
    -    refresh_tokens: Dict[TokenTransferMethod, Optional[str]] = {}
    -
    -    # We check all token transfer methods for available refresh tokens
    -    # We do this so that we can later clear all we are not overwriting
    -    for transfer_method in available_token_transfer_methods:
    -        refresh_token = get_token(
    -            request,
    -            "refresh",
    -            transfer_method,
    -        )
    -        if refresh_token is not None:
    -            log_debug_message(
    -                "refreshSession: got refresh token from %s", transfer_method
    -            )
    -
    -        refresh_tokens[transfer_method] = refresh_token
    -
    -    allowed_transfer_method = self.config.get_token_transfer_method(
    -        request, False, user_context
    -    )
    -    log_debug_message(
    -        "refreshSession: getTokenTransferMethod returned: %s",
    -        allowed_transfer_method,
    -    )
    -
    -    request_transfer_method: TokenTransferMethod
    -    refresh_token: Optional[str]
    -
    -    if (allowed_transfer_method in ("any", "header")) and (
    -        refresh_tokens.get("header")
    -    ):
    -        log_debug_message("refreshSession: using header transfer method")
    -        request_transfer_method = "header"
    -        refresh_token = refresh_tokens["header"]
    -    elif (allowed_transfer_method in ("any", "cookie")) and (
    -        refresh_tokens.get("cookie")
    +    if (
    +        disable_anti_csrf is not True
    +        and self.config.anti_csrf == "VIA_CUSTOM_HEADER"
         ):
    -        log_debug_message("refreshSession: using cookie transfer method")
    -        request_transfer_method = "cookie"
    -        refresh_token = refresh_tokens["cookie"]
    -    else:
    -        # This token isn't handled by getToken/setToken to limit the scope of this legacy/migration code
    -        if request.get_cookie(LEGACY_ID_REFRESH_TOKEN_COOKIE_NAME) is not None:
    -            log_debug_message(
    -                "refreshSession: cleared legacy id refresh token because refresh token was not found"
    -            )
    -            response_mutators.append(
    -                set_cookie_response_mutator(
    -                    self.config,
    -                    LEGACY_ID_REFRESH_TOKEN_COOKIE_NAME,
    -                    "",
    -                    0,
    -                    "access_token_path",
    -                )
    -            )
    -
    -        log_debug_message(
    -            "refreshSession: UNAUTHORISED because refresh_token in request is None"
    -        )
    -        return raise_unauthorised_exception(
    -            "Refresh token not found. Are you sending the refresh token in the request?",
    -            clear_tokens=False,
    -        )
    -
    -    assert refresh_token is not None
    -
    -    try:
    -        anti_csrf_token = get_anti_csrf_header(request)
    -        result = await session_functions.refresh_session(
    -            self,
    -            refresh_token,
    -            anti_csrf_token,
    -            get_rid_header(request) is not None,
    -            request_transfer_method,
    -        )
    -        log_debug_message(
    -            "refreshSession: Attaching refresh session info as %s",
    -            request_transfer_method,
    +        raise Exception(
    +            "Since the anti-csrf mode is VIA_CUSTOM_HEADER getSession can't check the CSRF token. Please either use VIA_TOKEN or set antiCsrfCheck to false"
             )
     
    -        for transfer_method in available_token_transfer_methods:
    -            if (
    -                transfer_method != request_transfer_method
    -                and refresh_tokens.get(transfer_method) is not None
    -            ):
    -                response_mutators.append(
    -                    clear_session_response_mutator(self.config, transfer_method)
    -                )
    -
    -        if request.get_cookie(LEGACY_ID_REFRESH_TOKEN_COOKIE_NAME) is not None:
    -            log_debug_message(
    -                "refreshSession: cleared legacy id refresh token after successful refresh"
    -            )
    -            response_mutators.append(
    -                set_cookie_response_mutator(
    -                    self.config,
    -                    LEGACY_ID_REFRESH_TOKEN_COOKIE_NAME,
    -                    "",
    -                    0,
    -                    "access_token_path",
    -                )
    -            )
    +    log_debug_message("refreshSession: Started")
     
    -        session = Session(
    -            self,
    -            self.config,
    -            result["accessToken"]["token"],
    -            result["session"]["handle"],
    -            result["session"]["userId"],
    -            result["session"]["userDataInJWT"],
    -            request_transfer_method,
    -        )
    -        new_access_token_info = result["accessToken"]
    -        new_refresh_token_info = result["refreshToken"]
    -        new_anti_csrf_token = result.get("antiCsrfToken")
    -
    -        if new_access_token_info is not None:
    -            response_mutators.append(
    -                front_token_response_mutator(
    -                    session.user_id,
    -                    new_access_token_info["expiry"],
    -                    session.access_token_payload,
    -                )
    -            )
    -            # We set the expiration to 100 years, because we can't really access the expiration of the refresh token everywhere we are setting it.
    -            # 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.
    -            # Even if the token is expired the presence of the token indicates that the user could have a valid refresh
    -            # Setting them to infinity would require special case handling on the frontend and just adding 100 years seems enough.
    -            response_mutators.append(
    -                token_response_mutator(
    -                    self.config,
    -                    "access",
    -                    new_access_token_info["token"],
    -                    get_timestamp_ms() + HUNDRED_YEARS_IN_MS,  # 100 years
    -                    session.transfer_method,
    -                )
    -            )
    -        if new_refresh_token_info is not None:
    -            response_mutators.append(
    -                token_response_mutator(
    -                    self.config,
    -                    "refresh",
    -                    new_refresh_token_info["token"],
    -                    new_refresh_token_info[
    -                        "expiry"
    -                    ],  # This comes from the core and is 100 days
    -                    session.transfer_method,
    -                )
    -            )
    +    response = await session_functions.refresh_session(
    +        self,
    +        refresh_token,
    +        anti_csrf_token,
    +        disable_anti_csrf,
    +    )
     
    -        anti_csrf_token = new_anti_csrf_token
    -        if anti_csrf_token is not None:
    -            response_mutators.append(anti_csrf_response_mutator(anti_csrf_token))
    +    log_debug_message("refreshSession: Success!")
     
    -        session.response_mutators.extend(response_mutators)
    +    payload = parse_jwt_without_signature_verification(
    +        response.accessToken.token,
    +    ).payload
     
    -        log_debug_message("refreshSession: Success!")
    -        request.set_session(session)
    -        return session
    -    except (TokenTheftError, UnauthorisedError) as e:
    -        if (
    -            isinstance(e, UnauthorisedError) and e.clear_tokens is True
    -        ) or isinstance(e, TokenTheftError):
    -            # This token isn't handled by getToken/setToken to limit the scope of this legacy/migration code
    -            if request.get_cookie(LEGACY_ID_REFRESH_TOKEN_COOKIE_NAME) is not None:
    -                log_debug_message(
    -                    "refreshSession: cleared legacy id refresh token because refresh token was not found"
    -                )
    -                response_mutators.append(
    -                    set_cookie_response_mutator(
    -                        self.config,
    -                        LEGACY_ID_REFRESH_TOKEN_COOKIE_NAME,
    -                        "",
    -                        0,
    -                        "access_token_path",
    -                    )
    -                )
    -                e.response_mutators.extend(response_mutators)
    +    session = Session(
    +        self,
    +        self.config,
    +        response.accessToken.token,
    +        build_front_token(
    +            response.session.userId,
    +            response.accessToken.expiry,
    +            payload,
    +        ),
    +        response.refreshToken,
    +        response.antiCsrfToken,
    +        response.session.handle,
    +        response.session.userId,
    +        user_data_in_access_token=payload,
    +        req_res_info=None,
    +        access_token_updated=True,
    +    )
     
    -        raise e
    + return session
    @@ -2490,36 +1382,8 @@

    Methods

    )
    -
    -def update_jwt_signing_public_key_info(self, key_list: Union[List[Dict[str, Any]], None], public_key: str, expiry_time: int) -
    -
    -
    -
    - -Expand source code - -
    def update_jwt_signing_public_key_info(
    -    self,
    -    key_list: Union[List[Dict[str, Any]], None],
    -    public_key: str,
    -    expiry_time: int,
    -):
    -    if key_list is None:
    -        key_list = [
    -            {
    -                "publicKey": public_key,
    -                "expiryTime": expiry_time,
    -                "createdAt": get_timestamp_ms(),
    -            }
    -        ]
    -
    -    if self.handshake_info is not None:
    -        self.handshake_info.set_jwt_signing_public_key_list(key_list)
    -
    -
    -
    -async def update_session_data(self, session_handle: str, new_session_data: Dict[str, Any], user_context: Dict[str, Any]) ‑> bool +
    +async def update_session_data_in_database(self, session_handle: str, new_session_data: Dict[str, Any], user_context: Dict[str, Any]) ‑> bool
    @@ -2527,13 +1391,13 @@

    Methods

    Expand source code -
    async def update_session_data(
    +
    async def update_session_data_in_database(
         self,
         session_handle: str,
         new_session_data: Dict[str, Any],
         user_context: Dict[str, Any],
     ) -> bool:
    -    return await session_functions.update_session_data(
    +    return await session_functions.update_session_data_in_database(
             self, session_handle, new_session_data
         )
    @@ -2617,14 +1481,6 @@

    Methods

    -

    Inherited members

    -
    @@ -2643,23 +1499,14 @@

    Index

  • Classes

  • @@ -270,7 +344,7 @@

    Classes

    class Session -(recipe_implementation: RecipeInterface, config: SessionConfig, access_token: str, session_handle: str, user_id: str, access_token_payload: Dict[str, Any], transfer_method: TokenTransferMethod) +(recipe_implementation: RecipeInterface, config: SessionConfig, access_token: str, front_token: str, refresh_token: Optional[TokenInfo], anti_csrf_token: Optional[str], session_handle: str, user_id: str, user_data_in_access_token: Optional[Dict[str, Any]], req_res_info: Optional[ReqResInfo], access_token_updated: bool)

    Helper class that provides a standard way to create an ABC using @@ -280,21 +354,61 @@

    Classes

    Expand source code
    class Session(SessionContainer):
    +    async def attach_to_request_response(
    +        self, request: BaseRequest, transfer_method: TokenTransferMethod
    +    ) -> None:
    +        self.req_res_info = ReqResInfo(request, transfer_method)
    +
    +        if self.access_token_updated:
    +            self.response_mutators.append(
    +                access_token_mutator(
    +                    self.access_token,
    +                    self.front_token,
    +                    self.config,
    +                    transfer_method,
    +                )
    +            )
    +            if self.refresh_token is not None:
    +                self.response_mutators.append(
    +                    token_response_mutator(
    +                        self.config,
    +                        "refresh",
    +                        self.refresh_token.token,
    +                        self.refresh_token.expiry,
    +                        transfer_method,
    +                    )
    +                )
    +            if self.anti_csrf_token is not None:
    +                self.response_mutators.append(
    +                    anti_csrf_response_mutator(self.anti_csrf_token)
    +                )
    +
    +        request.set_session(self)
    +
         async def revoke_session(self, user_context: Union[Any, None] = None) -> None:
             if user_context is None:
                 user_context = {}
    +
             await self.recipe_implementation.revoke_session(
                 self.session_handle, user_context
             )
     
    -        self.response_mutators.append(
    -            clear_session_response_mutator(
    -                self.config,
    -                self.transfer_method,
    +        if self.req_res_info is not None:
    +            # we do not check the output of calling revokeSession
    +            # before clearing the cookies because we are revoking the
    +            # current API request's session.
    +            # If we instead clear the cookies only when revokeSession
    +            # returns true, it can cause this kind of bug:
    +            # https://github.com/supertokens/supertokens-node/issues/343
    +            transfer_method: TokenTransferMethod = self.req_res_info.transfer_method  # type: ignore
    +            self.response_mutators.append(
    +                clear_session_response_mutator(
    +                    self.config,
    +                    transfer_method,
    +                )
                 )
    -        )
     
    -    async def get_session_data(
    +    async def get_session_data_from_database(
             self, user_context: Union[Dict[str, Any], None] = None
         ) -> Dict[str, Any]:
             if user_context is None:
    @@ -305,67 +419,28 @@ 

    Classes

    if session_info is None: raise_unauthorised_exception("Session does not exist anymore.") - return session_info.session_data + return session_info.session_data_in_database - async def update_session_data( + async def update_session_data_in_database( self, new_session_data: Dict[str, Any], user_context: Union[Dict[str, Any], None] = None, ) -> None: if user_context is None: user_context = {} - updated = await self.recipe_implementation.update_session_data( + updated = await self.recipe_implementation.update_session_data_in_database( self.session_handle, new_session_data, user_context ) if not updated: raise_unauthorised_exception("Session does not exist anymore.") - async def update_access_token_payload( - self, - new_access_token_payload: Dict[str, Any], - user_context: Union[Dict[str, Any], None] = None, - ) -> None: - if user_context is None: - user_context = {} - - result = await self.recipe_implementation.regenerate_access_token( - self.access_token, new_access_token_payload, user_context - ) - if result is None: - raise_unauthorised_exception("Session does not exist anymore.") - - self.access_token_payload = result.session.user_data_in_jwt - if result.access_token is not None: - self.access_token = result.access_token.token - - self.response_mutators.append( - front_token_response_mutator( - self.user_id, - result.access_token.expiry, - self.access_token_payload, - ) - ) - # We set the expiration to 100 years, because we can't really access the expiration of the refresh token everywhere we are setting it. - # 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. - # Even if the token is expired the presence of the token indicates that the user could have a valid refresh - # Setting them to infinity would require special case handling on the frontend and just adding 100 years seems enough. - self.response_mutators.append( - token_response_mutator( - self.config, - "access", - result.access_token.token, - get_timestamp_ms() + HUNDRED_YEARS_IN_MS, - self.transfer_method, - ) - ) - def get_user_id(self, user_context: Union[Dict[str, Any], None] = None) -> str: return self.user_id def get_access_token_payload( self, user_context: Union[Dict[str, Any], None] = None ) -> Dict[str, Any]: - return self.access_token_payload + return self.user_data_in_access_token def get_handle(self, user_context: Union[Dict[str, Any], None] = None) -> str: return self.session_handle @@ -373,6 +448,17 @@

    Classes

    def get_access_token(self, user_context: Union[Dict[str, Any], None] = None) -> str: return self.access_token + def get_all_session_tokens_dangerously(self) -> GetSessionTokensDangerouslyDict: + return { + "accessToken": self.access_token, + "accessAndFrontTokenUpdated": self.access_token_updated, + "refreshToken": None + if self.refresh_token is None + else self.refresh_token.token, + "frontToken": self.front_token, + "antiCsrfToken": self.anti_csrf_token, + } + async def get_time_created( self, user_context: Union[Dict[str, Any], None] = None ) -> int: @@ -413,6 +499,11 @@

    Classes

    ) if validate_claim_res.access_token_payload_update is not None: + for k in protected_props: + try: + del validate_claim_res.access_token_payload_update[k] + except ValueError: + pass await self.merge_into_access_token_payload( validate_claim_res.access_token_payload_update, user_context ) @@ -469,15 +560,62 @@

    Classes

    if user_context is None: user_context = {} - update_payload = { - **self.get_access_token_payload(user_context), + new_access_token_payload = {**self.get_access_token_payload(user_context)} + for k in protected_props: + try: + del new_access_token_payload[k] + except KeyError: + pass + + new_access_token_payload = { + **new_access_token_payload, **access_token_payload_update, } + for k in access_token_payload_update.keys(): if access_token_payload_update[k] is None: - del update_payload[k] + del new_access_token_payload[k] + + response = await self.recipe_implementation.regenerate_access_token( + self.get_access_token(), new_access_token_payload, user_context + ) - await self.update_access_token_payload(update_payload, user_context)
    + if response is None: + raise_unauthorised_exception("Session does not exist anymore.") + + if response.access_token is not None: + resp_token = parse_jwt_without_signature_verification( + response.access_token.token + ) + payload = ( + resp_token.payload + if resp_token.version >= 3 + else response.session.user_data_in_jwt + ) + self.user_data_in_access_token = payload + self.access_token = response.access_token.token + self.front_token = build_front_token( + self.get_user_id(), response.access_token.expiry, payload + ) + self.access_token_updated = True + if self.req_res_info is not None: + transfer_method: TokenTransferMethod = self.req_res_info.transfer_method # type: ignore + self.response_mutators.append( + access_token_mutator( + self.access_token, + self.front_token, + self.config, + transfer_method, + ) + ) + else: + # This case means that the access token has expired between the validation and this update + # We can't update the access token on the FE, as it will need to call refresh anyway but we handle this as a successful update during this request + # the changes will be reflected on the FE after refresh is called + self.user_data_in_access_token = { + **self.get_access_token_payload(), + **response.session.user_data_in_jwt, + }

    Ancestors

      @@ -511,6 +649,11 @@

      Methods

      ) if validate_claim_res.access_token_payload_update is not None: + for k in protected_props: + try: + del validate_claim_res.access_token_payload_update[k] + except ValueError: + pass await self.merge_into_access_token_payload( validate_claim_res.access_token_payload_update, user_context ) @@ -520,6 +663,47 @@

      Methods

      raise_invalid_claims_exception("INVALID_CLAIMS", validation_errors)
    +
    +async def attach_to_request_response(self, request: BaseRequest, transfer_method: Literal['cookie', 'header']) ‑> None +
    +
    +
    +
    + +Expand source code + +
    async def attach_to_request_response(
    +    self, request: BaseRequest, transfer_method: TokenTransferMethod
    +) -> None:
    +    self.req_res_info = ReqResInfo(request, transfer_method)
    +
    +    if self.access_token_updated:
    +        self.response_mutators.append(
    +            access_token_mutator(
    +                self.access_token,
    +                self.front_token,
    +                self.config,
    +                transfer_method,
    +            )
    +        )
    +        if self.refresh_token is not None:
    +            self.response_mutators.append(
    +                token_response_mutator(
    +                    self.config,
    +                    "refresh",
    +                    self.refresh_token.token,
    +                    self.refresh_token.expiry,
    +                    transfer_method,
    +                )
    +            )
    +        if self.anti_csrf_token is not None:
    +            self.response_mutators.append(
    +                anti_csrf_response_mutator(self.anti_csrf_token)
    +            )
    +
    +    request.set_session(self)
    +
    +
    async def fetch_and_set_claim(self, claim: SessionClaim[typing.Any], user_context: Optional[Dict[str, Any]] = None) ‑> None
    @@ -564,7 +748,28 @@

    Methods

    def get_access_token_payload(
         self, user_context: Union[Dict[str, Any], None] = None
     ) -> Dict[str, Any]:
    -    return self.access_token_payload
    + return self.user_data_in_access_token
    + + +
    +def get_all_session_tokens_dangerously(self) ‑> GetSessionTokensDangerouslyDict +
    +
    +
    +
    + +Expand source code + +
    def get_all_session_tokens_dangerously(self) -> GetSessionTokensDangerouslyDict:
    +    return {
    +        "accessToken": self.access_token,
    +        "accessAndFrontTokenUpdated": self.access_token_updated,
    +        "refreshToken": None
    +        if self.refresh_token is None
    +        else self.refresh_token.token,
    +        "frontToken": self.front_token,
    +        "antiCsrfToken": self.anti_csrf_token,
    +    }
    @@ -621,8 +826,8 @@

    Methods

    return self.session_handle
    -
    -async def get_session_data(self, user_context: Optional[Dict[str, Any]] = None) ‑> Dict[str, Any] +
    +async def get_session_data_from_database(self, user_context: Optional[Dict[str, Any]] = None) ‑> Dict[str, Any]
    @@ -630,7 +835,7 @@

    Methods

    Expand source code -
    async def get_session_data(
    +
    async def get_session_data_from_database(
         self, user_context: Union[Dict[str, Any], None] = None
     ) -> Dict[str, Any]:
         if user_context is None:
    @@ -641,7 +846,7 @@ 

    Methods

    if session_info is None: raise_unauthorised_exception("Session does not exist anymore.") - return session_info.session_data
    + return session_info.session_data_in_database
    @@ -697,15 +902,62 @@

    Methods

    if user_context is None: user_context = {} - update_payload = { - **self.get_access_token_payload(user_context), + new_access_token_payload = {**self.get_access_token_payload(user_context)} + for k in protected_props: + try: + del new_access_token_payload[k] + except KeyError: + pass + + new_access_token_payload = { + **new_access_token_payload, **access_token_payload_update, } + for k in access_token_payload_update.keys(): if access_token_payload_update[k] is None: - del update_payload[k] + del new_access_token_payload[k] + + response = await self.recipe_implementation.regenerate_access_token( + self.get_access_token(), new_access_token_payload, user_context + ) + + if response is None: + raise_unauthorised_exception("Session does not exist anymore.") - await self.update_access_token_payload(update_payload, user_context)
    + if response.access_token is not None: + resp_token = parse_jwt_without_signature_verification( + response.access_token.token + ) + payload = ( + resp_token.payload + if resp_token.version >= 3 + else response.session.user_data_in_jwt + ) + self.user_data_in_access_token = payload + self.access_token = response.access_token.token + self.front_token = build_front_token( + self.get_user_id(), response.access_token.expiry, payload + ) + self.access_token_updated = True + if self.req_res_info is not None: + transfer_method: TokenTransferMethod = self.req_res_info.transfer_method # type: ignore + self.response_mutators.append( + access_token_mutator( + self.access_token, + self.front_token, + self.config, + transfer_method, + ) + ) + else: + # This case means that the access token has expired between the validation and this update + # We can't update the access token on the FE, as it will need to call refresh anyway but we handle this as a successful update during this request + # the changes will be reflected on the FE after refresh is called + self.user_data_in_access_token = { + **self.get_access_token_payload(), + **response.session.user_data_in_jwt, + }
    @@ -739,16 +991,25 @@

    Methods

    async def revoke_session(self, user_context: Union[Any, None] = None) -> None:
         if user_context is None:
             user_context = {}
    +
         await self.recipe_implementation.revoke_session(
             self.session_handle, user_context
         )
     
    -    self.response_mutators.append(
    -        clear_session_response_mutator(
    -            self.config,
    -            self.transfer_method,
    -        )
    -    )
    + if self.req_res_info is not None: + # we do not check the output of calling revokeSession + # before clearing the cookies because we are revoking the + # current API request's session. + # If we instead clear the cookies only when revokeSession + # returns true, it can cause this kind of bug: + # https://github.com/supertokens/supertokens-node/issues/343 + transfer_method: TokenTransferMethod = self.req_res_info.transfer_method # type: ignore + self.response_mutators.append( + clear_session_response_mutator( + self.config, + transfer_method, + ) + )
    @@ -773,8 +1034,8 @@

    Methods

    return await self.merge_into_access_token_payload(update, user_context)
    -
    -async def update_session_data(self, new_session_data: Dict[str, Any], user_context: Optional[Dict[str, Any]] = None) ‑> None +
    +async def update_session_data_in_database(self, new_session_data: Dict[str, Any], user_context: Optional[Dict[str, Any]] = None) ‑> None
    @@ -782,14 +1043,14 @@

    Methods

    Expand source code -
    async def update_session_data(
    +
    async def update_session_data_in_database(
         self,
         new_session_data: Dict[str, Any],
         user_context: Union[Dict[str, Any], None] = None,
     ) -> None:
         if user_context is None:
             user_context = {}
    -    updated = await self.recipe_implementation.update_session_data(
    +    updated = await self.recipe_implementation.update_session_data_in_database(
             self.session_handle, new_session_data, user_context
         )
         if not updated:
    @@ -797,14 +1058,6 @@ 

    Methods

    -

    Inherited members

    -
    @@ -826,20 +1079,22 @@

    Index

    Session

    diff --git a/html/supertokens_python/recipe/session/session_functions.html b/html/supertokens_python/recipe/session/session_functions.html index b5693db6d..bc7db80c0 100644 --- a/html/supertokens_python/recipe/session/session_functions.html +++ b/html/supertokens_python/recipe/session/session_functions.html @@ -42,20 +42,21 @@

    Module supertokens_python.recipe.session.session_functio from __future__ import annotations import time -from typing import TYPE_CHECKING, Any, Dict, List, Union +from typing import TYPE_CHECKING, Any, Dict, List, Union, Optional from supertokens_python.recipe.session.interfaces import SessionInformationResult from .access_token import get_info_from_access_token +from .constants import JWKCacheMaxAgeInMs from .jwt import ParsedJWTInfo if TYPE_CHECKING: from .recipe_implementation import RecipeImplementation - from .utils import TokenTransferMethod from supertokens_python.logger import log_debug_message from supertokens_python.normalised_url_path import NormalisedURLPath from supertokens_python.process_state import AllowedProcessStates, ProcessState +from supertokens_python.recipe.session.interfaces import TokenInfo from .exceptions import ( TryRefreshTokenError, @@ -65,42 +66,105 @@

    Module supertokens_python.recipe.session.session_functio ) +class CreateOrRefreshAPIResponseSession: + def __init__(self, handle: str, userId: str, userDataInJWT: Any): + self.handle = handle + self.userId = userId + self.userDataInJWT = userDataInJWT + + +class CreateOrRefreshAPIResponse: + def __init__( + self, + session: CreateOrRefreshAPIResponseSession, + accessToken: TokenInfo, + refreshToken: TokenInfo, + antiCsrfToken: Optional[str], + ): + self.session = session + self.accessToken = accessToken + self.refreshToken = refreshToken + self.antiCsrfToken = antiCsrfToken + + +class GetSessionAPIResponseSession: + def __init__( + self, + handle: str, + userId: str, + userDataInJWT: Dict[str, Any], + expiryTime: int, + ) -> None: + self.handle = handle + self.userId = userId + self.userDataInJWT = userDataInJWT + self.expiryTime = expiryTime + + +class GetSessionAPIResponseAccessToken: + def __init__(self, token: str, expiry: int, createdTime: int) -> None: + self.token = token + self.expiry = expiry + self.createdTime = createdTime + + +class GetSessionAPIResponse: + def __init__( + self, + session: GetSessionAPIResponseSession, + accessToken: Optional[GetSessionAPIResponseAccessToken] = None, + ) -> None: + self.session = session + self.accessToken = accessToken + + async def create_new_session( recipe_implementation: RecipeImplementation, user_id: str, disable_anti_csrf: bool, access_token_payload: Union[None, Dict[str, Any]], - session_data: Union[None, Dict[str, Any]], -) -> Dict[str, Any]: - if session_data is None: - session_data = {} + session_data_in_database: Union[None, Dict[str, Any]], +) -> CreateOrRefreshAPIResponse: + if session_data_in_database is None: + session_data_in_database = {} if access_token_payload is None: access_token_payload = {} - handshake_info = await recipe_implementation.get_handshake_info() enable_anti_csrf = ( - disable_anti_csrf is False and handshake_info.anti_csrf == "VIA_TOKEN" + disable_anti_csrf is False + and recipe_implementation.config.anti_csrf == "VIA_TOKEN" ) response = await recipe_implementation.querier.send_post_request( NormalisedURLPath("/recipe/session"), { "userId": user_id, "userDataInJWT": access_token_payload, - "userDataInDatabase": session_data, + "userDataInDatabase": session_data_in_database, + "useDynamicSigningKey": recipe_implementation.config.use_dynamic_access_token_signing_key, "enableAntiCsrf": enable_anti_csrf, }, ) - recipe_implementation.update_jwt_signing_public_key_info( - response["jwtSigningPublicKeyList"], - response["jwtSigningPublicKey"], - response["jwtSigningPublicKeyExpiryTime"], - ) + response.pop("status", None) - response.pop("jwtSigningPublicKey", None) - response.pop("jwtSigningPublicKeyExpiryTime", None) - response.pop("jwtSigningPublicKeyList", None) - return response + return CreateOrRefreshAPIResponse( + CreateOrRefreshAPIResponseSession( + response["session"]["handle"], + response["session"]["userId"], + response["session"]["userDataInJWT"], + ), + TokenInfo( + response["accessToken"]["token"], + response["accessToken"]["expiry"], + response["accessToken"]["createdTime"], + ), + TokenInfo( + response["refreshToken"]["token"], + response["refreshToken"]["expiry"], + response["refreshToken"]["createdTime"], + ), + response["antiCsrfToken"] if "antiCsrfToken" in response else None, + ) async def get_session( @@ -108,89 +172,119 @@

    Module supertokens_python.recipe.session.session_functio parsed_access_token: ParsedJWTInfo, anti_csrf_token: Union[str, None], do_anti_csrf_check: bool, - contains_custom_header: bool, -) -> Dict[str, Any]: - handshake_info = await recipe_implementation.get_handshake_info() - access_token_info = None - found_a_sign_key_that_is_older_than_the_access_token = False - - for key in handshake_info.get_jwt_signing_public_key_list(): - try: - access_token_info = get_info_from_access_token( - parsed_access_token, - key["publicKey"], - handshake_info.anti_csrf == "VIA_TOKEN" and do_anti_csrf_check, - ) + always_check_core: bool, +) -> GetSessionAPIResponse: + config = recipe_implementation.config + access_token_info: Optional[Dict[str, Any]] = None + + try: + access_token_info = get_info_from_access_token( + parsed_access_token, + recipe_implementation.JWK_clients, + config.anti_csrf == "VIA_TOKEN" and do_anti_csrf_check, + ) - found_a_sign_key_that_is_older_than_the_access_token = True + except Exception as e: + if not isinstance(e, TryRefreshTokenError): + raise e - except Exception as e: - if not isinstance(e, TryRefreshTokenError): - raise e + # if it comes here, it means token verification has failed. + # It may be due to: + # - signing key was updated and this token was signed with new key + # - access token is actually expired + # - access token was signed with the older signing key - payload = parsed_access_token.payload + # if access token is actually expired, we don't need to call core and + # just return TRY_REFRESH_TOKEN to the client - if not isinstance(payload["timeCreated"], int) or not isinstance( - payload["expiryTime"], int - ): - raise e + # if access token creation time is after this signing key was created + # we need to call core as there are chances that the token + # was signed with the updated signing key - if payload["expiryTime"] < time.time(): - raise e + # if access token creation time is before oldest signing key was created, + # so if foundASigningKeyThatIsOlderThanTheAccessToken is still false after + # the loop we just return TRY_REFRESH_TOKEN - if payload["timeCreated"] >= key["createdAt"]: - found_a_sign_key_that_is_older_than_the_access_token = True - break + payload = parsed_access_token.payload - if not found_a_sign_key_that_is_older_than_the_access_token: - log_debug_message( - "getSession: Returning TRY_REFRESH_TOKEN because signing key in handshake info is not up to date." - ) - raise_try_refresh_token_exception( - "access token has expired. Please call the refresh API" + time_created = payload.get("timeCreated") + expiry_time = payload.get("expiryTime") + + if not isinstance(time_created, int) or not isinstance(expiry_time, int): + raise e + + if parsed_access_token.version < 3: + if expiry_time < time.time(): + raise e + + # We check if the token was created since the last time we refreshed the keys from the core + # Since we do not know the exact timing of the last refresh, we check against the max age + if time_created <= time.time() - JWKCacheMaxAgeInMs: + raise e + else: + # Since v3 (and above) tokens contain a kid we can trust the cache-refresh mechanism of the pyjwt library + # This means we do not need to call the core since the signature wouldn't pass verification anyway. + raise e + + if parsed_access_token.version >= 3: + token_use_dynamic_key = ( + parsed_access_token.kid.startswith("d-") + if parsed_access_token.kid is not None + else False ) - if handshake_info.anti_csrf == "VIA_TOKEN" and do_anti_csrf_check: - if access_token_info is not None: - if ( - anti_csrf_token is None - or anti_csrf_token != access_token_info["antiCsrfToken"] - ): - if anti_csrf_token is None: - log_debug_message( - "getSession: Returning TRY_REFRESH_TOKEN because antiCsrfToken is missing from request" - ) - raise_try_refresh_token_exception( - "Provided antiCsrfToken is undefined. If you do not want anti-csrf check for this API, please set doAntiCsrfCheck to false for this API" - ) - else: - log_debug_message( - "getSession: Returning TRY_REFRESH_TOKEN because the passed antiCsrfToken is not the same as in the access token" - ) - raise_try_refresh_token_exception("anti-csrf check failed") - - elif handshake_info.anti_csrf == "VIA_CUSTOM_HEADER" and do_anti_csrf_check: - if not contains_custom_header: + if token_use_dynamic_key != config.use_dynamic_access_token_signing_key: log_debug_message( - "getSession: Returning TRY_REFRESH_TOKEN because custom header (rid) was not passed" + "getSession: Returning TRY_REFRESH_TOKEN because the access token doesn't match the useDynamicAccessTokenSigningKey in the config" ) + raise_try_refresh_token_exception( - "anti-csrf check failed. Please pass 'rid: \"anti-csrf\"' header in the request, or set doAntiCsrfCheck to false " - "for this API" + "The access token doesn't match the useDynamicAccessTokenSigningKey setting" + ) + + # If we get here we either have a V2 token that doesn't pass verification or a valid V3> token + # anti-csrf check if accesstokenInfo is not undefined which means token verification was successful + + if do_anti_csrf_check: + if config.anti_csrf == "VIA_TOKEN": + if access_token_info is not None: + if ( + anti_csrf_token is None + or anti_csrf_token != access_token_info["antiCsrfToken"] + ): + if anti_csrf_token is None: + log_debug_message( + "getSession: Returning TRY_REFRESH_TOKEN because antiCsrfToken is missing from request" + ) + raise_try_refresh_token_exception( + "Provided antiCsrfToken is undefined. If you do not want anti-csrf check for this API, please set doAntiCsrfCheck to false for this API" + ) + else: + log_debug_message( + "getSession: Returning TRY_REFRESH_TOKEN because the passed antiCsrfToken is not the same as in the access token" + ) + raise_try_refresh_token_exception("anti-csrf check failed") + + elif config.anti_csrf == "VIA_CUSTOM_HEADER": + # The function should never be called by this (we check this outside the function as well) + # There we can add a bit more information to the error, so that's the primary check, this is just making sure. + raise Exception( + "Please either use VIA_TOKEN, NONE or call with doAntiCsrfCheck false" ) if ( access_token_info is not None - and not handshake_info.access_token_blacklisting_enabled + and not always_check_core and access_token_info["parentRefreshTokenHash1"] is None ): - return { - "session": { - "handle": access_token_info["sessionHandle"], - "userId": access_token_info["userId"], - "userDataInJWT": access_token_info["userData"], - } - } + return GetSessionAPIResponse( + GetSessionAPIResponseSession( + access_token_info["sessionHandle"], + access_token_info["userId"], + access_token_info["userData"], + access_token_info["expiryTime"], + ) + ) ProcessState.get_instance().add_state( AllowedProcessStates.CALLING_SERVICE_IN_VERIFY @@ -199,7 +293,8 @@

    Module supertokens_python.recipe.session.session_functio data = { "accessToken": parsed_access_token.raw_token_string, "doAntiCsrfCheck": do_anti_csrf_check, - "enableAntiCsrf": handshake_info.anti_csrf == "VIA_TOKEN", + "enableAntiCsrf": config.anti_csrf == "VIA_TOKEN", + "checkDatabase": always_check_core, } if anti_csrf_token is not None: data["antiCsrfToken"] = anti_csrf_token @@ -208,31 +303,23 @@

    Module supertokens_python.recipe.session.session_functio NormalisedURLPath("/recipe/session/verify"), data ) if response["status"] == "OK": - recipe_implementation.update_jwt_signing_public_key_info( - response["jwtSigningPublicKeyList"], - response["jwtSigningPublicKey"], - response["jwtSigningPublicKeyExpiryTime"], - ) response.pop("status", None) - response.pop("jwtSigningPublicKey", None) - response.pop("jwtSigningPublicKeyExpiryTime", None) - response.pop("jwtSigningPublicKeyList", None) - return response + return GetSessionAPIResponse( + GetSessionAPIResponseSession( + response["session"]["handle"], + response["session"]["userId"], + response["session"]["userDataInJWT"], + response["accessToken"]["expiry"], + ), + GetSessionAPIResponseAccessToken( + response["accessToken"]["token"], + response["accessToken"]["expiry"], + response["accessToken"]["createdTime"], + ), + ) if response["status"] == "UNAUTHORISED": log_debug_message("getSession: Returning UNAUTHORISED because of core response") raise_unauthorised_exception(response["message"]) - if ( - response["jwtSigningPublicKeyList"] is not None - or response["jwtSigningPublicKey"] is not None - or response["jwtSigningPublicKeyExpiryTime"] is not None - ): - recipe_implementation.update_jwt_signing_public_key_info( - response["jwtSigningPublicKeyList"], - response["jwtSigningPublicKey"], - response["jwtSigningPublicKeyExpiryTime"], - ) - else: - await recipe_implementation.get_handshake_info(True) log_debug_message( "getSession: Returning TRY_REFRESH_TOKEN because of core response" @@ -244,34 +331,52 @@

    Module supertokens_python.recipe.session.session_functio recipe_implementation: RecipeImplementation, refresh_token: str, anti_csrf_token: Union[str, None], - contains_custom_header: bool, - transfer_method: TokenTransferMethod, -) -> Dict[str, Any]: - handshake_info = await recipe_implementation.get_handshake_info() + disable_anti_csrf: bool, +) -> CreateOrRefreshAPIResponse: data = { "refreshToken": refresh_token, - "enableAntiCsrf": transfer_method == "cookie" - and handshake_info.anti_csrf == "VIA_TOKEN", + "enableAntiCsrf": ( + not disable_anti_csrf + and recipe_implementation.config.anti_csrf == "VIA_TOKEN" + ), } + if anti_csrf_token is not None: data["antiCsrfToken"] = anti_csrf_token - if handshake_info.anti_csrf == "VIA_CUSTOM_HEADER" and transfer_method == "cookie": - if not contains_custom_header: - log_debug_message( - "refreshSession: Returning UNAUTHORISED because custom header (rid) was not passed" - ) - raise_unauthorised_exception( - "anti-csrf check failed. Please pass 'rid: \"session\"' header " - "in the request.", - False, - ) + if ( + recipe_implementation.config.anti_csrf == "VIA_CUSTOM_HEADER" + and not disable_anti_csrf + ): + # The function should never be called by this (we check this outside the function as well) + # There we can add a bit more information to the error, so that's the primary check, this is just making sure. + raise Exception( + "Please either use VIA_TOKEN, NONE or call with doAntiCsrfCheck false" + ) + response = await recipe_implementation.querier.send_post_request( NormalisedURLPath("/recipe/session/refresh"), data ) if response["status"] == "OK": response.pop("status", None) - return response + return CreateOrRefreshAPIResponse( + CreateOrRefreshAPIResponseSession( + response["session"]["handle"], + response["session"]["userId"], + response["session"]["userDataInJWT"], + ), + TokenInfo( + response["accessToken"]["token"], + response["accessToken"]["expiry"], + response["accessToken"]["createdTime"], + ), + TokenInfo( + response["refreshToken"]["token"], + response["refreshToken"]["expiry"], + response["refreshToken"]["createdTime"], + ), + response["antiCsrfToken"] if "antiCsrfToken" in response else None, + ) if response["status"] == "UNAUTHORISED": log_debug_message( "refreshSession: Returning UNAUTHORISED because of core response" @@ -322,7 +427,7 @@

    Module supertokens_python.recipe.session.session_functio return response["sessionHandlesRevoked"] -async def update_session_data( +async def update_session_data_in_database( recipe_implementation: RecipeImplementation, session_handle: str, new_session_data: Dict[str, Any], @@ -378,7 +483,7 @@

    Module supertokens_python.recipe.session.session_functio

    Functions

    -async def create_new_session(recipe_implementation: RecipeImplementation, user_id: str, disable_anti_csrf: bool, access_token_payload: Union[None, Dict[str, Any]], session_data: Union[None, Dict[str, Any]]) ‑> Dict[str, Any] +async def create_new_session(recipe_implementation: RecipeImplementation, user_id: str, disable_anti_csrf: bool, access_token_payload: Union[None, Dict[str, Any]], session_data_in_database: Union[None, Dict[str, Any]]) ‑> CreateOrRefreshAPIResponse
    @@ -391,37 +496,48 @@

    Functions

    user_id: str, disable_anti_csrf: bool, access_token_payload: Union[None, Dict[str, Any]], - session_data: Union[None, Dict[str, Any]], -) -> Dict[str, Any]: - if session_data is None: - session_data = {} + session_data_in_database: Union[None, Dict[str, Any]], +) -> CreateOrRefreshAPIResponse: + if session_data_in_database is None: + session_data_in_database = {} if access_token_payload is None: access_token_payload = {} - handshake_info = await recipe_implementation.get_handshake_info() enable_anti_csrf = ( - disable_anti_csrf is False and handshake_info.anti_csrf == "VIA_TOKEN" + disable_anti_csrf is False + and recipe_implementation.config.anti_csrf == "VIA_TOKEN" ) response = await recipe_implementation.querier.send_post_request( NormalisedURLPath("/recipe/session"), { "userId": user_id, "userDataInJWT": access_token_payload, - "userDataInDatabase": session_data, + "userDataInDatabase": session_data_in_database, + "useDynamicSigningKey": recipe_implementation.config.use_dynamic_access_token_signing_key, "enableAntiCsrf": enable_anti_csrf, }, ) - recipe_implementation.update_jwt_signing_public_key_info( - response["jwtSigningPublicKeyList"], - response["jwtSigningPublicKey"], - response["jwtSigningPublicKeyExpiryTime"], - ) + response.pop("status", None) - response.pop("jwtSigningPublicKey", None) - response.pop("jwtSigningPublicKeyExpiryTime", None) - response.pop("jwtSigningPublicKeyList", None) - return response
    + return CreateOrRefreshAPIResponse( + CreateOrRefreshAPIResponseSession( + response["session"]["handle"], + response["session"]["userId"], + response["session"]["userDataInJWT"], + ), + TokenInfo( + response["accessToken"]["token"], + response["accessToken"]["expiry"], + response["accessToken"]["createdTime"], + ), + TokenInfo( + response["refreshToken"]["token"], + response["refreshToken"]["expiry"], + response["refreshToken"]["createdTime"], + ), + response["antiCsrfToken"] if "antiCsrfToken" in response else None, + )
    @@ -443,7 +559,7 @@

    Functions

    -async def get_session(recipe_implementation: RecipeImplementation, parsed_access_token: ParsedJWTInfo, anti_csrf_token: Union[str, None], do_anti_csrf_check: bool, contains_custom_header: bool) ‑> Dict[str, Any] +async def get_session(recipe_implementation: RecipeImplementation, parsed_access_token: ParsedJWTInfo, anti_csrf_token: Union[str, None], do_anti_csrf_check: bool, always_check_core: bool) ‑> GetSessionAPIResponse
    @@ -456,89 +572,119 @@

    Functions

    parsed_access_token: ParsedJWTInfo, anti_csrf_token: Union[str, None], do_anti_csrf_check: bool, - contains_custom_header: bool, -) -> Dict[str, Any]: - handshake_info = await recipe_implementation.get_handshake_info() - access_token_info = None - found_a_sign_key_that_is_older_than_the_access_token = False - - for key in handshake_info.get_jwt_signing_public_key_list(): - try: - access_token_info = get_info_from_access_token( - parsed_access_token, - key["publicKey"], - handshake_info.anti_csrf == "VIA_TOKEN" and do_anti_csrf_check, - ) + always_check_core: bool, +) -> GetSessionAPIResponse: + config = recipe_implementation.config + access_token_info: Optional[Dict[str, Any]] = None + + try: + access_token_info = get_info_from_access_token( + parsed_access_token, + recipe_implementation.JWK_clients, + config.anti_csrf == "VIA_TOKEN" and do_anti_csrf_check, + ) - found_a_sign_key_that_is_older_than_the_access_token = True + except Exception as e: + if not isinstance(e, TryRefreshTokenError): + raise e - except Exception as e: - if not isinstance(e, TryRefreshTokenError): - raise e + # if it comes here, it means token verification has failed. + # It may be due to: + # - signing key was updated and this token was signed with new key + # - access token is actually expired + # - access token was signed with the older signing key - payload = parsed_access_token.payload + # if access token is actually expired, we don't need to call core and + # just return TRY_REFRESH_TOKEN to the client - if not isinstance(payload["timeCreated"], int) or not isinstance( - payload["expiryTime"], int - ): - raise e + # if access token creation time is after this signing key was created + # we need to call core as there are chances that the token + # was signed with the updated signing key - if payload["expiryTime"] < time.time(): - raise e + # if access token creation time is before oldest signing key was created, + # so if foundASigningKeyThatIsOlderThanTheAccessToken is still false after + # the loop we just return TRY_REFRESH_TOKEN - if payload["timeCreated"] >= key["createdAt"]: - found_a_sign_key_that_is_older_than_the_access_token = True - break + payload = parsed_access_token.payload - if not found_a_sign_key_that_is_older_than_the_access_token: - log_debug_message( - "getSession: Returning TRY_REFRESH_TOKEN because signing key in handshake info is not up to date." - ) - raise_try_refresh_token_exception( - "access token has expired. Please call the refresh API" + time_created = payload.get("timeCreated") + expiry_time = payload.get("expiryTime") + + if not isinstance(time_created, int) or not isinstance(expiry_time, int): + raise e + + if parsed_access_token.version < 3: + if expiry_time < time.time(): + raise e + + # We check if the token was created since the last time we refreshed the keys from the core + # Since we do not know the exact timing of the last refresh, we check against the max age + if time_created <= time.time() - JWKCacheMaxAgeInMs: + raise e + else: + # Since v3 (and above) tokens contain a kid we can trust the cache-refresh mechanism of the pyjwt library + # This means we do not need to call the core since the signature wouldn't pass verification anyway. + raise e + + if parsed_access_token.version >= 3: + token_use_dynamic_key = ( + parsed_access_token.kid.startswith("d-") + if parsed_access_token.kid is not None + else False ) - if handshake_info.anti_csrf == "VIA_TOKEN" and do_anti_csrf_check: - if access_token_info is not None: - if ( - anti_csrf_token is None - or anti_csrf_token != access_token_info["antiCsrfToken"] - ): - if anti_csrf_token is None: - log_debug_message( - "getSession: Returning TRY_REFRESH_TOKEN because antiCsrfToken is missing from request" - ) - raise_try_refresh_token_exception( - "Provided antiCsrfToken is undefined. If you do not want anti-csrf check for this API, please set doAntiCsrfCheck to false for this API" - ) - else: - log_debug_message( - "getSession: Returning TRY_REFRESH_TOKEN because the passed antiCsrfToken is not the same as in the access token" - ) - raise_try_refresh_token_exception("anti-csrf check failed") - - elif handshake_info.anti_csrf == "VIA_CUSTOM_HEADER" and do_anti_csrf_check: - if not contains_custom_header: + if token_use_dynamic_key != config.use_dynamic_access_token_signing_key: log_debug_message( - "getSession: Returning TRY_REFRESH_TOKEN because custom header (rid) was not passed" + "getSession: Returning TRY_REFRESH_TOKEN because the access token doesn't match the useDynamicAccessTokenSigningKey in the config" ) + raise_try_refresh_token_exception( - "anti-csrf check failed. Please pass 'rid: \"anti-csrf\"' header in the request, or set doAntiCsrfCheck to false " - "for this API" + "The access token doesn't match the useDynamicAccessTokenSigningKey setting" + ) + + # If we get here we either have a V2 token that doesn't pass verification or a valid V3> token + # anti-csrf check if accesstokenInfo is not undefined which means token verification was successful + + if do_anti_csrf_check: + if config.anti_csrf == "VIA_TOKEN": + if access_token_info is not None: + if ( + anti_csrf_token is None + or anti_csrf_token != access_token_info["antiCsrfToken"] + ): + if anti_csrf_token is None: + log_debug_message( + "getSession: Returning TRY_REFRESH_TOKEN because antiCsrfToken is missing from request" + ) + raise_try_refresh_token_exception( + "Provided antiCsrfToken is undefined. If you do not want anti-csrf check for this API, please set doAntiCsrfCheck to false for this API" + ) + else: + log_debug_message( + "getSession: Returning TRY_REFRESH_TOKEN because the passed antiCsrfToken is not the same as in the access token" + ) + raise_try_refresh_token_exception("anti-csrf check failed") + + elif config.anti_csrf == "VIA_CUSTOM_HEADER": + # The function should never be called by this (we check this outside the function as well) + # There we can add a bit more information to the error, so that's the primary check, this is just making sure. + raise Exception( + "Please either use VIA_TOKEN, NONE or call with doAntiCsrfCheck false" ) if ( access_token_info is not None - and not handshake_info.access_token_blacklisting_enabled + and not always_check_core and access_token_info["parentRefreshTokenHash1"] is None ): - return { - "session": { - "handle": access_token_info["sessionHandle"], - "userId": access_token_info["userId"], - "userDataInJWT": access_token_info["userData"], - } - } + return GetSessionAPIResponse( + GetSessionAPIResponseSession( + access_token_info["sessionHandle"], + access_token_info["userId"], + access_token_info["userData"], + access_token_info["expiryTime"], + ) + ) ProcessState.get_instance().add_state( AllowedProcessStates.CALLING_SERVICE_IN_VERIFY @@ -547,7 +693,8 @@

    Functions

    data = { "accessToken": parsed_access_token.raw_token_string, "doAntiCsrfCheck": do_anti_csrf_check, - "enableAntiCsrf": handshake_info.anti_csrf == "VIA_TOKEN", + "enableAntiCsrf": config.anti_csrf == "VIA_TOKEN", + "checkDatabase": always_check_core, } if anti_csrf_token is not None: data["antiCsrfToken"] = anti_csrf_token @@ -556,31 +703,23 @@

    Functions

    NormalisedURLPath("/recipe/session/verify"), data ) if response["status"] == "OK": - recipe_implementation.update_jwt_signing_public_key_info( - response["jwtSigningPublicKeyList"], - response["jwtSigningPublicKey"], - response["jwtSigningPublicKeyExpiryTime"], - ) response.pop("status", None) - response.pop("jwtSigningPublicKey", None) - response.pop("jwtSigningPublicKeyExpiryTime", None) - response.pop("jwtSigningPublicKeyList", None) - return response + return GetSessionAPIResponse( + GetSessionAPIResponseSession( + response["session"]["handle"], + response["session"]["userId"], + response["session"]["userDataInJWT"], + response["accessToken"]["expiry"], + ), + GetSessionAPIResponseAccessToken( + response["accessToken"]["token"], + response["accessToken"]["expiry"], + response["accessToken"]["createdTime"], + ), + ) if response["status"] == "UNAUTHORISED": log_debug_message("getSession: Returning UNAUTHORISED because of core response") raise_unauthorised_exception(response["message"]) - if ( - response["jwtSigningPublicKeyList"] is not None - or response["jwtSigningPublicKey"] is not None - or response["jwtSigningPublicKeyExpiryTime"] is not None - ): - recipe_implementation.update_jwt_signing_public_key_info( - response["jwtSigningPublicKeyList"], - response["jwtSigningPublicKey"], - response["jwtSigningPublicKeyExpiryTime"], - ) - else: - await recipe_implementation.get_handshake_info(True) log_debug_message( "getSession: Returning TRY_REFRESH_TOKEN because of core response" @@ -616,7 +755,7 @@

    Functions

    -async def refresh_session(recipe_implementation: RecipeImplementation, refresh_token: str, anti_csrf_token: Union[str, None], contains_custom_header: bool, transfer_method: TokenTransferMethod) ‑> Dict[str, Any] +async def refresh_session(recipe_implementation: RecipeImplementation, refresh_token: str, anti_csrf_token: Union[str, None], disable_anti_csrf: bool) ‑> CreateOrRefreshAPIResponse
    @@ -628,34 +767,52 @@

    Functions

    recipe_implementation: RecipeImplementation, refresh_token: str, anti_csrf_token: Union[str, None], - contains_custom_header: bool, - transfer_method: TokenTransferMethod, -) -> Dict[str, Any]: - handshake_info = await recipe_implementation.get_handshake_info() + disable_anti_csrf: bool, +) -> CreateOrRefreshAPIResponse: data = { "refreshToken": refresh_token, - "enableAntiCsrf": transfer_method == "cookie" - and handshake_info.anti_csrf == "VIA_TOKEN", + "enableAntiCsrf": ( + not disable_anti_csrf + and recipe_implementation.config.anti_csrf == "VIA_TOKEN" + ), } + if anti_csrf_token is not None: data["antiCsrfToken"] = anti_csrf_token - if handshake_info.anti_csrf == "VIA_CUSTOM_HEADER" and transfer_method == "cookie": - if not contains_custom_header: - log_debug_message( - "refreshSession: Returning UNAUTHORISED because custom header (rid) was not passed" - ) - raise_unauthorised_exception( - "anti-csrf check failed. Please pass 'rid: \"session\"' header " - "in the request.", - False, - ) + if ( + recipe_implementation.config.anti_csrf == "VIA_CUSTOM_HEADER" + and not disable_anti_csrf + ): + # The function should never be called by this (we check this outside the function as well) + # There we can add a bit more information to the error, so that's the primary check, this is just making sure. + raise Exception( + "Please either use VIA_TOKEN, NONE or call with doAntiCsrfCheck false" + ) + response = await recipe_implementation.querier.send_post_request( NormalisedURLPath("/recipe/session/refresh"), data ) if response["status"] == "OK": response.pop("status", None) - return response + return CreateOrRefreshAPIResponse( + CreateOrRefreshAPIResponseSession( + response["session"]["handle"], + response["session"]["userId"], + response["session"]["userDataInJWT"], + ), + TokenInfo( + response["accessToken"]["token"], + response["accessToken"]["expiry"], + response["accessToken"]["createdTime"], + ), + TokenInfo( + response["refreshToken"]["token"], + response["refreshToken"]["expiry"], + response["refreshToken"]["createdTime"], + ), + response["antiCsrfToken"] if "antiCsrfToken" in response else None, + ) if response["status"] == "UNAUTHORISED": log_debug_message( "refreshSession: Returning UNAUTHORISED because of core response" @@ -748,8 +905,8 @@

    Functions

    return True
    -
    -async def update_session_data(recipe_implementation: RecipeImplementation, session_handle: str, new_session_data: Dict[str, Any]) ‑> bool +
    +async def update_session_data_in_database(recipe_implementation: RecipeImplementation, session_handle: str, new_session_data: Dict[str, Any]) ‑> bool
    @@ -757,7 +914,7 @@

    Functions

    Expand source code -
    async def update_session_data(
    +
    async def update_session_data_in_database(
         recipe_implementation: RecipeImplementation,
         session_handle: str,
         new_session_data: Dict[str, Any],
    @@ -775,6 +932,111 @@ 

    Functions

    +

    Classes

    +
    +
    +class CreateOrRefreshAPIResponse +(session: CreateOrRefreshAPIResponseSession, accessToken: TokenInfo, refreshToken: TokenInfo, antiCsrfToken: Optional[str]) +
    +
    +
    +
    + +Expand source code + +
    class CreateOrRefreshAPIResponse:
    +    def __init__(
    +        self,
    +        session: CreateOrRefreshAPIResponseSession,
    +        accessToken: TokenInfo,
    +        refreshToken: TokenInfo,
    +        antiCsrfToken: Optional[str],
    +    ):
    +        self.session = session
    +        self.accessToken = accessToken
    +        self.refreshToken = refreshToken
    +        self.antiCsrfToken = antiCsrfToken
    +
    +
    +
    +class CreateOrRefreshAPIResponseSession +(handle: str, userId: str, userDataInJWT: Any) +
    +
    +
    +
    + +Expand source code + +
    class CreateOrRefreshAPIResponseSession:
    +    def __init__(self, handle: str, userId: str, userDataInJWT: Any):
    +        self.handle = handle
    +        self.userId = userId
    +        self.userDataInJWT = userDataInJWT
    +
    +
    +
    +class GetSessionAPIResponse +(session: GetSessionAPIResponseSession, accessToken: Optional[GetSessionAPIResponseAccessToken] = None) +
    +
    +
    +
    + +Expand source code + +
    class GetSessionAPIResponse:
    +    def __init__(
    +        self,
    +        session: GetSessionAPIResponseSession,
    +        accessToken: Optional[GetSessionAPIResponseAccessToken] = None,
    +    ) -> None:
    +        self.session = session
    +        self.accessToken = accessToken
    +
    +
    +
    +class GetSessionAPIResponseAccessToken +(token: str, expiry: int, createdTime: int) +
    +
    +
    +
    + +Expand source code + +
    class GetSessionAPIResponseAccessToken:
    +    def __init__(self, token: str, expiry: int, createdTime: int) -> None:
    +        self.token = token
    +        self.expiry = expiry
    +        self.createdTime = createdTime
    +
    +
    +
    +class GetSessionAPIResponseSession +(handle: str, userId: str, userDataInJWT: Dict[str, Any], expiryTime: int) +
    +
    +
    +
    + +Expand source code + +
    class GetSessionAPIResponseSession:
    +    def __init__(
    +        self,
    +        handle: str,
    +        userId: str,
    +        userDataInJWT: Dict[str, Any],
    +        expiryTime: int,
    +    ) -> None:
    +        self.handle = handle
    +        self.userId = userId
    +        self.userDataInJWT = userDataInJWT
    +        self.expiryTime = expiryTime
    +
    +
    +

    Args

    with urllib.request.urlopen(self.uri, timeout=self.timeout_sec) as response: self.jwk_set = PyJWKSet.from_dict(json.load(response)) # type: ignore self.last_fetch_time = get_timestamp_ms() - except URLError as e: - raise JWKSRequestError(f'Failed to fetch data from the url, err: "{e}"') + except URLError: + raise JWKSRequestError("Failed to fetch jwk set from the configured uri") def is_cooling_down(self) -> bool: return (self.last_fetch_time > 0) and ( @@ -235,15 +237,17 @@

    Args

    try: return self.jwk_set[kid] # type: ignore - except IndexError: + except KeyError: if not self.is_cooling_down(): # One more attempt to fetch the latest keys # and then try to find the key again. self.reload() try: return self.jwk_set[kid] # type: ignore - except IndexError: + except KeyError: pass + except Exception: + raise JWKSKeyNotFoundError("No key found for the given kid") raise JWKSKeyNotFoundError("No key found for the given kid")
    @@ -290,15 +294,17 @@

    Methods

    try: return self.jwk_set[kid] # type: ignore - except IndexError: + except KeyError: if not self.is_cooling_down(): # One more attempt to fetch the latest keys # and then try to find the key again. self.reload() try: return self.jwk_set[kid] # type: ignore - except IndexError: + except KeyError: pass + except Exception: + raise JWKSKeyNotFoundError("No key found for the given kid") raise JWKSKeyNotFoundError("No key found for the given kid")
    @@ -347,8 +353,8 @@

    Methods

    with urllib.request.urlopen(self.uri, timeout=self.timeout_sec) as response: self.jwk_set = PyJWKSet.from_dict(json.load(response)) # type: ignore self.last_fetch_time = get_timestamp_ms() - except URLError as e: - raise JWKSRequestError(f'Failed to fetch data from the url, err: "{e}"')
    + except URLError: + raise JWKSRequestError("Failed to fetch jwk set from the configured uri")
    diff --git a/html/supertokens_python/recipe/session/recipe.html b/html/supertokens_python/recipe/session/recipe.html index 68b07b4a4..8e72abedd 100644 --- a/html/supertokens_python/recipe/session/recipe.html +++ b/html/supertokens_python/recipe/session/recipe.html @@ -124,13 +124,6 @@

    Module supertokens_python.recipe.session.recipeModule supertokens_python.recipe.session.recipeModule supertokens_python.recipe.session.recipeClasses

    expose_access_token_to_frontend_in_cookie_based_auth: Union[bool, None] = None, ): super().__init__(recipe_id, app_info) - self.openid_recipe = OpenIdRecipe( - recipe_id, - app_info, - None, - None, - override.openid_feature if override is not None else None, - ) self.config = validate_and_normalise_user_input( app_info, cookie_domain, @@ -464,6 +460,13 @@

    Classes

    use_dynamic_access_token_signing_key, expose_access_token_to_frontend_in_cookie_based_auth, ) + self.openid_recipe = OpenIdRecipe( + recipe_id, + app_info, + None, + None, + override.openid_feature if override is not None else None, + ) log_debug_message("session init: anti_csrf: %s", self.config.anti_csrf) if self.config.cookie_domain is not None: log_debug_message( @@ -568,7 +571,10 @@

    Classes

    async def handle_error( self, request: BaseRequest, err: SuperTokensError, response: BaseResponse ) -> BaseResponse: - if isinstance(err, SuperTokensSessionError): + if ( + isinstance(err, SuperTokensSessionError) + and err.response_mutators is not None + ): for mutator in err.response_mutators: mutator(response) @@ -984,7 +990,10 @@

    Methods

    async def handle_error(
         self, request: BaseRequest, err: SuperTokensError, response: BaseResponse
     ) -> BaseResponse:
    -    if isinstance(err, SuperTokensSessionError):
    +    if (
    +        isinstance(err, SuperTokensSessionError)
    +        and err.response_mutators is not None
    +    ):
             for mutator in err.response_mutators:
                 mutator(response)
     
    diff --git a/html/supertokens_python/recipe/session/session_class.html b/html/supertokens_python/recipe/session/session_class.html
    index 8776b1f01..a7a651bfc 100644
    --- a/html/supertokens_python/recipe/session/session_class.html
    +++ b/html/supertokens_python/recipe/session/session_class.html
    @@ -98,7 +98,9 @@ 

    Module supertokens_python.recipe.session.session_classClasses

    anti_csrf_response_mutator(self.anti_csrf_token) ) - request.set_session(self) + request.set_session( + self + ) # Although this is called in recipe/session/framework/**/__init__.py. It's required in case of python because functions like create_new_session(req, "user-id") can be called in the framework view handler as well async def revoke_session(self, user_context: Union[Any, None] = None) -> None: if user_context is None: @@ -701,7 +705,9 @@

    Methods

    anti_csrf_response_mutator(self.anti_csrf_token) ) - request.set_session(self)
    + request.set_session( + self + ) # Although this is called in recipe/session/framework/**/__init__.py. It's required in case of python because functions like create_new_session(req, "user-id") can be called in the framework view handler as well
    diff --git a/html/supertokens_python/recipe/session/session_functions.html b/html/supertokens_python/recipe/session/session_functions.html index bc7db80c0..d4da714f6 100644 --- a/html/supertokens_python/recipe/session/session_functions.html +++ b/html/supertokens_python/recipe/session/session_functions.html @@ -222,7 +222,7 @@

    Module supertokens_python.recipe.session.session_functio if time_created <= time.time() - JWKCacheMaxAgeInMs: raise e else: - # Since v3 (and above) tokens contain a kid we can trust the cache-refresh mechanism of the pyjwt library + # Since v3 (and above) tokens contain a kid we can trust the cache refresh mechanism built on top of the pyjwt lib # This means we do not need to call the core since the signature wouldn't pass verification anyway. raise e @@ -309,13 +309,26 @@

    Module supertokens_python.recipe.session.session_functio response["session"]["handle"], response["session"]["userId"], response["session"]["userDataInJWT"], - response["accessToken"]["expiry"], + ( + response.get("accessToken", {}).get( + "expiry" + ) # if we got a new accesstoken we take the expiry time from there + or ( + access_token_info is not None + and access_token_info.get("expiryTime") + ) # if we didn't get a new access token but could validate the token take that info (alwaysCheckCore === true, or parentRefreshTokenHash1 !== null) + or parsed_access_token.payload[ + "expiryTime" + ] # if the token didn't pass validation, but we got here, it means it was a v2 token that we didn't have the key cached for. + ), # This will throw error if others are none and 'expiryTime' key doesn't exist in the payload ), 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") @@ -622,7 +635,7 @@

    Functions

    if time_created <= time.time() - JWKCacheMaxAgeInMs: raise e else: - # Since v3 (and above) tokens contain a kid we can trust the cache-refresh mechanism of the pyjwt library + # Since v3 (and above) tokens contain a kid we can trust the cache refresh mechanism built on top of the pyjwt lib # This means we do not need to call the core since the signature wouldn't pass verification anyway. raise e @@ -709,13 +722,26 @@

    Functions

    response["session"]["handle"], response["session"]["userId"], response["session"]["userDataInJWT"], - response["accessToken"]["expiry"], + ( + response.get("accessToken", {}).get( + "expiry" + ) # if we got a new accesstoken we take the expiry time from there + or ( + access_token_info is not None + and access_token_info.get("expiryTime") + ) # if we didn't get a new access token but could validate the token take that info (alwaysCheckCore === true, or parentRefreshTokenHash1 !== null) + or parsed_access_token.payload[ + "expiryTime" + ] # if the token didn't pass validation, but we got here, it means it was a v2 token that we didn't have the key cached for. + ), # This will throw error if others are none and 'expiryTime' key doesn't exist in the payload ), 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") diff --git a/html/supertokens_python/recipe/session/session_request_functions.html b/html/supertokens_python/recipe/session/session_request_functions.html index ace38da5a..ca64a74f0 100644 --- a/html/supertokens_python/recipe/session/session_request_functions.html +++ b/html/supertokens_python/recipe/session/session_request_functions.html @@ -257,7 +257,13 @@

    Module supertokens_python.recipe.session.session_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() - final_access_token_payload = access_token_payload + app_info = recipe_instance.app_info + issuer = ( + app_info.api_domain.get_as_string_dangerous() + + app_info.api_base_path.get_as_string_dangerous() + ) + + final_access_token_payload = {**access_token_payload, "iss": issuer} for claim in claims_added_by_other_recipes: update = await claim.build(user_id, user_context) @@ -292,7 +298,7 @@

    Module supertokens_python.recipe.session.session_request # We can allow insecure cookie when both website & API domain are localhost or an IP # When either of them is a different domain, API domain needs to have https and a secure cookie to work raise Exception( - "Since your API and website domain are different, for sessions to work, please use https on your apiDomain and dont set cookieSecure to false." + "Since your API and website domain are different, for sessions to work, please use https on your apiDomain and don't set cookieSecure to false." ) disable_anti_csrf = output_transfer_method == "header" @@ -519,7 +525,13 @@

    Functions

    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() - final_access_token_payload = access_token_payload + app_info = recipe_instance.app_info + issuer = ( + app_info.api_domain.get_as_string_dangerous() + + app_info.api_base_path.get_as_string_dangerous() + ) + + final_access_token_payload = {**access_token_payload, "iss": issuer} for claim in claims_added_by_other_recipes: update = await claim.build(user_id, user_context) @@ -554,7 +566,7 @@

    Functions

    # We can allow insecure cookie when both website & API domain are localhost or an IP # When either of them is a different domain, API domain needs to have https and a secure cookie to work raise Exception( - "Since your API and website domain are different, for sessions to work, please use https on your apiDomain and dont set cookieSecure to false." + "Since your API and website domain are different, for sessions to work, please use https on your apiDomain and don't set cookieSecure to false." ) disable_anti_csrf = output_transfer_method == "header" diff --git a/html/supertokens_python/utils.html b/html/supertokens_python/utils.html index 42943a9e1..f84a7776c 100644 --- a/html/supertokens_python/utils.html +++ b/html/supertokens_python/utils.html @@ -127,9 +127,8 @@

    Module supertokens_python.utils

    return max_v -def is_version_gte(version: str, minimum_minor_version: str) -> bool: - assert len(minimum_minor_version.split(".")) == 2 - return _get_max_version(version, minimum_minor_version) == version +def is_version_gte(version: str, minimum_version: str) -> bool: + return _get_max_version(version, minimum_version) == version def _get_max_version(v1: str, v2: str) -> str: @@ -636,7 +635,7 @@

    Functions

    -def is_version_gte(version: str, minimum_minor_version: str) ‑> bool +def is_version_gte(version: str, minimum_version: str) ‑> bool
    @@ -644,9 +643,8 @@

    Functions

    Expand source code -
    def is_version_gte(version: str, minimum_minor_version: str) -> bool:
    -    assert len(minimum_minor_version.split(".")) == 2
    -    return _get_max_version(version, minimum_minor_version) == version
    +
    def is_version_gte(version: str, minimum_version: str) -> bool:
    +    return _get_max_version(version, minimum_version) == version
    From eee9d1fc51071a2070684e2356f437cabc9091dd Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Tue, 9 May 2023 15:48:17 +0530 Subject: [PATCH 131/192] adding dev-v0.13.0 tag to this commit to ensure building From a4d101c3ac5f26ba859114ad2eee9819220c0ccb Mon Sep 17 00:00:00 2001 From: KShivendu Date: Tue, 9 May 2023 18:37:53 +0530 Subject: [PATCH 132/192] fix: Fix failing django2x frontend integration tests --- tests/frontendIntegration/django2x/polls/views.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/frontendIntegration/django2x/polls/views.py b/tests/frontendIntegration/django2x/polls/views.py index c727d0aec..f044ce808 100644 --- a/tests/frontendIntegration/django2x/polls/views.py +++ b/tests/frontendIntegration/django2x/polls/views.py @@ -545,7 +545,13 @@ def set_enable_jwt(request: HttpRequest): def feature_flags(request: HttpRequest): global last_set_enable_jwt - return JsonResponse({"sessionJwt": last_set_enable_jwt}) + return JsonResponse( + { + "sessionJwt": last_set_enable_jwt, + "sessionClaims": is_version_gte(VERSION, "0.11.0"), + "v3AccessToken": is_version_gte(VERSION, "0.13.0"), + } + ) def reinitialize(request: HttpRequest): From 5a58f792164006e48009774fb72875b7a56f0b5c Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Tue, 9 May 2023 18:41:47 +0530 Subject: [PATCH 133/192] adding dev-v0.13.0 tag to this commit to ensure building From 57dfeeb99c6e82d105c891a988e49a8e95e4418c Mon Sep 17 00:00:00 2001 From: KShivendu Date: Fri, 12 May 2023 20:06:57 +0530 Subject: [PATCH 134/192] feat: make loading access token from the request overrideable --- supertokens_python/constants.py | 2 +- .../recipe/session/interfaces.py | 4 +- .../recipe/session/recipe_implementation.py | 21 +++++++++- .../session/session_request_functions.py | 42 +++++++++---------- 4 files changed, 41 insertions(+), 28 deletions(-) diff --git a/supertokens_python/constants.py b/supertokens_python/constants.py index 7d4b9dec1..b74f9ed4c 100644 --- a/supertokens_python/constants.py +++ b/supertokens_python/constants.py @@ -14,7 +14,7 @@ from __future__ import annotations SUPPORTED_CDI_VERSIONS = ["2.21"] -VERSION = "0.13.0" +VERSION = "0.13.1" TELEMETRY = "/telemetry" USER_COUNT = "/users/count" USER_DELETE = "/user/remove" diff --git a/supertokens_python/recipe/session/interfaces.py b/supertokens_python/recipe/session/interfaces.py index 598ce9033..49345864a 100644 --- a/supertokens_python/recipe/session/interfaces.py +++ b/supertokens_python/recipe/session/interfaces.py @@ -156,8 +156,8 @@ def get_global_claim_validators( @abstractmethod async def get_session( self, - access_token: str, - anti_csrf_token: Optional[str], + access_token: Optional[str] = None, + anti_csrf_token: Optional[str] = None, anti_csrf_check: Optional[bool] = None, session_required: Optional[bool] = None, check_database: Optional[bool] = None, diff --git a/supertokens_python/recipe/session/recipe_implementation.py b/supertokens_python/recipe/session/recipe_implementation.py index 8e708ff03..15203658b 100644 --- a/supertokens_python/recipe/session/recipe_implementation.py +++ b/supertokens_python/recipe/session/recipe_implementation.py @@ -171,8 +171,8 @@ async def validate_claims_in_jwt_payload( async def get_session( self, - access_token: str, - anti_csrf_token: Optional[str], + access_token: Optional[str] = None, + anti_csrf_token: Optional[str] = None, anti_csrf_check: Optional[bool] = None, session_required: Optional[bool] = None, check_database: Optional[bool] = None, @@ -194,6 +194,23 @@ async def get_session( log_debug_message("getSession: Started") + if access_token is None: + if session_required is False: + log_debug_message( + "getSession: returning None because access_token is undefined and session_required is False" + ) + # there is no session that exists here, and the user wants session verification to be optional. So we return None + return None + + log_debug_message( + "getSession: UNAUTHORISED because accessToken in request is undefined" + ) + # we do not clear the session here because of a race condition mentioned in https://github.com/supertokens/supertokens-node/issues/17 + raise UnauthorisedError( + "Session does not exist. Are you sending the session tokens in the request with the appropriate token transfer method?", + clear_tokens=False, + ) + access_token_obj: Optional[ParsedJWTInfo] = None try: access_token_obj = parse_jwt_without_signature_verification(access_token) diff --git a/supertokens_python/recipe/session/session_request_functions.py b/supertokens_python/recipe/session/session_request_functions.py index 2c31e8ace..3fe4897e1 100644 --- a/supertokens_python/recipe/session/session_request_functions.py +++ b/supertokens_python/recipe/session/session_request_functions.py @@ -127,8 +127,8 @@ async def get_session_from_request( allowed_transfer_method = config.get_token_transfer_method( request, False, user_context ) - request_transfer_method: TokenTransferMethod - request_access_token: Union[ParsedJWTInfo, None] + request_transfer_method: Optional[TokenTransferMethod] = None + request_access_token: Union[ParsedJWTInfo, None] = None if (allowed_transfer_method in ("any", "header")) and access_tokens.get( "header" @@ -142,25 +142,6 @@ async def get_session_from_request( log_debug_message("getSession: using cookie transfer method") request_transfer_method = "cookie" request_access_token = access_tokens["cookie"] - else: - if session_optional: - log_debug_message( - "getSession: returning None because accessToken is undefined and sessionRequired is false" - ) - # there is no session that exists here, and the user wants session verification - # to be optional. So we return None - return None - - log_debug_message( - "getSession: UNAUTHORISED because access_token in request is None" - ) - # we do not clear the session here because of a race condition mentioned in: - # https://github.com/supertokens/supertokens-node/issues/17 - raise_unauthorised_exception( - "Session does not exist. Are you sending the session tokens in the " - "request with the appropriate token transfer method?", - clear_tokens=False, - ) anti_csrf_token = get_anti_csrf_header(request) do_anti_csrf_check = anti_csrf_check @@ -186,7 +167,9 @@ 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, + 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, check_database=check_database, @@ -200,9 +183,22 @@ async def get_session_from_request( ) await session.assert_claims(claim_validators, user_context) + # request_transfer_method can only be None here if the user overriddes get_session + # to load the session by a custom method in that (very niche) case they also need to + # override how the session is attached to the response. + # In that scenario the transferMethod passed to attachToRequestResponse likely doesn't + # matter, still, we follow the general fallback logic + + if request_transfer_method is not None: + final_transfer_method = request_transfer_method + elif allowed_transfer_method != "any": + final_transfer_method = allowed_transfer_method + else: + final_transfer_method = "header" + await session.attach_to_request_response( request, - request_transfer_method, + final_transfer_method, ) return session From f8958ee219e37771314eeb0e9833c34338fad7fe Mon Sep 17 00:00:00 2001 From: KShivendu Date: Mon, 15 May 2023 15:15:56 +0530 Subject: [PATCH 135/192] fix: Keep it mandatory to pass accesss_token in get_session --- CHANGELOG.md | 6 ++++++ setup.py | 2 +- supertokens_python/recipe/session/interfaces.py | 2 +- supertokens_python/recipe/session/recipe_implementation.py | 2 +- 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ed85a0a1..2f2e6f6d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [unreleased] + +### Changes +- Made the access token string optional in the overrideable `get_session` function +- Moved checking if the access token is defined into the overrideable `get_session` function + ## [0.13.0] - 2023-05-04 ### Breaking changes diff --git a/setup.py b/setup.py index 78df67b11..a706d1335 100644 --- a/setup.py +++ b/setup.py @@ -70,7 +70,7 @@ setup( name="supertokens_python", - version="0.13.0", + version="0.13.1", author="SuperTokens", license="Apache 2.0", author_email="team@supertokens.com", diff --git a/supertokens_python/recipe/session/interfaces.py b/supertokens_python/recipe/session/interfaces.py index 49345864a..d9e15696d 100644 --- a/supertokens_python/recipe/session/interfaces.py +++ b/supertokens_python/recipe/session/interfaces.py @@ -156,7 +156,7 @@ def get_global_claim_validators( @abstractmethod async def get_session( self, - access_token: Optional[str] = None, + access_token: Optional[str], anti_csrf_token: Optional[str] = None, anti_csrf_check: Optional[bool] = None, session_required: Optional[bool] = None, diff --git a/supertokens_python/recipe/session/recipe_implementation.py b/supertokens_python/recipe/session/recipe_implementation.py index 15203658b..5d265d406 100644 --- a/supertokens_python/recipe/session/recipe_implementation.py +++ b/supertokens_python/recipe/session/recipe_implementation.py @@ -171,7 +171,7 @@ async def validate_claims_in_jwt_payload( async def get_session( self, - access_token: Optional[str] = None, + access_token: Optional[str], anti_csrf_token: Optional[str] = None, anti_csrf_check: Optional[bool] = None, session_required: Optional[bool] = None, From 0fab4af9f24d351a302f3480ad42c31d333d6715 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Mon, 15 May 2023 19:35:29 +0530 Subject: [PATCH 136/192] updates to changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f2e6f6d2..40fd9515f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [unreleased] +## [0.13.1] - 2023-05-15 ### Changes - Made the access token string optional in the overrideable `get_session` function - Moved checking if the access token is defined into the overrideable `get_session` function From 45dd9124a8cf6bd5be2d783c621f03ba3374759f Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Mon, 15 May 2023 19:36:37 +0530 Subject: [PATCH 137/192] adding dev-v0.13.1 tag to this commit to ensure building --- html/supertokens_python/constants.html | 2 +- .../recipe/session/interfaces.html | 14 ++-- .../recipe/session/recipe_implementation.html | 65 ++++++++++++-- .../session/session_request_functions.html | 84 +++++++++---------- 4 files changed, 104 insertions(+), 61 deletions(-) diff --git a/html/supertokens_python/constants.html b/html/supertokens_python/constants.html index 793f97e67..1995c9c95 100644 --- a/html/supertokens_python/constants.html +++ b/html/supertokens_python/constants.html @@ -42,7 +42,7 @@

    Module supertokens_python.constants

    from __future__ import annotations SUPPORTED_CDI_VERSIONS = ["2.21"] -VERSION = "0.13.0" +VERSION = "0.13.1" TELEMETRY = "/telemetry" USER_COUNT = "/users/count" USER_DELETE = "/user/remove" diff --git a/html/supertokens_python/recipe/session/interfaces.html b/html/supertokens_python/recipe/session/interfaces.html index 1a3f77e3f..c8de17d2a 100644 --- a/html/supertokens_python/recipe/session/interfaces.html +++ b/html/supertokens_python/recipe/session/interfaces.html @@ -184,8 +184,8 @@

    Module supertokens_python.recipe.session.interfacesClass variables

    @abstractmethod async def get_session( self, - access_token: str, - anti_csrf_token: Optional[str], + access_token: Optional[str], + anti_csrf_token: Optional[str] = None, anti_csrf_check: Optional[bool] = None, session_required: Optional[bool] = None, check_database: Optional[bool] = None, @@ -1284,7 +1284,7 @@

    Methods

    -async def get_session(self, access_token: str, anti_csrf_token: Optional[str], anti_csrf_check: Optional[bool] = None, session_required: Optional[bool] = None, check_database: Optional[bool] = None, override_global_claim_validators: Optional[Callable[[List[SessionClaimValidator], SessionContainer, Dict[str, Any]], MaybeAwaitable[List[SessionClaimValidator]]]] = None, user_context: Optional[Dict[str, Any]] = None) ‑> Optional[SessionContainer] +async def get_session(self, access_token: Optional[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[[List[SessionClaimValidator], SessionContainer, Dict[str, Any]], MaybeAwaitable[List[SessionClaimValidator]]]] = None, user_context: Optional[Dict[str, Any]] = None) ‑> Optional[SessionContainer]
    @@ -1295,8 +1295,8 @@

    Methods

    @abstractmethod
     async def get_session(
         self,
    -    access_token: str,
    -    anti_csrf_token: Optional[str],
    +    access_token: Optional[str],
    +    anti_csrf_token: Optional[str] = None,
         anti_csrf_check: Optional[bool] = None,
         session_required: Optional[bool] = None,
         check_database: Optional[bool] = None,
    diff --git a/html/supertokens_python/recipe/session/recipe_implementation.html b/html/supertokens_python/recipe/session/recipe_implementation.html
    index 434ef3077..0f3125a6f 100644
    --- a/html/supertokens_python/recipe/session/recipe_implementation.html
    +++ b/html/supertokens_python/recipe/session/recipe_implementation.html
    @@ -199,8 +199,8 @@ 

    Module supertokens_python.recipe.session.recipe_implemen async def get_session( self, - access_token: str, - anti_csrf_token: Optional[str], + access_token: Optional[str], + anti_csrf_token: Optional[str] = None, anti_csrf_check: Optional[bool] = None, session_required: Optional[bool] = None, check_database: Optional[bool] = None, @@ -222,6 +222,23 @@

    Module supertokens_python.recipe.session.recipe_implemen log_debug_message("getSession: Started") + if access_token is None: + if session_required is False: + log_debug_message( + "getSession: returning None because access_token is undefined and session_required is False" + ) + # there is no session that exists here, and the user wants session verification to be optional. So we return None + return None + + log_debug_message( + "getSession: UNAUTHORISED because accessToken in request is undefined" + ) + # we do not clear the session here because of a race condition mentioned in https://github.com/supertokens/supertokens-node/issues/17 + raise UnauthorisedError( + "Session does not exist. Are you sending the session tokens in the request with the appropriate token transfer method?", + clear_tokens=False, + ) + access_token_obj: Optional[ParsedJWTInfo] = None try: access_token_obj = parse_jwt_without_signature_verification(access_token) @@ -621,8 +638,8 @@

    Classes

    async def get_session( self, - access_token: str, - anti_csrf_token: Optional[str], + access_token: Optional[str], + anti_csrf_token: Optional[str] = None, anti_csrf_check: Optional[bool] = None, session_required: Optional[bool] = None, check_database: Optional[bool] = None, @@ -644,6 +661,23 @@

    Classes

    log_debug_message("getSession: Started") + if access_token is None: + if session_required is False: + log_debug_message( + "getSession: returning None because access_token is undefined and session_required is False" + ) + # there is no session that exists here, and the user wants session verification to be optional. So we return None + return None + + log_debug_message( + "getSession: UNAUTHORISED because accessToken in request is undefined" + ) + # we do not clear the session here because of a race condition mentioned in https://github.com/supertokens/supertokens-node/issues/17 + raise UnauthorisedError( + "Session does not exist. Are you sending the session tokens in the request with the appropriate token transfer method?", + clear_tokens=False, + ) + access_token_obj: Optional[ParsedJWTInfo] = None try: access_token_obj = parse_jwt_without_signature_verification(access_token) @@ -1055,7 +1089,7 @@

    Methods

    -async def get_session(self, access_token: str, anti_csrf_token: Optional[str], anti_csrf_check: Optional[bool] = None, session_required: Optional[bool] = None, check_database: Optional[bool] = None, override_global_claim_validators: Optional[Callable[[List[SessionClaimValidator], SessionContainer, Dict[str, Any]], MaybeAwaitable[List[SessionClaimValidator]]]] = None, user_context: Optional[Dict[str, Any]] = None) ‑> Optional[SessionContainer] +async def get_session(self, access_token: Optional[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[[List[SessionClaimValidator], SessionContainer, Dict[str, Any]], MaybeAwaitable[List[SessionClaimValidator]]]] = None, user_context: Optional[Dict[str, Any]] = None) ‑> Optional[SessionContainer]
    @@ -1065,8 +1099,8 @@

    Methods

    async def get_session(
         self,
    -    access_token: str,
    -    anti_csrf_token: Optional[str],
    +    access_token: Optional[str],
    +    anti_csrf_token: Optional[str] = None,
         anti_csrf_check: Optional[bool] = None,
         session_required: Optional[bool] = None,
         check_database: Optional[bool] = None,
    @@ -1088,6 +1122,23 @@ 

    Methods

    log_debug_message("getSession: Started") + if access_token is None: + if session_required is False: + log_debug_message( + "getSession: returning None because access_token is undefined and session_required is False" + ) + # there is no session that exists here, and the user wants session verification to be optional. So we return None + return None + + log_debug_message( + "getSession: UNAUTHORISED because accessToken in request is undefined" + ) + # we do not clear the session here because of a race condition mentioned in https://github.com/supertokens/supertokens-node/issues/17 + raise UnauthorisedError( + "Session does not exist. Are you sending the session tokens in the request with the appropriate token transfer method?", + clear_tokens=False, + ) + access_token_obj: Optional[ParsedJWTInfo] = None try: access_token_obj = parse_jwt_without_signature_verification(access_token) diff --git a/html/supertokens_python/recipe/session/session_request_functions.html b/html/supertokens_python/recipe/session/session_request_functions.html index ca64a74f0..5c282e3a6 100644 --- a/html/supertokens_python/recipe/session/session_request_functions.html +++ b/html/supertokens_python/recipe/session/session_request_functions.html @@ -155,8 +155,8 @@

    Module supertokens_python.recipe.session.session_request allowed_transfer_method = config.get_token_transfer_method( request, False, user_context ) - request_transfer_method: TokenTransferMethod - request_access_token: Union[ParsedJWTInfo, None] + request_transfer_method: Optional[TokenTransferMethod] = None + request_access_token: Union[ParsedJWTInfo, None] = None if (allowed_transfer_method in ("any", "header")) and access_tokens.get( "header" @@ -170,25 +170,6 @@

    Module supertokens_python.recipe.session.session_request log_debug_message("getSession: using cookie transfer method") request_transfer_method = "cookie" request_access_token = access_tokens["cookie"] - else: - if session_optional: - log_debug_message( - "getSession: returning None because accessToken is undefined and sessionRequired is false" - ) - # there is no session that exists here, and the user wants session verification - # to be optional. So we return None - return None - - log_debug_message( - "getSession: UNAUTHORISED because access_token in request is None" - ) - # we do not clear the session here because of a race condition mentioned in: - # https://github.com/supertokens/supertokens-node/issues/17 - raise_unauthorised_exception( - "Session does not exist. Are you sending the session tokens in the " - "request with the appropriate token transfer method?", - clear_tokens=False, - ) anti_csrf_token = get_anti_csrf_header(request) do_anti_csrf_check = anti_csrf_check @@ -214,7 +195,9 @@

    Module supertokens_python.recipe.session.session_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, + 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, check_database=check_database, @@ -228,9 +211,22 @@

    Module supertokens_python.recipe.session.session_request ) await session.assert_claims(claim_validators, user_context) + # request_transfer_method can only be None here if the user overriddes get_session + # to load the session by a custom method in that (very niche) case they also need to + # override how the session is attached to the response. + # In that scenario the transferMethod passed to attachToRequestResponse likely doesn't + # matter, still, we follow the general fallback logic + + if request_transfer_method is not None: + final_transfer_method = request_transfer_method + elif allowed_transfer_method != "any": + final_transfer_method = allowed_transfer_method + else: + final_transfer_method = "header" + await session.attach_to_request_response( request, - request_transfer_method, + final_transfer_method, ) return session @@ -665,8 +661,8 @@

    Functions

    allowed_transfer_method = config.get_token_transfer_method( request, False, user_context ) - request_transfer_method: TokenTransferMethod - request_access_token: Union[ParsedJWTInfo, None] + request_transfer_method: Optional[TokenTransferMethod] = None + request_access_token: Union[ParsedJWTInfo, None] = None if (allowed_transfer_method in ("any", "header")) and access_tokens.get( "header" @@ -680,25 +676,6 @@

    Functions

    log_debug_message("getSession: using cookie transfer method") request_transfer_method = "cookie" request_access_token = access_tokens["cookie"] - else: - if session_optional: - log_debug_message( - "getSession: returning None because accessToken is undefined and sessionRequired is false" - ) - # there is no session that exists here, and the user wants session verification - # to be optional. So we return None - return None - - log_debug_message( - "getSession: UNAUTHORISED because access_token in request is None" - ) - # we do not clear the session here because of a race condition mentioned in: - # https://github.com/supertokens/supertokens-node/issues/17 - raise_unauthorised_exception( - "Session does not exist. Are you sending the session tokens in the " - "request with the appropriate token transfer method?", - clear_tokens=False, - ) anti_csrf_token = get_anti_csrf_header(request) do_anti_csrf_check = anti_csrf_check @@ -724,7 +701,9 @@

    Functions

    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, + 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, check_database=check_database, @@ -738,9 +717,22 @@

    Functions

    ) await session.assert_claims(claim_validators, user_context) + # request_transfer_method can only be None here if the user overriddes get_session + # to load the session by a custom method in that (very niche) case they also need to + # override how the session is attached to the response. + # In that scenario the transferMethod passed to attachToRequestResponse likely doesn't + # matter, still, we follow the general fallback logic + + if request_transfer_method is not None: + final_transfer_method = request_transfer_method + elif allowed_transfer_method != "any": + final_transfer_method = allowed_transfer_method + else: + final_transfer_method = "header" + await session.attach_to_request_response( request, - request_transfer_method, + final_transfer_method, ) return session

    From b309d9a4e66c23d8044c98ba37e1afcc17d169bc Mon Sep 17 00:00:00 2001 From: KShivendu Date: Tue, 16 May 2023 12:55:51 +0530 Subject: [PATCH 138/192] fix: Fix failing tests --- supertokens_python/recipe/session/session_request_functions.py | 1 + 1 file changed, 1 insertion(+) diff --git a/supertokens_python/recipe/session/session_request_functions.py b/supertokens_python/recipe/session/session_request_functions.py index 3fe4897e1..6e9ea465e 100644 --- a/supertokens_python/recipe/session/session_request_functions.py +++ b/supertokens_python/recipe/session/session_request_functions.py @@ -172,6 +172,7 @@ async def get_session_from_request( else None, anti_csrf_token=anti_csrf_token, anti_csrf_check=do_anti_csrf_check, + session_required=session_required, check_database=check_database, override_global_claim_validators=override_global_claim_validators, user_context=user_context, From 1fd5acb68b07b1a8250c5f6547a47425f72b5c03 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Tue, 16 May 2023 12:59:34 +0530 Subject: [PATCH 139/192] adding dev-v0.13.1 tag to this commit to ensure building --- .../recipe/session/session_request_functions.html | 2 ++ 1 file changed, 2 insertions(+) diff --git a/html/supertokens_python/recipe/session/session_request_functions.html b/html/supertokens_python/recipe/session/session_request_functions.html index 5c282e3a6..352910794 100644 --- a/html/supertokens_python/recipe/session/session_request_functions.html +++ b/html/supertokens_python/recipe/session/session_request_functions.html @@ -200,6 +200,7 @@

    Module supertokens_python.recipe.session.session_request else None, anti_csrf_token=anti_csrf_token, anti_csrf_check=do_anti_csrf_check, + session_required=session_required, check_database=check_database, override_global_claim_validators=override_global_claim_validators, user_context=user_context, @@ -706,6 +707,7 @@

    Functions

    else None, anti_csrf_token=anti_csrf_token, anti_csrf_check=do_anti_csrf_check, + session_required=session_required, check_database=check_database, override_global_claim_validators=override_global_claim_validators, user_context=user_context, From 81d4661b1a98fcfed3714042bd8fef0c733b31a9 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Thu, 18 May 2023 23:20:22 +0530 Subject: [PATCH 140/192] adds missing check_database boolean in verify_session --- CHANGELOG.md | 3 +++ setup.py | 2 +- supertokens_python/constants.py | 2 +- supertokens_python/recipe/session/api/implementation.py | 2 ++ .../recipe/session/framework/django/asyncio/__init__.py | 2 ++ .../recipe/session/framework/django/syncio/__init__.py | 2 ++ .../recipe/session/framework/fastapi/__init__.py | 2 ++ supertokens_python/recipe/session/framework/flask/__init__.py | 2 ++ supertokens_python/recipe/session/interfaces.py | 1 + supertokens_python/recipe/session/recipe.py | 2 ++ 10 files changed, 18 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 40fd9515f..8d74623f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [unreleased] +## [0.14.0] - 2023-05-18 +- Adds missing `check_database` boolean in `verify_session` + ## [0.13.1] - 2023-05-15 ### Changes - Made the access token string optional in the overrideable `get_session` function diff --git a/setup.py b/setup.py index a706d1335..0ba03bead 100644 --- a/setup.py +++ b/setup.py @@ -70,7 +70,7 @@ setup( name="supertokens_python", - version="0.13.1", + version="0.14.0", author="SuperTokens", license="Apache 2.0", author_email="team@supertokens.com", diff --git a/supertokens_python/constants.py b/supertokens_python/constants.py index b74f9ed4c..4b4efbeff 100644 --- a/supertokens_python/constants.py +++ b/supertokens_python/constants.py @@ -14,7 +14,7 @@ from __future__ import annotations SUPPORTED_CDI_VERSIONS = ["2.21"] -VERSION = "0.13.1" +VERSION = "0.14.0" TELEMETRY = "/telemetry" USER_COUNT = "/users/count" USER_DELETE = "/user/remove" diff --git a/supertokens_python/recipe/session/api/implementation.py b/supertokens_python/recipe/session/api/implementation.py index 648c54e1c..fe174ed14 100644 --- a/supertokens_python/recipe/session/api/implementation.py +++ b/supertokens_python/recipe/session/api/implementation.py @@ -62,6 +62,7 @@ async def verify_session( api_options: APIOptions, anti_csrf_check: Union[bool, None], session_required: bool, + check_database: bool, override_global_claim_validators: Optional[ Callable[ [List[SessionClaimValidator], SessionContainer, Dict[str, Any]], @@ -90,6 +91,7 @@ async def verify_session( api_options.recipe_implementation, session_required=session_required, anti_csrf_check=anti_csrf_check, + check_database=check_database, override_global_claim_validators=override_global_claim_validators, user_context=user_context, ) diff --git a/supertokens_python/recipe/session/framework/django/asyncio/__init__.py b/supertokens_python/recipe/session/framework/django/asyncio/__init__.py index bf9d46259..d071f9a60 100644 --- a/supertokens_python/recipe/session/framework/django/asyncio/__init__.py +++ b/supertokens_python/recipe/session/framework/django/asyncio/__init__.py @@ -28,6 +28,7 @@ def verify_session( anti_csrf_check: Union[bool, None] = None, session_required: bool = True, + check_database: bool = False, override_global_claim_validators: Optional[ Callable[ [List[SessionClaimValidator], SessionContainer, Dict[str, Any]], @@ -53,6 +54,7 @@ async def wrapped_function(request: HttpRequest, *args: Any, **kwargs: Any): baseRequest, anti_csrf_check, session_required, + check_database, override_global_claim_validators, user_context, ) diff --git a/supertokens_python/recipe/session/framework/django/syncio/__init__.py b/supertokens_python/recipe/session/framework/django/syncio/__init__.py index d1891d556..c263cae53 100644 --- a/supertokens_python/recipe/session/framework/django/syncio/__init__.py +++ b/supertokens_python/recipe/session/framework/django/syncio/__init__.py @@ -29,6 +29,7 @@ def verify_session( anti_csrf_check: Union[bool, None] = None, session_required: bool = True, + check_database: bool = False, override_global_claim_validators: Optional[ Callable[ [List[SessionClaimValidator], SessionContainer, Dict[str, Any]], @@ -55,6 +56,7 @@ def wrapped_function(request: HttpRequest, *args: Any, **kwargs: Any): baseRequest, anti_csrf_check, session_required, + check_database, override_global_claim_validators, user_context, ) diff --git a/supertokens_python/recipe/session/framework/fastapi/__init__.py b/supertokens_python/recipe/session/framework/fastapi/__init__.py index 380e321b2..2329f9dfe 100644 --- a/supertokens_python/recipe/session/framework/fastapi/__init__.py +++ b/supertokens_python/recipe/session/framework/fastapi/__init__.py @@ -23,6 +23,7 @@ def verify_session( anti_csrf_check: Union[bool, None] = None, session_required: bool = True, + check_database: bool = False, override_global_claim_validators: Optional[ Callable[ [List[SessionClaimValidator], SessionContainer, Dict[str, Any]], @@ -42,6 +43,7 @@ async def func(request: Request) -> Union[SessionContainer, None]: baseRequest, anti_csrf_check, session_required, + check_database, override_global_claim_validators, user_context, ) diff --git a/supertokens_python/recipe/session/framework/flask/__init__.py b/supertokens_python/recipe/session/framework/flask/__init__.py index 77143f0ad..2b7b11cb7 100644 --- a/supertokens_python/recipe/session/framework/flask/__init__.py +++ b/supertokens_python/recipe/session/framework/flask/__init__.py @@ -26,6 +26,7 @@ def verify_session( anti_csrf_check: Union[bool, None] = None, session_required: bool = True, + check_database: bool = False, override_global_claim_validators: Optional[ Callable[ [List[SessionClaimValidator], SessionContainer, Dict[str, Any]], @@ -49,6 +50,7 @@ def wrapped_function(*args: Any, **kwargs: Any): baseRequest, anti_csrf_check, session_required, + check_database, override_global_claim_validators, user_context, ) diff --git a/supertokens_python/recipe/session/interfaces.py b/supertokens_python/recipe/session/interfaces.py index d9e15696d..0cdefe2ad 100644 --- a/supertokens_python/recipe/session/interfaces.py +++ b/supertokens_python/recipe/session/interfaces.py @@ -350,6 +350,7 @@ async def verify_session( api_options: APIOptions, anti_csrf_check: Union[bool, None], session_required: bool, + check_database: bool, override_global_claim_validators: Optional[ Callable[ [List[SessionClaimValidator], SessionContainer, Dict[str, Any]], diff --git a/supertokens_python/recipe/session/recipe.py b/supertokens_python/recipe/session/recipe.py index bc9b7db51..3d94eb805 100644 --- a/supertokens_python/recipe/session/recipe.py +++ b/supertokens_python/recipe/session/recipe.py @@ -344,6 +344,7 @@ async def verify_session( request: BaseRequest, anti_csrf_check: Union[bool, None], session_required: bool, + check_database: bool, override_global_claim_validators: Optional[ Callable[ [List[SessionClaimValidator], SessionContainer, Dict[str, Any]], @@ -364,6 +365,7 @@ async def verify_session( ), anti_csrf_check, session_required, + check_database, override_global_claim_validators, user_context=default_user_context(request), ) From 98efe744b4a5adc6c6f4489d28c2915ac6be9829 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Thu, 18 May 2023 23:21:31 +0530 Subject: [PATCH 141/192] adding dev-v0.14.0 tag to this commit to ensure building --- html/supertokens_python/constants.html | 2 +- .../recipe/session/api/implementation.html | 8 +++++++- .../recipe/session/framework/django/asyncio/index.html | 6 +++++- .../recipe/session/framework/django/syncio/index.html | 6 +++++- .../recipe/session/framework/fastapi/index.html | 6 +++++- .../recipe/session/framework/flask/index.html | 6 +++++- html/supertokens_python/recipe/session/interfaces.html | 5 ++++- html/supertokens_python/recipe/session/recipe.html | 8 +++++++- 8 files changed, 39 insertions(+), 8 deletions(-) diff --git a/html/supertokens_python/constants.html b/html/supertokens_python/constants.html index 1995c9c95..e210b2490 100644 --- a/html/supertokens_python/constants.html +++ b/html/supertokens_python/constants.html @@ -42,7 +42,7 @@

    Module supertokens_python.constants

    from __future__ import annotations SUPPORTED_CDI_VERSIONS = ["2.21"] -VERSION = "0.13.1" +VERSION = "0.14.0" TELEMETRY = "/telemetry" USER_COUNT = "/users/count" USER_DELETE = "/user/remove" diff --git a/html/supertokens_python/recipe/session/api/implementation.html b/html/supertokens_python/recipe/session/api/implementation.html index 8ce30237f..0435d24cb 100644 --- a/html/supertokens_python/recipe/session/api/implementation.html +++ b/html/supertokens_python/recipe/session/api/implementation.html @@ -90,6 +90,7 @@

    Module supertokens_python.recipe.session.api.implementat api_options: APIOptions, anti_csrf_check: Union[bool, None], session_required: bool, + check_database: bool, override_global_claim_validators: Optional[ Callable[ [List[SessionClaimValidator], SessionContainer, Dict[str, Any]], @@ -118,6 +119,7 @@

    Module supertokens_python.recipe.session.api.implementat api_options.recipe_implementation, session_required=session_required, anti_csrf_check=anti_csrf_check, + check_database=check_database, override_global_claim_validators=override_global_claim_validators, user_context=user_context, ) @@ -168,6 +170,7 @@

    Classes

    api_options: APIOptions, anti_csrf_check: Union[bool, None], session_required: bool, + check_database: bool, override_global_claim_validators: Optional[ Callable[ [List[SessionClaimValidator], SessionContainer, Dict[str, Any]], @@ -196,6 +199,7 @@

    Classes

    api_options.recipe_implementation, session_required=session_required, anti_csrf_check=anti_csrf_check, + check_database=check_database, override_global_claim_validators=override_global_claim_validators, user_context=user_context, )
    @@ -248,7 +252,7 @@

    Methods

    -async def verify_session(self, api_options: APIOptions, anti_csrf_check: Union[bool, None], session_required: bool, override_global_claim_validators: Optional[Callable[[List[SessionClaimValidator], SessionContainer, Dict[str, Any]], MaybeAwaitable[List[SessionClaimValidator]]]], user_context: Dict[str, Any]) ‑> Union[SessionContainer, None] +async def verify_session(self, api_options: APIOptions, anti_csrf_check: Union[bool, None], session_required: bool, check_database: bool, override_global_claim_validators: Optional[Callable[[List[SessionClaimValidator], SessionContainer, Dict[str, Any]], MaybeAwaitable[List[SessionClaimValidator]]]], user_context: Dict[str, Any]) ‑> Union[SessionContainer, None]
    @@ -261,6 +265,7 @@

    Methods

    api_options: APIOptions, anti_csrf_check: Union[bool, None], session_required: bool, + check_database: bool, override_global_claim_validators: Optional[ Callable[ [List[SessionClaimValidator], SessionContainer, Dict[str, Any]], @@ -289,6 +294,7 @@

    Methods

    api_options.recipe_implementation, session_required=session_required, anti_csrf_check=anti_csrf_check, + check_database=check_database, override_global_claim_validators=override_global_claim_validators, user_context=user_context, )
    diff --git a/html/supertokens_python/recipe/session/framework/django/asyncio/index.html b/html/supertokens_python/recipe/session/framework/django/asyncio/index.html index 639b66882..927274085 100644 --- a/html/supertokens_python/recipe/session/framework/django/asyncio/index.html +++ b/html/supertokens_python/recipe/session/framework/django/asyncio/index.html @@ -56,6 +56,7 @@

    Module supertokens_python.recipe.session.framework.djang def verify_session( anti_csrf_check: Union[bool, None] = None, session_required: bool = True, + check_database: bool = False, override_global_claim_validators: Optional[ Callable[ [List[SessionClaimValidator], SessionContainer, Dict[str, Any]], @@ -81,6 +82,7 @@

    Module supertokens_python.recipe.session.framework.djang baseRequest, anti_csrf_check, session_required, + check_database, override_global_claim_validators, user_context, ) @@ -113,7 +115,7 @@

    Module supertokens_python.recipe.session.framework.djang

    Functions

    -def verify_session(anti_csrf_check: Optional[bool] = None, session_required: bool = True, override_global_claim_validators: Optional[Callable[[List[SessionClaimValidator], SessionContainer, Dict[str, Any]], Union[Awaitable[List[SessionClaimValidator]], List[SessionClaimValidator]]]] = None, user_context: Optional[Dict[str, Any]] = None) ‑> Callable[[~_T], ~_T] +def verify_session(anti_csrf_check: Optional[bool] = None, session_required: bool = True, check_database: bool = False, override_global_claim_validators: Optional[Callable[[List[SessionClaimValidator], SessionContainer, Dict[str, Any]], Union[Awaitable[List[SessionClaimValidator]], List[SessionClaimValidator]]]] = None, user_context: Optional[Dict[str, Any]] = None) ‑> Callable[[~_T], ~_T]
    @@ -124,6 +126,7 @@

    Functions

    def verify_session(
         anti_csrf_check: Union[bool, None] = None,
         session_required: bool = True,
    +    check_database: bool = False,
         override_global_claim_validators: Optional[
             Callable[
                 [List[SessionClaimValidator], SessionContainer, Dict[str, Any]],
    @@ -149,6 +152,7 @@ 

    Functions

    baseRequest, anti_csrf_check, session_required, + check_database, override_global_claim_validators, user_context, ) diff --git a/html/supertokens_python/recipe/session/framework/django/syncio/index.html b/html/supertokens_python/recipe/session/framework/django/syncio/index.html index 50ed91b27..4e8e61e3e 100644 --- a/html/supertokens_python/recipe/session/framework/django/syncio/index.html +++ b/html/supertokens_python/recipe/session/framework/django/syncio/index.html @@ -57,6 +57,7 @@

    Module supertokens_python.recipe.session.framework.djang def verify_session( anti_csrf_check: Union[bool, None] = None, session_required: bool = True, + check_database: bool = False, override_global_claim_validators: Optional[ Callable[ [List[SessionClaimValidator], SessionContainer, Dict[str, Any]], @@ -83,6 +84,7 @@

    Module supertokens_python.recipe.session.framework.djang baseRequest, anti_csrf_check, session_required, + check_database, override_global_claim_validators, user_context, ) @@ -118,7 +120,7 @@

    Module supertokens_python.recipe.session.framework.djang

    Functions

    -def verify_session(anti_csrf_check: Optional[bool] = None, session_required: bool = True, override_global_claim_validators: Optional[Callable[[List[SessionClaimValidator], SessionContainer, Dict[str, Any]], Union[Awaitable[List[SessionClaimValidator]], List[SessionClaimValidator]]]] = None, user_context: Optional[Dict[str, Any]] = None) ‑> Callable[[~_T], ~_T] +def verify_session(anti_csrf_check: Optional[bool] = None, session_required: bool = True, check_database: bool = False, override_global_claim_validators: Optional[Callable[[List[SessionClaimValidator], SessionContainer, Dict[str, Any]], Union[Awaitable[List[SessionClaimValidator]], List[SessionClaimValidator]]]] = None, user_context: Optional[Dict[str, Any]] = None) ‑> Callable[[~_T], ~_T]
    @@ -129,6 +131,7 @@

    Functions

    def verify_session(
         anti_csrf_check: Union[bool, None] = None,
         session_required: bool = True,
    +    check_database: bool = False,
         override_global_claim_validators: Optional[
             Callable[
                 [List[SessionClaimValidator], SessionContainer, Dict[str, Any]],
    @@ -155,6 +158,7 @@ 

    Functions

    baseRequest, anti_csrf_check, session_required, + check_database, override_global_claim_validators, user_context, ) diff --git a/html/supertokens_python/recipe/session/framework/fastapi/index.html b/html/supertokens_python/recipe/session/framework/fastapi/index.html index 6a913e22c..01a9e4b20 100644 --- a/html/supertokens_python/recipe/session/framework/fastapi/index.html +++ b/html/supertokens_python/recipe/session/framework/fastapi/index.html @@ -51,6 +51,7 @@

    Module supertokens_python.recipe.session.framework.fasta def verify_session( anti_csrf_check: Union[bool, None] = None, session_required: bool = True, + check_database: bool = False, override_global_claim_validators: Optional[ Callable[ [List[SessionClaimValidator], SessionContainer, Dict[str, Any]], @@ -70,6 +71,7 @@

    Module supertokens_python.recipe.session.framework.fasta baseRequest, anti_csrf_check, session_required, + check_database, override_global_claim_validators, user_context, ) @@ -92,7 +94,7 @@

    Module supertokens_python.recipe.session.framework.fasta

    Functions

    -def verify_session(anti_csrf_check: Optional[bool] = None, session_required: bool = True, override_global_claim_validators: Optional[Callable[[List[SessionClaimValidator], SessionContainer, Dict[str, Any]], Union[Awaitable[List[SessionClaimValidator]], List[SessionClaimValidator]]]] = None, user_context: Optional[Dict[str, Any]] = None) ‑> Callable[..., Coroutine[Any, Any, Optional[SessionContainer]]] +def verify_session(anti_csrf_check: Optional[bool] = None, session_required: bool = True, check_database: bool = False, override_global_claim_validators: Optional[Callable[[List[SessionClaimValidator], SessionContainer, Dict[str, Any]], Union[Awaitable[List[SessionClaimValidator]], List[SessionClaimValidator]]]] = None, user_context: Optional[Dict[str, Any]] = None) ‑> Callable[..., Coroutine[Any, Any, Optional[SessionContainer]]]
    @@ -103,6 +105,7 @@

    Functions

    def verify_session(
         anti_csrf_check: Union[bool, None] = None,
         session_required: bool = True,
    +    check_database: bool = False,
         override_global_claim_validators: Optional[
             Callable[
                 [List[SessionClaimValidator], SessionContainer, Dict[str, Any]],
    @@ -122,6 +125,7 @@ 

    Functions

    baseRequest, anti_csrf_check, session_required, + check_database, override_global_claim_validators, user_context, ) diff --git a/html/supertokens_python/recipe/session/framework/flask/index.html b/html/supertokens_python/recipe/session/framework/flask/index.html index 0179ed3f6..03e572ba1 100644 --- a/html/supertokens_python/recipe/session/framework/flask/index.html +++ b/html/supertokens_python/recipe/session/framework/flask/index.html @@ -54,6 +54,7 @@

    Module supertokens_python.recipe.session.framework.flask def verify_session( anti_csrf_check: Union[bool, None] = None, session_required: bool = True, + check_database: bool = False, override_global_claim_validators: Optional[ Callable[ [List[SessionClaimValidator], SessionContainer, Dict[str, Any]], @@ -77,6 +78,7 @@

    Module supertokens_python.recipe.session.framework.flask baseRequest, anti_csrf_check, session_required, + check_database, override_global_claim_validators, user_context, ) @@ -103,7 +105,7 @@

    Module supertokens_python.recipe.session.framework.flask

    Functions

    -def verify_session(anti_csrf_check: Optional[bool] = None, session_required: bool = True, override_global_claim_validators: Optional[Callable[[List[SessionClaimValidator], SessionContainer, Dict[str, Any]], Union[Awaitable[List[SessionClaimValidator]], List[SessionClaimValidator]]]] = None, user_context: Optional[Dict[str, Any]] = None) ‑> Callable[[~_T], ~_T] +def verify_session(anti_csrf_check: Optional[bool] = None, session_required: bool = True, check_database: bool = False, override_global_claim_validators: Optional[Callable[[List[SessionClaimValidator], SessionContainer, Dict[str, Any]], Union[Awaitable[List[SessionClaimValidator]], List[SessionClaimValidator]]]] = None, user_context: Optional[Dict[str, Any]] = None) ‑> Callable[[~_T], ~_T]
    @@ -114,6 +116,7 @@

    Functions

    def verify_session(
         anti_csrf_check: Union[bool, None] = None,
         session_required: bool = True,
    +    check_database: bool = False,
         override_global_claim_validators: Optional[
             Callable[
                 [List[SessionClaimValidator], SessionContainer, Dict[str, Any]],
    @@ -137,6 +140,7 @@ 

    Functions

    baseRequest, anti_csrf_check, session_required, + check_database, override_global_claim_validators, user_context, ) diff --git a/html/supertokens_python/recipe/session/interfaces.html b/html/supertokens_python/recipe/session/interfaces.html index c8de17d2a..1599ac5a9 100644 --- a/html/supertokens_python/recipe/session/interfaces.html +++ b/html/supertokens_python/recipe/session/interfaces.html @@ -378,6 +378,7 @@

    Module supertokens_python.recipe.session.interfacesClasses

    api_options: APIOptions, anti_csrf_check: Union[bool, None], session_required: bool, + check_database: bool, override_global_claim_validators: Optional[ Callable[ [List[SessionClaimValidator], SessionContainer, Dict[str, Any]], @@ -801,7 +803,7 @@

    Methods

    -async def verify_session(self, api_options: APIOptions, anti_csrf_check: Union[bool, None], session_required: bool, override_global_claim_validators: Optional[Callable[[List[SessionClaimValidator], SessionContainer, Dict[str, Any]], MaybeAwaitable[List[SessionClaimValidator]]]], user_context: Dict[str, Any]) ‑> Optional[SessionContainer] +async def verify_session(self, api_options: APIOptions, anti_csrf_check: Union[bool, None], session_required: bool, check_database: bool, override_global_claim_validators: Optional[Callable[[List[SessionClaimValidator], SessionContainer, Dict[str, Any]], MaybeAwaitable[List[SessionClaimValidator]]]], user_context: Dict[str, Any]) ‑> Optional[SessionContainer]
    @@ -815,6 +817,7 @@

    Methods

    api_options: APIOptions, anti_csrf_check: Union[bool, None], session_required: bool, + check_database: bool, override_global_claim_validators: Optional[ Callable[ [List[SessionClaimValidator], SessionContainer, Dict[str, Any]], diff --git a/html/supertokens_python/recipe/session/recipe.html b/html/supertokens_python/recipe/session/recipe.html index 8e72abedd..ee100ca0e 100644 --- a/html/supertokens_python/recipe/session/recipe.html +++ b/html/supertokens_python/recipe/session/recipe.html @@ -372,6 +372,7 @@

    Module supertokens_python.recipe.session.recipeModule supertokens_python.recipe.session.recipe

    @@ -694,6 +696,7 @@

    Classes

    request: BaseRequest, anti_csrf_check: Union[bool, None], session_required: bool, + check_database: bool, override_global_claim_validators: Optional[ Callable[ [List[SessionClaimValidator], SessionContainer, Dict[str, Any]], @@ -714,6 +717,7 @@

    Classes

    ), anti_csrf_check, session_required, + check_database, override_global_claim_validators, user_context=default_user_context(request), )

    @@ -1036,7 +1040,7 @@

    Methods

    -async def verify_session(self, request: BaseRequest, anti_csrf_check: Union[bool, None], session_required: bool, override_global_claim_validators: Optional[Callable[[List[SessionClaimValidator], SessionContainer, Dict[str, Any]], MaybeAwaitable[List[SessionClaimValidator]]]], user_context: Dict[str, Any]) +async def verify_session(self, request: BaseRequest, anti_csrf_check: Union[bool, None], session_required: bool, check_database: bool, override_global_claim_validators: Optional[Callable[[List[SessionClaimValidator], SessionContainer, Dict[str, Any]], MaybeAwaitable[List[SessionClaimValidator]]]], user_context: Dict[str, Any])
    @@ -1049,6 +1053,7 @@

    Methods

    request: BaseRequest, anti_csrf_check: Union[bool, None], session_required: bool, + check_database: bool, override_global_claim_validators: Optional[ Callable[ [List[SessionClaimValidator], SessionContainer, Dict[str, Any]], @@ -1069,6 +1074,7 @@

    Methods

    ), anti_csrf_check, session_required, + check_database, override_global_claim_validators, user_context=default_user_context(request), )

    From 8e75570f5d62320e2f5fc54117f7bbe694c606aa Mon Sep 17 00:00:00 2001 From: Nemi Shah Date: Tue, 23 May 2023 15:00:21 +0530 Subject: [PATCH 142/192] Add helper function to get the original request from user context provided to APIs and functions --- supertokens_python/asyncio/__init__.py | 17 ++++++++++++----- supertokens_python/supertokens.py | 15 +++++++++++++++ supertokens_python/syncio/__init__.py | 17 ++++++++++++----- 3 files changed, 39 insertions(+), 10 deletions(-) diff --git a/supertokens_python/asyncio/__init__.py b/supertokens_python/asyncio/__init__.py index f6ce6a6bd..a588308f2 100644 --- a/supertokens_python/asyncio/__init__.py +++ b/supertokens_python/asyncio/__init__.py @@ -11,18 +11,19 @@ # 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 List, Union, Optional, Dict +from typing import Any, Dict, List, Optional, Union from supertokens_python import Supertokens +from supertokens_python.framework.request import BaseRequest from supertokens_python.interfaces import ( CreateUserIdMappingOkResult, + DeleteUserIdMappingOkResult, + GetUserIdMappingOkResult, + UnknownMappingError, UnknownSupertokensUserIDError, + UpdateOrDeleteUserIdMappingInfoOkResult, UserIdMappingAlreadyExistsError, UserIDTypes, - UnknownMappingError, - GetUserIdMappingOkResult, - DeleteUserIdMappingOkResult, - UpdateOrDeleteUserIdMappingInfoOkResult, ) from supertokens_python.types import UsersResponse @@ -97,3 +98,9 @@ async def update_or_delete_user_id_mapping_info( return await Supertokens.get_instance().update_or_delete_user_id_mapping_info( user_id, user_id_type, external_user_id_info ) + + +def get_request_from_user_context( + user_context: Optional[Dict[str, Any]], +) -> Optional[BaseRequest]: + return Supertokens.get_instance().get_request_from_user_context(user_context) diff --git a/supertokens_python/supertokens.py b/supertokens_python/supertokens.py index 0a0e88227..5e9814ff4 100644 --- a/supertokens_python/supertokens.py +++ b/supertokens_python/supertokens.py @@ -552,3 +552,18 @@ async def handle_supertokens_error( ) return await recipe.handle_error(request, err, response) raise err + + def get_request_from_user_context( # pylint: disable=no-self-use + self, + user_context: Optional[Dict[str, Any]] = None, + ) -> Optional[BaseRequest]: + if user_context is None: + return None + + if "_default" not in user_context: + return None + + if not isinstance(user_context["_default"], dict): + return None + + return user_context.get("_default", {}).get("request") diff --git a/supertokens_python/syncio/__init__.py b/supertokens_python/syncio/__init__.py index 86830c34a..65e805557 100644 --- a/supertokens_python/syncio/__init__.py +++ b/supertokens_python/syncio/__init__.py @@ -11,19 +11,20 @@ # 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 List, Union, Optional, Dict +from typing import Any, Dict, List, Optional, Union from supertokens_python import Supertokens from supertokens_python.async_to_sync_wrapper import sync +from supertokens_python.framework.request import BaseRequest from supertokens_python.interfaces import ( CreateUserIdMappingOkResult, + DeleteUserIdMappingOkResult, + GetUserIdMappingOkResult, + UnknownMappingError, UnknownSupertokensUserIDError, + UpdateOrDeleteUserIdMappingInfoOkResult, UserIdMappingAlreadyExistsError, UserIDTypes, - UnknownMappingError, - GetUserIdMappingOkResult, - DeleteUserIdMappingOkResult, - UpdateOrDeleteUserIdMappingInfoOkResult, ) from supertokens_python.types import UsersResponse @@ -103,3 +104,9 @@ def update_or_delete_user_id_mapping_info( user_id, user_id_type, external_user_id_info ) ) + + +def get_request_from_user_context( + user_context: Optional[Dict[str, Any]], +) -> Optional[BaseRequest]: + return Supertokens.get_instance().get_request_from_user_context(user_context) From b49c5533c334f748b8095a57a73ee01a2594c153 Mon Sep 17 00:00:00 2001 From: Nemi Shah Date: Tue, 23 May 2023 15:42:39 +0530 Subject: [PATCH 143/192] Add test for get_request_from_user_context --- tests/test_user_context.py | 117 ++++++++++++++++++++++++++++++++++++- 1 file changed, 114 insertions(+), 3 deletions(-) diff --git a/tests/test_user_context.py b/tests/test_user_context.py index 2fb9adbcc..eeae771fd 100644 --- a/tests/test_user_context.py +++ b/tests/test_user_context.py @@ -13,8 +13,12 @@ # under the License. from typing import Any, Dict, List, Optional +from fastapi import FastAPI +from fastapi.testclient import TestClient from pytest import fixture, mark + from supertokens_python import InputAppInfo, SupertokensConfig, init +from supertokens_python.asyncio import get_request_from_user_context from supertokens_python.framework.fastapi import get_middleware from supertokens_python.recipe import emailpassword, session from supertokens_python.recipe.emailpassword.asyncio import sign_up @@ -28,9 +32,6 @@ RecipeInterface as SRecipeInterface, ) -from fastapi import FastAPI -from fastapi.testclient import TestClient - from .utils import clean_st, reset, setup_st, sign_in_request, start_st works = False @@ -277,3 +278,113 @@ async def create_new_session( create_new_session_context_works, ] ) + + +@mark.asyncio +async def test_get_request_from_user_context(driver_config_client: TestClient): + signin_api_context_works, signin_context_works, create_new_session_context_works = ( + False, + False, + False, + ) + + def apis_override_email_password(param: APIInterface): + og_sign_in_post = param.sign_in_post + + async def sign_in_post( + form_fields: List[FormField], + api_options: APIOptions, + user_context: Dict[str, Any], + ): + req = get_request_from_user_context(user_context) + if req: + assert req.method() == "POST" + assert req.get_path() == "/auth/signin" + nonlocal signin_api_context_works + signin_api_context_works = True + + return await og_sign_in_post(form_fields, api_options, user_context) + + param.sign_in_post = sign_in_post + return param + + def functions_override_email_password(param: RecipeInterface): + og_sign_in = param.sign_in + + async def sign_in(email: str, password: str, user_context: Dict[str, Any]): + req = get_request_from_user_context(user_context) + if req: + assert req.method() == "POST" + assert req.get_path() == "/auth/signin" + nonlocal signin_context_works + signin_context_works = True + + return await og_sign_in(email, password, user_context) + + param.sign_in = sign_in + return param + + def functions_override_session(param: SRecipeInterface): + og_create_new_session = param.create_new_session + + async def create_new_session( + user_id: str, + access_token_payload: Optional[Dict[str, Any]], + session_data_in_database: Optional[Dict[str, Any]], + disable_anti_csrf: Optional[bool], + user_context: Dict[str, Any], + ): + req = get_request_from_user_context(user_context) + if req: + assert req.method() == "POST" + assert req.get_path() == "/auth/signin" + nonlocal create_new_session_context_works + create_new_session_context_works = True + + response = await og_create_new_session( + user_id, + access_token_payload, + session_data_in_database, + disable_anti_csrf, + user_context, + ) + return response + + param.create_new_session = create_new_session + return param + + init( + supertokens_config=SupertokensConfig("http://localhost:3567"), + app_info=InputAppInfo( + app_name="SuperTokens Demo", + api_domain="http://api.supertokens.io", + website_domain="http://supertokens.io", + ), + framework="fastapi", + recipe_list=[ + emailpassword.init( + override=emailpassword.InputOverrideConfig( + apis=apis_override_email_password, + functions=functions_override_email_password, + ) + ), + session.init( + override=session.InputOverrideConfig( + functions=functions_override_session + ) + ), + ], + ) + start_st() + + await sign_up("random@gmail.com", "validpass123", {"manualCall": True}) + res = sign_in_request(driver_config_client, "random@gmail.com", "validpass123") + + assert res.status_code == 200 + assert all( + [ + signin_api_context_works, + signin_context_works, + create_new_session_context_works, + ] + ) From fb1286fda8f9b156a4903fd522f96c3bb9c02199 Mon Sep 17 00:00:00 2001 From: Nemi Shah Date: Tue, 23 May 2023 16:00:55 +0530 Subject: [PATCH 144/192] Update package version and CHANGELOG --- CHANGELOG.md | 6 ++++++ setup.py | 2 +- supertokens_python/constants.py | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d74623f1..5797d50c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [unreleased] +## [0.14.1] - 2023-05-23 + +### Changes + +- Added a new `get_request_from_user_context` function that can be used to read the original network request from the user context in overridden APIs and recipe functions + ## [0.14.0] - 2023-05-18 - Adds missing `check_database` boolean in `verify_session` diff --git a/setup.py b/setup.py index 0ba03bead..93e78d8d6 100644 --- a/setup.py +++ b/setup.py @@ -70,7 +70,7 @@ setup( name="supertokens_python", - version="0.14.0", + version="0.14.1", author="SuperTokens", license="Apache 2.0", author_email="team@supertokens.com", diff --git a/supertokens_python/constants.py b/supertokens_python/constants.py index 4b4efbeff..0969f1889 100644 --- a/supertokens_python/constants.py +++ b/supertokens_python/constants.py @@ -14,7 +14,7 @@ from __future__ import annotations SUPPORTED_CDI_VERSIONS = ["2.21"] -VERSION = "0.14.0" +VERSION = "0.14.1" TELEMETRY = "/telemetry" USER_COUNT = "/users/count" USER_DELETE = "/user/remove" From 42539be385f8fc90e8faadeb17becccf279d9d16 Mon Sep 17 00:00:00 2001 From: Nemi Shah Date: Tue, 23 May 2023 16:17:26 +0530 Subject: [PATCH 145/192] Refactor based on PR reviews --- supertokens_python/__init__.py | 11 ++++++++++- supertokens_python/asyncio/__init__.py | 9 +-------- supertokens_python/syncio/__init__.py | 9 +-------- tests/test_user_context.py | 8 ++++++-- 4 files changed, 18 insertions(+), 19 deletions(-) diff --git a/supertokens_python/__init__.py b/supertokens_python/__init__.py index 519e7411e..79035af08 100644 --- a/supertokens_python/__init__.py +++ b/supertokens_python/__init__.py @@ -12,8 +12,11 @@ # License for the specific language governing permissions and limitations # under the License. +from typing import Any, Callable, Dict, List, Optional, Union + from typing_extensions import Literal -from typing import Callable, List, Union + +from supertokens_python.framework.request import BaseRequest from . import supertokens from .recipe_module import RecipeModule @@ -39,3 +42,9 @@ def init( def get_all_cors_headers() -> List[str]: return supertokens.Supertokens.get_instance().get_all_cors_headers() + + +def get_request_from_user_context( + user_context: Optional[Dict[str, Any]], +) -> Optional[BaseRequest]: + return Supertokens.get_instance().get_request_from_user_context(user_context) diff --git a/supertokens_python/asyncio/__init__.py b/supertokens_python/asyncio/__init__.py index a588308f2..ab3b7c89c 100644 --- a/supertokens_python/asyncio/__init__.py +++ b/supertokens_python/asyncio/__init__.py @@ -11,10 +11,9 @@ # 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, Optional, Union +from typing import Dict, List, Optional, Union from supertokens_python import Supertokens -from supertokens_python.framework.request import BaseRequest from supertokens_python.interfaces import ( CreateUserIdMappingOkResult, DeleteUserIdMappingOkResult, @@ -98,9 +97,3 @@ async def update_or_delete_user_id_mapping_info( return await Supertokens.get_instance().update_or_delete_user_id_mapping_info( user_id, user_id_type, external_user_id_info ) - - -def get_request_from_user_context( - user_context: Optional[Dict[str, Any]], -) -> Optional[BaseRequest]: - return Supertokens.get_instance().get_request_from_user_context(user_context) diff --git a/supertokens_python/syncio/__init__.py b/supertokens_python/syncio/__init__.py index 65e805557..24a8ea476 100644 --- a/supertokens_python/syncio/__init__.py +++ b/supertokens_python/syncio/__init__.py @@ -11,11 +11,10 @@ # 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, Optional, Union +from typing import Dict, List, Optional, Union from supertokens_python import Supertokens from supertokens_python.async_to_sync_wrapper import sync -from supertokens_python.framework.request import BaseRequest from supertokens_python.interfaces import ( CreateUserIdMappingOkResult, DeleteUserIdMappingOkResult, @@ -104,9 +103,3 @@ def update_or_delete_user_id_mapping_info( user_id, user_id_type, external_user_id_info ) ) - - -def get_request_from_user_context( - user_context: Optional[Dict[str, Any]], -) -> Optional[BaseRequest]: - return Supertokens.get_instance().get_request_from_user_context(user_context) diff --git a/tests/test_user_context.py b/tests/test_user_context.py index eeae771fd..bd87d065c 100644 --- a/tests/test_user_context.py +++ b/tests/test_user_context.py @@ -17,8 +17,12 @@ from fastapi.testclient import TestClient from pytest import fixture, mark -from supertokens_python import InputAppInfo, SupertokensConfig, init -from supertokens_python.asyncio import get_request_from_user_context +from supertokens_python import ( + InputAppInfo, + SupertokensConfig, + get_request_from_user_context, + init, +) from supertokens_python.framework.fastapi import get_middleware from supertokens_python.recipe import emailpassword, session from supertokens_python.recipe.emailpassword.asyncio import sign_up From 6e55f1e250043354165fe5ba7819412550ef255c Mon Sep 17 00:00:00 2001 From: Nemi Shah Date: Tue, 23 May 2023 16:30:14 +0530 Subject: [PATCH 146/192] Make test better --- tests/test_user_context.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/test_user_context.py b/tests/test_user_context.py index bd87d065c..dd2e36f15 100644 --- a/tests/test_user_context.py +++ b/tests/test_user_context.py @@ -323,6 +323,14 @@ async def sign_in(email: str, password: str, user_context: Dict[str, Any]): nonlocal signin_context_works signin_context_works = True + orginal_request = req + user_context["_default"]["request"] = None + + newReq = get_request_from_user_context(user_context) + assert newReq is None + + user_context["_default"]["request"] = orginal_request + return await og_sign_in(email, password, user_context) param.sign_in = sign_in From 8567ab58099f7fd13e29ac6f07dc701ed647ff15 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Tue, 23 May 2023 16:35:28 +0530 Subject: [PATCH 147/192] adding dev-v0.14.1 tag to this commit to ensure building --- html/supertokens_python/asyncio/index.html | 10 ++-- html/supertokens_python/constants.html | 2 +- html/supertokens_python/index.html | 29 ++++++++++- html/supertokens_python/supertokens.html | 60 +++++++++++++++++++++- html/supertokens_python/syncio/index.html | 10 ++-- 5 files changed, 96 insertions(+), 15 deletions(-) diff --git a/html/supertokens_python/asyncio/index.html b/html/supertokens_python/asyncio/index.html index 3e8204ea4..ded291023 100644 --- a/html/supertokens_python/asyncio/index.html +++ b/html/supertokens_python/asyncio/index.html @@ -39,18 +39,18 @@

    Module supertokens_python.asyncio

    # 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 List, Union, Optional, Dict +from typing import Dict, List, Optional, Union from supertokens_python import Supertokens from supertokens_python.interfaces import ( CreateUserIdMappingOkResult, + DeleteUserIdMappingOkResult, + GetUserIdMappingOkResult, + UnknownMappingError, UnknownSupertokensUserIDError, + UpdateOrDeleteUserIdMappingInfoOkResult, UserIdMappingAlreadyExistsError, UserIDTypes, - UnknownMappingError, - GetUserIdMappingOkResult, - DeleteUserIdMappingOkResult, - UpdateOrDeleteUserIdMappingInfoOkResult, ) from supertokens_python.types import UsersResponse diff --git a/html/supertokens_python/constants.html b/html/supertokens_python/constants.html index e210b2490..775221cf2 100644 --- a/html/supertokens_python/constants.html +++ b/html/supertokens_python/constants.html @@ -42,7 +42,7 @@

    Module supertokens_python.constants

    from __future__ import annotations SUPPORTED_CDI_VERSIONS = ["2.21"] -VERSION = "0.14.0" +VERSION = "0.14.1" TELEMETRY = "/telemetry" USER_COUNT = "/users/count" USER_DELETE = "/user/remove" diff --git a/html/supertokens_python/index.html b/html/supertokens_python/index.html index dd67e8ea6..d59f7af52 100644 --- a/html/supertokens_python/index.html +++ b/html/supertokens_python/index.html @@ -40,8 +40,11 @@

    Package supertokens_python

    # License for the specific language governing permissions and limitations # under the License. +from typing import Any, Callable, Dict, List, Optional, Union + from typing_extensions import Literal -from typing import Callable, List, Union + +from supertokens_python.framework.request import BaseRequest from . import supertokens from .recipe_module import RecipeModule @@ -66,7 +69,13 @@

    Package supertokens_python

    def get_all_cors_headers() -> List[str]: - return supertokens.Supertokens.get_instance().get_all_cors_headers()
    + return supertokens.Supertokens.get_instance().get_all_cors_headers() + + +def get_request_from_user_context( + user_context: Optional[Dict[str, Any]], +) -> Optional[BaseRequest]: + return Supertokens.get_instance().get_request_from_user_context(user_context)
    @@ -168,6 +177,21 @@

    Functions

    return supertokens.Supertokens.get_instance().get_all_cors_headers()
    +
    +def get_request_from_user_context(user_context: Optional[Dict[str, Any]]) ‑> Optional[BaseRequest] +
    +
    +
    +
    + +Expand source code + +
    def get_request_from_user_context(
    +    user_context: Optional[Dict[str, Any]],
    +) -> Optional[BaseRequest]:
    +    return Supertokens.get_instance().get_request_from_user_context(user_context)
    +
    +
    def init(app_info: InputAppInfo, framework: Literal['fastapi', 'flask', 'django'], supertokens_config: SupertokensConfig, recipe_list: List[Callable[[AppInfo], RecipeModule]], mode: Optional[Literal['asgi', 'wsgi']] = None, telemetry: Optional[bool] = None)
    @@ -227,6 +251,7 @@

    Index

  • Functions

  • diff --git a/html/supertokens_python/supertokens.html b/html/supertokens_python/supertokens.html index 440c21796..c271995a2 100644 --- a/html/supertokens_python/supertokens.html +++ b/html/supertokens_python/supertokens.html @@ -579,7 +579,22 @@

    Module supertokens_python.supertokens

    "errorHandler: Matched with recipeID: %s", recipe.get_recipe_id() ) return await recipe.handle_error(request, err, response) - raise err
    + raise err + + def get_request_from_user_context( # pylint: disable=no-self-use + self, + user_context: Optional[Dict[str, Any]] = None, + ) -> Optional[BaseRequest]: + if user_context is None: + return None + + if "_default" not in user_context: + return None + + if not isinstance(user_context["_default"], dict): + return None + + return user_context.get("_default", {}).get("request")
    @@ -1150,7 +1165,22 @@

    Methods

    "errorHandler: Matched with recipeID: %s", recipe.get_recipe_id() ) return await recipe.handle_error(request, err, response) - raise err
    + raise err + + def get_request_from_user_context( # pylint: disable=no-self-use + self, + user_context: Optional[Dict[str, Any]] = None, + ) -> Optional[BaseRequest]: + if user_context is None: + return None + + if "_default" not in user_context: + return None + + if not isinstance(user_context["_default"], dict): + return None + + return user_context.get("_default", {}).get("request")

    Static methods

    @@ -1353,6 +1383,31 @@

    Methods

    return list(headers_set)

    +
    +def get_request_from_user_context(self, user_context: Optional[Dict[str, Any]] = None) ‑> Optional[BaseRequest] +
    +
    +
    +
    + +Expand source code + +
    def get_request_from_user_context(  # pylint: disable=no-self-use
    +    self,
    +    user_context: Optional[Dict[str, Any]] = None,
    +) -> Optional[BaseRequest]:
    +    if user_context is None:
    +        return None
    +
    +    if "_default" not in user_context:
    +        return None
    +
    +    if not isinstance(user_context["_default"], dict):
    +        return None
    +
    +    return user_context.get("_default", {}).get("request")
    +
    +
    async def get_user_count(self, include_recipe_ids: Union[None, List[str]]) ‑> int
    @@ -1705,6 +1760,7 @@

    delete_user_id_mapping
  • get_all_cors_headers
  • get_instance
  • +
  • get_request_from_user_context
  • get_user_count
  • get_user_id_mapping
  • get_users
  • diff --git a/html/supertokens_python/syncio/index.html b/html/supertokens_python/syncio/index.html index 97f149a60..7fe00ff8e 100644 --- a/html/supertokens_python/syncio/index.html +++ b/html/supertokens_python/syncio/index.html @@ -39,19 +39,19 @@

    Module supertokens_python.syncio

    # 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 List, Union, Optional, Dict +from typing import Dict, List, Optional, Union from supertokens_python import Supertokens from supertokens_python.async_to_sync_wrapper import sync from supertokens_python.interfaces import ( CreateUserIdMappingOkResult, + DeleteUserIdMappingOkResult, + GetUserIdMappingOkResult, + UnknownMappingError, UnknownSupertokensUserIDError, + UpdateOrDeleteUserIdMappingInfoOkResult, UserIdMappingAlreadyExistsError, UserIDTypes, - UnknownMappingError, - GetUserIdMappingOkResult, - DeleteUserIdMappingOkResult, - UpdateOrDeleteUserIdMappingInfoOkResult, ) from supertokens_python.types import UsersResponse From b00acdd44e9a7a64b54a8ace6c92664c2669e206 Mon Sep 17 00:00:00 2001 From: iresharma Date: Wed, 24 May 2023 14:04:50 +0530 Subject: [PATCH 148/192] email templates updated --- CHANGELOG.md | 6 ++++++ setup.py | 2 +- supertokens_python/constants.py | 2 +- .../services/smtp/email_verify_email.py | 9 +++++++++ .../emaildelivery/services/smtp/pless_login_email.py | 12 ++++++++++++ 5 files changed, 29 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5797d50c8..c06682e4c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [unreleased] +## [0.14.2] - 2023-05-24 + +### Changes + +- Made changes to email templates to support superhuman + ## [0.14.1] - 2023-05-23 ### Changes diff --git a/setup.py b/setup.py index 93e78d8d6..5f01ee9fb 100644 --- a/setup.py +++ b/setup.py @@ -70,7 +70,7 @@ setup( name="supertokens_python", - version="0.14.1", + version="0.14.2", author="SuperTokens", license="Apache 2.0", author_email="team@supertokens.com", diff --git a/supertokens_python/constants.py b/supertokens_python/constants.py index 0969f1889..d74280266 100644 --- a/supertokens_python/constants.py +++ b/supertokens_python/constants.py @@ -14,7 +14,7 @@ from __future__ import annotations SUPPORTED_CDI_VERSIONS = ["2.21"] -VERSION = "0.14.1" +VERSION = "0.14.2" TELEMETRY = "/telemetry" USER_COUNT = "/users/count" USER_DELETE = "/user/remove" diff --git a/supertokens_python/recipe/emailverification/emaildelivery/services/smtp/email_verify_email.py b/supertokens_python/recipe/emailverification/emaildelivery/services/smtp/email_verify_email.py index 55699c099..07b62f194 100644 --- a/supertokens_python/recipe/emailverification/emaildelivery/services/smtp/email_verify_email.py +++ b/supertokens_python/recipe/emailverification/emaildelivery/services/smtp/email_verify_email.py @@ -10,6 +10,10 @@ *|MC:SUBJECT|* diff --git a/supertokens_python/recipe/passwordless/emaildelivery/services/smtp/pless_login_email.py b/supertokens_python/recipe/passwordless/emaildelivery/services/smtp/pless_login_email.py index c29cd7577..58d94e162 100644 --- a/supertokens_python/recipe/passwordless/emaildelivery/services/smtp/pless_login_email.py +++ b/supertokens_python/recipe/passwordless/emaildelivery/services/smtp/pless_login_email.py @@ -10,6 +10,10 @@ *|MC:SUBJECT|*