Skip to content

Commit 989155e

Browse files
authored
feat: webauthn (#583)
### Adds Webauthn (Passkeys) support - Adds Webauthn recipe with support for: - Registration, sign-in, and credential verification flows - Account recovery - Adds new API endpoints for WebAuthn operations: - GET `/api/webauthn/email/exists` - Check if email exists in system - POST `/api/webauthn/options/register` - Handle registration options - POST `/api/webauthn/options/signin` - Handle sign-in options - POST `/api/webauthn/signin` - Handle WebAuthn sign-in - POST `/api/webauthn/signup` - Handle WebAuthn sign-up - POST `/api/user/webauthn/reset` - Handle account recovery - POST `/api/user/webauthn/reset/token` - Generate recovery tokens - Adds WebAuthn support to account linking functionality: - Support for linking users based on WebAuthn `credential_id` - Updates `AccountInfo` type to `AccountInfoInput` with WebAuthn fields - Adds `has_same_webauthn_info_as` method for credential comparison - Adds FDI support for version `4.1` - Recipe functions are directly importable from the Webauthn recipe module - ```python from supertokens_python.recipe.webauthn import sign_in await sign_in(...) # Async sign_in.sync(...) # Sync ``` ### Breaking Changes - Updates supported CDI version from `5.2` to `5.3` - Changes `AccountInfo` to `AccountInfoInput` in various methods - This is required to allow querying by a single Webauthn `credential_id`, while the Webauthn login method contains an array of `credential_ids` - Affected functions: - `supertokens_python.asyncio.list_users_by_account_info` - `supertokens_python.syncio.list_users_by_account_info` - `supertokens_python.recipe.accountlinking.interface.RecipeInterface.list_users_by_account_info` - `supertokens_python.recipe.accountlinking.recipe_implementation.RecipeImplementation.list_users_by_account_info`
1 parent 1d431dc commit 989155e

File tree

130 files changed

+7913
-265
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

130 files changed

+7913
-265
lines changed

.github/workflows/backend-sdk-testing.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,10 +82,11 @@ jobs:
8282
run: |
8383
source venv/bin/activate
8484
docker compose up --build --wait
85-
python3 tests/test-server/app.py &
85+
python3 tests/test-server/app.py &> python.log &
8686
8787
- uses: supertokens/backend-sdk-testing-action@main
8888
with:
8989
version: ${{ matrix.fdi-version }}
9090
check-name-suffix: '[CDI=${{ matrix.cdi-version }}][Core=${{ steps.versions.outputs.coreVersionXy }}][FDI=${{ matrix.fdi-version }}][Py=${{ matrix.py-version }}][Node=${{ matrix.node-version }}]'
9191
path: backend-sdk-testing
92+
app-server-logs: ${{ github.workspace }}/supertokens-python/python.log

.github/workflows/lint-code.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ jobs:
1919
outputs:
2020
pyVersions: '["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]'
2121

22+
steps:
23+
# Required to avoid errors in runs due to no steps
24+
- name: Placeholder
25+
run: echo "Placeholder"
26+
2227
lint-format:
2328
name: Check linting and formatting
2429
runs-on: ubuntu-latest

CHANGELOG.md

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

99
## [unreleased]
1010

11+
## [0.30.0] - 2025-05-27
12+
### Adds Webauthn (Passkeys) support
13+
- Adds Webauthn recipe with support for:
14+
- Registration, sign-in, and credential verification flows
15+
- Account recovery
16+
- Adds new API endpoints for WebAuthn operations:
17+
- GET `/api/webauthn/email/exists` - Check if email exists in system
18+
- POST `/api/webauthn/options/register` - Handle registration options
19+
- POST `/api/webauthn/options/signin` - Handle sign-in options
20+
- POST `/api/webauthn/signin` - Handle WebAuthn sign-in
21+
- POST `/api/webauthn/signup` - Handle WebAuthn sign-up
22+
- POST `/api/user/webauthn/reset` - Handle account recovery
23+
- POST `/api/user/webauthn/reset/token` - Generate recovery tokens
24+
- Adds WebAuthn support to account linking functionality:
25+
- Support for linking users based on WebAuthn `credential_id`
26+
- Updates `AccountInfo` type to `AccountInfoInput` with WebAuthn fields
27+
- Adds `has_same_webauthn_info_as` method for credential comparison
28+
- Adds FDI support for version `4.1`
29+
- Recipe functions are directly importable from the Webauthn recipe module
30+
- ```python
31+
from supertokens_python.recipe.webauthn import sign_in
32+
33+
await sign_in(...) # Async
34+
sign_in.sync(...) # Sync
35+
```
36+
37+
### Breaking Changes
38+
- Updates supported CDI version from `5.2` to `5.3`
39+
- Changes `AccountInfo` to `AccountInfoInput` in various methods
40+
- This is required to allow querying by a single Webauthn `credential_id`, while the Webauthn login method contains an array of `credential_ids`
41+
- Affected functions:
42+
- `supertokens_python.asyncio.list_users_by_account_info`
43+
- `supertokens_python.syncio.list_users_by_account_info`
44+
- `supertokens_python.recipe.accountlinking.interface.RecipeInterface.list_users_by_account_info`
45+
- `supertokens_python.recipe.accountlinking.recipe_implementation.RecipeImplementation.list_users_by_account_info`
46+
- `supertokens_python.recipe.passwordless.api.implementation.get_passwordless_user_by_account_info`
47+
- `supertokens_python.recipe.passwordless.api.implementation.get_passwordless_user_by_account_info`
48+
49+
1150
## [0.29.2] - 2025-05-19
1251
- Fixes cookies being set without expiry in Django
1352
- Reverts timezone change from 0.28.0 and uses GMT

coreDriverInterfaceSupported.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"_comment": "contains a list of core-driver interfaces branch names that this core supports",
33
"versions": [
4-
"5.2"
4+
"5.3"
55
]
66
}

