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 naming was introduced in 2016 without explanation [1]. I find it
confusing because:
- It's reminiscent of "_app", which is a Python convention indicating
the variable is internal, so maybe avoid using it.
- It suggests there's some other "app" fixture I should be using (there
isn't, though).
The Python style guide describes using an underscore suffix to avoid
clashes with inbuilt names [1], which is sort of applicable if we need
to import the "app" module [2]. However, we can also avoid clashes by
choosing a different name, without the strange underscore.
[1]: 3b1d521c10
[2]: 78824f54fd/tests/app/main/views/test_forgot_password.py (L5)
We need to re-initialise the webauthn_server module with original
app config, since this state is global across all tests. Since the
behaviour of the original fixture wasn't specific to verifying the
origin, I've renamed the fixture as part of making it global.
In order to keep the fixture simple, I've rewritten the test for
the webauthn_server module, so they don't touch the app fixture.
This links up the `get_webauthn_credentials_for_user` and
`create_webauthn_credential_for_user` methods of the user api client to
notifications-api.
To send data to the API we need strings to be unicode, so we call
decode('utf-8') on base64 objects.
Co-authored-by: Leo Hemsted <leo.hemsted@digital.cabinet-office.gov.uk>
This passes existing credentials in the server response, to allow
the browser to prevent re-registering the same key for the same
user. Registering the same key multiple times doesn't seem to be
an issue technically; the user has likely got their keys mixed up.
- Chrome says "you don't need to register it again".
- Safari exits with an InvalidStateError.
- Firefox exits with a DOMException.
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]: c42d9628a4/examples/server/server.py
[2]: c42d9628a4/examples/server/static/register.html
[3]: 91453d3639/app/assets/javascripts/updateContent.js (L33)
[4]: https://stackoverflow.com/questions/55971593/navigator-credentials-is-null-on-local-server
[5]: c42d9628a4/fido2/rpid.py (L69)
[6]: https://stackoverflow.com/questions/12394622/does-jquery-ajax-or-load-allow-for-responsetype-arraybuffer
This adds a new platform admin settings row, leading a page which
shows any existing keys and allows a new one to be registered. Until
the APIs for this are implemented, the user API client just returns
some stubbed data for manual testing.
This also includes a basic JavaScript module to do the main work of
registering a new authenticator, to be implemented in the next commits.
Some more minor notes:
- Setting the headings in the mapping_table is necessary to get the
horizontal rule along the top (to match the design).
- Setting caption to False in the mapping_table is necessary to stop
an extra margin appearing at the top.
Do not allow platform admins to:
- create broadcasts
- approve broadcasts
- reject broadcasts
that is, unless they have a send_messages permission
for a given service.
This is so platform admins have the minimum permissions necessary
to cancel a broadcast that might have been sent out accidentally.
Two reasons to not hide rejected broadcasts:
- if a broadcast was rejected by mistake then it’s useful to have an
audit of who did that
- it means you can still see old broadcasts without having to leave
in pending-approval, which is dangerous because they might
accidentally be approved
The `/accounts` page was listing trial mode services twice if a user
belonged to an org. They were shown under both the 'Live services' and
'Trial mode services' sections. After this change, 'Live services' will
show all live services (whether or not they belong to an org) and 'Trial
mode services' will show all trial mode services. If a user belongs to an
org, they will also see the summary of how many services per org at the
top of the page.
A couple of services in tests were renamed for clarity.
first of a two step process to remove invited user objects from the
session. we're removing them because they're of variable size, and with
a lot of folder permissions they can cause the session to exceed the 4kb
cookie size limit and not save properly.
this commit looks at invited org users only.
in this step, start saving the invited org user's id to the
session alongside the session object. Then, if the invited_org_user_id
is present in the next step of the invite flow, fetch the user object
from the API instead of from the session. If it's not present (due to a
session set by an older instance of the admin app), then just use the
old code to get the entire object out of the session.
For invites where the user is small enough to persist to the cookie,
this will still save both the old and the new way, but will always make
an extra check to the API, I think this minor performance hit is totally
fine. For invites where the user is too big to persist, they'll still
fail for now, and will need to wait until the next PR comes along and
stops saving the large invited user object to the session entirely.
We think that in some cases alerts will be composed in the moment, and
therefore making people first create a template is:
- not a good use of their time
- adding some conceptual complexity which they don’t need
This commit makes it possible to type some words and have them go
straight into the `content` field in the database.
In the future we might want to progressively enhance the radio buttons
so they show on the same page (like we do with the grey buttons on the
templates page).
We shouldn’t have a page where someone can look up any other user’s
email address based on their user ID.
We also don’t want a page where a malicious user could send someone an
link which would get them invited to the service.
Restricting the invite to be populated just from users in their own
organisation doesn’t mitigate against this stuff completely, but they
probably have a way of finding out the email address of someone in their
organisation already.
When we get a support ticket we need to check whether a user has any
live services.
We have a method for this on the user model now, so we don’t need a
separate function in the feedback code.
It wasn’t very well tested so I’ve adapted the old tests from the
feedback view to work against the method on the user model too.
On the uploads page we only show jobs which are within a service’s data
retention.
This commit does the same for when we’re listing the jobs for a contact
list. This matches the UI, which says a contact list has been ‘used
`<count_of_jobs>` in the last <data_retention> days’
Because we’re be grouping jobs under their parent contact lists it’s
good to have some information ‘scent’ to help people find their jobs,
ie by clicking into a contact list. It also lets you see which list have
been used more than others, maybe because the update hasn’t been sent
to that group of people yet.
The hint text under uploads always says when they were used. For contact
lists this is a bit more complicated, since they can:
- never have been used
- been used multiple times
This commit makes use of the new fields being returned by the API to say
determine when these messages are relevant. They also let us
differentiate between a contact list that’s never been used, and one
that has been used, but not recently enough to show any jobs against it.
It’s a bit unintuitive that starting a job from a contact list makes a
copy of the file, which has no relationship to the list it was copied
from. This is more of an implementation detail, rather than something
that comes from people’s mental models of what is going on. Or at least
that’s what I hypothesise.
I think it’s clearer to show jobs that come from contact lists within
the lists that they were created from. By naming the jobs by template
this gives a clearer view of what messages have been sent to the group
over time.
The `.send_from_contact_list` function redirected to `.check_messages`
with `original_file_name` in the query string. Contact lists already
have `original_file_name` as part of their metadata, so we can stop
sending it in the query string and use the metadata instead.
For emails and text messages we sort by the time the user (or API) sent
them.
This makes sense for broadcasts too, since most users will receive the
alert within seconds of it being broadcast.
For alerts that haven’t started yet we can sort by `updated_at`, which
is when the user preparing the broadcast submitted it for approval.
we want to keep track of all broadcast services across govt easily. As
such, when broadcasting is enabled for a service, we've decided we're
going to add the service to a special broadcasting organisation.
This organisation is defined in the config file. It's hard coded for
production, if you want to test locally, you should set
BROADCAST_ORGANISATION_ID in your local environment.
This shows the green banner with a tick when cancelling a user's
invitation to a service or organisation. The accessibility audit noted
that 'When cancelling an invite a new page loads, however, there is no
immediate indication that the invite has been cancelled.'
In order to display the invited user's email address as part of the
flash message, this adds new methods to the api clients for invites to get
a single invite.
When we have an approval flow, `pending-approval` will be the state a
broadcast is in between being a draft and broadcasting.
This means it is the earliest stage at which a broadcast can appear on
the dashboard, so this commit adds a new section at the top of the
dashboard to display these broadcasts (since the dashboard is in a
reverse chronological order).
Rather than displaying the scheduled time, the extra information shown
is the person who drafted the broadcast, since I reckon you’ll be coming
to this page because they’ve asked you to approve their broadcast.
The api returns letter details split by postage, so international
letters are returned with a postage of `europe` or `rest-of-world` not
`international` and these rows need to be added together when the rate
is the same before they are displayed on the usage page.
To do this, we need to replace the postage of `europe` and
`rest-of-world` with `international`. The data then needs to be sorted
by postage and rate before the letter units for rows which are
international and have the same rate are added together.
When a service is switched over to broadcast it has the email, text
message and letter permissions removed. And the links to switch these
settings back on are hidden.
This commit ensures that even if the user manually goes to the URLs for
these pages, they still won’t be able to switch the other channels back
on.
This commit adds a page to view a single broadcast. This is important
for two reasons:
- users need an audit of what happened when, and who else was involved
in approving or cancelling a broadcast
- we need a place to put actions (approving, cancelling) on a broadcast
so that you can confirm details of the message and the areas before
performing the action