Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix validation of credantials with pydantic v1 #186

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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': # type: ignore[attr-defined]
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