mirror of
https://github.com/GSA/notifications-admin.git
synced 2026-07-28 11:49:13 -04:00
add test for succesfully logging in with security key
this is a bit complex, but essentially we're using the test variables defined in the duolabs py_webauthn library [1]. We're already using their test variables in tests/app/models/test_webauthn_credential.py and in the webauthn_credential fixture in conftest.py. By using sample signature, authenticatordata and clientdatajson from the same key we can test that the library correctly verifies the signed challenge matches the original. We needed to transform some of this data as the yubico/fido2 library we use has a slightly different way of formatting the fields for the request body, which is why we're doing things like base64 decoding and converting from hex to bytes in the post data. The pytest fixture has changed - before it was incomplete/corrupted and would error when trying to verify the signature. We took the credential_data from the pytest fixture, converted it to an AttestedCredentialData using WebauthnCredential.to_credential_data, modified the public_key private dictionary to add `public_key[-1]: 1`, and then called `AttestedCredentialData.create` to re-CBOR-encode the blob. The `-1: 1` is the numeric ID of the "SECP256R1" elliptic curve algorithm. The py_webauthn library forces this particular algorithm, which differs from the sample creds we took from the fido2 lib tests, which is why we've had to update our data. [1] https://github.com/duo-labs/py_webauthn/blob/master/tests/test_webauthn.py#L13-L32
This commit is contained in:
@@ -1,8 +1,35 @@
|
||||
import base64
|
||||
|
||||
import pytest
|
||||
from fido2 import cbor
|
||||
from flask import url_for
|
||||
|
||||
from app.models.webauthn_credential import RegistrationError
|
||||
from app.models.webauthn_credential import RegistrationError, WebAuthnCredential
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def webauthn_authentication_post_data(fake_uuid, webauthn_credential, client):
|
||||
"""
|
||||
Sets up session, challenge, etc as if a user with uuid `fake_uuid` has logged in and touched the webauthn token
|
||||
as found in the `webauthn_credential` fixture. Sets up the session as if `begin_authentication` had been called
|
||||
so that the challenge matches and the credential will validate (provided that the key belongs to the user referenced
|
||||
in the session).
|
||||
"""
|
||||
with client.session_transaction() as session:
|
||||
session['user_details'] = {'id': fake_uuid}
|
||||
session['webauthn_authentication_state'] = {
|
||||
"challenge": "e-g-nXaRxMagEiqTJSyD82RsEc5if_6jyfJDy8bNKlw",
|
||||
"user_verification": None
|
||||
}
|
||||
|
||||
credential_id = WebAuthnCredential(webauthn_credential).to_credential_data().credential_id
|
||||
|
||||
return cbor.encode({
|
||||
'credentialId': credential_id,
|
||||
'authenticatorData': base64.b64decode(b'dKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvABAAACfQ=='),
|
||||
'clientDataJSON': b'{"challenge":"e-g-nXaRxMagEiqTJSyD82RsEc5if_6jyfJDy8bNKlw","origin":"https://webauthn.io","type":"webauthn.get"}', # noqa
|
||||
'signature': bytes.fromhex('304502204a76f05cd52a778cdd4df1565e0004e5cc1ead360419d0f5c3a0143bf37e7f15022100932b5c308a560cfe4f244214843075b904b3eda64e85d64662a81198c386cdde'), # noqa
|
||||
})
|
||||
|
||||
|
||||
@pytest.mark.parametrize('endpoint', [
|
||||
@@ -218,9 +245,63 @@ def test_begin_authentication_stores_state_in_session(client, mocker, webauthn_c
|
||||
assert 'challenge' in session['webauthn_authentication_state']
|
||||
|
||||
|
||||
def test_complete_authentication_403s_if_key_isnt_in_users_credentials(client):
|
||||
pass
|
||||
def test_complete_authentication_checks_credentials(
|
||||
client,
|
||||
mocker,
|
||||
webauthn_credential,
|
||||
webauthn_dev_server,
|
||||
mock_create_event,
|
||||
webauthn_authentication_post_data,
|
||||
platform_admin_user
|
||||
):
|
||||
platform_admin_user['auth_type'] = 'webauthn_auth'
|
||||
mocker.patch('app.user_api_client.get_user', return_value=platform_admin_user)
|
||||
mocker.patch('app.user_api_client.get_webauthn_credentials_for_user', return_value=[webauthn_credential])
|
||||
|
||||
response = client.post(url_for('main.webauthn_complete_authentication'), data=webauthn_authentication_post_data)
|
||||
assert response.status_code == 302
|
||||
|
||||
|
||||
def test_complete_authentication_clears_session(client):
|
||||
pass
|
||||
def test_complete_authentication_403s_if_key_isnt_in_users_credentials(
|
||||
client,
|
||||
mocker,
|
||||
webauthn_credential,
|
||||
webauthn_dev_server,
|
||||
webauthn_authentication_post_data,
|
||||
platform_admin_user
|
||||
):
|
||||
platform_admin_user['auth_type'] = 'webauthn_auth'
|
||||
mocker.patch('app.user_api_client.get_user', return_value=platform_admin_user)
|
||||
# user has no keys in the database
|
||||
mocker.patch('app.user_api_client.get_webauthn_credentials_for_user', return_value=[])
|
||||
|
||||
response = client.post(url_for('main.webauthn_complete_authentication'), data=webauthn_authentication_post_data)
|
||||
assert response.status_code == 403
|
||||
|
||||
with client.session_transaction() as session:
|
||||
assert session['user_details']['id'] == platform_admin_user['id']
|
||||
# user not logged in
|
||||
assert 'user_id' not in session
|
||||
# webauthn state reset so can't replay
|
||||
assert 'webauthn_authentication_state' not in session
|
||||
|
||||
|
||||
def test_complete_authentication_clears_session(
|
||||
client,
|
||||
mocker,
|
||||
webauthn_credential,
|
||||
webauthn_dev_server,
|
||||
webauthn_authentication_post_data,
|
||||
mock_create_event,
|
||||
platform_admin_user
|
||||
):
|
||||
platform_admin_user['auth_type'] = 'webauthn_auth'
|
||||
mocker.patch('app.user_api_client.get_user', return_value=platform_admin_user)
|
||||
mocker.patch('app.user_api_client.get_webauthn_credentials_for_user', return_value=[webauthn_credential])
|
||||
|
||||
response = client.post(url_for('main.webauthn_complete_authentication'), data=webauthn_authentication_post_data)
|
||||
assert response.status_code == 302
|
||||
|
||||
with client.session_transaction() as session:
|
||||
# it's important that we clear the session to ensure that we don't re-use old login artifacts in future
|
||||
assert 'webauthn_authentication_state' not in session
|
||||
|
||||
@@ -3963,7 +3963,7 @@ def create_active_user_manage_template_permissions(with_unique_id=False):
|
||||
}
|
||||
|
||||
|
||||
def create_platform_admin_user(with_unique_id=False, permissions=None):
|
||||
def create_platform_admin_user(with_unique_id=False, auth_type='sms_auth', permissions=None):
|
||||
return {
|
||||
'id': str(uuid4()) if with_unique_id else sample_uuid(),
|
||||
'name': 'Platform admin user',
|
||||
@@ -3974,7 +3974,7 @@ def create_platform_admin_user(with_unique_id=False, permissions=None):
|
||||
'failed_login_count': 0,
|
||||
'permissions': permissions or {},
|
||||
'platform_admin': True,
|
||||
'auth_type': 'sms_auth',
|
||||
'auth_type': auth_type,
|
||||
'password_changed_at': str(datetime.utcnow()),
|
||||
'services': [],
|
||||
'organisations': [],
|
||||
@@ -4481,7 +4481,7 @@ def webauthn_credential():
|
||||
return {
|
||||
'id': str(uuid4()),
|
||||
'name': 'Test credential',
|
||||
'credential_data': 'WJ0AAAAAAAAAAAAAAAAAAAAAAECKU1ppjl9gmhHWyDkgHsUvZmhr6oF3/lD3llzLE2SaOSgOGIsIuAQqgp8JQSUu3r/oOaP8RS44dlQjrH+ALfYtpAECAyYhWCAxnqAfESXOYjKUc2WACuXZ3ch0JHxV0VFrrTyjyjIHXCJYIFnx8H87L4bApR4M+hPcV+fHehEOeW+KCyd0H+WGY8s6', # noqa
|
||||
'credential_data': 'WJ8AAAAAAAAAAAAAAAAAAAAAAECKU1ppjl9gmhHWyDkgHsUvZmhr6oF3/lD3llzLE2SaOSgOGIsIuAQqgp8JQSUu3r/oOaP8RS44dlQjrH+ALfYtpQECAyYgASFYIDGeoB8RJc5iMpRzZYAK5dndyHQkfFXRUWutPKPKMgdcIlggWfHwfzsvhsClHgz6E9xX58d6EQ55b4oLJ3Qf5YZjyzo=', # noqa
|
||||
'registration_response': 'anything',
|
||||
'created_at': '2017-10-18T16:57:14.154185Z',
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user