Skip to content

Commit

Permalink
Fix validation of credantials with pydantic v1
Browse files Browse the repository at this point in the history
  • Loading branch information
bschoenmaeckers committed Nov 9, 2023
1 parent 494373e commit b018ee9
Show file tree
Hide file tree
Showing 5 changed files with 86 additions and 47 deletions.
42 changes: 42 additions & 0 deletions tests/test_verify_authentication_response.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from webauthn import verify_authentication_response
from webauthn.helpers import base64url_to_bytes, parse_authentication_credential_json
from webauthn.helpers.exceptions import InvalidAuthenticationResponse
from webauthn.helpers.structs import AuthenticationCredential, PYDANTIC_V2


class TestVerifyAuthenticationResponse(TestCase):
Expand Down Expand Up @@ -290,3 +291,44 @@ def test_supports_dict_credential(self) -> None:
)

assert verification.new_sign_count == 1

def test_supports_credential_pydantic_validated_credential(self):
credential = {
"id": "ZoIKP1JQvKdrYj1bTUPJ2eTUsbLeFkv-X5xJQNr4k6s",
"rawId": "ZoIKP1JQvKdrYj1bTUPJ2eTUsbLeFkv-X5xJQNr4k6s",
"response": {
"authenticatorData": "SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFAAAAAQ",
"clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiaVBtQWkxUHAxWEw2b0FncTNQV1p0WlBuWmExekZVRG9HYmFRMF9LdlZHMWxGMnMzUnRfM280dVN6Y2N5MHRtY1RJcFRUVDRCVTFULUk0bWFhdm5kalEiLCJvcmlnaW4iOiJodHRwOi8vbG9jYWxob3N0OjUwMDAiLCJjcm9zc09yaWdpbiI6ZmFsc2V9",
"signature": "iOHKX3erU5_OYP_r_9HLZ-CexCE4bQRrxM8WmuoKTDdhAnZSeTP0sjECjvjfeS8MJzN1ArmvV0H0C3yy_FdRFfcpUPZzdZ7bBcmPh1XPdxRwY747OrIzcTLTFQUPdn1U-izCZtP_78VGw9pCpdMsv4CUzZdJbEcRtQuRS03qUjqDaovoJhOqEBmxJn9Wu8tBi_Qx7A33RbYjlfyLm_EDqimzDZhyietyop6XUcpKarKqVH0M6mMrM5zTjp8xf3W7odFCadXEJg-ERZqFM0-9Uup6kJNLbr6C5J4NDYmSm3HCSA6lp2iEiMPKU8Ii7QZ61kybXLxsX4w4Dm3fOLjmDw",
"userHandle": "T1RWa1l6VXdPRFV0WW1NNVlTMDBOVEkxTFRnd056Z3RabVZpWVdZNFpEVm1ZMk5p"
},
"type": "public-key",
"clientExtensionResults": {}
}

if PYDANTIC_V2:
parsed_credential = AuthenticationCredential.model_validate(credential)
else:
parsed_credential = AuthenticationCredential.validate(credential)

challenge = base64url_to_bytes(
"iPmAi1Pp1XL6oAgq3PWZtZPnZa1zFUDoGbaQ0_KvVG1lF2s3Rt_3o4uSzccy0tmcTIpTTT4BU1T-I4maavndjQ"
)
expected_rp_id = "localhost"
expected_origin = "http://localhost:5000"
credential_public_key = base64url_to_bytes(
"pAEDAzkBACBZAQDfV20epzvQP-HtcdDpX-cGzdOxy73WQEvsU7Dnr9UWJophEfpngouvgnRLXaEUn_d8HGkp_HIx8rrpkx4BVs6X_B6ZjhLlezjIdJbLbVeb92BaEsmNn1HW2N9Xj2QM8cH-yx28_vCjf82ahQ9gyAr552Bn96G22n8jqFRQKdVpO-f-bvpvaP3IQ9F5LCX7CUaxptgbog1SFO6FI6ob5SlVVB00lVXsaYg8cIDZxCkkENkGiFPgwEaZ7995SCbiyCpUJbMqToLMgojPkAhWeyktu7TlK6UBWdJMHc3FPAIs0lH_2_2hKS-mGI1uZAFVAfW1X-mzKL0czUm2P1UlUox7IUMBAAE"
)
sign_count = 0

verification = verify_authentication_response(
credential=parsed_credential,
expected_challenge=challenge,
expected_rp_id=expected_rp_id,
expected_origin=expected_origin,
credential_public_key=credential_public_key,
credential_current_sign_count=sign_count,
require_user_verification=True,
)

assert verification.new_sign_count == 1
40 changes: 39 additions & 1 deletion tests/test_verify_registration_response.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,17 @@

import cbor2
from pydantic import ValidationError

from webauthn import verify_registration_response
from webauthn.helpers import base64url_to_bytes, bytes_to_base64url, parse_registration_credential_json
from webauthn.helpers.exceptions import InvalidRegistrationResponse, InvalidCBORData
from webauthn.helpers.known_root_certs import globalsign_r2
from webauthn.helpers.structs import (
AttestationFormat,
PublicKeyCredentialType,
RegistrationCredential,
PYDANTIC_V2,
)
from webauthn import verify_registration_response


class TestVerifyRegistrationResponse(TestCase):
Expand Down Expand Up @@ -252,6 +255,41 @@ def test_supports_dict_credential(self) -> None:

