Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@

setup(
name="supertokens_python",
version="0.30.1",
version="0.31.0",
author="SuperTokens",
license="Apache 2.0",
author_email="[email protected]",
Expand Down
4 changes: 2 additions & 2 deletions supertokens_python/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from __future__ import annotations

SUPPORTED_CDI_VERSIONS = ["5.3"]
VERSION = "0.30.1"
VERSION = "0.31.0"
TELEMETRY = "/telemetry"
USER_COUNT = "/users/count"
USER_DELETE = "/user/remove"
Expand All @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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, "")
Expand Down
30 changes: 30 additions & 0 deletions supertokens_python/recipe/dashboard/api/userdetails/user_put.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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")

Expand Down
11 changes: 10 additions & 1 deletion supertokens_python/recipe/dashboard/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand Down
3 changes: 2 additions & 1 deletion supertokens_python/recipe/multitenancy/api/implementation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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,
)
13 changes: 13 additions & 0 deletions supertokens_python/recipe/multitenancy/interfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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]:
Expand All @@ -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,
}

Expand Down
Loading