diff --git a/.github/workflows/auth-react-test-1-django.yml b/.github/workflows/auth-react-test-1-django.yml index ad74b1bcc..3f4a1fb11 100644 --- a/.github/workflows/auth-react-test-1-django.yml +++ b/.github/workflows/auth-react-test-1-django.yml @@ -23,6 +23,8 @@ jobs: fdiVersions: ${{ steps.versions.outputs.fdiVersions }} cdiVersions: ${{ steps.versions.outputs.cdiVersions }} pyVersions: '["3.8", "3.13"]' + nodeFdiVersionMap: ${{ steps.node-versions.outputs.fdiVersions }} + authReactFdiVersionMap: ${{ steps.auth-react-versions.outputs.fdiVersions }} steps: - uses: actions/checkout@v4 @@ -32,6 +34,20 @@ jobs: has-fdi: true has-cdi: true + - uses: supertokens/actions/get-versions-from-repo@main + id: auth-react-versions + with: + repo: supertokens-auth-react + github-token: ${{ secrets.GITHUB_TOKEN }} + fdi-versions: ${{ steps.versions.outputs.fdiVersions }} + + - uses: supertokens/actions/get-versions-from-repo@main + id: node-versions + with: + repo: supertokens-node + github-token: ${{ secrets.GITHUB_TOKEN }} + fdi-versions: ${{ steps.versions.outputs.fdiVersions }} + setup-auth-react: runs-on: ubuntu-latest needs: define-versions @@ -51,19 +67,20 @@ jobs: matrix: ${{ steps.setup-matrix.outputs.matrix }} steps: - - uses: supertokens/get-versions-action@main + - name: Get node and auth-react versions for FDI id: versions - with: - driver-name: python - fdi-version: ${{ matrix.fdi-version }} - env: - SUPERTOKENS_API_KEY: ${{ secrets.SUPERTOKENS_API_KEY }} + run: | + nodeVersion=$( echo '${{ needs.define-versions.outputs.nodeFdiVersionMap }}' | jq -r '.["${{ matrix.fdi-version }}"]' ) + authReactVersion=$( echo '${{ needs.define-versions.outputs.authReactFdiVersionMap }}' | jq -r '.["${{ matrix.fdi-version }}"]' ) + + echo "nodeVersion=${nodeVersion}" >> $GITHUB_OUTPUT + echo "authReactVersion=${authReactVersion}" >> $GITHUB_OUTPUT - uses: supertokens/auth-react-testing-action/setup@main id: envs with: - auth-react-version: ${{ steps.versions.outputs.authReactVersionXy }} - node-sdk-version: ${{ steps.versions.outputs.nodeTag }} + auth-react-version: ${{ steps.versions.outputs.authReactVersion }} + node-sdk-version: ${{ steps.versions.outputs.nodeVersion }} fdi-version: ${{ matrix.fdi-version }} - id: setup-matrix diff --git a/.github/workflows/auth-react-test-1-fastapi.yml b/.github/workflows/auth-react-test-1-fastapi.yml index 5f0e08cc9..1afba68a3 100644 --- a/.github/workflows/auth-react-test-1-fastapi.yml +++ b/.github/workflows/auth-react-test-1-fastapi.yml @@ -23,6 +23,8 @@ jobs: fdiVersions: ${{ steps.versions.outputs.fdiVersions }} cdiVersions: ${{ steps.versions.outputs.cdiVersions }} pyVersions: '["3.8", "3.13"]' + nodeFdiVersionMap: ${{ steps.node-versions.outputs.fdiVersions }} + authReactFdiVersionMap: ${{ steps.auth-react-versions.outputs.fdiVersions }} steps: - uses: actions/checkout@v4 @@ -32,6 +34,20 @@ jobs: has-fdi: true has-cdi: true + - uses: supertokens/actions/get-versions-from-repo@main + id: auth-react-versions + with: + repo: supertokens-auth-react + github-token: ${{ secrets.GITHUB_TOKEN }} + fdi-versions: ${{ steps.versions.outputs.fdiVersions }} + + - uses: supertokens/actions/get-versions-from-repo@main + id: node-versions + with: + repo: supertokens-node + github-token: ${{ secrets.GITHUB_TOKEN }} + fdi-versions: ${{ steps.versions.outputs.fdiVersions }} + setup-auth-react: runs-on: ubuntu-latest needs: define-versions @@ -51,19 +67,20 @@ jobs: matrix: ${{ steps.setup-matrix.outputs.matrix }} steps: - - uses: supertokens/get-versions-action@main + - name: Get node and auth-react versions for FDI id: versions - with: - driver-name: python - fdi-version: ${{ matrix.fdi-version }} - env: - SUPERTOKENS_API_KEY: ${{ secrets.SUPERTOKENS_API_KEY }} + run: | + nodeVersion=$( echo '${{ needs.define-versions.outputs.nodeFdiVersionMap }}' | jq -r '.["${{ matrix.fdi-version }}"]' ) + authReactVersion=$( echo '${{ needs.define-versions.outputs.authReactFdiVersionMap }}' | jq -r '.["${{ matrix.fdi-version }}"]' ) + + echo "nodeVersion=${nodeVersion}" >> $GITHUB_OUTPUT + echo "authReactVersion=${authReactVersion}" >> $GITHUB_OUTPUT - uses: supertokens/auth-react-testing-action/setup@main id: envs with: - auth-react-version: ${{ steps.versions.outputs.authReactVersionXy }} - node-sdk-version: ${{ steps.versions.outputs.nodeTag }} + auth-react-version: ${{ steps.versions.outputs.authReactVersion }} + node-sdk-version: ${{ steps.versions.outputs.nodeVersion }} fdi-version: ${{ matrix.fdi-version }} - id: setup-matrix diff --git a/.github/workflows/auth-react-test-1-flask.yml b/.github/workflows/auth-react-test-1-flask.yml index 991381bb7..241945588 100644 --- a/.github/workflows/auth-react-test-1-flask.yml +++ b/.github/workflows/auth-react-test-1-flask.yml @@ -23,6 +23,8 @@ jobs: fdiVersions: ${{ steps.versions.outputs.fdiVersions }} cdiVersions: ${{ steps.versions.outputs.cdiVersions }} pyVersions: '["3.8", "3.13"]' + nodeFdiVersionMap: ${{ steps.node-versions.outputs.fdiVersions }} + authReactFdiVersionMap: ${{ steps.auth-react-versions.outputs.fdiVersions }} steps: - uses: actions/checkout@v4 @@ -32,6 +34,20 @@ jobs: has-fdi: true has-cdi: true + - uses: supertokens/actions/get-versions-from-repo@main + id: auth-react-versions + with: + repo: supertokens-auth-react + github-token: ${{ secrets.GITHUB_TOKEN }} + fdi-versions: ${{ steps.versions.outputs.fdiVersions }} + + - uses: supertokens/actions/get-versions-from-repo@main + id: node-versions + with: + repo: supertokens-node + github-token: ${{ secrets.GITHUB_TOKEN }} + fdi-versions: ${{ steps.versions.outputs.fdiVersions }} + setup-auth-react: runs-on: ubuntu-latest needs: define-versions @@ -51,19 +67,20 @@ jobs: matrix: ${{ steps.setup-matrix.outputs.matrix }} steps: - - uses: supertokens/get-versions-action@main + - name: Get node and auth-react versions for FDI id: versions - with: - driver-name: python - fdi-version: ${{ matrix.fdi-version }} - env: - SUPERTOKENS_API_KEY: ${{ secrets.SUPERTOKENS_API_KEY }} + run: | + nodeVersion=$( echo '${{ needs.define-versions.outputs.nodeFdiVersionMap }}' | jq -r '.["${{ matrix.fdi-version }}"]' ) + authReactVersion=$( echo '${{ needs.define-versions.outputs.authReactFdiVersionMap }}' | jq -r '.["${{ matrix.fdi-version }}"]' ) + + echo "nodeVersion=${nodeVersion}" >> $GITHUB_OUTPUT + echo "authReactVersion=${authReactVersion}" >> $GITHUB_OUTPUT - uses: supertokens/auth-react-testing-action/setup@main id: envs with: - auth-react-version: ${{ steps.versions.outputs.authReactVersionXy }} - node-sdk-version: ${{ steps.versions.outputs.nodeTag }} + auth-react-version: ${{ steps.versions.outputs.authReactVersion }} + node-sdk-version: ${{ steps.versions.outputs.nodeVersion }} fdi-version: ${{ matrix.fdi-version }} - id: setup-matrix diff --git a/.github/workflows/auth-react-test-3.yml b/.github/workflows/auth-react-test-3.yml index 23a5b108d..0482417e5 100644 --- a/.github/workflows/auth-react-test-3.yml +++ b/.github/workflows/auth-react-test-3.yml @@ -83,8 +83,32 @@ jobs: python3 -m pip install pip setuptools --upgrade make dev-install && rm -rf src + - name: Get supported Python CDI versions + id: cdi-versions + uses: supertokens/get-supported-versions-action@main + with: + has-cdi: true + working-directory: supertokens-python + + - uses: supertokens/actions/get-versions-from-repo@main + id: core-versions + with: + repo: supertokens-core + github-token: ${{ secrets.GITHUB_TOKEN }} + cdi-versions: ${{ steps.cdi-versions.outputs.cdiVersions }} + + - name: Get core version from latest Python CDI version + id: core-version + run: | + lastPythonCdiVersion=$(echo '${{ steps.cdi-versions.outputs.cdiVersions }}' | jq -r '.[-1]') | sed -e 's/"/\\"/g' + coreVersion=$(echo '${{ steps.core-versions.outputs.cdiVersions }}' | jq -r ".[$lastPythonCdiVersion]") + + echo "coreVersion=${coreVersion}" >> $GITHUB_OUTPUT + - name: Start core working-directory: supertokens-python + env: + SUPERTOKENS_CORE_VERSION: ${{ steps.core-version.outputs.coreVersion }} run: docker compose up --wait - name: Start Server (django) diff --git a/.github/workflows/backend-sdk-testing.yml b/.github/workflows/backend-sdk-testing.yml index 94798d06c..41cba1724 100644 --- a/.github/workflows/backend-sdk-testing.yml +++ b/.github/workflows/backend-sdk-testing.yml @@ -24,6 +24,7 @@ jobs: cdiVersions: ${{ steps.versions.outputs.cdiVersions }} pyVersions: '["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]' nodeVersions: '["20"]' + coreCdiVersionMap: ${{ steps.core-versions.outputs.cdiVersions }} steps: - uses: actions/checkout@v4 @@ -33,6 +34,13 @@ jobs: has-fdi: true has-cdi: true + - uses: supertokens/actions/get-versions-from-repo@main + id: core-versions + with: + repo: supertokens-core + github-token: ${{ secrets.GITHUB_TOKEN }} + cdi-versions: ${{steps.versions.outputs.cdiVersions }} + test: runs-on: ubuntu-latest needs: define-versions @@ -56,14 +64,12 @@ jobs: # Checking out to a custom path since the test repo will also be cloned path: supertokens-python - - uses: supertokens/get-versions-action@main - id: versions - with: - driver-name: python - cdi-version: ${{ matrix.cdi-version }} - fdi-version: ${{ matrix.fdi-version }} - env: - SUPERTOKENS_API_KEY: ${{ secrets.SUPERTOKENS_API_KEY }} + - name: Get core version from current CDI version + id: core-version + run: | + coreVersion=$(echo '${{ needs.define-versions.outputs.coreCdiVersionMap }}' | jq -r '.["${{ matrix.cdi-version }}"]') + + echo "coreVersion=${coreVersion}" >> $GITHUB_OUTPUT - uses: actions/setup-node@v4 with: @@ -82,7 +88,7 @@ jobs: working-directory: supertokens-python env: SUPERTOKENS_ENV: testing - SUPERTOKENS_CORE_VERSION: ${{ steps.versions.outputs.coreVersionXy }} + SUPERTOKENS_CORE_VERSION: ${{ steps.core-version.outputs.coreVersion }} run: | source venv/bin/activate docker compose up --build --wait @@ -91,6 +97,6 @@ jobs: - uses: supertokens/backend-sdk-testing-action@main with: version: ${{ matrix.fdi-version }} - check-name-suffix: '[CDI=${{ matrix.cdi-version }}][Core=${{ steps.versions.outputs.coreVersionXy }}][FDI=${{ matrix.fdi-version }}][Py=${{ matrix.py-version }}][Node=${{ matrix.node-version }}]' + check-name-suffix: '[CDI=${{ matrix.cdi-version }}][Core=${{ steps.core-version.outputs.coreVersion }}][FDI=${{ matrix.fdi-version }}][Py=${{ matrix.py-version }}][Node=${{ matrix.node-version }}]' path: backend-sdk-testing app-server-logs: ${{ github.workspace }}/supertokens-python/python.log diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index a74f9ec51..5163e2c50 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -24,6 +24,7 @@ jobs: fdiVersions: ${{ steps.versions.outputs.fdiVersions }} cdiVersions: ${{ steps.versions.outputs.cdiVersions }} pyVersions: '["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]' + coreCdiVersionMap: ${{ steps.core-versions.outputs.cdiVersions }} steps: - uses: actions/checkout@v4 @@ -34,6 +35,13 @@ jobs: has-fdi: true has-cdi: true + - uses: supertokens/actions/get-versions-from-repo@main + id: core-versions + with: + repo: supertokens-core + github-token: ${{ secrets.GITHUB_TOKEN }} + cdi-versions: ${{steps.versions.outputs.cdiVersions }} + test: runs-on: ubuntu-latest needs: define-versions @@ -47,13 +55,12 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: supertokens/get-versions-action@main - id: versions - with: - driver-name: python - cdi-version: ${{ matrix.cdi-version }} - env: - SUPERTOKENS_API_KEY: ${{ secrets.SUPERTOKENS_API_KEY }} + - name: Get core version from current CDI version + id: core-version + run: | + coreVersion=$(echo '${{ needs.define-versions.outputs.coreCdiVersionMap }}' | jq -r '.["${{ matrix.cdi-version }}"]') + + echo "coreVersion=${coreVersion}" >> $GITHUB_OUTPUT - uses: actions/setup-python@v5 with: @@ -72,7 +79,7 @@ jobs: source venv/bin/activate make test env: - SUPERTOKENS_CORE_VERSION: ${{ steps.versions.outputs.coreVersionXy }} + SUPERTOKENS_CORE_VERSION: ${{ steps.core-version.outputs.coreVersion }} - uses: pmeier/pytest-results-action@main name: Surface failing tests @@ -80,4 +87,4 @@ jobs: with: path: test-results/junit.xml summary: true - title: "[Core=${{ steps.versions.outputs.coreVersionXy }}][py=${{ matrix.py-version }}] Unit Test Results" + title: "[Core=${{ steps.core-version.outputs.coreVersion }}][py=${{ matrix.py-version }}] Unit Test Results" diff --git a/.github/workflows/website-test.yml b/.github/workflows/website-test.yml index f63530500..5aba206e6 100644 --- a/.github/workflows/website-test.yml +++ b/.github/workflows/website-test.yml @@ -23,6 +23,9 @@ jobs: fdiVersions: ${{ steps.versions.outputs.fdiVersions }} cdiVersions: ${{ steps.versions.outputs.cdiVersions }} pyVersions: '["3.8", "3.13"]' + nodeFdiVersionMap: ${{ steps.node-versions.outputs.fdiVersions }} + websiteFdiVersionMap: ${{ steps.website-versions.outputs.fdiVersions }} + coreCdiVersionMap: ${{ steps.core-versions.outputs.cdiVersions }} steps: - uses: actions/checkout@v4 @@ -32,6 +35,27 @@ jobs: has-fdi: true has-cdi: true + - uses: supertokens/actions/get-versions-from-repo@main + id: website-versions + with: + repo: supertokens-website + github-token: ${{ secrets.GITHUB_TOKEN }} + fdi-versions: ${{ steps.versions.outputs.fdiVersions }} + + - uses: supertokens/actions/get-versions-from-repo@main + id: node-versions + with: + repo: supertokens-node + github-token: ${{ secrets.GITHUB_TOKEN }} + fdi-versions: ${{ steps.versions.outputs.fdiVersions }} + + - uses: supertokens/actions/get-versions-from-repo@main + id: core-versions + with: + repo: supertokens-core + github-token: ${{ secrets.GITHUB_TOKEN }} + cdi-versions: ${{steps.versions.outputs.cdiVersions }} + test: runs-on: ubuntu-latest needs: define-versions @@ -67,16 +91,22 @@ jobs: with: python-version: ${{ matrix.py-version }} - - uses: supertokens/get-versions-action@main + - name: Get versions from current FDI/CDI version id: versions - with: - driver-name: python - fdi-version: ${{ matrix.fdi-version }} - env: - SUPERTOKENS_API_KEY: ${{ secrets.SUPERTOKENS_API_KEY }} + run: | + latestCdiVersion=$(echo '${{ needs.define-versions.outputs.cdiVersions }}' | jq -r '.[-1]') | sed -e 's/"/\\"/g' + coreVersion=$(echo '${{ needs.define-versions.outputs.coreCdiVersionMap }}' | jq -r ".[$latestCdiVersion]") + nodeVersion=$(echo '${{ needs.define-versions.outputs.nodeFdiVersionMap }}' | jq -r '.["${{ matrix.fdi-version }}"]') + websiteVersion=$(echo '${{ needs.define-versions.outputs.websiteFdiVersionMap }}' | jq -r '.["${{ matrix.fdi-version }}"]') + + echo "coreVersion=${coreVersion}" >> $GITHUB_OUTPUT + echo "nodeVersion=${nodeVersion}" >> $GITHUB_OUTPUT + echo "websiteVersion=${websiteVersion}" >> $GITHUB_OUTPUT - name: Start core working-directory: supertokens-python + env: + SUPERTOKENS_CORE_VERSION: ${{ steps.versions.outputs.coreVersion }} run: docker compose up --wait - name: Setup venv @@ -185,8 +215,8 @@ jobs: - uses: supertokens/website-testing-action@main with: - version: ${{ steps.versions.outputs.frontendVersionXy }} - node-sdk-version: ${{ steps.versions.outputs.nodeTag }} + version: ${{ steps.versions.outputs.websiteVersion }} + node-sdk-version: ${{ steps.versions.outputs.nodeVersion }} path: supertokens-website check-name-suffix: '[Py=${{ matrix.py-version }}][FDI=${{ matrix.fdi-version }}][Framework=${{ matrix.framework }}]' app-server-logs: ${{ steps.envs.outputs.APP_SERVER_LOG_DIR }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 9be579f48..ea0b1fcbc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -62,6 +62,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - UserMetadata `InputOverrideConfig` -> `UserMetadataOverrideConfig` - UserRoles `InputOverrideConfig` -> `UserRolesOverrideConfig` +## [0.30.2] - 2025-08-14 +- 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/html/supertokens_python/constants.html b/html/supertokens_python/constants.html index 2058bfbe4..2bc9744ac 100644 --- a/html/supertokens_python/constants.html +++ b/html/supertokens_python/constants.html @@ -43,7 +43,7 @@