dev-requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,5 @@ pyyaml==6.0.2
2121
requests-mock==1.12.1
2222
respx>=0.13.0, <1.0.0
2323
uvicorn==0.32.0
24+
wasmtime==25.0.0
2425
-e .

frontendDriverInterfaceSupported.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"2.0",
88
"3.0",
99
"3.1",
10-
"4.0"
10+
"4.0",
11+
"4.1"
1112
]
1213
}

setup.py

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

8383
setup(
8484
name="supertokens_python",
85-
version="0.29.2",
85+
version="0.30.0",
8686
author="SuperTokens",
8787
license="Apache 2.0",
8888
author_email="[email protected]",
@@ -127,6 +127,7 @@
127127
"pkce<1.1.0",
128128
"pyotp<3",
129129
"python-dateutil<3",
130+
"pydantic>=2.10.6,<3.0.0",
130131
],
131132
python_requires=">=3.8",
132133
include_package_data=True,

supertokens_python/async_to_sync_wrapper.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,20 @@
1313
# under the License.
1414

1515
import asyncio
16+
from functools import update_wrapper
1617
from os import getenv
17-
from typing import Any, Coroutine, TypeVar
18+
from typing import (
19+
Any,
20+
Callable,
21+
Coroutine,
22+
Generic,
23+
TypeVar,
24+
)
25+
26+
from typing_extensions import ParamSpec
27+
28+
Param = ParamSpec("Param")
29+
RetType = TypeVar("RetType", covariant=True)
1830

1931
_T = TypeVar("_T")
2032

@@ -43,3 +55,25 @@ def create_or_get_event_loop() -> asyncio.AbstractEventLoop:
4355
def sync(co: Coroutine[Any, Any, _T]) -> _T:
4456
loop = create_or_get_event_loop()
4557
return loop.run_until_complete(co)
58+
59+
60+
class syncify(Generic[Param, RetType]):
61+
"""
62+
Decorator to allow async functions to be executed synchronously
63+
using a `sync` attribute.
64+
"""
65+
66+
def __init__(self, func: Callable[Param, Coroutine[Any, Any, RetType]]):
67+
update_wrapper(self, func)
68+
self.func = func
69+
70+
def __call__(
71+
self, *args: Param.args, **kwargs: Param.kwargs
72+
) -> Coroutine[Any, Any, RetType]:
73+
return self.func(*args, **kwargs)
74+
75+
def sync(self, *args: Param.args, **kwargs: Param.kwargs) -> RetType:
76+
"""
77+
Synchronous version of the decorated function.
78+
"""
79+
return sync(self.func(*args, **kwargs))

