feat: add WebAuthnService with fido2 registration and authentication
This commit is contained in:
parent
e6f5ea7f0c
commit
872001c6de
2 changed files with 301 additions and 0 deletions
76
src/fastapi_oidc_op/authn/webauthn.py
Normal file
76
src/fastapi_oidc_op/authn/webauthn.py
Normal 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)
|
||||
Loading…
Add table
Add a link
Reference in a new issue