Module supertokens_python.constants

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" @@ -56,7 +56,7 @@

Module supertokens_python.constants

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/html/supertokens_python/recipe/dashboard/api/multitenancy/utils.html b/html/supertokens_python/recipe/dashboard/api/multitenancy/utils.html index 0b6bad016..fb5f440e8 100644 --- a/html/supertokens_python/recipe/dashboard/api/multitenancy/utils.html +++ b/html/supertokens_python/recipe/dashboard/api/multitenancy/utils.html @@ -106,6 +106,7 @@

Module supertokens_python.recipe.dashboard.api.multitena "link-email": "Passwordless", "link-phone": "Passwordless", "totp": "Totp", + "webauthn": "WebAuthn", } return factor_id_to_recipe_map.get(factor_id, "") diff --git a/html/supertokens_python/recipe/dashboard/api/userdetails/user_put.html b/html/supertokens_python/recipe/dashboard/api/userdetails/user_put.html index a8a1444d6..282c817e5 100644 --- a/html/supertokens_python/recipe/dashboard/api/userdetails/user_put.html +++ b/html/supertokens_python/recipe/dashboard/api/userdetails/user_put.html @@ -65,6 +65,11 @@

Module supertokens_python.recipe.dashboard.api.userdetai ) 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 @@ -229,6 +234,31 @@

