Note
This backend was an experiment and never made it to live use
Backend for storing data for the RCPCH Digital Growth Charts app
Requires Docker already installed and running.
Copy env-template to env and update the OAuth variables as appropriate.
s/upThe API and demo run on localhost directly rather than a specific port. This is because
Google doesn't allow oauth redirects to localhost with a port.
The API runs on localhost/api. It is using a self-signed Caddy TLS certificate so you may
need to accept the certificate by visiting it in your browser.
The demo client runs on localhost/demo.
Despite being built on top of Django, we don't use Django session management but our own bearer auth JWT.
To "log in" to the API, you pass a valid ID token or refresh token from Microsoft Entra ID or Google (Login with Apple TODO). The backend then vends an access token JWT. We have to issue our own tokens as Apple does not allow you to verify an access token.
Clients store the access token from the API alongside the refresh token from the third party auth provider.
We deliberately do not support our own authentication but delegate it to third party providers. This is a pragmatic choice to try and take advantage of the abuse protection those providers already do.
- Each person who logs in to the app is a
User. - Users are members of an
Organisation, viaUserOrganisation. - Each
Childhas multipleObservationsand can be in more than oneOrganisationviaUserOrganisation- Although not yet implemented the idea is that there is a single record for a child per linkage identifier (eg NHS number)
Access to an organisation grants you read/write access to all children within it. For parents and carers they will have their own organisation with just their children and invite in others. We envisage larger organisations to map to professional settings who want to use the app and not an EPR growth chart integration, but this will be in the future.
Some child data is encrypted as a defense against the database ever leaking. Identifying data is encrypted with
a key per organisation. The organisation key is not stored in the database directly but is stored encrypted
with a key statically derived (PBKDF2HMAC) from the sub claim in the token from the login provider. There is then
an additional child key to encrypt personal information like date of birth. This is itself stored encrypted with
each the key for each organisation the child is associated with. Encryption is performed using the
Fernet helper from the Python cryptography library.
We don't store sub directly but use the SHA-256 hash of it as the user ID. The defense relies on on sub being
random and difficult to guess, such that you couldn't rainbow table the list of IDs in the database. For example,
you can't use this project against a provider that vends sequential sub IDs.
Users are invited to join an organisation using a link with the following data
- Secure random UUID as the "invite ID"
- Secure randomly generated password (not stored in the database)
Each invite in the database stores the organisation key encrypted with an ephemeral key derived from the share password.
Joining an org involves decrypting the key, checking it works and then writing a new UserOrganisation linking model.
| Model | Field | Encryption Key | Notes |
|---|---|---|---|
| User | id | Generic secure random UUID stored plaintext. Placeholder for when users can log in with multiple methods (for example adding NHS login) | |
| UserRegistration | oauth_server | We don't expect any ID collision between providers but just in case | |
| UserRegistration | id | - | SHA-256 of sub from the login provider. Hash cannot be salted since we don't want to store sub anywhere in our database |
| UserRegistration | salt/iterations | - | Configuration for deriving the user encryption key |
| Organisation | encrypted_name | Organisation | Name describing the organisation (e.g. Michael's clinic). Encrypted with the organisation key as we have no idea what PII people might put in it |
| Organisation | default_reference | - | Which reference to use by default when creating new children under this organisation (e.g. uk-who for the NHS) |
| UserOrganisation | is_creator | - | Used to derive a name for the organisation if none set (e.g. organisation is a single one for a parent/carer user) |
| UserOrganisation | encrypted_organisation_key | User | |
| UserOrganisation | encrypted_user_name | Organisation | name claim. Used to display which users are in the organisation |
| UserOrganisation | encrypted_user_email | Organisation | email claim. Used to display which users are in the organisation |
| Child | id | - | Generic secure random UUID, stored plaintext. |
| Child | encrypted_name | Child | Displayed in the UI so the user can select from multiple children they can see |
| Child | encrypted_date_of_birth | Child | Required to call the dGC API. dGC key parameter - must regenerate all DGCResults if changed |
| Child | sex | - | Stored in plaintext so it can be used for analysis. dGC key parameter - must regenerate all DGCResults if changed. |
| Child | gestation_days | - | Unlike the API that has weeks and days as separate parameters we just store cumulative days. Only set if known otherwise term is assumed when calling the API. Again stored in plaintext so it can be used for analysis. dGC key parameter - must regenerate all DGCResults if changed |
| Child | reference | - | The reference to be used when adding new observations - avoids having to select it every time. We don't delete DGCResults if the reference changes |
| ChildOrganisation | encrypted_child_key | Organisation | |
| Observation | observation_type | - | height | weight | ofc |
| Observation | encrypted_observation_date | Child | Encrypted out of an abundance of caution that correlating multiple observations could infer a date of birth |
| Observation | days_since_birth | - | Not an identifier (unlike date of birth or potentially observation date). Stored plaintext for analysis |
| Observation | observation_value | - | Decimal |
| DGCResult | reference | - | The reference data used (e.g. uk-who, cdc, turners etc). Existing instances retained for analysis even if reference is changed |
| DGCResult | encrypted_dgc_api_result | Child | Data to render the chart. Includes date of birth and observation date so must be encrypted. |
| DGCResult | corrected/chronological_sds | - | Decimal. Stored as plaintext for analysis. |
| DGCResult | corrected/chronological_centile | - | Integer 0-100. Stored as plaintext for analysis. |
| OrganisationInvite | id | - | Generic secure random UUID, stored plaintext. Part of a pair with the randomly generated password, both required to join an org |
| OrganisationInvite | salt/iterations | - | Configuration for deriving the ephemeral key from the password to decrypt the organisation key |
| OrganisationInvite | encrypted_organisation_key | Share | Ronseal |