diff --git a/authentik/stages/authenticator_validate/challenge.py b/authentik/stages/authenticator_validate/challenge.py index d883b903a..229309fe1 100644 --- a/authentik/stages/authenticator_validate/challenge.py +++ b/authentik/stages/authenticator_validate/challenge.py @@ -1,4 +1,5 @@ """Validation stage challenge checking""" +from json import loads from typing import Optional from urllib.parse import urlencode @@ -10,11 +11,12 @@ from django.utils.translation import gettext_lazy as _ from rest_framework.fields import CharField from rest_framework.serializers import ValidationError from structlog.stdlib import get_logger +from webauthn import options_to_json from webauthn.authentication.generate_authentication_options import generate_authentication_options from webauthn.authentication.verify_authentication_response import verify_authentication_response from webauthn.helpers.base64url_to_bytes import base64url_to_bytes from webauthn.helpers.exceptions import InvalidAuthenticationResponse -from webauthn.helpers.structs import AuthenticationCredential +from webauthn.helpers.structs import UserVerificationRequirement from authentik.core.api.utils import JSONDictField, PassiveSerializer from authentik.core.models import Application, User @@ -66,12 +68,7 @@ def get_webauthn_challenge_without_user( ) request.session[SESSION_KEY_WEBAUTHN_CHALLENGE] = authentication_options.challenge - return authentication_options.model_dump( - mode="json", - by_alias=True, - exclude_unset=False, - exclude_none=True, - ) + return loads(options_to_json(authentication_options)) def get_webauthn_challenge( @@ -91,17 +88,12 @@ def get_webauthn_challenge( authentication_options = generate_authentication_options( rp_id=get_rp_id(request), allow_credentials=allowed_credentials, - user_verification=stage.webauthn_user_verification, + user_verification=UserVerificationRequirement(stage.webauthn_user_verification), ) request.session[SESSION_KEY_WEBAUTHN_CHALLENGE] = authentication_options.challenge - return authentication_options.model_dump( - mode="json", - by_alias=True, - exclude_unset=False, - exclude_none=True, - ) + return loads(options_to_json(authentication_options)) def select_challenge(request: HttpRequest, device: Device): @@ -152,7 +144,7 @@ def validate_challenge_webauthn(data: dict, stage_view: StageView, user: User) - try: authentication_verification = verify_authentication_response( - credential=AuthenticationCredential.model_validate(data), + credential=data, expected_challenge=challenge, expected_rp_id=get_rp_id(request), expected_origin=get_origin(request), diff --git a/authentik/stages/authenticator_webauthn/stage.py b/authentik/stages/authenticator_webauthn/stage.py index 35d424a00..1101fbe23 100644 --- a/authentik/stages/authenticator_webauthn/stage.py +++ b/authentik/stages/authenticator_webauthn/stage.py @@ -1,14 +1,18 @@ """WebAuthn stage""" +from json import loads + from django.http import HttpRequest, HttpResponse from django.http.request import QueryDict from rest_framework.fields import CharField from rest_framework.serializers import ValidationError +from webauthn import options_to_json from webauthn.helpers.bytes_to_base64url import bytes_to_base64url from webauthn.helpers.exceptions import InvalidRegistrationResponse from webauthn.helpers.structs import ( AuthenticatorSelectionCriteria, PublicKeyCredentialCreationOptions, - RegistrationCredential, + ResidentKeyRequirement, + UserVerificationRequirement, ) from webauthn.registration.generate_registration_options import generate_registration_options from webauthn.registration.verify_registration_response import ( @@ -53,7 +57,7 @@ class AuthenticatorWebAuthnChallengeResponse(ChallengeResponse): try: registration: VerifiedRegistration = verify_registration_response( - credential=RegistrationCredential.model_validate(response), + credential=response, expected_challenge=challenge, expected_rp_id=get_rp_id(self.request), expected_origin=get_origin(self.request), @@ -91,12 +95,12 @@ class AuthenticatorWebAuthnStageView(ChallengeStageView): registration_options: PublicKeyCredentialCreationOptions = generate_registration_options( rp_id=get_rp_id(self.request), rp_name=self.request.tenant.branding_title, - user_id=user.uid, + user_id=user.uid.encode("utf-8"), user_name=user.username, user_display_name=user.name, authenticator_selection=AuthenticatorSelectionCriteria( - resident_key=str(stage.resident_key_requirement), - user_verification=str(stage.user_verification), + resident_key=ResidentKeyRequirement(str(stage.resident_key_requirement)), + user_verification=UserVerificationRequirement(str(stage.user_verification)), authenticator_attachment=authenticator_attachment, ), ) @@ -106,12 +110,7 @@ class AuthenticatorWebAuthnStageView(ChallengeStageView): return AuthenticatorWebAuthnChallenge( data={ "type": ChallengeTypes.NATIVE.value, - "registration": registration_options.model_dump( - mode="json", - by_alias=True, - exclude_unset=False, - exclude_none=True, - ), + "registration": loads(options_to_json(registration_options)), } ) diff --git a/poetry.lock b/poetry.lock index 561c1df95..3223fe16d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -4153,21 +4153,20 @@ files = [ [[package]] name = "webauthn" -version = "1.11.1" +version = "2.0.0" description = "Pythonic WebAuthn" optional = false python-versions = "*" files = [ - {file = "webauthn-1.11.1-py3-none-any.whl", hash = "sha256:13592ee71489b571cb6e4a5d8b3c34f7b040cd3539a9d94b6b7d23fa88df5dfb"}, - {file = "webauthn-1.11.1.tar.gz", hash = "sha256:24eda57903897369797f52a377f8c470e7057e79da5525779d0720a9fcc11926"}, + {file = "webauthn-2.0.0-py3-none-any.whl", hash = "sha256:644dc68af5caaade06be6a2a2278775e85116e92dd755ad7a49d992d51c82033"}, + {file = "webauthn-2.0.0.tar.gz", hash = "sha256:12cc1759da98668b8242badc37c4129df300f89d89f5c183fac80e7b33c41dfd"}, ] [package.dependencies] asn1crypto = ">=1.4.0" cbor2 = ">=5.4.6" -cryptography = ">=41.0.4" -pydantic = ">=1.10.11" -pyOpenSSL = ">=23.2.0" +cryptography = ">=41.0.7" +pyOpenSSL = ">=23.3.0" [[package]] name = "websocket-client"