assert verification.fmt == AttestationFormat.NONE

def test_supports_pydantic_validated_credential(self) -> None:
credential = {
"id": "9y1xA8Tmg1FEmT-c7_fvWZ_uoTuoih3OvR45_oAK-cwHWhAbXrl2q62iLVTjiyEZ7O7n-CROOY494k7Q3xrs_w",
"rawId": "9y1xA8Tmg1FEmT-c7_fvWZ_uoTuoih3OvR45_oAK-cwHWhAbXrl2q62iLVTjiyEZ7O7n-CROOY494k7Q3xrs_w",
"response": {
"attestationObject": "o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVjESZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NFAAAAFwAAAAAAAAAAAAAAAAAAAAAAQPctcQPE5oNRRJk_nO_371mf7qE7qIodzr0eOf6ACvnMB1oQG165dqutoi1U44shGezu5_gkTjmOPeJO0N8a7P-lAQIDJiABIVggSFbUJF-42Ug3pdM8rDRFu_N5oiVEysPDB6n66r_7dZAiWCDUVnB39FlGypL-qAoIO9xWHtJygo2jfDmHl-_eKFRLDA",
"clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiVHdON240V1R5R0tMYzRaWS1xR3NGcUtuSE00bmdscXN5VjBJQ0psTjJUTzlYaVJ5RnRya2FEd1V2c3FsLWdrTEpYUDZmbkYxTWxyWjUzTW00UjdDdnciLCJvcmlnaW4iOiJodHRwOi8vbG9jYWxob3N0OjUwMDAiLCJjcm9zc09yaWdpbiI6ZmFsc2V9"
},
"type": "public-key",
"clientExtensionResults": {},
"transports": [
"cable"
]
}

if PYDANTIC_V2:
parsed_credential = RegistrationCredential.model_validate(credential)
else:
parsed_credential = RegistrationCredential.validate(credential)

challenge = base64url_to_bytes(
"TwN7n4WTyGKLc4ZY-qGsFqKnHM4nglqsyV0ICJlN2TO9XiRyFtrkaDwUvsql-gkLJXP6fnF1MlrZ53Mm4R7Cvw"
)
rp_id = "localhost"
expected_origin = "http://localhost:5000"

verification = verify_registration_response(
credential=parsed_credential,
expected_challenge=challenge,
expected_origin=expected_origin,
expected_rp_id=rp_id,
)

assert verification.fmt == AttestationFormat.NONE

def test_raises_useful_error_on_bad_attestation_object(self) -> None:
credential = {
"id": "9y1xA8Tmg1FEmT-c7_fvWZ_uoTuoih3OvR45_oAK-cwHWhAbXrl2q62iLVTjiyEZ7O7n-CROOY494k7Q3xrs_w",
Expand Down
2 changes: 0 additions & 2 deletions webauthn/helpers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
from .generate_challenge import generate_challenge
from .generate_user_handle import generate_user_handle
from .hash_by_alg import hash_by_alg
from .json_loads_base64url_to_bytes import json_loads_base64url_to_bytes
from .options_to_json import options_to_json
from .parse_attestation_object import parse_attestation_object
from .parse_authentication_credential_json import parse_authentication_credential_json
Expand All @@ -28,7 +27,6 @@
"generate_challenge",
"generate_user_handle",
"hash_by_alg",
"json_loads_base64url_to_bytes",
"options_to_json",
"parse_attestation_object",
"parse_authenticator_data",
Expand Down
39 changes: 0 additions & 39 deletions webauthn/helpers/json_loads_base64url_to_bytes.py

This file was deleted.

10 changes: 5 additions & 5 deletions webauthn/helpers/structs.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from enum import Enum
from typing import Callable, List, Literal, Optional, Any, Dict


try:
from pydantic import ( # type: ignore[attr-defined]
BaseModel,
Expand All @@ -21,7 +20,6 @@
from .base64url_to_bytes import base64url_to_bytes
from .bytes_to_base64url import bytes_to_base64url
from .cose import COSEAlgorithmIdentifier
from .json_loads_base64url_to_bytes import json_loads_base64url_to_bytes
from .snake_case_to_camel_case import snake_case_to_camel_case


Expand Down Expand Up @@ -85,7 +83,7 @@ def _pydantic_v2_validate_bytes_fields(
"""
field = cls.model_fields[info.field_name] # type: ignore[attr-defined]

if field.annotation != bytes:
if field.annotation != bytes or info.field_name == 'user_handle':
return v

if isinstance(v, str):
Expand Down Expand Up @@ -117,7 +115,6 @@ def _pydantic_v2_serialize_bytes_fields(

class Config:
json_encoders = {bytes: bytes_to_base64url}
json_loads = json_loads_base64url_to_bytes
alias_generator = snake_case_to_camel_case
allow_population_by_field_name = True

Expand All @@ -128,9 +125,12 @@ def _pydantic_v1_validate_bytes_fields(cls, v: Any, field: ModelField) -> Any:
specify bytes-adjacent values (bytes subclasses, memoryviews, etc...) that otherwise
function like `bytes`. Keeps the library Pythonic.
"""
if field.type_ != bytes:
if field.type_ != bytes or field.name == 'user_handle':
return v

if isinstance(v, str):
return base64url_to_bytes(v)

return _to_bytes(v)


Expand Down

0 comments on commit b018ee9

Please sign in to comment.