2021-05-17 12:37:04 +01:00
|
|
|
|
import base64
|
2021-05-25 15:50:14 +01:00
|
|
|
|
from unittest.mock import ANY, Mock
|
2021-05-17 12:37:04 +01:00
|
|
|
|
|
Support registering a new authenticator
This adds Yubico's FIDO2 library and two APIs for working with the
"navigator.credentials.create()" function in JavaScript. The GET
API uses the library to generate options for the "create()" function,
and the POST API decodes and verifies the resulting credential. While
the options and response are dict-like, CBOR is necessary to encode
some of the byte-level values, which can't be represented in JSON.
Much of the code here is based on the Yubico library example [1][2].
Implementation notes:
- There are definitely better ways to alert the user about failure, but
window.alert() will do for the time being. Using location.reload() is
also a bit jarring if the page scrolls, but not a major issue.
- Ideally we would use window.fetch() to do AJAX calls, but we don't
have a polyfill for this, and we use $.ajax() elsewhere [3]. We need
to do a few weird tricks [6] to stop jQuery trashing the data.
- The FIDO2 server doesn't serve web requests; it's just a "server" in
the sense of WebAuthn terminology. It lives in its own module, since it
needs to be initialised with the app / config.
- $.ajax returns a promise-like object. Although we've used ".fail()"
elsewhere [3], I couldn't find a stub object that supports it, so I've
gone for ".catch()", and used a Promise stub object in tests.
- WebAuthn only works over HTTPS, but there's an exception for "localhost"
[4]. However, the library is a bit too strict [5], so we have to disable
origin verification to avoid needing HTTPS for dev work.
[1]: https://github.com/Yubico/python-fido2/blob/c42d9628a4f33d20c4401096fa8d3fc466d5b77f/examples/server/server.py
[2]: https://github.com/Yubico/python-fido2/blob/c42d9628a4f33d20c4401096fa8d3fc466d5b77f/examples/server/static/register.html
[3]: https://github.com/alphagov/notifications-admin/blob/91453d36395b7a0cf2998dfb8a5f52cc9e96640f/app/assets/javascripts/updateContent.js#L33
[4]: https://stackoverflow.com/questions/55971593/navigator-credentials-is-null-on-local-server
[5]: https://github.com/Yubico/python-fido2/blob/c42d9628a4f33d20c4401096fa8d3fc466d5b77f/fido2/rpid.py#L69
[6]: https://stackoverflow.com/questions/12394622/does-jquery-ajax-or-load-allow-for-responsetype-arraybuffer
2021-05-07 18:10:07 +01:00
|
|
|
|
import pytest
|
|
|
|
|
|
from fido2 import cbor
|
|
|
|
|
|
from flask import url_for
|
|
|
|
|
|
|
2021-05-17 12:37:04 +01:00
|
|
|
|
from app.models.webauthn_credential import RegistrationError, WebAuthnCredential
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
2022-01-04 18:33:23 +00:00
|
|
|
|
def webauthn_authentication_post_data(fake_uuid, webauthn_credential, client_request):
|
2022-01-04 15:40:42 +00:00
|
|
|
|
|
2022-01-04 18:33:23 +00:00
|
|
|
|
_set_up_webauthn_session(fake_uuid, client_request)
|
2022-01-04 15:40:42 +00:00
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _set_up_webauthn_session(user_id, client):
|
2021-05-17 12:37:04 +01:00
|
|
|
|
"""
|
|
|
|
|
|
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:
|
2022-01-04 15:40:42 +00:00
|
|
|
|
session['user_details'] = {'id': user_id}
|
2021-05-17 12:37:04 +01:00
|
|
|
|
session['webauthn_authentication_state'] = {
|
|
|
|
|
|
"challenge": "e-g-nXaRxMagEiqTJSyD82RsEc5if_6jyfJDy8bNKlw",
|
|
|
|
|
|
"user_verification": None
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Support registering a new authenticator
This adds Yubico's FIDO2 library and two APIs for working with the
"navigator.credentials.create()" function in JavaScript. The GET
API uses the library to generate options for the "create()" function,
and the POST API decodes and verifies the resulting credential. While
the options and response are dict-like, CBOR is necessary to encode
some of the byte-level values, which can't be represented in JSON.
Much of the code here is based on the Yubico library example [1][2].
Implementation notes:
- There are definitely better ways to alert the user about failure, but
window.alert() will do for the time being. Using location.reload() is
also a bit jarring if the page scrolls, but not a major issue.
- Ideally we would use window.fetch() to do AJAX calls, but we don't
have a polyfill for this, and we use $.ajax() elsewhere [3]. We need
to do a few weird tricks [6] to stop jQuery trashing the data.
- The FIDO2 server doesn't serve web requests; it's just a "server" in
the sense of WebAuthn terminology. It lives in its own module, since it
needs to be initialised with the app / config.
- $.ajax returns a promise-like object. Although we've used ".fail()"
elsewhere [3], I couldn't find a stub object that supports it, so I've
gone for ".catch()", and used a Promise stub object in tests.
- WebAuthn only works over HTTPS, but there's an exception for "localhost"
[4]. However, the library is a bit too strict [5], so we have to disable
origin verification to avoid needing HTTPS for dev work.
[1]: https://github.com/Yubico/python-fido2/blob/c42d9628a4f33d20c4401096fa8d3fc466d5b77f/examples/server/server.py
[2]: https://github.com/Yubico/python-fido2/blob/c42d9628a4f33d20c4401096fa8d3fc466d5b77f/examples/server/static/register.html
[3]: https://github.com/alphagov/notifications-admin/blob/91453d36395b7a0cf2998dfb8a5f52cc9e96640f/app/assets/javascripts/updateContent.js#L33
[4]: https://stackoverflow.com/questions/55971593/navigator-credentials-is-null-on-local-server
[5]: https://github.com/Yubico/python-fido2/blob/c42d9628a4f33d20c4401096fa8d3fc466d5b77f/fido2/rpid.py#L69
[6]: https://stackoverflow.com/questions/12394622/does-jquery-ajax-or-load-allow-for-responsetype-arraybuffer
2021-05-07 18:10:07 +01:00
|
|
|
|
|
2021-06-30 15:30:29 +01:00
|
|
|
|
def test_begin_register_forbidden_unless_can_use_webauthn(
|
Support registering a new authenticator
This adds Yubico's FIDO2 library and two APIs for working with the
"navigator.credentials.create()" function in JavaScript. The GET
API uses the library to generate options for the "create()" function,
and the POST API decodes and verifies the resulting credential. While
the options and response are dict-like, CBOR is necessary to encode
some of the byte-level values, which can't be represented in JSON.
Much of the code here is based on the Yubico library example [1][2].
Implementation notes:
- There are definitely better ways to alert the user about failure, but
window.alert() will do for the time being. Using location.reload() is
also a bit jarring if the page scrolls, but not a major issue.
- Ideally we would use window.fetch() to do AJAX calls, but we don't
have a polyfill for this, and we use $.ajax() elsewhere [3]. We need
to do a few weird tricks [6] to stop jQuery trashing the data.
- The FIDO2 server doesn't serve web requests; it's just a "server" in
the sense of WebAuthn terminology. It lives in its own module, since it
needs to be initialised with the app / config.
- $.ajax returns a promise-like object. Although we've used ".fail()"
elsewhere [3], I couldn't find a stub object that supports it, so I've
gone for ".catch()", and used a Promise stub object in tests.
- WebAuthn only works over HTTPS, but there's an exception for "localhost"
[4]. However, the library is a bit too strict [5], so we have to disable
origin verification to avoid needing HTTPS for dev work.
[1]: https://github.com/Yubico/python-fido2/blob/c42d9628a4f33d20c4401096fa8d3fc466d5b77f/examples/server/server.py
[2]: https://github.com/Yubico/python-fido2/blob/c42d9628a4f33d20c4401096fa8d3fc466d5b77f/examples/server/static/register.html
[3]: https://github.com/alphagov/notifications-admin/blob/91453d36395b7a0cf2998dfb8a5f52cc9e96640f/app/assets/javascripts/updateContent.js#L33
[4]: https://stackoverflow.com/questions/55971593/navigator-credentials-is-null-on-local-server
[5]: https://github.com/Yubico/python-fido2/blob/c42d9628a4f33d20c4401096fa8d3fc466d5b77f/fido2/rpid.py#L69
[6]: https://stackoverflow.com/questions/12394622/does-jquery-ajax-or-load-allow-for-responsetype-arraybuffer
2021-05-07 18:10:07 +01:00
|
|
|
|
client_request,
|
2021-06-30 15:30:29 +01:00
|
|
|
|
platform_admin_user,
|
|
|
|
|
|
mocker,
|
Support registering a new authenticator
This adds Yubico's FIDO2 library and two APIs for working with the
"navigator.credentials.create()" function in JavaScript. The GET
API uses the library to generate options for the "create()" function,
and the POST API decodes and verifies the resulting credential. While
the options and response are dict-like, CBOR is necessary to encode
some of the byte-level values, which can't be represented in JSON.
Much of the code here is based on the Yubico library example [1][2].
Implementation notes:
- There are definitely better ways to alert the user about failure, but
window.alert() will do for the time being. Using location.reload() is
also a bit jarring if the page scrolls, but not a major issue.
- Ideally we would use window.fetch() to do AJAX calls, but we don't
have a polyfill for this, and we use $.ajax() elsewhere [3]. We need
to do a few weird tricks [6] to stop jQuery trashing the data.
- The FIDO2 server doesn't serve web requests; it's just a "server" in
the sense of WebAuthn terminology. It lives in its own module, since it
needs to be initialised with the app / config.
- $.ajax returns a promise-like object. Although we've used ".fail()"
elsewhere [3], I couldn't find a stub object that supports it, so I've
gone for ".catch()", and used a Promise stub object in tests.
- WebAuthn only works over HTTPS, but there's an exception for "localhost"
[4]. However, the library is a bit too strict [5], so we have to disable
origin verification to avoid needing HTTPS for dev work.
[1]: https://github.com/Yubico/python-fido2/blob/c42d9628a4f33d20c4401096fa8d3fc466d5b77f/examples/server/server.py
[2]: https://github.com/Yubico/python-fido2/blob/c42d9628a4f33d20c4401096fa8d3fc466d5b77f/examples/server/static/register.html
[3]: https://github.com/alphagov/notifications-admin/blob/91453d36395b7a0cf2998dfb8a5f52cc9e96640f/app/assets/javascripts/updateContent.js#L33
[4]: https://stackoverflow.com/questions/55971593/navigator-credentials-is-null-on-local-server
[5]: https://github.com/Yubico/python-fido2/blob/c42d9628a4f33d20c4401096fa8d3fc466d5b77f/fido2/rpid.py#L69
[6]: https://stackoverflow.com/questions/12394622/does-jquery-ajax-or-load-allow-for-responsetype-arraybuffer
2021-05-07 18:10:07 +01:00
|
|
|
|
):
|
2021-06-30 15:30:29 +01:00
|
|
|
|
platform_admin_user['can_use_webauthn'] = False
|
|
|
|
|
|
mocker.patch('app.user_api_client.get_user', return_value=platform_admin_user)
|
|
|
|
|
|
client_request.get('main.webauthn_begin_register', _expected_status=403)
|
Support registering a new authenticator
This adds Yubico's FIDO2 library and two APIs for working with the
"navigator.credentials.create()" function in JavaScript. The GET
API uses the library to generate options for the "create()" function,
and the POST API decodes and verifies the resulting credential. While
the options and response are dict-like, CBOR is necessary to encode
some of the byte-level values, which can't be represented in JSON.
Much of the code here is based on the Yubico library example [1][2].
Implementation notes:
- There are definitely better ways to alert the user about failure, but
window.alert() will do for the time being. Using location.reload() is
also a bit jarring if the page scrolls, but not a major issue.
- Ideally we would use window.fetch() to do AJAX calls, but we don't
have a polyfill for this, and we use $.ajax() elsewhere [3]. We need
to do a few weird tricks [6] to stop jQuery trashing the data.
- The FIDO2 server doesn't serve web requests; it's just a "server" in
the sense of WebAuthn terminology. It lives in its own module, since it
needs to be initialised with the app / config.
- $.ajax returns a promise-like object. Although we've used ".fail()"
elsewhere [3], I couldn't find a stub object that supports it, so I've
gone for ".catch()", and used a Promise stub object in tests.
- WebAuthn only works over HTTPS, but there's an exception for "localhost"
[4]. However, the library is a bit too strict [5], so we have to disable
origin verification to avoid needing HTTPS for dev work.
[1]: https://github.com/Yubico/python-fido2/blob/c42d9628a4f33d20c4401096fa8d3fc466d5b77f/examples/server/server.py
[2]: https://github.com/Yubico/python-fido2/blob/c42d9628a4f33d20c4401096fa8d3fc466d5b77f/examples/server/static/register.html
[3]: https://github.com/alphagov/notifications-admin/blob/91453d36395b7a0cf2998dfb8a5f52cc9e96640f/app/assets/javascripts/updateContent.js#L33
[4]: https://stackoverflow.com/questions/55971593/navigator-credentials-is-null-on-local-server
[5]: https://github.com/Yubico/python-fido2/blob/c42d9628a4f33d20c4401096fa8d3fc466d5b77f/fido2/rpid.py#L69
[6]: https://stackoverflow.com/questions/12394622/does-jquery-ajax-or-load-allow-for-responsetype-arraybuffer
2021-05-07 18:10:07 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_begin_register_returns_encoded_options(
|
|
|
|
|
|
mocker,
|
|
|
|
|
|
platform_admin_user,
|
2021-12-30 16:13:49 +00:00
|
|
|
|
client_request,
|
2021-05-17 11:37:47 +01:00
|
|
|
|
webauthn_dev_server,
|
Support registering a new authenticator
This adds Yubico's FIDO2 library and two APIs for working with the
"navigator.credentials.create()" function in JavaScript. The GET
API uses the library to generate options for the "create()" function,
and the POST API decodes and verifies the resulting credential. While
the options and response are dict-like, CBOR is necessary to encode
some of the byte-level values, which can't be represented in JSON.
Much of the code here is based on the Yubico library example [1][2].
Implementation notes:
- There are definitely better ways to alert the user about failure, but
window.alert() will do for the time being. Using location.reload() is
also a bit jarring if the page scrolls, but not a major issue.
- Ideally we would use window.fetch() to do AJAX calls, but we don't
have a polyfill for this, and we use $.ajax() elsewhere [3]. We need
to do a few weird tricks [6] to stop jQuery trashing the data.
- The FIDO2 server doesn't serve web requests; it's just a "server" in
the sense of WebAuthn terminology. It lives in its own module, since it
needs to be initialised with the app / config.
- $.ajax returns a promise-like object. Although we've used ".fail()"
elsewhere [3], I couldn't find a stub object that supports it, so I've
gone for ".catch()", and used a Promise stub object in tests.
- WebAuthn only works over HTTPS, but there's an exception for "localhost"
[4]. However, the library is a bit too strict [5], so we have to disable
origin verification to avoid needing HTTPS for dev work.
[1]: https://github.com/Yubico/python-fido2/blob/c42d9628a4f33d20c4401096fa8d3fc466d5b77f/examples/server/server.py
[2]: https://github.com/Yubico/python-fido2/blob/c42d9628a4f33d20c4401096fa8d3fc466d5b77f/examples/server/static/register.html
[3]: https://github.com/alphagov/notifications-admin/blob/91453d36395b7a0cf2998dfb8a5f52cc9e96640f/app/assets/javascripts/updateContent.js#L33
[4]: https://stackoverflow.com/questions/55971593/navigator-credentials-is-null-on-local-server
[5]: https://github.com/Yubico/python-fido2/blob/c42d9628a4f33d20c4401096fa8d3fc466d5b77f/fido2/rpid.py#L69
[6]: https://stackoverflow.com/questions/12394622/does-jquery-ajax-or-load-allow-for-responsetype-arraybuffer
2021-05-07 18:10:07 +01:00
|
|
|
|
):
|
2021-06-08 09:41:39 +01:00
|
|
|
|
mocker.patch('app.models.webauthn_credential.WebAuthnCredentials.client_method', return_value=[])
|
2021-05-17 15:56:15 +01:00
|
|
|
|
|
2021-12-30 16:13:49 +00:00
|
|
|
|
client_request.login(platform_admin_user)
|
2021-12-31 12:16:12 +00:00
|
|
|
|
response = client_request.get_response(
|
2021-12-30 16:13:49 +00:00
|
|
|
|
'main.webauthn_begin_register',
|
|
|
|
|
|
)
|
Support registering a new authenticator
This adds Yubico's FIDO2 library and two APIs for working with the
"navigator.credentials.create()" function in JavaScript. The GET
API uses the library to generate options for the "create()" function,
and the POST API decodes and verifies the resulting credential. While
the options and response are dict-like, CBOR is necessary to encode
some of the byte-level values, which can't be represented in JSON.
Much of the code here is based on the Yubico library example [1][2].
Implementation notes:
- There are definitely better ways to alert the user about failure, but
window.alert() will do for the time being. Using location.reload() is
also a bit jarring if the page scrolls, but not a major issue.
- Ideally we would use window.fetch() to do AJAX calls, but we don't
have a polyfill for this, and we use $.ajax() elsewhere [3]. We need
to do a few weird tricks [6] to stop jQuery trashing the data.
- The FIDO2 server doesn't serve web requests; it's just a "server" in
the sense of WebAuthn terminology. It lives in its own module, since it
needs to be initialised with the app / config.
- $.ajax returns a promise-like object. Although we've used ".fail()"
elsewhere [3], I couldn't find a stub object that supports it, so I've
gone for ".catch()", and used a Promise stub object in tests.
- WebAuthn only works over HTTPS, but there's an exception for "localhost"
[4]. However, the library is a bit too strict [5], so we have to disable
origin verification to avoid needing HTTPS for dev work.
[1]: https://github.com/Yubico/python-fido2/blob/c42d9628a4f33d20c4401096fa8d3fc466d5b77f/examples/server/server.py
[2]: https://github.com/Yubico/python-fido2/blob/c42d9628a4f33d20c4401096fa8d3fc466d5b77f/examples/server/static/register.html
[3]: https://github.com/alphagov/notifications-admin/blob/91453d36395b7a0cf2998dfb8a5f52cc9e96640f/app/assets/javascripts/updateContent.js#L33
[4]: https://stackoverflow.com/questions/55971593/navigator-credentials-is-null-on-local-server
[5]: https://github.com/Yubico/python-fido2/blob/c42d9628a4f33d20c4401096fa8d3fc466d5b77f/fido2/rpid.py#L69
[6]: https://stackoverflow.com/questions/12394622/does-jquery-ajax-or-load-allow-for-responsetype-arraybuffer
2021-05-07 18:10:07 +01:00
|
|
|
|
|
|
|
|
|
|
webauthn_options = cbor.decode(response.data)['publicKey']
|
|
|
|
|
|
assert webauthn_options['attestation'] == 'direct'
|
|
|
|
|
|
assert webauthn_options['timeout'] == 30_000
|
|
|
|
|
|
|
|
|
|
|
|
auth_selection = webauthn_options['authenticatorSelection']
|
|
|
|
|
|
assert auth_selection['authenticatorAttachment'] == 'cross-platform'
|
|
|
|
|
|
assert auth_selection['userVerification'] == 'discouraged'
|
|
|
|
|
|
|
|
|
|
|
|
user_options = webauthn_options['user']
|
|
|
|
|
|
assert user_options['name'] == platform_admin_user['email_address']
|
|
|
|
|
|
assert user_options['id'] == bytes(platform_admin_user['id'], 'utf-8')
|
|
|
|
|
|
|
|
|
|
|
|
relying_party_options = webauthn_options['rp']
|
2022-12-06 11:03:47 -05:00
|
|
|
|
assert relying_party_options['name'] == 'U.S. Notify'
|
2021-05-17 11:37:47 +01:00
|
|
|
|
assert relying_party_options['id'] == 'webauthn.io'
|
Support registering a new authenticator
This adds Yubico's FIDO2 library and two APIs for working with the
"navigator.credentials.create()" function in JavaScript. The GET
API uses the library to generate options for the "create()" function,
and the POST API decodes and verifies the resulting credential. While
the options and response are dict-like, CBOR is necessary to encode
some of the byte-level values, which can't be represented in JSON.
Much of the code here is based on the Yubico library example [1][2].
Implementation notes:
- There are definitely better ways to alert the user about failure, but
window.alert() will do for the time being. Using location.reload() is
also a bit jarring if the page scrolls, but not a major issue.
- Ideally we would use window.fetch() to do AJAX calls, but we don't
have a polyfill for this, and we use $.ajax() elsewhere [3]. We need
to do a few weird tricks [6] to stop jQuery trashing the data.
- The FIDO2 server doesn't serve web requests; it's just a "server" in
the sense of WebAuthn terminology. It lives in its own module, since it
needs to be initialised with the app / config.
- $.ajax returns a promise-like object. Although we've used ".fail()"
elsewhere [3], I couldn't find a stub object that supports it, so I've
gone for ".catch()", and used a Promise stub object in tests.
- WebAuthn only works over HTTPS, but there's an exception for "localhost"
[4]. However, the library is a bit too strict [5], so we have to disable
origin verification to avoid needing HTTPS for dev work.
[1]: https://github.com/Yubico/python-fido2/blob/c42d9628a4f33d20c4401096fa8d3fc466d5b77f/examples/server/server.py
[2]: https://github.com/Yubico/python-fido2/blob/c42d9628a4f33d20c4401096fa8d3fc466d5b77f/examples/server/static/register.html
[3]: https://github.com/alphagov/notifications-admin/blob/91453d36395b7a0cf2998dfb8a5f52cc9e96640f/app/assets/javascripts/updateContent.js#L33
[4]: https://stackoverflow.com/questions/55971593/navigator-credentials-is-null-on-local-server
[5]: https://github.com/Yubico/python-fido2/blob/c42d9628a4f33d20c4401096fa8d3fc466d5b77f/fido2/rpid.py#L69
[6]: https://stackoverflow.com/questions/12394622/does-jquery-ajax-or-load-allow-for-responsetype-arraybuffer
2021-05-07 18:10:07 +01:00
|
|
|
|
|
|
|
|
|
|
|
2021-05-11 14:22:41 +01:00
|
|
|
|
def test_begin_register_includes_existing_credentials(
|
2021-12-30 16:13:49 +00:00
|
|
|
|
client_request,
|
|
|
|
|
|
platform_admin_user,
|
2021-05-11 14:22:41 +01:00
|
|
|
|
webauthn_credential,
|
|
|
|
|
|
mocker,
|
|
|
|
|
|
):
|
|
|
|
|
|
mocker.patch(
|
2021-06-08 09:41:39 +01:00
|
|
|
|
'app.models.webauthn_credential.WebAuthnCredentials.client_method',
|
2021-05-11 14:22:41 +01:00
|
|
|
|
return_value=[webauthn_credential, webauthn_credential]
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2021-12-30 16:13:49 +00:00
|
|
|
|
client_request.login(platform_admin_user)
|
2021-12-31 12:16:12 +00:00
|
|
|
|
response = client_request.get_response(
|
2021-12-30 16:13:49 +00:00
|
|
|
|
'main.webauthn_begin_register',
|
2021-05-11 14:22:41 +01:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
webauthn_options = cbor.decode(response.data)['publicKey']
|
|
|
|
|
|
assert len(webauthn_options['excludeCredentials']) == 2
|
|
|
|
|
|
|
|
|
|
|
|
|
Support registering a new authenticator
This adds Yubico's FIDO2 library and two APIs for working with the
"navigator.credentials.create()" function in JavaScript. The GET
API uses the library to generate options for the "create()" function,
and the POST API decodes and verifies the resulting credential. While
the options and response are dict-like, CBOR is necessary to encode
some of the byte-level values, which can't be represented in JSON.
Much of the code here is based on the Yubico library example [1][2].
Implementation notes:
- There are definitely better ways to alert the user about failure, but
window.alert() will do for the time being. Using location.reload() is
also a bit jarring if the page scrolls, but not a major issue.
- Ideally we would use window.fetch() to do AJAX calls, but we don't
have a polyfill for this, and we use $.ajax() elsewhere [3]. We need
to do a few weird tricks [6] to stop jQuery trashing the data.
- The FIDO2 server doesn't serve web requests; it's just a "server" in
the sense of WebAuthn terminology. It lives in its own module, since it
needs to be initialised with the app / config.
- $.ajax returns a promise-like object. Although we've used ".fail()"
elsewhere [3], I couldn't find a stub object that supports it, so I've
gone for ".catch()", and used a Promise stub object in tests.
- WebAuthn only works over HTTPS, but there's an exception for "localhost"
[4]. However, the library is a bit too strict [5], so we have to disable
origin verification to avoid needing HTTPS for dev work.
[1]: https://github.com/Yubico/python-fido2/blob/c42d9628a4f33d20c4401096fa8d3fc466d5b77f/examples/server/server.py
[2]: https://github.com/Yubico/python-fido2/blob/c42d9628a4f33d20c4401096fa8d3fc466d5b77f/examples/server/static/register.html
[3]: https://github.com/alphagov/notifications-admin/blob/91453d36395b7a0cf2998dfb8a5f52cc9e96640f/app/assets/javascripts/updateContent.js#L33
[4]: https://stackoverflow.com/questions/55971593/navigator-credentials-is-null-on-local-server
[5]: https://github.com/Yubico/python-fido2/blob/c42d9628a4f33d20c4401096fa8d3fc466d5b77f/fido2/rpid.py#L69
[6]: https://stackoverflow.com/questions/12394622/does-jquery-ajax-or-load-allow-for-responsetype-arraybuffer
2021-05-07 18:10:07 +01:00
|
|
|
|
def test_begin_register_stores_state_in_session(
|
2021-12-30 16:13:49 +00:00
|
|
|
|
client_request,
|
|
|
|
|
|
platform_admin_user,
|
2021-05-13 15:54:05 +01:00
|
|
|
|
mocker,
|
Support registering a new authenticator
This adds Yubico's FIDO2 library and two APIs for working with the
"navigator.credentials.create()" function in JavaScript. The GET
API uses the library to generate options for the "create()" function,
and the POST API decodes and verifies the resulting credential. While
the options and response are dict-like, CBOR is necessary to encode
some of the byte-level values, which can't be represented in JSON.
Much of the code here is based on the Yubico library example [1][2].
Implementation notes:
- There are definitely better ways to alert the user about failure, but
window.alert() will do for the time being. Using location.reload() is
also a bit jarring if the page scrolls, but not a major issue.
- Ideally we would use window.fetch() to do AJAX calls, but we don't
have a polyfill for this, and we use $.ajax() elsewhere [3]. We need
to do a few weird tricks [6] to stop jQuery trashing the data.
- The FIDO2 server doesn't serve web requests; it's just a "server" in
the sense of WebAuthn terminology. It lives in its own module, since it
needs to be initialised with the app / config.
- $.ajax returns a promise-like object. Although we've used ".fail()"
elsewhere [3], I couldn't find a stub object that supports it, so I've
gone for ".catch()", and used a Promise stub object in tests.
- WebAuthn only works over HTTPS, but there's an exception for "localhost"
[4]. However, the library is a bit too strict [5], so we have to disable
origin verification to avoid needing HTTPS for dev work.
[1]: https://github.com/Yubico/python-fido2/blob/c42d9628a4f33d20c4401096fa8d3fc466d5b77f/examples/server/server.py
[2]: https://github.com/Yubico/python-fido2/blob/c42d9628a4f33d20c4401096fa8d3fc466d5b77f/examples/server/static/register.html
[3]: https://github.com/alphagov/notifications-admin/blob/91453d36395b7a0cf2998dfb8a5f52cc9e96640f/app/assets/javascripts/updateContent.js#L33
[4]: https://stackoverflow.com/questions/55971593/navigator-credentials-is-null-on-local-server
[5]: https://github.com/Yubico/python-fido2/blob/c42d9628a4f33d20c4401096fa8d3fc466d5b77f/fido2/rpid.py#L69
[6]: https://stackoverflow.com/questions/12394622/does-jquery-ajax-or-load-allow-for-responsetype-arraybuffer
2021-05-07 18:10:07 +01:00
|
|
|
|
):
|
2021-05-13 15:54:05 +01:00
|
|
|
|
mocker.patch(
|
2021-06-08 09:41:39 +01:00
|
|
|
|
'app.models.webauthn_credential.WebAuthnCredentials.client_method',
|
2021-05-13 15:54:05 +01:00
|
|
|
|
return_value=[])
|
|
|
|
|
|
|
2021-12-30 16:13:49 +00:00
|
|
|
|
client_request.login(platform_admin_user)
|
2021-12-31 12:16:12 +00:00
|
|
|
|
client_request.get_response(
|
2021-12-30 16:13:49 +00:00
|
|
|
|
'main.webauthn_begin_register',
|
Support registering a new authenticator
This adds Yubico's FIDO2 library and two APIs for working with the
"navigator.credentials.create()" function in JavaScript. The GET
API uses the library to generate options for the "create()" function,
and the POST API decodes and verifies the resulting credential. While
the options and response are dict-like, CBOR is necessary to encode
some of the byte-level values, which can't be represented in JSON.
Much of the code here is based on the Yubico library example [1][2].
Implementation notes:
- There are definitely better ways to alert the user about failure, but
window.alert() will do for the time being. Using location.reload() is
also a bit jarring if the page scrolls, but not a major issue.
- Ideally we would use window.fetch() to do AJAX calls, but we don't
have a polyfill for this, and we use $.ajax() elsewhere [3]. We need
to do a few weird tricks [6] to stop jQuery trashing the data.
- The FIDO2 server doesn't serve web requests; it's just a "server" in
the sense of WebAuthn terminology. It lives in its own module, since it
needs to be initialised with the app / config.
- $.ajax returns a promise-like object. Although we've used ".fail()"
elsewhere [3], I couldn't find a stub object that supports it, so I've
gone for ".catch()", and used a Promise stub object in tests.
- WebAuthn only works over HTTPS, but there's an exception for "localhost"
[4]. However, the library is a bit too strict [5], so we have to disable
origin verification to avoid needing HTTPS for dev work.
[1]: https://github.com/Yubico/python-fido2/blob/c42d9628a4f33d20c4401096fa8d3fc466d5b77f/examples/server/server.py
[2]: https://github.com/Yubico/python-fido2/blob/c42d9628a4f33d20c4401096fa8d3fc466d5b77f/examples/server/static/register.html
[3]: https://github.com/alphagov/notifications-admin/blob/91453d36395b7a0cf2998dfb8a5f52cc9e96640f/app/assets/javascripts/updateContent.js#L33
[4]: https://stackoverflow.com/questions/55971593/navigator-credentials-is-null-on-local-server
[5]: https://github.com/Yubico/python-fido2/blob/c42d9628a4f33d20c4401096fa8d3fc466d5b77f/fido2/rpid.py#L69
[6]: https://stackoverflow.com/questions/12394622/does-jquery-ajax-or-load-allow-for-responsetype-arraybuffer
2021-05-07 18:10:07 +01:00
|
|
|
|
)
|
|
|
|
|
|
|
2021-12-30 16:13:49 +00:00
|
|
|
|
with client_request.session_transaction() as session:
|
Support registering a new authenticator
This adds Yubico's FIDO2 library and two APIs for working with the
"navigator.credentials.create()" function in JavaScript. The GET
API uses the library to generate options for the "create()" function,
and the POST API decodes and verifies the resulting credential. While
the options and response are dict-like, CBOR is necessary to encode
some of the byte-level values, which can't be represented in JSON.
Much of the code here is based on the Yubico library example [1][2].
Implementation notes:
- There are definitely better ways to alert the user about failure, but
window.alert() will do for the time being. Using location.reload() is
also a bit jarring if the page scrolls, but not a major issue.
- Ideally we would use window.fetch() to do AJAX calls, but we don't
have a polyfill for this, and we use $.ajax() elsewhere [3]. We need
to do a few weird tricks [6] to stop jQuery trashing the data.
- The FIDO2 server doesn't serve web requests; it's just a "server" in
the sense of WebAuthn terminology. It lives in its own module, since it
needs to be initialised with the app / config.
- $.ajax returns a promise-like object. Although we've used ".fail()"
elsewhere [3], I couldn't find a stub object that supports it, so I've
gone for ".catch()", and used a Promise stub object in tests.
- WebAuthn only works over HTTPS, but there's an exception for "localhost"
[4]. However, the library is a bit too strict [5], so we have to disable
origin verification to avoid needing HTTPS for dev work.
[1]: https://github.com/Yubico/python-fido2/blob/c42d9628a4f33d20c4401096fa8d3fc466d5b77f/examples/server/server.py
[2]: https://github.com/Yubico/python-fido2/blob/c42d9628a4f33d20c4401096fa8d3fc466d5b77f/examples/server/static/register.html
[3]: https://github.com/alphagov/notifications-admin/blob/91453d36395b7a0cf2998dfb8a5f52cc9e96640f/app/assets/javascripts/updateContent.js#L33
[4]: https://stackoverflow.com/questions/55971593/navigator-credentials-is-null-on-local-server
[5]: https://github.com/Yubico/python-fido2/blob/c42d9628a4f33d20c4401096fa8d3fc466d5b77f/fido2/rpid.py#L69
[6]: https://stackoverflow.com/questions/12394622/does-jquery-ajax-or-load-allow-for-responsetype-arraybuffer
2021-05-07 18:10:07 +01:00
|
|
|
|
assert session['webauthn_registration_state'] is not None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_complete_register_creates_credential(
|
|
|
|
|
|
platform_admin_user,
|
2021-12-30 16:13:49 +00:00
|
|
|
|
client_request,
|
2021-06-07 13:53:33 +01:00
|
|
|
|
mock_update_user_attribute,
|
Support registering a new authenticator
This adds Yubico's FIDO2 library and two APIs for working with the
"navigator.credentials.create()" function in JavaScript. The GET
API uses the library to generate options for the "create()" function,
and the POST API decodes and verifies the resulting credential. While
the options and response are dict-like, CBOR is necessary to encode
some of the byte-level values, which can't be represented in JSON.
Much of the code here is based on the Yubico library example [1][2].
Implementation notes:
- There are definitely better ways to alert the user about failure, but
window.alert() will do for the time being. Using location.reload() is
also a bit jarring if the page scrolls, but not a major issue.
- Ideally we would use window.fetch() to do AJAX calls, but we don't
have a polyfill for this, and we use $.ajax() elsewhere [3]. We need
to do a few weird tricks [6] to stop jQuery trashing the data.
- The FIDO2 server doesn't serve web requests; it's just a "server" in
the sense of WebAuthn terminology. It lives in its own module, since it
needs to be initialised with the app / config.
- $.ajax returns a promise-like object. Although we've used ".fail()"
elsewhere [3], I couldn't find a stub object that supports it, so I've
gone for ".catch()", and used a Promise stub object in tests.
- WebAuthn only works over HTTPS, but there's an exception for "localhost"
[4]. However, the library is a bit too strict [5], so we have to disable
origin verification to avoid needing HTTPS for dev work.
[1]: https://github.com/Yubico/python-fido2/blob/c42d9628a4f33d20c4401096fa8d3fc466d5b77f/examples/server/server.py
[2]: https://github.com/Yubico/python-fido2/blob/c42d9628a4f33d20c4401096fa8d3fc466d5b77f/examples/server/static/register.html
[3]: https://github.com/alphagov/notifications-admin/blob/91453d36395b7a0cf2998dfb8a5f52cc9e96640f/app/assets/javascripts/updateContent.js#L33
[4]: https://stackoverflow.com/questions/55971593/navigator-credentials-is-null-on-local-server
[5]: https://github.com/Yubico/python-fido2/blob/c42d9628a4f33d20c4401096fa8d3fc466d5b77f/fido2/rpid.py#L69
[6]: https://stackoverflow.com/questions/12394622/does-jquery-ajax-or-load-allow-for-responsetype-arraybuffer
2021-05-07 18:10:07 +01:00
|
|
|
|
mocker,
|
|
|
|
|
|
):
|
2021-12-30 16:13:49 +00:00
|
|
|
|
with client_request.session_transaction() as session:
|
Support registering a new authenticator
This adds Yubico's FIDO2 library and two APIs for working with the
"navigator.credentials.create()" function in JavaScript. The GET
API uses the library to generate options for the "create()" function,
and the POST API decodes and verifies the resulting credential. While
the options and response are dict-like, CBOR is necessary to encode
some of the byte-level values, which can't be represented in JSON.
Much of the code here is based on the Yubico library example [1][2].
Implementation notes:
- There are definitely better ways to alert the user about failure, but
window.alert() will do for the time being. Using location.reload() is
also a bit jarring if the page scrolls, but not a major issue.
- Ideally we would use window.fetch() to do AJAX calls, but we don't
have a polyfill for this, and we use $.ajax() elsewhere [3]. We need
to do a few weird tricks [6] to stop jQuery trashing the data.
- The FIDO2 server doesn't serve web requests; it's just a "server" in
the sense of WebAuthn terminology. It lives in its own module, since it
needs to be initialised with the app / config.
- $.ajax returns a promise-like object. Although we've used ".fail()"
elsewhere [3], I couldn't find a stub object that supports it, so I've
gone for ".catch()", and used a Promise stub object in tests.
- WebAuthn only works over HTTPS, but there's an exception for "localhost"
[4]. However, the library is a bit too strict [5], so we have to disable
origin verification to avoid needing HTTPS for dev work.
[1]: https://github.com/Yubico/python-fido2/blob/c42d9628a4f33d20c4401096fa8d3fc466d5b77f/examples/server/server.py
[2]: https://github.com/Yubico/python-fido2/blob/c42d9628a4f33d20c4401096fa8d3fc466d5b77f/examples/server/static/register.html
[3]: https://github.com/alphagov/notifications-admin/blob/91453d36395b7a0cf2998dfb8a5f52cc9e96640f/app/assets/javascripts/updateContent.js#L33
[4]: https://stackoverflow.com/questions/55971593/navigator-credentials-is-null-on-local-server
[5]: https://github.com/Yubico/python-fido2/blob/c42d9628a4f33d20c4401096fa8d3fc466d5b77f/fido2/rpid.py#L69
[6]: https://stackoverflow.com/questions/12394622/does-jquery-ajax-or-load-allow-for-responsetype-arraybuffer
2021-05-07 18:10:07 +01:00
|
|
|
|
session['webauthn_registration_state'] = 'state'
|
|
|
|
|
|
|
|
|
|
|
|
user_api_mock = mocker.patch(
|
|
|
|
|
|
'app.user_api_client.create_webauthn_credential_for_user'
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
credential_mock = mocker.patch(
|
|
|
|
|
|
'app.models.webauthn_credential.WebAuthnCredential.from_registration',
|
|
|
|
|
|
return_value='cred'
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2021-12-30 16:13:49 +00:00
|
|
|
|
client_request.login(platform_admin_user)
|
2021-12-31 12:16:12 +00:00
|
|
|
|
client_request.post_response(
|
2021-12-30 16:13:49 +00:00
|
|
|
|
'main.webauthn_begin_register',
|
|
|
|
|
|
_data=cbor.encode('public_key_credential'),
|
|
|
|
|
|
_expected_status=200,
|
Support registering a new authenticator
This adds Yubico's FIDO2 library and two APIs for working with the
"navigator.credentials.create()" function in JavaScript. The GET
API uses the library to generate options for the "create()" function,
and the POST API decodes and verifies the resulting credential. While
the options and response are dict-like, CBOR is necessary to encode
some of the byte-level values, which can't be represented in JSON.
Much of the code here is based on the Yubico library example [1][2].
Implementation notes:
- There are definitely better ways to alert the user about failure, but
window.alert() will do for the time being. Using location.reload() is
also a bit jarring if the page scrolls, but not a major issue.
- Ideally we would use window.fetch() to do AJAX calls, but we don't
have a polyfill for this, and we use $.ajax() elsewhere [3]. We need
to do a few weird tricks [6] to stop jQuery trashing the data.
- The FIDO2 server doesn't serve web requests; it's just a "server" in
the sense of WebAuthn terminology. It lives in its own module, since it
needs to be initialised with the app / config.
- $.ajax returns a promise-like object. Although we've used ".fail()"
elsewhere [3], I couldn't find a stub object that supports it, so I've
gone for ".catch()", and used a Promise stub object in tests.
- WebAuthn only works over HTTPS, but there's an exception for "localhost"
[4]. However, the library is a bit too strict [5], so we have to disable
origin verification to avoid needing HTTPS for dev work.
[1]: https://github.com/Yubico/python-fido2/blob/c42d9628a4f33d20c4401096fa8d3fc466d5b77f/examples/server/server.py
[2]: https://github.com/Yubico/python-fido2/blob/c42d9628a4f33d20c4401096fa8d3fc466d5b77f/examples/server/static/register.html
[3]: https://github.com/alphagov/notifications-admin/blob/91453d36395b7a0cf2998dfb8a5f52cc9e96640f/app/assets/javascripts/updateContent.js#L33
[4]: https://stackoverflow.com/questions/55971593/navigator-credentials-is-null-on-local-server
[5]: https://github.com/Yubico/python-fido2/blob/c42d9628a4f33d20c4401096fa8d3fc466d5b77f/fido2/rpid.py#L69
[6]: https://stackoverflow.com/questions/12394622/does-jquery-ajax-or-load-allow-for-responsetype-arraybuffer
2021-05-07 18:10:07 +01:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
credential_mock.assert_called_once_with('state', 'public_key_credential')
|
|
|
|
|
|
user_api_mock.assert_called_once_with(platform_admin_user['id'], 'cred')
|
2021-06-07 13:53:33 +01:00
|
|
|
|
mock_update_user_attribute.assert_called_once_with(
|
|
|
|
|
|
platform_admin_user['id'],
|
|
|
|
|
|
auth_type='webauthn_auth',
|
|
|
|
|
|
)
|
Support registering a new authenticator
This adds Yubico's FIDO2 library and two APIs for working with the
"navigator.credentials.create()" function in JavaScript. The GET
API uses the library to generate options for the "create()" function,
and the POST API decodes and verifies the resulting credential. While
the options and response are dict-like, CBOR is necessary to encode
some of the byte-level values, which can't be represented in JSON.
Much of the code here is based on the Yubico library example [1][2].
Implementation notes:
- There are definitely better ways to alert the user about failure, but
window.alert() will do for the time being. Using location.reload() is
also a bit jarring if the page scrolls, but not a major issue.
- Ideally we would use window.fetch() to do AJAX calls, but we don't
have a polyfill for this, and we use $.ajax() elsewhere [3]. We need
to do a few weird tricks [6] to stop jQuery trashing the data.
- The FIDO2 server doesn't serve web requests; it's just a "server" in
the sense of WebAuthn terminology. It lives in its own module, since it
needs to be initialised with the app / config.
- $.ajax returns a promise-like object. Although we've used ".fail()"
elsewhere [3], I couldn't find a stub object that supports it, so I've
gone for ".catch()", and used a Promise stub object in tests.
- WebAuthn only works over HTTPS, but there's an exception for "localhost"
[4]. However, the library is a bit too strict [5], so we have to disable
origin verification to avoid needing HTTPS for dev work.
[1]: https://github.com/Yubico/python-fido2/blob/c42d9628a4f33d20c4401096fa8d3fc466d5b77f/examples/server/server.py
[2]: https://github.com/Yubico/python-fido2/blob/c42d9628a4f33d20c4401096fa8d3fc466d5b77f/examples/server/static/register.html
[3]: https://github.com/alphagov/notifications-admin/blob/91453d36395b7a0cf2998dfb8a5f52cc9e96640f/app/assets/javascripts/updateContent.js#L33
[4]: https://stackoverflow.com/questions/55971593/navigator-credentials-is-null-on-local-server
[5]: https://github.com/Yubico/python-fido2/blob/c42d9628a4f33d20c4401096fa8d3fc466d5b77f/fido2/rpid.py#L69
[6]: https://stackoverflow.com/questions/12394622/does-jquery-ajax-or-load-allow-for-responsetype-arraybuffer
2021-05-07 18:10:07 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_complete_register_clears_session(
|
2021-12-30 16:13:49 +00:00
|
|
|
|
client_request,
|
|
|
|
|
|
platform_admin_user,
|
Support registering a new authenticator
This adds Yubico's FIDO2 library and two APIs for working with the
"navigator.credentials.create()" function in JavaScript. The GET
API uses the library to generate options for the "create()" function,
and the POST API decodes and verifies the resulting credential. While
the options and response are dict-like, CBOR is necessary to encode
some of the byte-level values, which can't be represented in JSON.
Much of the code here is based on the Yubico library example [1][2].
Implementation notes:
- There are definitely better ways to alert the user about failure, but
window.alert() will do for the time being. Using location.reload() is
also a bit jarring if the page scrolls, but not a major issue.
- Ideally we would use window.fetch() to do AJAX calls, but we don't
have a polyfill for this, and we use $.ajax() elsewhere [3]. We need
to do a few weird tricks [6] to stop jQuery trashing the data.
- The FIDO2 server doesn't serve web requests; it's just a "server" in
the sense of WebAuthn terminology. It lives in its own module, since it
needs to be initialised with the app / config.
- $.ajax returns a promise-like object. Although we've used ".fail()"
elsewhere [3], I couldn't find a stub object that supports it, so I've
gone for ".catch()", and used a Promise stub object in tests.
- WebAuthn only works over HTTPS, but there's an exception for "localhost"
[4]. However, the library is a bit too strict [5], so we have to disable
origin verification to avoid needing HTTPS for dev work.
[1]: https://github.com/Yubico/python-fido2/blob/c42d9628a4f33d20c4401096fa8d3fc466d5b77f/examples/server/server.py
[2]: https://github.com/Yubico/python-fido2/blob/c42d9628a4f33d20c4401096fa8d3fc466d5b77f/examples/server/static/register.html
[3]: https://github.com/alphagov/notifications-admin/blob/91453d36395b7a0cf2998dfb8a5f52cc9e96640f/app/assets/javascripts/updateContent.js#L33
[4]: https://stackoverflow.com/questions/55971593/navigator-credentials-is-null-on-local-server
[5]: https://github.com/Yubico/python-fido2/blob/c42d9628a4f33d20c4401096fa8d3fc466d5b77f/fido2/rpid.py#L69
[6]: https://stackoverflow.com/questions/12394622/does-jquery-ajax-or-load-allow-for-responsetype-arraybuffer
2021-05-07 18:10:07 +01:00
|
|
|
|
mocker,
|
|
|
|
|
|
):
|
2021-12-30 16:13:49 +00:00
|
|
|
|
with client_request.session_transaction() as session:
|
Support registering a new authenticator
This adds Yubico's FIDO2 library and two APIs for working with the
"navigator.credentials.create()" function in JavaScript. The GET
API uses the library to generate options for the "create()" function,
and the POST API decodes and verifies the resulting credential. While
the options and response are dict-like, CBOR is necessary to encode
some of the byte-level values, which can't be represented in JSON.
Much of the code here is based on the Yubico library example [1][2].
Implementation notes:
- There are definitely better ways to alert the user about failure, but
window.alert() will do for the time being. Using location.reload() is
also a bit jarring if the page scrolls, but not a major issue.
- Ideally we would use window.fetch() to do AJAX calls, but we don't
have a polyfill for this, and we use $.ajax() elsewhere [3]. We need
to do a few weird tricks [6] to stop jQuery trashing the data.
- The FIDO2 server doesn't serve web requests; it's just a "server" in
the sense of WebAuthn terminology. It lives in its own module, since it
needs to be initialised with the app / config.
- $.ajax returns a promise-like object. Although we've used ".fail()"
elsewhere [3], I couldn't find a stub object that supports it, so I've
gone for ".catch()", and used a Promise stub object in tests.
- WebAuthn only works over HTTPS, but there's an exception for "localhost"
[4]. However, the library is a bit too strict [5], so we have to disable
origin verification to avoid needing HTTPS for dev work.
[1]: https://github.com/Yubico/python-fido2/blob/c42d9628a4f33d20c4401096fa8d3fc466d5b77f/examples/server/server.py
[2]: https://github.com/Yubico/python-fido2/blob/c42d9628a4f33d20c4401096fa8d3fc466d5b77f/examples/server/static/register.html
[3]: https://github.com/alphagov/notifications-admin/blob/91453d36395b7a0cf2998dfb8a5f52cc9e96640f/app/assets/javascripts/updateContent.js#L33
[4]: https://stackoverflow.com/questions/55971593/navigator-credentials-is-null-on-local-server
[5]: https://github.com/Yubico/python-fido2/blob/c42d9628a4f33d20c4401096fa8d3fc466d5b77f/fido2/rpid.py#L69
[6]: https://stackoverflow.com/questions/12394622/does-jquery-ajax-or-load-allow-for-responsetype-arraybuffer
2021-05-07 18:10:07 +01:00
|
|
|
|
session['webauthn_registration_state'] = 'state'
|
|
|
|
|
|
|
|
|
|
|
|
mocker.patch('app.user_api_client.create_webauthn_credential_for_user')
|
|
|
|
|
|
mocker.patch('app.models.webauthn_credential.WebAuthnCredential.from_registration')
|
|
|
|
|
|
|
2021-12-30 16:13:49 +00:00
|
|
|
|
client_request.login(platform_admin_user)
|
|
|
|
|
|
client_request.post(
|
|
|
|
|
|
'main.webauthn_complete_register',
|
|
|
|
|
|
_data=cbor.encode('public_key_credential'),
|
|
|
|
|
|
_expected_status=200,
|
Support registering a new authenticator
This adds Yubico's FIDO2 library and two APIs for working with the
"navigator.credentials.create()" function in JavaScript. The GET
API uses the library to generate options for the "create()" function,
and the POST API decodes and verifies the resulting credential. While
the options and response are dict-like, CBOR is necessary to encode
some of the byte-level values, which can't be represented in JSON.
Much of the code here is based on the Yubico library example [1][2].
Implementation notes:
- There are definitely better ways to alert the user about failure, but
window.alert() will do for the time being. Using location.reload() is
also a bit jarring if the page scrolls, but not a major issue.
- Ideally we would use window.fetch() to do AJAX calls, but we don't
have a polyfill for this, and we use $.ajax() elsewhere [3]. We need
to do a few weird tricks [6] to stop jQuery trashing the data.
- The FIDO2 server doesn't serve web requests; it's just a "server" in
the sense of WebAuthn terminology. It lives in its own module, since it
needs to be initialised with the app / config.
- $.ajax returns a promise-like object. Although we've used ".fail()"
elsewhere [3], I couldn't find a stub object that supports it, so I've
gone for ".catch()", and used a Promise stub object in tests.
- WebAuthn only works over HTTPS, but there's an exception for "localhost"
[4]. However, the library is a bit too strict [5], so we have to disable
origin verification to avoid needing HTTPS for dev work.
[1]: https://github.com/Yubico/python-fido2/blob/c42d9628a4f33d20c4401096fa8d3fc466d5b77f/examples/server/server.py
[2]: https://github.com/Yubico/python-fido2/blob/c42d9628a4f33d20c4401096fa8d3fc466d5b77f/examples/server/static/register.html
[3]: https://github.com/alphagov/notifications-admin/blob/91453d36395b7a0cf2998dfb8a5f52cc9e96640f/app/assets/javascripts/updateContent.js#L33
[4]: https://stackoverflow.com/questions/55971593/navigator-credentials-is-null-on-local-server
[5]: https://github.com/Yubico/python-fido2/blob/c42d9628a4f33d20c4401096fa8d3fc466d5b77f/fido2/rpid.py#L69
[6]: https://stackoverflow.com/questions/12394622/does-jquery-ajax-or-load-allow-for-responsetype-arraybuffer
2021-05-07 18:10:07 +01:00
|
|
|
|
)
|
|
|
|
|
|
|
2021-12-30 16:13:49 +00:00
|
|
|
|
with client_request.session_transaction() as session:
|
Support registering a new authenticator
This adds Yubico's FIDO2 library and two APIs for working with the
"navigator.credentials.create()" function in JavaScript. The GET
API uses the library to generate options for the "create()" function,
and the POST API decodes and verifies the resulting credential. While
the options and response are dict-like, CBOR is necessary to encode
some of the byte-level values, which can't be represented in JSON.
Much of the code here is based on the Yubico library example [1][2].
Implementation notes:
- There are definitely better ways to alert the user about failure, but
window.alert() will do for the time being. Using location.reload() is
also a bit jarring if the page scrolls, but not a major issue.
- Ideally we would use window.fetch() to do AJAX calls, but we don't
have a polyfill for this, and we use $.ajax() elsewhere [3]. We need
to do a few weird tricks [6] to stop jQuery trashing the data.
- The FIDO2 server doesn't serve web requests; it's just a "server" in
the sense of WebAuthn terminology. It lives in its own module, since it
needs to be initialised with the app / config.
- $.ajax returns a promise-like object. Although we've used ".fail()"
elsewhere [3], I couldn't find a stub object that supports it, so I've
gone for ".catch()", and used a Promise stub object in tests.
- WebAuthn only works over HTTPS, but there's an exception for "localhost"
[4]. However, the library is a bit too strict [5], so we have to disable
origin verification to avoid needing HTTPS for dev work.
[1]: https://github.com/Yubico/python-fido2/blob/c42d9628a4f33d20c4401096fa8d3fc466d5b77f/examples/server/server.py
[2]: https://github.com/Yubico/python-fido2/blob/c42d9628a4f33d20c4401096fa8d3fc466d5b77f/examples/server/static/register.html
[3]: https://github.com/alphagov/notifications-admin/blob/91453d36395b7a0cf2998dfb8a5f52cc9e96640f/app/assets/javascripts/updateContent.js#L33
[4]: https://stackoverflow.com/questions/55971593/navigator-credentials-is-null-on-local-server
[5]: https://github.com/Yubico/python-fido2/blob/c42d9628a4f33d20c4401096fa8d3fc466d5b77f/fido2/rpid.py#L69
[6]: https://stackoverflow.com/questions/12394622/does-jquery-ajax-or-load-allow-for-responsetype-arraybuffer
2021-05-07 18:10:07 +01:00
|
|
|
|
assert 'webauthn_registration_state' not in session
|
2021-06-01 17:07:23 +01:00
|
|
|
|
assert session['_flashes'] == [('default_with_tick', (
|
|
|
|
|
|
'Registration complete. Next time you sign in to Notify '
|
|
|
|
|
|
'you’ll be asked to use your security key.'
|
|
|
|
|
|
))]
|
Handle errors when registration fails
Previously we would raise a 500 error in a variety of cases:
- If a second key was being registered simultaneously (e.g. in a
separate tab), which means the registration state could be missing
after the first registration completes. That smells like an attack.
- If the server-side verification failed e.g. origin verification,
challenge verification, etc. The library seems to use 'ValueError'
for all such errors [1] (after auditing its 'raise' statements, and
excluding AttestationError [2], since we're not doing that).
- If a key is used that attempts to sign with an unsupported
algorithm. This would normally raise a NotImplemented error as part
of verifying attestation [3], but we don't do that, so we need to
verify the algorithm is supported by the library manually.
This adds error handling to return a 400 response and error message
in these cases, since the error is not unexpected (i.e. not a 500).
A 400 seems more appropriate than a 403, since in many cases it's
not clear if the request data is valid.
I've used CBOR for the transport encoding, to match the successful
request / response encoding. Note that the ordering of then/catch
matters in JS - we don't want to catch our own throws!
[1]: https://github.com/Yubico/python-fido2/blob/142587b3e698ca0e253c78d75758fda635cac51a/fido2/server.py#L255
[2]: https://github.com/Yubico/python-fido2/blob/c42d9628a4f33d20c4401096fa8d3fc466d5b77f/fido2/attestation/base.py#L39
[3]: https://github.com/Yubico/python-fido2/blob/c42d9628a4f33d20c4401096fa8d3fc466d5b77f/fido2/cose.py#L92
2021-05-14 09:17:12 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_complete_register_handles_library_errors(
|
2021-12-30 16:13:49 +00:00
|
|
|
|
client_request,
|
|
|
|
|
|
platform_admin_user,
|
Handle errors when registration fails
Previously we would raise a 500 error in a variety of cases:
- If a second key was being registered simultaneously (e.g. in a
separate tab), which means the registration state could be missing
after the first registration completes. That smells like an attack.
- If the server-side verification failed e.g. origin verification,
challenge verification, etc. The library seems to use 'ValueError'
for all such errors [1] (after auditing its 'raise' statements, and
excluding AttestationError [2], since we're not doing that).
- If a key is used that attempts to sign with an unsupported
algorithm. This would normally raise a NotImplemented error as part
of verifying attestation [3], but we don't do that, so we need to
verify the algorithm is supported by the library manually.
This adds error handling to return a 400 response and error message
in these cases, since the error is not unexpected (i.e. not a 500).
A 400 seems more appropriate than a 403, since in many cases it's
not clear if the request data is valid.
I've used CBOR for the transport encoding, to match the successful
request / response encoding. Note that the ordering of then/catch
matters in JS - we don't want to catch our own throws!
[1]: https://github.com/Yubico/python-fido2/blob/142587b3e698ca0e253c78d75758fda635cac51a/fido2/server.py#L255
[2]: https://github.com/Yubico/python-fido2/blob/c42d9628a4f33d20c4401096fa8d3fc466d5b77f/fido2/attestation/base.py#L39
[3]: https://github.com/Yubico/python-fido2/blob/c42d9628a4f33d20c4401096fa8d3fc466d5b77f/fido2/cose.py#L92
2021-05-14 09:17:12 +01:00
|
|
|
|
mocker,
|
|
|
|
|
|
):
|
2021-12-30 16:13:49 +00:00
|
|
|
|
with client_request.session_transaction() as session:
|
Handle errors when registration fails
Previously we would raise a 500 error in a variety of cases:
- If a second key was being registered simultaneously (e.g. in a
separate tab), which means the registration state could be missing
after the first registration completes. That smells like an attack.
- If the server-side verification failed e.g. origin verification,
challenge verification, etc. The library seems to use 'ValueError'
for all such errors [1] (after auditing its 'raise' statements, and
excluding AttestationError [2], since we're not doing that).
- If a key is used that attempts to sign with an unsupported
algorithm. This would normally raise a NotImplemented error as part
of verifying attestation [3], but we don't do that, so we need to
verify the algorithm is supported by the library manually.
This adds error handling to return a 400 response and error message
in these cases, since the error is not unexpected (i.e. not a 500).
A 400 seems more appropriate than a 403, since in many cases it's
not clear if the request data is valid.
I've used CBOR for the transport encoding, to match the successful
request / response encoding. Note that the ordering of then/catch
matters in JS - we don't want to catch our own throws!
[1]: https://github.com/Yubico/python-fido2/blob/142587b3e698ca0e253c78d75758fda635cac51a/fido2/server.py#L255
[2]: https://github.com/Yubico/python-fido2/blob/c42d9628a4f33d20c4401096fa8d3fc466d5b77f/fido2/attestation/base.py#L39
[3]: https://github.com/Yubico/python-fido2/blob/c42d9628a4f33d20c4401096fa8d3fc466d5b77f/fido2/cose.py#L92
2021-05-14 09:17:12 +01:00
|
|
|
|
session['webauthn_registration_state'] = 'state'
|
|
|
|
|
|
|
|
|
|
|
|
mocker.patch(
|
|
|
|
|
|
'app.models.webauthn_credential.WebAuthnCredential.from_registration',
|
|
|
|
|
|
side_effect=RegistrationError('error')
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2021-12-30 16:13:49 +00:00
|
|
|
|
client_request.login(platform_admin_user)
|
2021-12-31 12:16:12 +00:00
|
|
|
|
client_request.post_response(
|
2021-12-30 16:13:49 +00:00
|
|
|
|
'main.webauthn_complete_register',
|
|
|
|
|
|
_data=cbor.encode('public_key_credential'),
|
|
|
|
|
|
_expected_status=400,
|
Handle errors when registration fails
Previously we would raise a 500 error in a variety of cases:
- If a second key was being registered simultaneously (e.g. in a
separate tab), which means the registration state could be missing
after the first registration completes. That smells like an attack.
- If the server-side verification failed e.g. origin verification,
challenge verification, etc. The library seems to use 'ValueError'
for all such errors [1] (after auditing its 'raise' statements, and
excluding AttestationError [2], since we're not doing that).
- If a key is used that attempts to sign with an unsupported
algorithm. This would normally raise a NotImplemented error as part
of verifying attestation [3], but we don't do that, so we need to
verify the algorithm is supported by the library manually.
This adds error handling to return a 400 response and error message
in these cases, since the error is not unexpected (i.e. not a 500).
A 400 seems more appropriate than a 403, since in many cases it's
not clear if the request data is valid.
I've used CBOR for the transport encoding, to match the successful
request / response encoding. Note that the ordering of then/catch
matters in JS - we don't want to catch our own throws!
[1]: https://github.com/Yubico/python-fido2/blob/142587b3e698ca0e253c78d75758fda635cac51a/fido2/server.py#L255
[2]: https://github.com/Yubico/python-fido2/blob/c42d9628a4f33d20c4401096fa8d3fc466d5b77f/fido2/attestation/base.py#L39
[3]: https://github.com/Yubico/python-fido2/blob/c42d9628a4f33d20c4401096fa8d3fc466d5b77f/fido2/cose.py#L92
2021-05-14 09:17:12 +01:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_complete_register_handles_missing_state(
|
2021-12-30 16:13:49 +00:00
|
|
|
|
client_request,
|
|
|
|
|
|
platform_admin_user,
|
Handle errors when registration fails
Previously we would raise a 500 error in a variety of cases:
- If a second key was being registered simultaneously (e.g. in a
separate tab), which means the registration state could be missing
after the first registration completes. That smells like an attack.
- If the server-side verification failed e.g. origin verification,
challenge verification, etc. The library seems to use 'ValueError'
for all such errors [1] (after auditing its 'raise' statements, and
excluding AttestationError [2], since we're not doing that).
- If a key is used that attempts to sign with an unsupported
algorithm. This would normally raise a NotImplemented error as part
of verifying attestation [3], but we don't do that, so we need to
verify the algorithm is supported by the library manually.
This adds error handling to return a 400 response and error message
in these cases, since the error is not unexpected (i.e. not a 500).
A 400 seems more appropriate than a 403, since in many cases it's
not clear if the request data is valid.
I've used CBOR for the transport encoding, to match the successful
request / response encoding. Note that the ordering of then/catch
matters in JS - we don't want to catch our own throws!
[1]: https://github.com/Yubico/python-fido2/blob/142587b3e698ca0e253c78d75758fda635cac51a/fido2/server.py#L255
[2]: https://github.com/Yubico/python-fido2/blob/c42d9628a4f33d20c4401096fa8d3fc466d5b77f/fido2/attestation/base.py#L39
[3]: https://github.com/Yubico/python-fido2/blob/c42d9628a4f33d20c4401096fa8d3fc466d5b77f/fido2/cose.py#L92
2021-05-14 09:17:12 +01:00
|
|
|
|
mocker,
|
|
|
|
|
|
):
|
2021-12-30 16:13:49 +00:00
|
|
|
|
client_request.login(platform_admin_user)
|
2021-12-31 12:16:12 +00:00
|
|
|
|
response = client_request.post_response(
|
2021-12-30 16:13:49 +00:00
|
|
|
|
'main.webauthn_complete_register',
|
|
|
|
|
|
_data=cbor.encode('public_key_credential'),
|
|
|
|
|
|
_expected_status=400,
|
Handle errors when registration fails
Previously we would raise a 500 error in a variety of cases:
- If a second key was being registered simultaneously (e.g. in a
separate tab), which means the registration state could be missing
after the first registration completes. That smells like an attack.
- If the server-side verification failed e.g. origin verification,
challenge verification, etc. The library seems to use 'ValueError'
for all such errors [1] (after auditing its 'raise' statements, and
excluding AttestationError [2], since we're not doing that).
- If a key is used that attempts to sign with an unsupported
algorithm. This would normally raise a NotImplemented error as part
of verifying attestation [3], but we don't do that, so we need to
verify the algorithm is supported by the library manually.
This adds error handling to return a 400 response and error message
in these cases, since the error is not unexpected (i.e. not a 500).
A 400 seems more appropriate than a 403, since in many cases it's
not clear if the request data is valid.
I've used CBOR for the transport encoding, to match the successful
request / response encoding. Note that the ordering of then/catch
matters in JS - we don't want to catch our own throws!
[1]: https://github.com/Yubico/python-fido2/blob/142587b3e698ca0e253c78d75758fda635cac51a/fido2/server.py#L255
[2]: https://github.com/Yubico/python-fido2/blob/c42d9628a4f33d20c4401096fa8d3fc466d5b77f/fido2/attestation/base.py#L39
[3]: https://github.com/Yubico/python-fido2/blob/c42d9628a4f33d20c4401096fa8d3fc466d5b77f/fido2/cose.py#L92
2021-05-14 09:17:12 +01:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
assert cbor.decode(response.data) == 'No registration in progress'
|
2021-05-14 17:37:57 +01:00
|
|
|
|
|
|
|
|
|
|
|
2022-01-04 15:40:42 +00:00
|
|
|
|
def test_begin_authentication_forbidden_for_users_without_webauthn(client_request, mocker, platform_admin_user):
|
2021-06-10 23:51:39 +01:00
|
|
|
|
platform_admin_user['auth_type'] = 'sms_auth'
|
2022-01-04 15:40:42 +00:00
|
|
|
|
client_request.logout()
|
2021-05-17 12:35:43 +01:00
|
|
|
|
mocker.patch('app.user_api_client.get_user', return_value=platform_admin_user)
|
2021-05-14 17:37:57 +01:00
|
|
|
|
|
2022-01-04 15:40:42 +00:00
|
|
|
|
with client_request.session_transaction() as session:
|
2021-05-17 12:35:43 +01:00
|
|
|
|
session['user_details'] = {'id': '1'}
|
2021-05-14 17:37:57 +01:00
|
|
|
|
|
2022-01-04 15:40:42 +00:00
|
|
|
|
client_request.get(
|
|
|
|
|
|
'main.webauthn_begin_authentication',
|
|
|
|
|
|
_expected_status=403,
|
|
|
|
|
|
)
|
2021-05-17 12:35:43 +01:00
|
|
|
|
|
|
|
|
|
|
|
2022-01-04 15:40:42 +00:00
|
|
|
|
def test_begin_authentication_returns_encoded_options(
|
|
|
|
|
|
client_request,
|
|
|
|
|
|
mocker,
|
|
|
|
|
|
webauthn_credential,
|
|
|
|
|
|
platform_admin_user,
|
|
|
|
|
|
):
|
|
|
|
|
|
client_request.login(platform_admin_user)
|
2021-05-17 12:35:43 +01:00
|
|
|
|
|
2022-01-04 15:40:42 +00:00
|
|
|
|
with client_request.session_transaction() as session:
|
2021-05-17 12:35:43 +01:00
|
|
|
|
session['user_details'] = {'id': platform_admin_user['id']}
|
|
|
|
|
|
|
|
|
|
|
|
get_creds_mock = mocker.patch(
|
2021-06-08 09:41:39 +01:00
|
|
|
|
'app.models.webauthn_credential.WebAuthnCredentials.client_method',
|
2021-05-17 12:35:43 +01:00
|
|
|
|
return_value=[webauthn_credential]
|
|
|
|
|
|
)
|
2022-01-04 15:40:42 +00:00
|
|
|
|
response = client_request.get_response('main.webauthn_begin_authentication')
|
2021-05-17 12:35:43 +01:00
|
|
|
|
|
|
|
|
|
|
decoded_data = cbor.decode(response.data)
|
|
|
|
|
|
allowed_credentials = decoded_data['publicKey']['allowCredentials']
|
|
|
|
|
|
|
|
|
|
|
|
assert len(allowed_credentials) == 1
|
|
|
|
|
|
assert decoded_data['publicKey']['timeout'] == 30000
|
|
|
|
|
|
get_creds_mock.assert_called_once_with(platform_admin_user['id'])
|
|
|
|
|
|
|
|
|
|
|
|
|
2022-01-04 15:40:42 +00:00
|
|
|
|
def test_begin_authentication_stores_state_in_session(
|
|
|
|
|
|
client_request,
|
|
|
|
|
|
mocker,
|
|
|
|
|
|
webauthn_credential,
|
|
|
|
|
|
platform_admin_user,
|
|
|
|
|
|
):
|
|
|
|
|
|
client_request.login(platform_admin_user)
|
2021-05-17 12:35:43 +01:00
|
|
|
|
|
2022-01-04 15:40:42 +00:00
|
|
|
|
with client_request.session_transaction() as session:
|
2021-05-17 12:35:43 +01:00
|
|
|
|
session['user_details'] = {'id': platform_admin_user['id']}
|
|
|
|
|
|
|
|
|
|
|
|
mocker.patch(
|
2021-06-08 09:41:39 +01:00
|
|
|
|
'app.models.webauthn_credential.WebAuthnCredentials.client_method',
|
2021-05-17 12:35:43 +01:00
|
|
|
|
return_value=[webauthn_credential]
|
|
|
|
|
|
)
|
2022-01-04 15:40:42 +00:00
|
|
|
|
client_request.get_response('main.webauthn_begin_authentication')
|
2021-05-17 12:35:43 +01:00
|
|
|
|
|
2022-01-04 15:40:42 +00:00
|
|
|
|
with client_request.session_transaction() as session:
|
2021-05-17 12:35:43 +01:00
|
|
|
|
assert 'challenge' in session['webauthn_authentication_state']
|
2021-05-14 17:37:57 +01:00
|
|
|
|
|
|
|
|
|
|
|
2021-05-17 12:37:04 +01:00
|
|
|
|
def test_complete_authentication_checks_credentials(
|
2022-01-04 15:40:42 +00:00
|
|
|
|
client_request,
|
2021-05-17 12:37:04 +01:00
|
|
|
|
mocker,
|
|
|
|
|
|
webauthn_credential,
|
|
|
|
|
|
webauthn_dev_server,
|
|
|
|
|
|
mock_create_event,
|
|
|
|
|
|
webauthn_authentication_post_data,
|
|
|
|
|
|
platform_admin_user
|
|
|
|
|
|
):
|
2022-01-04 15:40:42 +00:00
|
|
|
|
client_request.logout()
|
|
|
|
|
|
_set_up_webauthn_session(platform_admin_user['id'], client_request)
|
|
|
|
|
|
|
2021-05-17 12:37:04 +01:00
|
|
|
|
mocker.patch('app.user_api_client.get_user', return_value=platform_admin_user)
|
2021-06-08 09:41:39 +01:00
|
|
|
|
mocker.patch('app.models.webauthn_credential.WebAuthnCredentials.client_method', return_value=[webauthn_credential])
|
2021-06-02 11:25:02 +01:00
|
|
|
|
mocker.patch(
|
|
|
|
|
|
'app.main.views.webauthn_credentials._complete_webauthn_login_attempt',
|
|
|
|
|
|
return_value=Mock(location='/foo')
|
|
|
|
|
|
)
|
2021-05-17 12:37:04 +01:00
|
|
|
|
|
2022-01-04 15:40:42 +00:00
|
|
|
|
response = client_request.post_response(
|
|
|
|
|
|
'main.webauthn_complete_authentication',
|
|
|
|
|
|
_data=webauthn_authentication_post_data,
|
|
|
|
|
|
_expected_status=200,
|
|
|
|
|
|
)
|
2021-05-17 15:56:15 +01:00
|
|
|
|
|
2021-05-25 15:50:14 +01:00
|
|
|
|
assert cbor.decode(response.data) == {'redirect_url': '/foo'}
|
2021-05-14 17:37:57 +01:00
|
|
|
|
|
|
|
|
|
|
|
2021-05-17 12:37:04 +01:00
|
|
|
|
def test_complete_authentication_403s_if_key_isnt_in_users_credentials(
|
2022-01-04 15:40:42 +00:00
|
|
|
|
client_request,
|
2021-05-17 12:37:04 +01:00
|
|
|
|
mocker,
|
|
|
|
|
|
webauthn_credential,
|
|
|
|
|
|
webauthn_dev_server,
|
|
|
|
|
|
webauthn_authentication_post_data,
|
|
|
|
|
|
platform_admin_user
|
|
|
|
|
|
):
|
2022-01-04 15:40:42 +00:00
|
|
|
|
client_request.logout()
|
|
|
|
|
|
_set_up_webauthn_session(platform_admin_user['id'], client_request)
|
|
|
|
|
|
|
2021-05-17 12:37:04 +01:00
|
|
|
|
mocker.patch('app.user_api_client.get_user', return_value=platform_admin_user)
|
|
|
|
|
|
# user has no keys in the database
|
2021-06-08 09:41:39 +01:00
|
|
|
|
mocker.patch('app.models.webauthn_credential.WebAuthnCredentials.client_method', return_value=[])
|
2021-06-02 11:25:02 +01:00
|
|
|
|
mock_verify_webauthn_login = mocker.patch('app.main.views.webauthn_credentials._complete_webauthn_login_attempt')
|
|
|
|
|
|
mock_unsuccesful_login_api_call = mocker.patch('app.user_api_client.complete_webauthn_login_attempt')
|
2021-05-17 12:37:04 +01:00
|
|
|
|
|
2022-01-04 15:40:42 +00:00
|
|
|
|
client_request.post_response(
|
|
|
|
|
|
'main.webauthn_complete_authentication',
|
|
|
|
|
|
_data=webauthn_authentication_post_data,
|
|
|
|
|
|
_expected_status=403,
|
|
|
|
|
|
)
|
2021-05-17 12:37:04 +01:00
|
|
|
|
|
2022-01-04 15:40:42 +00:00
|
|
|
|
with client_request.session_transaction() as session:
|
2021-05-17 12:37:04 +01:00
|
|
|
|
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
|
|
|
|
|
|
|
2021-05-17 15:56:15 +01:00
|
|
|
|
assert mock_verify_webauthn_login.called is False
|
2021-05-19 18:18:09 +01:00
|
|
|
|
# make sure we incremented the failed login count
|
|
|
|
|
|
mock_unsuccesful_login_api_call.assert_called_once_with(platform_admin_user['id'], False)
|
2021-05-17 15:56:15 +01:00
|
|
|
|
|
2021-05-17 12:37:04 +01:00
|
|
|
|
|
|
|
|
|
|
def test_complete_authentication_clears_session(
|
2022-01-04 15:40:42 +00:00
|
|
|
|
client_request,
|
2021-05-17 12:37:04 +01:00
|
|
|
|
mocker,
|
|
|
|
|
|
webauthn_credential,
|
|
|
|
|
|
webauthn_dev_server,
|
|
|
|
|
|
webauthn_authentication_post_data,
|
|
|
|
|
|
mock_create_event,
|
|
|
|
|
|
platform_admin_user
|
|
|
|
|
|
):
|
2022-01-04 15:40:42 +00:00
|
|
|
|
client_request.logout()
|
2021-05-17 12:37:04 +01:00
|
|
|
|
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])
|
2021-06-02 11:25:02 +01:00
|
|
|
|
mocker.patch(
|
|
|
|
|
|
'app.main.views.webauthn_credentials._complete_webauthn_login_attempt',
|
|
|
|
|
|
return_value=Mock(location='/foo')
|
|
|
|
|
|
)
|
2021-05-17 12:37:04 +01:00
|
|
|
|
|
2022-01-04 15:40:42 +00:00
|
|
|
|
client_request.post('main.webauthn_complete_authentication', _data=webauthn_authentication_post_data)
|
2021-05-17 12:37:04 +01:00
|
|
|
|
|
2022-01-04 15:40:42 +00:00
|
|
|
|
with client_request.session_transaction() as session:
|
2021-05-17 12:37:04 +01:00
|
|
|
|
# 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
|
2021-05-17 15:56:15 +01:00
|
|
|
|
|
|
|
|
|
|
|
2021-05-27 12:07:11 +01:00
|
|
|
|
@pytest.mark.parametrize('url_kwargs, expected_redirect', [
|
|
|
|
|
|
({}, '/accounts-or-dashboard'),
|
|
|
|
|
|
({'next': '/bar'}, '/bar'),
|
|
|
|
|
|
])
|
|
|
|
|
|
def test_verify_webauthn_login_signs_user_in(
|
2022-01-04 15:40:42 +00:00
|
|
|
|
client_request,
|
2021-05-27 12:07:11 +01:00
|
|
|
|
mocker,
|
|
|
|
|
|
mock_create_event,
|
|
|
|
|
|
platform_admin_user,
|
|
|
|
|
|
url_kwargs,
|
|
|
|
|
|
expected_redirect,
|
|
|
|
|
|
):
|
2022-01-04 15:40:42 +00:00
|
|
|
|
client_request.logout()
|
|
|
|
|
|
with client_request.session_transaction() as session:
|
2021-05-17 15:56:15 +01:00
|
|
|
|
session['user_details'] = {
|
|
|
|
|
|
'id': platform_admin_user['id'],
|
|
|
|
|
|
'email': platform_admin_user['email_address']
|
|
|
|
|
|
}
|
2022-01-04 15:40:42 +00:00
|
|
|
|
client_request.login(platform_admin_user)
|
2021-06-02 11:25:02 +01:00
|
|
|
|
mocker.patch('app.main.views.webauthn_credentials._verify_webauthn_authentication')
|
|
|
|
|
|
mocker.patch('app.user_api_client.complete_webauthn_login_attempt', return_value=(True, None))
|
2021-06-14 12:40:12 +01:00
|
|
|
|
mocker.patch('app.main.views.webauthn_credentials.email_needs_revalidating', return_value=False)
|
2021-05-17 15:56:15 +01:00
|
|
|
|
|
2022-01-04 15:40:42 +00:00
|
|
|
|
resp = client_request.post_response(
|
|
|
|
|
|
'main.webauthn_complete_authentication',
|
|
|
|
|
|
_expected_status=200,
|
|
|
|
|
|
**url_kwargs
|
|
|
|
|
|
)
|
2021-05-17 15:56:15 +01:00
|
|
|
|
|
2021-05-27 12:07:11 +01:00
|
|
|
|
assert cbor.decode(resp.data)['redirect_url'] == expected_redirect
|
2021-05-17 15:56:15 +01:00
|
|
|
|
# removes stuff from session
|
2022-01-04 15:40:42 +00:00
|
|
|
|
with client_request.session_transaction() as session:
|
2021-05-17 15:56:15 +01:00
|
|
|
|
assert 'user_details' not in session
|
|
|
|
|
|
|
|
|
|
|
|
mock_create_event.assert_called_once_with('sucessful_login', ANY)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_verify_webauthn_login_signs_user_in_doesnt_sign_user_in_if_api_rejects(
|
2022-01-04 15:40:42 +00:00
|
|
|
|
client_request,
|
2021-05-17 15:56:15 +01:00
|
|
|
|
mocker,
|
|
|
|
|
|
platform_admin_user,
|
|
|
|
|
|
):
|
|
|
|
|
|
|
2022-01-04 15:40:42 +00:00
|
|
|
|
with client_request.session_transaction() as session:
|
2021-05-17 15:56:15 +01:00
|
|
|
|
session['user_details'] = {
|
|
|
|
|
|
'id': platform_admin_user['id'],
|
|
|
|
|
|
'email': platform_admin_user['email_address']
|
|
|
|
|
|
}
|
2022-01-04 15:40:42 +00:00
|
|
|
|
client_request.login(platform_admin_user)
|
2021-06-02 11:25:02 +01:00
|
|
|
|
mocker.patch('app.main.views.webauthn_credentials._verify_webauthn_authentication')
|
|
|
|
|
|
mocker.patch('app.user_api_client.complete_webauthn_login_attempt', return_value=(False, None))
|
2021-05-17 15:56:15 +01:00
|
|
|
|
|
2022-01-04 15:40:42 +00:00
|
|
|
|
client_request.post(
|
|
|
|
|
|
'main.webauthn_complete_authentication',
|
|
|
|
|
|
_expected_status=403,
|
|
|
|
|
|
)
|
2021-05-17 15:56:15 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_verify_webauthn_login_signs_user_in_sends_revalidation_email_if_needed(
|
2022-01-04 15:40:42 +00:00
|
|
|
|
client_request,
|
2021-05-17 15:56:15 +01:00
|
|
|
|
mocker,
|
|
|
|
|
|
mock_send_verify_code,
|
|
|
|
|
|
platform_admin_user,
|
|
|
|
|
|
):
|
|
|
|
|
|
user_details = {
|
|
|
|
|
|
'id': platform_admin_user['id'],
|
|
|
|
|
|
'email': platform_admin_user['email_address']
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2022-01-04 15:40:42 +00:00
|
|
|
|
with client_request.session_transaction() as session:
|
2021-05-17 15:56:15 +01:00
|
|
|
|
session['user_details'] = user_details
|
|
|
|
|
|
|
|
|
|
|
|
mocker.patch('app.user_api_client.get_user', return_value=platform_admin_user)
|
2021-06-02 11:25:02 +01:00
|
|
|
|
mocker.patch('app.main.views.webauthn_credentials._verify_webauthn_authentication')
|
|
|
|
|
|
mocker.patch('app.user_api_client.complete_webauthn_login_attempt', return_value=(True, None))
|
2021-06-14 12:40:12 +01:00
|
|
|
|
mocker.patch('app.main.views.webauthn_credentials.email_needs_revalidating', return_value=True)
|
2021-05-17 15:56:15 +01:00
|
|
|
|
|
2022-01-04 15:40:42 +00:00
|
|
|
|
resp = client_request.post_response(
|
|
|
|
|
|
'main.webauthn_complete_authentication',
|
|
|
|
|
|
_expected_status=200,
|
|
|
|
|
|
)
|
2021-05-17 15:56:15 +01:00
|
|
|
|
|
2021-05-25 15:50:14 +01:00
|
|
|
|
assert cbor.decode(resp.data)['redirect_url'] == url_for('main.revalidate_email_sent')
|
2021-05-17 15:56:15 +01:00
|
|
|
|
|
2022-01-04 15:40:42 +00:00
|
|
|
|
with client_request.session_transaction() as session:
|
2021-05-17 15:56:15 +01:00
|
|
|
|
# stuff stays in session so we can log them in later when they validate their email
|
|
|
|
|
|
assert session['user_details'] == user_details
|
|
|
|
|
|
|
|
|
|
|
|
mock_send_verify_code.assert_called_once_with(platform_admin_user['id'], 'email', ANY, ANY)
|