diff --git a/edb/lib/ext/auth.edgeql b/edb/lib/ext/auth.edgeql index fcdb69dcc50..8d69ce6f1d9 100644 --- a/edb/lib/ext/auth.edgeql +++ b/edb/lib/ext/auth.edgeql @@ -111,7 +111,7 @@ CREATE EXTENSION PACKAGE auth VERSION '1.0' { create required property challenge: std::bytes { create constraint exclusive; }; - create required link factor: ext::auth::WebAuthnFactor { + create required multi link factors: ext::auth::WebAuthnFactor { create constraint exclusive; }; }; diff --git a/edb/server/protocol/auth_ext/data.py b/edb/server/protocol/auth_ext/data.py index df0805b0fe8..a3fed0ec029 100644 --- a/edb/server/protocol/auth_ext/data.py +++ b/edb/server/protocol/auth_ext/data.py @@ -190,13 +190,14 @@ class WebAuthnAuthenticationChallenge: created_at: datetime.datetime modified_at: datetime.datetime challenge: bytes - factor: WebAuthnFactor + factors: list[WebAuthnFactor] - def __init__(self, *, id, created_at, modified_at, challenge, factor): + def __init__(self, *, id, created_at, modified_at, challenge, factors): self.id = id self.created_at = created_at self.modified_at = modified_at self.challenge = base64.b64decode(challenge) - self.factor = ( + self.factors = [ WebAuthnFactor(**factor) if isinstance(factor, dict) else factor - ) + for factor in factors + ] diff --git a/edb/server/protocol/auth_ext/webauthn.py b/edb/server/protocol/auth_ext/webauthn.py index 578fb5379e3..78737f4a3ee 100644 --- a/edb/server/protocol/auth_ext/webauthn.py +++ b/edb/server/protocol/auth_ext/webauthn.py @@ -315,18 +315,18 @@ async def create_authentication_options_for_email( challenge := $challenge, user_handle := $user_handle, email := $email, - factor := ( - assert_exists(assert_single(( + factors := ( + assert_exists(( select ext::auth::WebAuthnFactor filter .user_handle = user_handle and .email = email - ))) + )) ) insert ext::auth::WebAuthnAuthenticationChallenge { challenge := challenge, - factor := factor, + factors := factors, } -unless conflict on .factor +unless conflict on .factors else ( update ext::auth::WebAuthnAuthenticationChallenge set { @@ -389,7 +389,7 @@ async def _get_authentication_challenge( created_at, modified_at, challenge, - factor: { + factors: { id, created_at, modified_at, @@ -407,7 +407,7 @@ async def _get_authentication_challenge( } }, } -filter .factor.email = email and .factor.credential_id = credential_id;""", +filter .factors.email = email and .factors.credential_id = credential_id;""", variables={ "email": email, "credential_id": credential_id, @@ -437,7 +437,7 @@ async def _delete_authentication_challenges( email := $email, credential_id := $credential_id, delete ext::auth::WebAuthnAuthenticationChallenge -filter .factor.email = email and .factor.credential_id = credential_id;""", +filter .factors.email = email and .factors.credential_id = credential_id;""", variables={ "email": email, "credential_id": credential_id, @@ -461,13 +461,21 @@ async def authenticate( credential_id=credential.raw_id, ) + factor = next( + ( + f + for f in authentication_challenge.factors + if f.credential_id == credential.raw_id + ), + None, + ) + assert factor is not None, "Missing factor for the given credential." + try: webauthn.verify_authentication_response( credential=credential, expected_challenge=authentication_challenge.challenge, - credential_public_key=( - authentication_challenge.factor.public_key - ), + credential_public_key=factor.public_key, credential_current_sign_count=0, expected_rp_id=self.provider.relying_party_id, expected_origin=self.provider.relying_party_origin, @@ -477,4 +485,4 @@ async def authenticate( "Invalid authentication response. Please retry authentication." ) - return authentication_challenge.factor.identity + return factor.identity diff --git a/tests/test_http_ext_auth.py b/tests/test_http_ext_auth.py index defe9a81707..929e6460f99 100644 --- a/tests/test_http_ext_auth.py +++ b/tests/test_http_ext_auth.py @@ -3807,8 +3807,8 @@ async def test_http_auth_ext_webauthn_authenticate_options(self): SELECT EXISTS ( SELECT ext::auth::WebAuthnAuthenticationChallenge filter .challenge = $challenge - AND .factor.email = $email - AND .factor.user_handle = $user_handle + AND .factors.email = $email + AND .factors.user_handle = $user_handle ) ''', challenge=challenge_bytes,