Module supertokens_python.recipe.dashboard.api.userdetai 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/html/supertokens_python/recipe/dashboard/utils.html b/html/supertokens_python/recipe/dashboard/utils.html index 8ac5868d5..e546a2e23 100644 --- a/html/supertokens_python/recipe/dashboard/utils.html +++ b/html/supertokens_python/recipe/dashboard/utils.html @@ -46,6 +46,7 @@

Module supertokens_python.recipe.dashboard.utils< 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 @@ -245,7 +246,9 @@

Module supertokens_python.recipe.dashboard.utils< 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 @@ -285,6 +288,12 @@

Module supertokens_python.recipe.dashboard.utils< 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/html/supertokens_python/recipe/multitenancy/api/implementation.html b/html/supertokens_python/recipe/multitenancy/api/implementation.html index 736d1a895..afcb7678c 100644 --- a/html/supertokens_python/recipe/multitenancy/api/implementation.html +++ b/html/supertokens_python/recipe/multitenancy/api/implementation.html @@ -53,7 +53,7 @@

Module supertokens_python.recipe.multitenancy.api.implem 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): @@ -143,6 +143,7 @@

Module supertokens_python.recipe.multitenancy.api.implem enabled="thirdparty" in valid_first_factors, providers=final_provider_list, ), + webauthn=LoginMethodWebauthn(enabled="webauthn" in valid_first_factors), first_factors=valid_first_factors, ) @@ -253,6 +254,7 @@

