diff --git a/app/models/webauthn_credential.py b/app/models/webauthn_credential.py index 933d72698..97abb9dc1 100644 --- a/app/models/webauthn_credential.py +++ b/app/models/webauthn_credential.py @@ -32,15 +32,15 @@ class WebAuthnCredential(JSONModel): 'name': 'Unnamed key', 'credential_data': base64.b64encode( cbor.encode(auth_data.credential_data), - ), + ).decode('utf-8'), 'registration_response': base64.b64encode( cbor.encode(response), - ) + ).decode('utf-8') }) def to_credential_data(self): return AttestedCredentialData( - cbor.decode(base64.b64decode(self.credential_data)) + cbor.decode(base64.b64decode(self.credential_data.encode())) ) def serialize(self): diff --git a/app/notify_client/user_api_client.py b/app/notify_client/user_api_client.py index 09a1974b6..df9a2ba47 100644 --- a/app/notify_client/user_api_client.py +++ b/app/notify_client/user_api_client.py @@ -192,19 +192,14 @@ class UserApiClient(NotifyAdminAPIClient): return self.get(endpoint) def get_webauthn_credentials_for_user(self, user_id): - # TODO: remove when using real API - self.credentials = getattr(self, 'credentials', []) - return self.credentials + endpoint = f'/user/{user_id}/webauthn' + + return self.get(endpoint)['data'] def create_webauthn_credential_for_user(self, user_id, credential): - self.credentials = getattr(self, 'credentials', []) - credential_dict = credential.serialize() + endpoint = f'/user/{user_id}/webauthn' - # TODO: remove when using real API - from datetime import datetime - credential_dict['created_at'] = datetime.now().strftime("%Y-%m-%dT%H:%M:%S.%fZ") - - self.credentials += [credential_dict] + return self.post(endpoint, data=credential.serialize()) user_api_client = UserApiClient() diff --git a/tests/app/main/views/test_user_profile.py b/tests/app/main/views/test_user_profile.py index 700596559..b0c9d486b 100644 --- a/tests/app/main/views/test_user_profile.py +++ b/tests/app/main/views/test_user_profile.py @@ -19,8 +19,10 @@ def test_should_show_overview_page( def test_overview_page_shows_disable_for_platform_admin( client_request, - platform_admin_user + platform_admin_user, + mocker ): + mocker.patch('app.user_api_client.get_webauthn_credentials_for_user') client_request.login(platform_admin_user) page = client_request.get('main.user_profile') assert page.select_one('h1').text.strip() == 'Your profile' diff --git a/tests/app/main/views/test_webauthn_credentials.py b/tests/app/main/views/test_webauthn_credentials.py index 8ae74843a..f41aa62f7 100644 --- a/tests/app/main/views/test_webauthn_credentials.py +++ b/tests/app/main/views/test_webauthn_credentials.py @@ -27,6 +27,7 @@ def test_begin_register_returns_encoded_options( values={'ADMIN_BASE_URL': 'http://localhost:6012'} ) webauthn_server.init_app(app_) + mocker.patch('app.user_api_client.get_webauthn_credentials_for_user', return_value=[]) response = platform_admin_client.get( url_for('main.webauthn_begin_register') @@ -71,11 +72,18 @@ def test_begin_register_includes_existing_credentials( def test_begin_register_stores_state_in_session( platform_admin_client, + mocker, ): - platform_admin_client.get( + mocker.patch( + 'app.user_api_client.get_webauthn_credentials_for_user', + return_value=[]) + + response = platform_admin_client.get( url_for('main.webauthn_begin_register') ) + assert response.status_code == 200 + with platform_admin_client.session_transaction() as session: assert session['webauthn_registration_state'] is not None diff --git a/tests/app/models/test_webauthn_credential.py b/tests/app/models/test_webauthn_credential.py index 4dfd89be7..49e50bab9 100644 --- a/tests/app/models/test_webauthn_credential.py +++ b/tests/app/models/test_webauthn_credential.py @@ -1,5 +1,6 @@ import base64 +import pytest from fido2 import cbor from fido2.cose import ES256 @@ -14,7 +15,8 @@ CLIENT_DATA_JSON = b'{"type": "webauthn.create", "clientExtensions": {}, "challe ATTESTATION_OBJECT = base64.b64decode(b'o2NmbXRoZmlkby11MmZnYXR0U3RtdKJjc2lnWEgwRgIhAI1qbvWibQos/t3zsTU05IXw1Ek3SDApATok09uc4UBwAiEAv0fB/lgb5Ot3zJ691Vje6iQLAtLhJDiA8zDxaGjcE3hjeDVjgVkCUzCCAk8wggE3oAMCAQICBDxoKU0wDQYJKoZIhvcNAQELBQAwLjEsMCoGA1UEAxMjWXViaWNvIFUyRiBSb290IENBIFNlcmlhbCA0NTcyMDA2MzEwIBcNMTQwODAxMDAwMDAwWhgPMjA1MDA5MDQwMDAwMDBaMDExLzAtBgNVBAMMJll1YmljbyBVMkYgRUUgU2VyaWFsIDIzOTI1NzM0ODExMTE3OTAxMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEvd9nk9t3lMNQMXHtLE1FStlzZnUaSLql2fm1ajoggXlrTt8rzXuSehSTEPvEaEdv/FeSqX22L6Aoa8ajIAIOY6M7MDkwIgYJKwYBBAGCxAoCBBUxLjMuNi4xLjQuMS40MTQ4Mi4xLjUwEwYLKwYBBAGC5RwCAQEEBAMCBSAwDQYJKoZIhvcNAQELBQADggEBAKrADVEJfuwVpIazebzEg0D4Z9OXLs5qZ/ukcONgxkRZ8K04QtP/CB5x6olTlxsj+SXArQDCRzEYUgbws6kZKfuRt2a1P+EzUiqDWLjRILSr+3/o7yR7ZP/GpiFKwdm+czb94POoGD+TS1IYdfXj94mAr5cKWx4EKjh210uovu/pLdLjc8xkQciUrXzZpPR9rT2k/q9HkZhHU+NaCJzky+PTyDbq0KKnzqVhWtfkSBCGw3ezZkTS+5lrvOKbIa24lfeTgu7FST5OwTPCFn8HcfWZMXMSD/KNU+iBqJdAwTLPPDRoLLvPTl29weCAIh+HUpmBQd0UltcPOrA/LFvAf61oYXV0aERhdGFYwnSm6pITyZwvdLIkkrMgz0AmKpTBqVCgOX8pJQtghB7wQQAAAAAAAAAAAAAAAAAAAAAAAAAAAECKU1ppjl9gmhHWyDkgHsUvZmhr6oF3/lD3llzLE2SaOSgOGIsIuAQqgp8JQSUu3r/oOaP8RS44dlQjrH+ALfYtpAECAyYhWCAxnqAfESXOYjKUc2WACuXZ3ch0JHxV0VFrrTyjyjIHXCJYIFnx8H87L4bApR4M+hPcV+fHehEOeW+KCyd0H+WGY8s6') # noqa -def test_from_registration_verifies_response(app_, mocker): +@pytest.fixture +def disable_webauthn_origin_verification(app_, mocker): mocker.patch.dict( app_.config, values={ 'NOTIFY_ENVIRONMENT': 'development', @@ -25,6 +27,8 @@ def test_from_registration_verifies_response(app_, mocker): # disable origin verification for non-HTTPS test webauthn_server.init_app(app_) + +def test_from_registration_verifies_response(disable_webauthn_origin_verification): registration_response = { 'clientDataJSON': CLIENT_DATA_JSON, 'attestationObject': ATTESTATION_OBJECT, @@ -32,9 +36,23 @@ def test_from_registration_verifies_response(app_, mocker): credential = WebAuthnCredential.from_registration(SESSION_STATE, registration_response) assert credential.name == 'Unnamed key' - assert credential.registration_response == base64.b64encode(cbor.encode(registration_response)) + assert credential.registration_response == base64.b64encode(cbor.encode(registration_response)).decode('utf-8') credential_data = credential.to_credential_data() assert type(credential_data.credential_id) is bytes assert type(credential_data.aaguid) is bytes assert credential_data.public_key[3] == ES256.ALGORITHM + + +def test_from_registration_encodes_as_unicode(disable_webauthn_origin_verification): + registration_response = { + 'clientDataJSON': CLIENT_DATA_JSON, + 'attestationObject': ATTESTATION_OBJECT, + } + + credential = WebAuthnCredential.from_registration(SESSION_STATE, registration_response) + + serialized_credential = credential.serialize() + + assert type(serialized_credential['credential_data']) == str + assert type(serialized_credential['registration_response']) == str diff --git a/tests/app/notify_client/test_user_client.py b/tests/app/notify_client/test_user_client.py index 8fab217e8..f70b611d8 100644 --- a/tests/app/notify_client/test_user_client.py +++ b/tests/app/notify_client/test_user_client.py @@ -241,12 +241,25 @@ def test_add_user_to_service_calls_correct_endpoint_and_deletes_keys_from_cache( ] -def test_get_webauthn_credentials_for_user_returns_stubbed_data(): - credentials = user_api_client.get_webauthn_credentials_for_user('id') - assert len(credentials) == 0 +def test_get_webauthn_credentials_for_user(mocker, webauthn_credential, fake_uuid): + + mock_get = mocker.patch( + 'app.notify_client.user_api_client.UserApiClient.get', + return_value={'data': [webauthn_credential]} + ) + + credentials = user_api_client.get_webauthn_credentials_for_user(fake_uuid) + + mock_get.assert_called_once_with(f'/user/{fake_uuid}/webauthn') + assert len(credentials) == 1 + assert credentials[0]['name'] == 'Test credential' -def test_create_webauthn_credential_for_user_stores_stubbed_data(webauthn_credential): +def test_create_webauthn_credential_for_user(mocker, webauthn_credential, fake_uuid): credential = WebAuthnCredential(webauthn_credential) - user_api_client.create_webauthn_credential_for_user('id', credential) - assert len(user_api_client.credentials) == 1 + + mock_post = mocker.patch('app.notify_client.user_api_client.UserApiClient.post') + expected_url = f'/user/{fake_uuid}/webauthn' + + user_api_client.create_webauthn_credential_for_user(fake_uuid, credential) + mock_post.assert_called_once_with(expected_url, data=credential.serialize()) diff --git a/tests/conftest.py b/tests/conftest.py index 13238e333..36950f562 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4485,7 +4485,7 @@ def mock_get_invited_org_user_by_id(mocker, sample_org_invite): def webauthn_credential(): return { 'name': 'Test credential', - 'credential_data': b'WJ0AAAAAAAAAAAAAAAAAAAAAAECKU1ppjl9gmhHWyDkgHsUvZmhr6oF3/lD3llzLE2SaOSgOGIsIuAQqgp8JQSUu3r/oOaP8RS44dlQjrH+ALfYtpAECAyYhWCAxnqAfESXOYjKUc2WACuXZ3ch0JHxV0VFrrTyjyjIHXCJYIFnx8H87L4bApR4M+hPcV+fHehEOeW+KCyd0H+WGY8s6', # noqa + 'credential_data': 'WJ0AAAAAAAAAAAAAAAAAAAAAAECKU1ppjl9gmhHWyDkgHsUvZmhr6oF3/lD3llzLE2SaOSgOGIsIuAQqgp8JQSUu3r/oOaP8RS44dlQjrH+ALfYtpAECAyYhWCAxnqAfESXOYjKUc2WACuXZ3ch0JHxV0VFrrTyjyjIHXCJYIFnx8H87L4bApR4M+hPcV+fHehEOeW+KCyd0H+WGY8s6', # noqa 'registration_response': 'anything', 'created_at': '2017-10-18T16:57:14.154185Z', }