mirror of
https://github.com/GSA/notifications-admin.git
synced 2026-08-23 15:57:23 -04:00
Integrate with the API for adding and getting webauthn creds
This links up the `get_webauthn_credentials_for_user` and
`create_webauthn_credential_for_user` methods of the user api client to
notifications-api.
To send data to the API we need strings to be unicode, so we call
decode('utf-8') on base64 objects.
Co-authored-by: Leo Hemsted <leo.hemsted@digital.cabinet-office.gov.uk>
This commit is contained in:
@@ -32,15 +32,15 @@ class WebAuthnCredential(JSONModel):
|
|||||||
'name': 'Unnamed key',
|
'name': 'Unnamed key',
|
||||||
'credential_data': base64.b64encode(
|
'credential_data': base64.b64encode(
|
||||||
cbor.encode(auth_data.credential_data),
|
cbor.encode(auth_data.credential_data),
|
||||||
),
|
).decode('utf-8'),
|
||||||
'registration_response': base64.b64encode(
|
'registration_response': base64.b64encode(
|
||||||
cbor.encode(response),
|
cbor.encode(response),
|
||||||
)
|
).decode('utf-8')
|
||||||
})
|
})
|
||||||
|
|
||||||
def to_credential_data(self):
|
def to_credential_data(self):
|
||||||
return AttestedCredentialData(
|
return AttestedCredentialData(
|
||||||
cbor.decode(base64.b64decode(self.credential_data))
|
cbor.decode(base64.b64decode(self.credential_data.encode()))
|
||||||
)
|
)
|
||||||
|
|
||||||
def serialize(self):
|
def serialize(self):
|
||||||
|
|||||||
@@ -192,19 +192,14 @@ class UserApiClient(NotifyAdminAPIClient):
|
|||||||
return self.get(endpoint)
|
return self.get(endpoint)
|
||||||
|
|
||||||
def get_webauthn_credentials_for_user(self, user_id):
|
def get_webauthn_credentials_for_user(self, user_id):
|
||||||
# TODO: remove when using real API
|
endpoint = f'/user/{user_id}/webauthn'
|
||||||
self.credentials = getattr(self, 'credentials', [])
|
|
||||||
return self.credentials
|
return self.get(endpoint)['data']
|
||||||
|
|
||||||
def create_webauthn_credential_for_user(self, user_id, credential):
|
def create_webauthn_credential_for_user(self, user_id, credential):
|
||||||
self.credentials = getattr(self, 'credentials', [])
|
endpoint = f'/user/{user_id}/webauthn'
|
||||||
credential_dict = credential.serialize()
|
|
||||||
|
|
||||||
# TODO: remove when using real API
|
return self.post(endpoint, data=credential.serialize())
|
||||||
from datetime import datetime
|
|
||||||
credential_dict['created_at'] = datetime.now().strftime("%Y-%m-%dT%H:%M:%S.%fZ")
|
|
||||||
|
|
||||||
self.credentials += [credential_dict]
|
|
||||||
|
|
||||||
|
|
||||||
user_api_client = UserApiClient()
|
user_api_client = UserApiClient()
|
||||||
|
|||||||
@@ -19,8 +19,10 @@ def test_should_show_overview_page(
|
|||||||
|
|
||||||
def test_overview_page_shows_disable_for_platform_admin(
|
def test_overview_page_shows_disable_for_platform_admin(
|
||||||
client_request,
|
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)
|
client_request.login(platform_admin_user)
|
||||||
page = client_request.get('main.user_profile')
|
page = client_request.get('main.user_profile')
|
||||||
assert page.select_one('h1').text.strip() == 'Your profile'
|
assert page.select_one('h1').text.strip() == 'Your profile'
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ def test_begin_register_returns_encoded_options(
|
|||||||
values={'ADMIN_BASE_URL': 'http://localhost:6012'}
|
values={'ADMIN_BASE_URL': 'http://localhost:6012'}
|
||||||
)
|
)
|
||||||
webauthn_server.init_app(app_)
|
webauthn_server.init_app(app_)
|
||||||
|
mocker.patch('app.user_api_client.get_webauthn_credentials_for_user', return_value=[])
|
||||||
|
|
||||||
response = platform_admin_client.get(
|
response = platform_admin_client.get(
|
||||||
url_for('main.webauthn_begin_register')
|
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(
|
def test_begin_register_stores_state_in_session(
|
||||||
platform_admin_client,
|
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')
|
url_for('main.webauthn_begin_register')
|
||||||
)
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
with platform_admin_client.session_transaction() as session:
|
with platform_admin_client.session_transaction() as session:
|
||||||
assert session['webauthn_registration_state'] is not None
|
assert session['webauthn_registration_state'] is not None
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import base64
|
import base64
|
||||||
|
|
||||||
|
import pytest
|
||||||
from fido2 import cbor
|
from fido2 import cbor
|
||||||
from fido2.cose import ES256
|
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
|
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(
|
mocker.patch.dict(
|
||||||
app_.config, values={
|
app_.config, values={
|
||||||
'NOTIFY_ENVIRONMENT': 'development',
|
'NOTIFY_ENVIRONMENT': 'development',
|
||||||
@@ -25,6 +27,8 @@ def test_from_registration_verifies_response(app_, mocker):
|
|||||||
# disable origin verification for non-HTTPS test
|
# disable origin verification for non-HTTPS test
|
||||||
webauthn_server.init_app(app_)
|
webauthn_server.init_app(app_)
|
||||||
|
|
||||||
|
|
||||||
|
def test_from_registration_verifies_response(disable_webauthn_origin_verification):
|
||||||
registration_response = {
|
registration_response = {
|
||||||
'clientDataJSON': CLIENT_DATA_JSON,
|
'clientDataJSON': CLIENT_DATA_JSON,
|
||||||
'attestationObject': ATTESTATION_OBJECT,
|
'attestationObject': ATTESTATION_OBJECT,
|
||||||
@@ -32,9 +36,23 @@ def test_from_registration_verifies_response(app_, mocker):
|
|||||||
|
|
||||||
credential = WebAuthnCredential.from_registration(SESSION_STATE, registration_response)
|
credential = WebAuthnCredential.from_registration(SESSION_STATE, registration_response)
|
||||||
assert credential.name == 'Unnamed key'
|
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()
|
credential_data = credential.to_credential_data()
|
||||||
assert type(credential_data.credential_id) is bytes
|
assert type(credential_data.credential_id) is bytes
|
||||||
assert type(credential_data.aaguid) is bytes
|
assert type(credential_data.aaguid) is bytes
|
||||||
assert credential_data.public_key[3] == ES256.ALGORITHM
|
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
|
||||||
|
|||||||
@@ -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():
|
def test_get_webauthn_credentials_for_user(mocker, webauthn_credential, fake_uuid):
|
||||||
credentials = user_api_client.get_webauthn_credentials_for_user('id')
|
|
||||||
assert len(credentials) == 0
|
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)
|
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())
|
||||||
|
|||||||
@@ -4485,7 +4485,7 @@ def mock_get_invited_org_user_by_id(mocker, sample_org_invite):
|
|||||||
def webauthn_credential():
|
def webauthn_credential():
|
||||||
return {
|
return {
|
||||||
'name': 'Test credential',
|
'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',
|
'registration_response': 'anything',
|
||||||
'created_at': '2017-10-18T16:57:14.154185Z',
|
'created_at': '2017-10-18T16:57:14.154185Z',
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user