feat: add WebAuthnService with fido2 registration and authentication

This commit is contained in:
Johan Lundberg 2026-02-13 14:48:38 +01:00
parent e6f5ea7f0c
commit 872001c6de
No known key found for this signature in database
GPG key ID: A6C152738D03C7D1
2 changed files with 301 additions and 0 deletions

View file

@ -0,0 +1,76 @@
from collections.abc import Sequence
from typing import Any
from fido2.server import Fido2Server
from fido2.webauthn import (
AttestedCredentialData,
AuthenticationResponse,
AuthenticatorData,
PublicKeyCredentialDescriptor,
PublicKeyCredentialRpEntity,
PublicKeyCredentialUserEntity,
RegistrationResponse,
)
class WebAuthnService:
"""FIDO2 WebAuthn registration and authentication."""
def __init__(self, rp_id: str, rp_name: str, origin: str) -> None:
rp = PublicKeyCredentialRpEntity(name=rp_name, id=rp_id)
self._server = Fido2Server(rp, verify_origin=lambda o: o == origin)
def begin_registration(
self,
user_id: bytes,
username: str,
existing_credentials: Sequence[PublicKeyCredentialDescriptor] | None = None,
) -> tuple[dict[str, Any], dict[str, Any]]:
"""Begin WebAuthn registration.
Returns (options_dict, state_dict).
options_dict is JSON-serializable for sending to the browser.
state_dict must be stored by the caller and passed to complete_registration.
"""
user = PublicKeyCredentialUserEntity(id=user_id, name=username, display_name=username)
options, state = self._server.register_begin(
user=user,
credentials=existing_credentials,
)
return dict(options), state
def complete_registration(
self,
state: dict[str, Any],
response: RegistrationResponse | dict[str, Any],
) -> AuthenticatorData:
"""Complete WebAuthn registration.
Returns AuthenticatorData with credential_data containing the public key.
"""
return self._server.register_complete(state, response)
def begin_authentication(
self,
credentials: Sequence[PublicKeyCredentialDescriptor] | None = None,
) -> tuple[dict[str, Any], dict[str, Any]]:
"""Begin WebAuthn authentication.
Returns (options_dict, state_dict).
"""
options, state = self._server.authenticate_begin(credentials=credentials)
return dict(options), state
def complete_authentication(
self,
state: dict[str, Any],
credentials: Sequence[AttestedCredentialData],
response: AuthenticationResponse | dict[str, Any],
) -> AttestedCredentialData:
"""Complete WebAuthn authentication.
Verifies the assertion signature against stored credentials.
Returns the matched AttestedCredentialData.
Raises on verification failure.
"""
return self._server.authenticate_complete(state, credentials, response)