Classes

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/html/supertokens_python/recipe/multitenancy/interfaces.html b/html/supertokens_python/recipe/multitenancy/interfaces.html index badfac4ef..862fce8a3 100644 --- a/html/supertokens_python/recipe/multitenancy/interfaces.html +++ b/html/supertokens_python/recipe/multitenancy/interfaces.html @@ -358,6 +358,16 @@

Module supertokens_python.recipe.multitenancy.interfaces } +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 @@ -376,12 +386,14 @@

Module supertokens_python.recipe.multitenancy.interfaces 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]: @@ -390,6 +402,7 @@

Module supertokens_python.recipe.multitenancy.interfaces "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, } @@ -859,9 +872,38 @@

Methods

+
+class LoginMethodWebauthn +(enabled: bool) +
+
+
+
+ +Expand source code + +
class LoginMethodWebauthn:
+    def __init__(self, enabled: bool):
+        self.enabled = enabled
+
+    def to_json(self) -> Dict[str, Any]:
+        return {
+            "enabled": self.enabled,
+        }
+
+

Methods

+
+
+def to_json(self) ‑> Dict[str, Any] +
+
+
+
+
+
class LoginMethodsGetOkResult -(email_password: LoginMethodEmailPassword, passwordless: LoginMethodPasswordless, third_party: LoginMethodThirdParty, first_factors: List[str]) +(email_password: LoginMethodEmailPassword, passwordless: LoginMethodPasswordless, third_party: LoginMethodThirdParty, webauthn: LoginMethodWebauthn, first_factors: List[str])

