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:
Katie Smith
2021-05-17 12:37:04 +01:00
committed by Leo Hemsted
parent 28ee2a1f9a
commit d9fd37a485
2 changed files with 89 additions and 8 deletions

View File

@@ -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

View File

@@ -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',
}