From 8dba9764ceae14ef8d90f052e40a5c8b564dce12 Mon Sep 17 00:00:00 2001 From: Chris Wagner Date: Thu, 12 Oct 2023 08:45:42 -0700 Subject: [PATCH] Update to latest webauthn (1.11) (#851) * Update py_webauthn (1.11) Use new 1.11 helpers - parse_registration_credential_json and parse_authentication_credential_json Note in release notes that for now - must use pydantic 1.x with webauthn until user_handle issue solved. --- .pre-commit-config.yaml | 4 ++-- CHANGES.rst | 8 ++++++-- flask_security/webauthn.py | 14 ++++++++------ pyproject.toml | 5 +++-- pytest.ini | 1 + requirements/tests.txt | 3 ++- tests/test_webauthn.py | 20 ++++++++++---------- 7 files changed, 32 insertions(+), 23 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6646b77f..6a2f0c5e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,7 +4,7 @@ default_language_version: python: python3.9 repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 + rev: v4.5.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer @@ -14,7 +14,7 @@ repos: - id: check-merge-conflict - id: fix-byte-order-marker - repo: https://github.com/asottile/pyupgrade - rev: v3.14.0 + rev: v3.15.0 hooks: - id: pyupgrade args: [--py38-plus] diff --git a/CHANGES.rst b/CHANGES.rst index 4b45c734..afbbdb65 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -8,8 +8,12 @@ Version 5.3.1 Released October XX, 2023 -Please Note: If your application uses webauthn you must pin py_webauthn to 1.9.0 -until the issue with user_handle is resolved. +**Please Note:** + +- If your application uses webauthn you must use pydantic < 2.0 + until the issue with user_handle is resolved. +- If you want to use the latest Flask (3.0.0) you need to have Flask-Login changes - + those aren't currently released - use the 'main' branch. Fixes ++++++ diff --git a/flask_security/webauthn.py b/flask_security/webauthn.py index 03b2ccdd..479ddf5a 100644 --- a/flask_security/webauthn.py +++ b/flask_security/webauthn.py @@ -53,16 +53,18 @@ VerifiedAuthentication, ) from webauthn.registration.verify_registration_response import VerifiedRegistration + from webauthn.helpers import ( + parse_registration_credential_json, + parse_authentication_credential_json, + ) from webauthn.helpers.exceptions import ( InvalidAuthenticationResponse, InvalidRegistrationResponse, ) from webauthn.helpers.structs import ( - AuthenticationCredential, AuthenticatorTransport, PublicKeyCredentialDescriptor, PublicKeyCredentialType, - RegistrationCredential, UserVerificationRequirement, ) from webauthn.helpers import bytes_to_base64url @@ -168,8 +170,8 @@ def validate(self, **kwargs: t.Any) -> bool: self.credential.errors.append(msg) return False try: - reg_cred = RegistrationCredential.parse_raw(self.credential.data) - except (ValueError, KeyError): + reg_cred = parse_registration_credential_json(self.credential.data) + except (ValueError, KeyError, InvalidRegistrationResponse): self.credential.errors.append(get_message("API_ERROR")[0]) return False try: @@ -259,8 +261,8 @@ def validate(self, **kwargs: t.Any) -> bool: if not super().validate(**kwargs): return False # pragma: no cover try: - auth_cred = AuthenticationCredential.parse_raw(self.credential.data) - except (ValueError, KeyError): + auth_cred = parse_authentication_credential_json(self.credential.data) + except (ValueError, KeyError, InvalidAuthenticationResponse): self.credential.errors.append(get_message("API_ERROR")[0]) return False diff --git a/pyproject.toml b/pyproject.toml index 8cbdfa3d..8131b073 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,7 +55,7 @@ dependencies = [ babel = ["babel>=2.12.1", "flask_babel>=3.1.0"] fsqla = ["flask_sqlalchemy>=3.0.3", "sqlalchemy>=2.0.12", "sqlalchemy-utils>=0.41.1"] common = ["bcrypt>=4.0.1", "flask_mailman>=0.3.0", "bleach>=6.0.0"] -mfa = ["cryptography>=40.0.2", "qrcode>=7.4.2", "phonenumberslite>=8.13.11", "webauthn>=1.9.0"] +mfa = ["cryptography>=40.0.2", "qrcode>=7.4.2", "phonenumberslite>=8.13.11", "webauthn>=1.11.0"] low = [ # Lowest supported versions "Flask==2.3.2", @@ -78,12 +78,13 @@ low = [ "mongomock==4.1.2", "pony==0.7.16;python_version<'3.11'", "phonenumberslite==8.13.11", + "pydantic<2.0", "qrcode==7.4.2", # authlib requires requests "requests", "sqlalchemy==2.0.12", "sqlalchemy-utils==0.41.1", - "webauthn==1.9.0", + "webauthn==1.11.0", "werkzeug==2.3.3", "zxcvbn==4.4.28" ] diff --git a/pytest.ini b/pytest.ini index ddd1196a..6272c1f1 100644 --- a/pytest.ini +++ b/pytest.ini @@ -19,6 +19,7 @@ filterwarnings = error ignore::DeprecationWarning:mongoengine: ignore::DeprecationWarning:flask_login:0 + ignore::DeprecationWarning:pydantic_core:0 ignore:.*passwordless feature.*:DeprecationWarning:flask_security:0 ignore::DeprecationWarning:passlib:0 ignore:.*'sms' was enabled in SECURITY_US_ENABLED_METHODS;.*:UserWarning:flask_security:0 diff --git a/requirements/tests.txt b/requirements/tests.txt index e5ef2766..2a94b680 100644 --- a/requirements/tests.txt +++ b/requirements/tests.txt @@ -29,6 +29,7 @@ qrcode requests sqlalchemy sqlalchemy-utils -webauthn==1.9.0 +webauthn +pydantic<2.0 werkzeug zxcvbn diff --git a/tests/test_webauthn.py b/tests/test_webauthn.py index 3d1a31fd..cdccfe16 100644 --- a/tests/test_webauthn.py +++ b/tests/test_webauthn.py @@ -1177,20 +1177,20 @@ def test_reset(app, client): @pytest.mark.settings(webauthn_util_cls=HackWebauthnUtil) -def test_user_handle(app, client, get_message): +def test_user_handle(app, clients, get_message): """Test that we fail signin if user_handle doesn't match. Since we generated the SIGNIN_DATA_OH from view_scaffold - the user_handle has no way of matching. """ - authenticate(client) - register_options, response_url = _register_start_json(client, usage="first") - response = client.post(response_url, json=dict(credential=json.dumps(REG_DATA_UH))) + authenticate(clients) + register_options, response_url = _register_start_json(clients, usage="first") + response = clients.post(response_url, json=dict(credential=json.dumps(REG_DATA_UH))) assert response.status_code == 200 # verify can't sign in - logout(client) - signin_options, response_url, _ = _signin_start_json(client, "matt@lp.com") - response = client.post( + logout(clients) + signin_options, response_url, _ = _signin_start_json(clients, "matt@lp.com") + response = clients.post( response_url, json=dict(credential=json.dumps(SIGNIN_DATA_UH)) ) assert response.json["response"]["field_errors"]["credential"][0].encode( @@ -1210,12 +1210,12 @@ def test_user_handle(app, client, get_message): ) upd_signin_data = copy.deepcopy(SIGNIN_DATA_UH) upd_signin_data["response"]["userHandle"] = b64_user_handle - signin_options, response_url, _ = _signin_start_json(client, "matt@lp.com") - response = client.post( + signin_options, response_url, _ = _signin_start_json(clients, "matt@lp.com") + response = clients.post( response_url, json=dict(credential=json.dumps(upd_signin_data)) ) # verify actually logged in - response = client.get("/profile", headers={"accept": "application/json"}) + response = clients.get("/profile", headers={"accept": "application/json"}) assert response.status_code == 200