Helper class that provides a standard way to create an ABC using @@ -876,12 +918,14 @@

Methods

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]: @@ -890,6 +934,7 @@

Methods

"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, }
@@ -1378,6 +1423,12 @@

LoginMethodWebauthn

+ + +
  • LoginMethodsGetOkResult

  • -
    -class StatusErrResponseBaseModel[Literal['INVALID_EMAIL_ERROR']] -(**data: Any) -
    -
    -

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

    -

    Create a new model by parsing and validating input data from keyword arguments.

    -

    Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be -validated to form a valid model.

    -

    self is explicitly positional-only to allow self as a field name.

    -

    Ancestors

    - -

    Subclasses

    - -

    Class variables

    -
    -
    var model_config
    -
    -
    -
    -
    -

    Inherited members

    - -
    -
    -class StatusReasonResponseBaseModel -(**data: Any) -
    -
    -

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

    -

    Create a new model by parsing and validating input data from keyword arguments.

    -

    Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be -validated to form a valid model.

    -

    self is explicitly positional-only to allow self as a field name.

    -
    - -Expand source code - -
    class StatusReasonResponseBaseModel(
    -    StatusResponseBaseModel[Status], Generic[Status, Reason]
    -):
    -    reason: Reason
    -
    -

    Ancestors

    - -

    Subclasses

    - -

    Class variables

    -
    -
    var model_config
    -
    -
    -
    -
    var reason : ~Reason
    -
    -
    -
    -
    -

    Inherited members

    - -
    -
    -class StatusReasonResponseBaseModel[Literal['INVALID_AUTHENTICATOR_ERROR'], str] -(**data: Any) -
    -
    -

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

    -

    Create a new model by parsing and validating input data from keyword arguments.

    -

    Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be -validated to form a valid model.

    -

    self is explicitly positional-only to allow self as a field name.

    -

    Ancestors

    - -

    Subclasses

    - -

    Class variables

    -
    -
    var model_config
    -
    -
    -
    -
    -

    Inherited members

    - -
    -
    -class StatusReasonResponseBaseModel[Literal['LINKING_TO_SESSION_USER_FAILED'], Literal['EMAIL_VERIFICATION_REQUIRED', 'RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR', 'ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR', 'SESSION_USER_ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR', 'INPUT_USER_IS_NOT_A_PRIMARY_USER']] -(**data: Any) -
    -
    -

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

    -

    Create a new model by parsing and validating input data from keyword arguments.

    -

    Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be -validated to form a valid model.

    -

    self is explicitly positional-only to allow self as a field name.

    -

    Ancestors

    - -

    Subclasses

    - -

    Class variables

    -
    -
    var model_config
    -
    -
    -
    -
    -

    Inherited members

    - -
    -
    -class StatusReasonResponseBaseModel[Literal['RECOVER_ACCOUNT_NOT_ALLOWED'], str] -(**data: Any) -
    -
    -

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

    -

    Create a new model by parsing and validating input data from keyword arguments.

    -

    Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be -validated to form a valid model.

    -

    self is explicitly positional-only to allow self as a field name.

    -

    Ancestors

    - -

    Subclasses

    - -

    Class variables

    -
    -
    var model_config
    -
    -
    -
    -
    -

    Inherited members

    - -
    -
    -class StatusReasonResponseBaseModel[Literal['REGISTER_CREDENTIAL_NOT_ALLOWED'], str] -(**data: Any) -
    -
    -

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

    -

    Create a new model by parsing and validating input data from keyword arguments.

    -

    Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be -validated to form a valid model.

    -

    self is explicitly positional-only to allow self as a field name.

    -

    Ancestors

    - -

    Subclasses

    - -

    Class variables

    -
    -
    var model_config
    -
    -
    -
    -
    -

    Inherited members

    - -
    -
    -class StatusReasonResponseBaseModel[Literal['SIGN_IN_NOT_ALLOWED'], str] -(**data: Any) -
    -
    -

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

    -

    Create a new model by parsing and validating input data from keyword arguments.

    -

    Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be -validated to form a valid model.

    -

    self is explicitly positional-only to allow self as a field name.

    -

    Ancestors

    - -

    Subclasses

    - -

    Class variables

    -
    -
    var model_config
    -
    -
    -
    -
    -

    Inherited members

    - -
    -
    -class StatusReasonResponseBaseModel[Literal['SIGN_UP_NOT_ALLOWED'], str] -(**data: Any) -
    -
    -

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

    -

    Create a new model by parsing and validating input data from keyword arguments.

    -

    Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be -validated to form a valid model.

    -

    self is explicitly positional-only to allow self as a field name.

    -

    Ancestors

    - -

    Subclasses

    - -

    Class variables

    -
    -
    var model_config
    -
    -
    -
    -
    -

    Inherited members

    - -
    -
    -class StatusResponseBaseModel -(**data: Any) -
    -
    -

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

    -

    Create a new model by parsing and validating input data from keyword arguments.

    -

    Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be -validated to form a valid model.

    -

    self is explicitly positional-only to allow self as a field name.

    -
    - -Expand source code - -
    class StatusResponseBaseModel(CamelCaseBaseModel, Generic[Status]):
    -    status: Status
    -
    -

    Ancestors

    - -

    Subclasses

    - -

    Class variables

    -
    -
    var model_config
    -
    -
    -
    -
    var status : ~Status
    -
    -
    -
    -
    -

    Inherited members

    - -
    -
    -class StatusResponseBaseModel[Literal['CREDENTIAL_NOT_FOUND_ERROR']] +
    +class StatusReasonResponseBaseModel (**data: Any)
    @@ -1136,6 +723,15 @@

    Inherited members

    Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

    self is explicitly positional-only to allow self as a field name.

    +
    + +Expand source code + +
    class StatusReasonResponseBaseModel(
    +    StatusResponseBaseModel[Status], Generic[Status, Reason]
    +):
    +    reason: Reason
    +

    Ancestors

    Subclasses

    Class variables

    -
    var model_config
    +
    var model_config
    -
    -

    Inherited members

    - -
    -
    -class StatusResponseBaseModel[Literal['EMAIL_ALREADY_EXISTS_ERROR']] -(**data: Any) -
    -
    -

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

    -

    Create a new model by parsing and validating input data from keyword arguments.

    -

    Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be -validated to form a valid model.

    -

    self is explicitly positional-only to allow self as a field name.

    -

    Ancestors

    - -

    Subclasses

    - -

    Class variables

    -
    -
    var model_config
    +
    var reason : ~Reason
    @@ -1207,8 +771,8 @@

    Inherited members

    -
    -class StatusResponseBaseModel[Literal['INVALID_CREDENTIALS_ERROR']] +
    +class StatusReasonResponseBaseModel[Literal['LINKING_TO_SESSION_USER_FAILED'], Literal['EMAIL_VERIFICATION_REQUIRED', 'RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR', 'ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR', 'SESSION_USER_ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR', 'INPUT_USER_IS_NOT_A_PRIMARY_USER']] (**data: Any)
    @@ -1220,6 +784,7 @@

    Inherited members

    self is explicitly positional-only to allow self as a field name.

    Ancestors

    Subclasses

    Class variables

    -
    var model_config
    +
    var model_config

    Inherited members

    -
    -class StatusResponseBaseModel[Literal['INVALID_OPTIONS_ERROR']] +
    +class StatusResponseBaseModel (**data: Any)
    @@ -1259,9 +824,15 @@

    Inherited members

    Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

    self is explicitly positional-only to allow self as a field name.

    +
    + +Expand source code + +
    class StatusResponseBaseModel(CamelCaseBaseModel, Generic[Status]):
    +    status: Status
    +

    Ancestors

    Subclasses

      -
    • InvalidOptionsErrorResponse
    • +
    • StatusErrResponseBaseModel
    • +
    • StatusReasonResponseBaseModel
    • +
    • supertokens_python.types.response.StatusResponseBaseModel[Literal['CREDENTIAL_NOT_FOUND_ERROR']]
    • +
    • supertokens_python.types.response.StatusResponseBaseModel[Literal['EMAIL_ALREADY_EXISTS_ERROR']]
    • +
    • supertokens_python.types.response.StatusResponseBaseModel[Literal['INVALID_CREDENTIALS_ERROR']]
    • +
    • supertokens_python.types.response.StatusResponseBaseModel[Literal['INVALID_OPTIONS_ERROR']]
    • +
    • StatusResponseBaseModel[Literal['OK']]
    • +
    • supertokens_python.types.response.StatusResponseBaseModel[Literal['OPTIONS_NOT_FOUND_ERROR']]
    • +
    • supertokens_python.types.response.StatusResponseBaseModel[Literal['RECOVER_ACCOUNT_TOKEN_INVALID_ERROR']]
    • +
    • supertokens_python.types.response.StatusResponseBaseModel[Literal['UNKNOWN_USER_ID_ERROR']]

    Class variables

    -
    var model_config
    +
    var model_config
    +
    +
    +
    +
    var status : ~Status

    Inherited members

    @@ -1330,129 +914,6 @@

    Inherited members

    -
    -class StatusResponseBaseModel[Literal['OPTIONS_NOT_FOUND_ERROR']] -(**data: Any) -
    -
    -

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

    -

    Create a new model by parsing and validating input data from keyword arguments.

    -

    Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be -validated to form a valid model.

    -

    self is explicitly positional-only to allow self as a field name.

    -

    Ancestors

    - -

    Subclasses

    - -

    Class variables

    -
    -
    var model_config
    -
    -
    -
    -
    -

    Inherited members

    - -
    -
    -class StatusResponseBaseModel[Literal['RECOVER_ACCOUNT_TOKEN_INVALID_ERROR']] -(**data: Any) -
    -
    -

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

    -

    Create a new model by parsing and validating input data from keyword arguments.

    -

    Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be -validated to form a valid model.

    -

    self is explicitly positional-only to allow self as a field name.

    -

    Ancestors

    - -

    Subclasses

    - -

    Class variables

    -
    -
    var model_config
    -
    -
    -
    -
    -

    Inherited members

    - -
    -
    -class StatusResponseBaseModel[Literal['UNKNOWN_USER_ID_ERROR']] -(**data: Any) -
    -
    -

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

    -

    Create a new model by parsing and validating input data from keyword arguments.

    -

    Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be -validated to form a valid model.

    -

    self is explicitly positional-only to allow self as a field name.

    -

    Ancestors

    - -

    Subclasses

    - -

    Class variables

    -
    -
    var model_config
    -
    -
    -
    -
    -

    Inherited members

    - -
    @@ -1522,12 +983,6 @@

    StatusErrResponseBaseModel[Literal['INVALID_EMAIL_ERROR']]

    - - -
  • StatusReasonResponseBaseModel

    diff --git a/supertokens_python/constants.py b/supertokens_python/constants.py index 34c8a4118..16eaaa5d9 100644 --- a/supertokens_python/constants.py +++ b/supertokens_python/constants.py @@ -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 a49243b44..dc6aac2ac 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 from supertokens_python.types.config import ( BaseConfig, BaseNormalisedConfig, @@ -231,7 +232,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 @@ -271,6 +274,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 0af997881..dd1f03750 100644 --- a/supertokens_python/recipe/multitenancy/interfaces.py +++ b/supertokens_python/recipe/multitenancy/interfaces.py @@ -331,6 +331,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 @@ -349,12 +359,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]: @@ -363,6 +375,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, } diff --git a/tests/test_utils.py b/tests/test_utils.py index e5321ccf3..fe832a40c 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -220,6 +220,10 @@ def test_tldextract_http_toggle( ): import socket + import certifi + + fs.add_real_file(certifi.where()) + # Disable sockets, will raise errors on HTTP calls socket_patch = patch.object(socket, "socket", side_effect=RuntimeError) environ_patch = patch.dict(