From 6541c6ace04cfe40f785764a5ee3fb1bc5ad0569 Mon Sep 17 00:00:00 2001 From: Namit Nathwani Date: Thu, 14 Aug 2025 14:03:00 +0530 Subject: [PATCH 1/2] feat: adds webauthn support to the dashboard recipe --- setup.py | 2 +- supertokens_python/constants.py | 4 +-- .../dashboard/api/multitenancy/utils.py | 1 + .../dashboard/api/userdetails/user_put.py | 30 +++++++++++++++++++ supertokens_python/recipe/dashboard/utils.py | 11 ++++++- .../recipe/multitenancy/api/implementation.py | 3 +- .../recipe/multitenancy/interfaces.py | 13 ++++++++ 7 files changed, 59 insertions(+), 5 deletions(-) diff --git a/setup.py b/setup.py index 95fa79288..7c74eb247 100644 --- a/setup.py +++ b/setup.py @@ -82,7 +82,7 @@ setup( name="supertokens_python", - version="0.30.1", + version="0.30.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 c83ae6a46..1010900ea 100644 --- a/supertokens_python/constants.py +++ b/supertokens_python/constants.py @@ -15,7 +15,7 @@ from __future__ import annotations SUPPORTED_CDI_VERSIONS = ["5.3"] -VERSION = "0.30.1" +VERSION = "0.30.2" TELEMETRY = "/telemetry" USER_COUNT = "/users/count" USER_DELETE = "/user/remove" @@ -28,6 +28,6 @@ FDI_KEY_HEADER = "fdi-version" API_VERSION = "/apiversion" API_VERSION_HEADER = "cdi-version" -DASHBOARD_VERSION = "0.13" +DASHBOARD_VERSION = "0.15" ONE_YEAR_IN_MS = 31536000000 RATE_LIMIT_STATUS_CODE = 429 diff --git a/supertokens_python/recipe/dashboard/api/multitenancy/utils.py b/supertokens_python/recipe/dashboard/api/multitenancy/utils.py index 199f83c45..24aef6819 100644 --- a/supertokens_python/recipe/dashboard/api/multitenancy/utils.py +++ b/supertokens_python/recipe/dashboard/api/multitenancy/utils.py @@ -78,6 +78,7 @@ def factor_id_to_recipe(factor_id: str) -> str: "link-email": "Passwordless", "link-phone": "Passwordless", "totp": "Totp", + "webauthn": "WebAuthn", } return factor_id_to_recipe_map.get(factor_id, "") diff --git a/supertokens_python/recipe/dashboard/api/userdetails/user_put.py b/supertokens_python/recipe/dashboard/api/userdetails/user_put.py index a6123db6a..c348eefe8 100644 --- a/supertokens_python/recipe/dashboard/api/userdetails/user_put.py +++ b/supertokens_python/recipe/dashboard/api/userdetails/user_put.py @@ -37,6 +37,11 @@ ) from supertokens_python.recipe.usermetadata import UserMetadataRecipe from supertokens_python.recipe.usermetadata.asyncio import update_user_metadata +from supertokens_python.recipe.webauthn.functions import update_user_email +from supertokens_python.recipe.webauthn.interfaces.recipe import ( + UnknownUserIdErrorResponse, +) +from supertokens_python.recipe.webauthn.recipe import WebauthnRecipe from supertokens_python.types import RecipeUserId from .....types.response import APIResponse @@ -201,6 +206,31 @@ async def update_email_for_recipe_id( return OkResponse() + if recipe_id == "webauthn": + validation_error = ( + await WebauthnRecipe.get_instance().config.validate_email_address( + email=email, + tenant_id=tenant_id, + user_context=user_context, + ) + ) + + if validation_error is not None: + return InvalidEmailErrorResponse(validation_error) + + email_update_response = await update_user_email( + email=email, + recipe_user_id=recipe_user_id.get_as_string(), + tenant_id=tenant_id, + user_context=user_context, + ) + + if isinstance(email_update_response, EmailAlreadyExistsError): + return EmailAlreadyExistsErrorResponse() + + if isinstance(email_update_response, UnknownUserIdErrorResponse): + raise Exception("Should never come here") + # If it comes here then the user is a third party user in which case the UI should not have allowed this raise Exception("Should never come here") diff --git a/supertokens_python/recipe/dashboard/utils.py b/supertokens_python/recipe/dashboard/utils.py index 50324c0fd..2d4de009b 100644 --- a/supertokens_python/recipe/dashboard/utils.py +++ b/supertokens_python/recipe/dashboard/utils.py @@ -18,6 +18,7 @@ from typing_extensions import Literal from supertokens_python.recipe.accountlinking.recipe import AccountLinkingRecipe +from supertokens_python.recipe.webauthn.recipe import WebauthnRecipe if TYPE_CHECKING: from supertokens_python.framework.request import BaseRequest @@ -217,7 +218,9 @@ async def get_user_for_recipe_id( async def _get_user_for_recipe_id( recipe_user_id: RecipeUserId, recipe_id: str, user_context: Dict[str, Any] ) -> GetUserForRecipeIdHelperResult: - recipe: Optional[Literal["emailpassword", "thirdparty", "passwordless"]] = None + recipe: Optional[ + Literal["emailpassword", "thirdparty", "passwordless", "webauthn"] + ] = None user = await AccountLinkingRecipe.get_instance().recipe_implementation.get_user( recipe_user_id.get_as_string(), user_context @@ -257,6 +260,12 @@ async def _get_user_for_recipe_id( recipe = "passwordless" except Exception: pass + elif recipe_id == WebauthnRecipe.recipe_id: + try: + WebauthnRecipe.get_instance() + recipe = "webauthn" + except Exception: + pass return GetUserForRecipeIdHelperResult(user=user, recipe=recipe) diff --git a/supertokens_python/recipe/multitenancy/api/implementation.py b/supertokens_python/recipe/multitenancy/api/implementation.py index f901e07b1..568cf92af 100644 --- a/supertokens_python/recipe/multitenancy/api/implementation.py +++ b/supertokens_python/recipe/multitenancy/api/implementation.py @@ -25,7 +25,7 @@ from supertokens_python.types.response import GeneralErrorResponse from ..constants import DEFAULT_TENANT_ID -from ..interfaces import APIInterface, ThirdPartyProvider +from ..interfaces import APIInterface, LoginMethodWebauthn, ThirdPartyProvider class APIImplementation(APIInterface): @@ -115,5 +115,6 @@ async def login_methods_get( enabled="thirdparty" in valid_first_factors, providers=final_provider_list, ), + webauthn=LoginMethodWebauthn(enabled="webauthn" in valid_first_factors), first_factors=valid_first_factors, ) diff --git a/supertokens_python/recipe/multitenancy/interfaces.py b/supertokens_python/recipe/multitenancy/interfaces.py index 03bd8d012..0679f0f57 100644 --- a/supertokens_python/recipe/multitenancy/interfaces.py +++ b/supertokens_python/recipe/multitenancy/interfaces.py @@ -330,6 +330,16 @@ def to_json(self) -> Dict[str, Any]: } +class LoginMethodWebauthn: + def __init__(self, enabled: bool): + self.enabled = enabled + + def to_json(self) -> Dict[str, Any]: + return { + "enabled": self.enabled, + } + + class LoginMethodThirdParty: def __init__(self, enabled: bool, providers: List[ThirdPartyProvider]): self.enabled = enabled @@ -348,12 +358,14 @@ def __init__( email_password: LoginMethodEmailPassword, passwordless: LoginMethodPasswordless, third_party: LoginMethodThirdParty, + webauthn: LoginMethodWebauthn, first_factors: List[str], ): self.status = "OK" self.email_password = email_password self.passwordless = passwordless self.third_party = third_party + self.webauthn = webauthn self.first_factors = first_factors def to_json(self) -> Dict[str, Any]: @@ -362,6 +374,7 @@ def to_json(self) -> Dict[str, Any]: "emailPassword": self.email_password.to_json(), "passwordless": self.passwordless.to_json(), "thirdParty": self.third_party.to_json(), + "webauthn": self.webauthn.to_json(), "firstFactors": self.first_factors, } From a26706e992e726ae6eef1fb52e13327ea0db7b96 Mon Sep 17 00:00:00 2001 From: Namit Nathwani Date: Thu, 14 Aug 2025 17:14:02 +0530 Subject: [PATCH 2/2] update: version --- CHANGELOG.md | 1 + setup.py | 2 +- supertokens_python/constants.py | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 65ed5469f..4254d4619 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [unreleased] +- Adds Webauthn user editing support to the Dashboard ## [0.30.1] - 2025-07-21 - Adds missing register credential endpoint to the Webauthn recipe diff --git a/setup.py b/setup.py index 7c74eb247..31aa4d19a 100644 --- a/setup.py +++ b/setup.py @@ -82,7 +82,7 @@ setup( name="supertokens_python", - version="0.30.2", + version="0.31.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 1010900ea..16eaaa5d9 100644 --- a/supertokens_python/constants.py +++ b/supertokens_python/constants.py @@ -15,7 +15,7 @@ from __future__ import annotations SUPPORTED_CDI_VERSIONS = ["5.3"] -VERSION = "0.30.2" +VERSION = "0.31.0" TELEMETRY = "/telemetry" USER_COUNT = "/users/count" USER_DELETE = "/user/remove"