Skip to content

Commit 04699cb

Browse files
authored
feat: adds webauthn support to the dashboard recipe (#613)
- Bumps dashboard version to `0.15` - Bumps SDK to `0.30.2`
1 parent 8b541d7 commit 04699cb

File tree

8 files changed

+62
-5
lines changed

8 files changed

+62
-5
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88

99
## [unreleased]
1010

11+
## [0.30.2] - 2025-08-14
12+
- Adds Webauthn user editing support to the Dashboard
13+
1114
## [0.30.1] - 2025-07-21
1215
- Adds missing register credential endpoint to the Webauthn recipe
1316

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@
8282

8383
setup(
8484
name="supertokens_python",
85-
version="0.30.1",
85+
version="0.30.2",
8686
author="SuperTokens",
8787
license="Apache 2.0",
8888
author_email="[email protected]",

supertokens_python/constants.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from __future__ import annotations
1616

1717
SUPPORTED_CDI_VERSIONS = ["5.3"]
18-
VERSION = "0.30.1"
18+
VERSION = "0.30.2"
1919
TELEMETRY = "/telemetry"
2020
USER_COUNT = "/users/count"
2121
USER_DELETE = "/user/remove"
@@ -28,6 +28,6 @@
2828
FDI_KEY_HEADER = "fdi-version"
2929
API_VERSION = "/apiversion"
3030
API_VERSION_HEADER = "cdi-version"
31-
DASHBOARD_VERSION = "0.13"
31+
DASHBOARD_VERSION = "0.15"
3232
ONE_YEAR_IN_MS = 31536000000
3333
RATE_LIMIT_STATUS_CODE = 429

supertokens_python/recipe/dashboard/api/multitenancy/utils.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ def factor_id_to_recipe(factor_id: str) -> str:
7878
"link-email": "Passwordless",
7979
"link-phone": "Passwordless",
8080
"totp": "Totp",
81+
"webauthn": "WebAuthn",
8182
}
8283

8384
return factor_id_to_recipe_map.get(factor_id, "")

supertokens_python/recipe/dashboard/api/userdetails/user_put.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@
3737
)
3838
from supertokens_python.recipe.usermetadata import UserMetadataRecipe
3939
from supertokens_python.recipe.usermetadata.asyncio import update_user_metadata
40+
from supertokens_python.recipe.webauthn.functions import update_user_email
41+
from supertokens_python.recipe.webauthn.interfaces.recipe import (
42+
UnknownUserIdErrorResponse,
43+
)
44+
from supertokens_python.recipe.webauthn.recipe import WebauthnRecipe
4045
from supertokens_python.types import RecipeUserId
4146

4247
from .....types.response import APIResponse
@@ -201,6 +206,31 @@ async def update_email_for_recipe_id(
201206

202207
return OkResponse()
203208

209+
if recipe_id == "webauthn":
210+
validation_error = (
211+
await WebauthnRecipe.get_instance().config.validate_email_address(
212+
email=email,
213+
tenant_id=tenant_id,
214+
user_context=user_context,
215+
)
216+
)
217+
218+
if validation_error is not None:
219+
return InvalidEmailErrorResponse(validation_error)
220+
221+
email_update_response = await update_user_email(
222+
email=email,
223+
recipe_user_id=recipe_user_id.get_as_string(),
224+
tenant_id=tenant_id,
225+
user_context=user_context,
226+
)
227+
228+
if isinstance(email_update_response, EmailAlreadyExistsError):
229+
return EmailAlreadyExistsErrorResponse()
230+
231+
if isinstance(email_update_response, UnknownUserIdErrorResponse):
232+
raise Exception("Should never come here")
233+
204234
# If it comes here then the user is a third party user in which case the UI should not have allowed this
205235
raise Exception("Should never come here")
206236

supertokens_python/recipe/dashboard/utils.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from typing_extensions import Literal
1919

2020
from supertokens_python.recipe.accountlinking.recipe import AccountLinkingRecipe
21+
from supertokens_python.recipe.webauthn.recipe import WebauthnRecipe
2122

2223
if TYPE_CHECKING:
2324
from supertokens_python.framework.request import BaseRequest
@@ -217,7 +218,9 @@ async def get_user_for_recipe_id(
217218
async def _get_user_for_recipe_id(
218219
recipe_user_id: RecipeUserId, recipe_id: str, user_context: Dict[str, Any]
219220
) -> GetUserForRecipeIdHelperResult:
220-
recipe: Optional[Literal["emailpassword", "thirdparty", "passwordless"]] = None
221+
recipe: Optional[
222+
Literal["emailpassword", "thirdparty", "passwordless", "webauthn"]
223+
] = None
221224

222225
user = await AccountLinkingRecipe.get_instance().recipe_implementation.get_user(
223226
recipe_user_id.get_as_string(), user_context
@@ -257,6 +260,12 @@ async def _get_user_for_recipe_id(
257260
recipe = "passwordless"
258261
except Exception:
259262
pass
263+
elif recipe_id == WebauthnRecipe.recipe_id:
264+
try:
265+
WebauthnRecipe.get_instance()
266+
recipe = "webauthn"
267+
except Exception:
268+
pass
260269

261270
return GetUserForRecipeIdHelperResult(user=user, recipe=recipe)
262271

supertokens_python/recipe/multitenancy/api/implementation.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
from supertokens_python.types.response import GeneralErrorResponse
2626

2727
from ..constants import DEFAULT_TENANT_ID
28-
from ..interfaces import APIInterface, ThirdPartyProvider
28+
from ..interfaces import APIInterface, LoginMethodWebauthn, ThirdPartyProvider
2929

3030

3131
class APIImplementation(APIInterface):
@@ -115,5 +115,6 @@ async def login_methods_get(
115115
enabled="thirdparty" in valid_first_factors,
116116
providers=final_provider_list,
117117
),
118+
webauthn=LoginMethodWebauthn(enabled="webauthn" in valid_first_factors),
118119
first_factors=valid_first_factors,
119120
)

supertokens_python/recipe/multitenancy/interfaces.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,16 @@ def to_json(self) -> Dict[str, Any]:
330330
}
331331

332332

333+
class LoginMethodWebauthn:
334+
def __init__(self, enabled: bool):
335+
self.enabled = enabled
336+
337+
def to_json(self) -> Dict[str, Any]:
338+
return {
339+
"enabled": self.enabled,
340+
}
341+
342+
333343
class LoginMethodThirdParty:
334344
def __init__(self, enabled: bool, providers: List[ThirdPartyProvider]):
335345
self.enabled = enabled
@@ -348,12 +358,14 @@ def __init__(
348358
email_password: LoginMethodEmailPassword,
349359
passwordless: LoginMethodPasswordless,
350360
third_party: LoginMethodThirdParty,
361+
webauthn: LoginMethodWebauthn,
351362
first_factors: List[str],
352363
):
353364
self.status = "OK"
354365
self.email_password = email_password
355366
self.passwordless = passwordless
356367
self.third_party = third_party
368+
self.webauthn = webauthn
357369
self.first_factors = first_factors
358370

359371
def to_json(self) -> Dict[str, Any]:
@@ -362,6 +374,7 @@ def to_json(self) -> Dict[str, Any]:
362374
"emailPassword": self.email_password.to_json(),
363375
"passwordless": self.passwordless.to_json(),
364376
"thirdParty": self.third_party.to_json(),
377+
"webauthn": self.webauthn.to_json(),
365378
"firstFactors": self.first_factors,
366379
}
367380

0 commit comments

Comments
 (0)