supertokens_python/asyncio/__init__.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@
2626
)
2727
from supertokens_python.recipe.accountlinking.interfaces import GetUsersResult
2828
from supertokens_python.recipe.accountlinking.recipe import AccountLinkingRecipe
29-
from supertokens_python.types import AccountInfo, User
29+
from supertokens_python.types import User
30+
from supertokens_python.types.base import AccountInfoInput
3031

3132

3233
async def get_users_oldest_first(
@@ -159,7 +160,7 @@ async def update_or_delete_user_id_mapping_info(
159160

160161
async def list_users_by_account_info(
161162
tenant_id: str,
162-
account_info: AccountInfo,
163+
account_info: AccountInfoInput,
163164
do_union_of_account_info: bool = False,
164165
user_context: Optional[Dict[str, Any]] = None,
165166
) -> List[User]:

supertokens_python/auth_utils.py

Lines changed: 12 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Any, Awaitable, Callable, Dict, List, Optional, Union
1+
from typing import TYPE_CHECKING, Any, Awaitable, Callable, Dict, List, Optional, Union
22

33
from typing_extensions import Literal
44

@@ -31,37 +31,18 @@
3131
from supertokens_python.recipe.session.interfaces import SessionContainer
3232
from supertokens_python.recipe.thirdparty.types import ThirdPartyInfo
3333
from supertokens_python.types import (
34-
AccountInfo,
3534
LoginMethod,
3635
RecipeUserId,
3736
User,
3837
)
38+
from supertokens_python.types.auth_utils import LinkingToSessionUserFailedError
39+
from supertokens_python.types.base import AccountInfoInput
3940
from supertokens_python.utils import log_debug_message
4041

4142
from .asyncio import get_user
4243

43-
44-
class LinkingToSessionUserFailedError:
45-
status: Literal["LINKING_TO_SESSION_USER_FAILED"] = "LINKING_TO_SESSION_USER_FAILED"
46-
reason: Literal[
47-
"EMAIL_VERIFICATION_REQUIRED",
48-
"RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR",
49-
"ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR",
50-
"SESSION_USER_ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR",
51-
"INPUT_USER_IS_NOT_A_PRIMARY_USER",
52-
]
53-
54-
def __init__(
55-
self,
56-
reason: Literal[
57-
"EMAIL_VERIFICATION_REQUIRED",
58-
"RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR",
59-
"ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR",
60-
"SESSION_USER_ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR",
61-
"INPUT_USER_IS_NOT_A_PRIMARY_USER",
62-
],
63-
):
64-
self.reason = reason
44+
if TYPE_CHECKING:
45+
from supertokens_python.recipe.webauthn.types.base import WebauthnInfoInput
6546

6647

6748
class OkResponse:
@@ -290,6 +271,7 @@ async def get_authenticating_user_and_add_to_current_tenant_if_required(
290271
session: Optional[SessionContainer],
291272
check_credentials_on_tenant: Callable[[str], Awaitable[bool]],
292273
user_context: Dict[str, Any],
274+
webauthn: Optional["WebauthnInfoInput"] = None,
293275
) -> Optional[AuthenticatingUserInfo]:
294276
i = 0
295277
while i < 300:
@@ -303,8 +285,11 @@ async def get_authenticating_user_and_add_to_current_tenant_if_required(
303285
)
304286
existing_users = await AccountLinkingRecipe.get_instance().recipe_implementation.list_users_by_account_info(
305287
tenant_id=tenant_id,
306-
account_info=AccountInfo(
307-
email=email, phone_number=phone_number, third_party=third_party
288+
account_info=AccountInfoInput(
289+
email=email,
290+
phone_number=phone_number,
291+
third_party=third_party,
292+
webauthn=webauthn,
308293
),
309294
do_union_of_account_info=True,
310295
user_context=user_context,
@@ -324,6 +309,7 @@ async def get_authenticating_user_and_add_to_current_tenant_if_required(
324309
(email is not None and lm.has_same_email_as(email))
325310
or lm.has_same_phone_number_as(phone_number)
326311
or lm.has_same_third_party_info_as(third_party)
312+
or lm.has_same_webauthn_info_as(webauthn)
327313
)
328314
),
329315
None,

0 commit comments

Comments
 (0)