Platform
Authentication and security
One component, the identity hub, handles sign-in for the whole platform. People prove who they are with a one-time code sent to their email; the hub then gives each app a short-lived signed token. The apps never see a code, a session or an encryption key.
Design principles
- Credentials live in one place. Sign-in codes, sessions and the keys that protect personal data exist only in the hub.
- Apps trust a signature, not a database. Each app holds one verification key and checks the token's signature, expiry and audience. No app queries the hub on every request.
- Every app gets its own key. The hub signs Site Profiles' tokens with a different key from Triage's, and each token names the app it is for, so a token for one app is refused by another.
- Personal data is encrypted, and found without decrypting. Names and email addresses are encrypted at rest. Lookups use a keyed hash of the address (a blind index), so finding a person never requires decrypting the table.
- Environments cannot read each other. Production and preview use different keys for everything.
How a person signs in
Tokens and sessions
| Hub session | App token | |
|---|---|---|
| What it is | A 32-byte random value in a cookie on the hub's hostname. | A JSON Web Token signed with HMAC-SHA256 using that app's own key. |
| Stored as | SHA-256 hash in hub D1, with expiry, user agent and IP. | Not stored by the hub. Held by the app in the browser. |
| Lifetime | 24 hours, or 30 days with "remember me". Owners and Site Profiles or Service Sizing admins always get 30 days. No idle timeout. | 15 minutes. When it expires, the next API call fails and the app sends the person through the hub, which returns a new token without a code while the session lasts. The failed request is not retried. |
| Carries | Nothing readable. The cookie is HttpOnly, but the hub's own sign-in and admin pages also receive the raw value in script (R7). | Opaque person ID, the keyed blind index of their primary address, display name, owner flag, their tier in every app, all their grants and regions, the role for this app, issuer and audience. |
| Revoked by | Sign-out, or deactivating the person (deletes all their sessions). | Expiry. An issued token stays valid for up to 15 minutes. |
Controls on the sign-in path
| Control | Detail |
|---|---|
| One-time code | Six digits from a cryptographic random source, valid 10 minutes, five attempts, then locked. Only a keyed hash (HMAC) of the code is stored. |
| Rate limits | Five codes per address per 10 minutes; ten per IP address per hour. |
| No account discovery | The hub gives the same answer for known and unknown addresses, and adds a short delay to the unknown case. Timing differences are narrowed, not removed. |
| Return-address allowlist | The hub redirects only to an exact list of the platform's own hostnames, so it cannot be used to send a token elsewhere. |
| State check | The app creates a random state value before redirecting and accepts a token only if the same value comes back, which blocks login cross-site request forgery. Site Profiles' older portal handoff does not use this yet (R7). |
| Cross-origin rules | The hub answers browser requests only from an exact list of the platform's origins. |
| Admin API | Admin calls need the session value as a bearer header, not the cookie, so another site cannot make them on an admin's behalf. |
| Sign-in page | Served with a Content-Security-Policy that blocks all external sources (inline script is still allowed, R19) and no referrer. |
Data classification and protection
None of the five application databases holds information about patients. They hold staff directory information, facility and equipment data, workforce figures, and generic clinical guidance. Two exceptions sit outside the databases and are in the risk register: historical referral extracts in the repository (R1) and free text that users can paste into Triage's AI search (R5).
| Class | Examples | Protection |
|---|---|---|
| Staff personal data | Names and email addresses in the hub and in the radiologist, MRT and member registry. | Encrypted with AES-256-GCM, a fresh random IV per value. Looked up by HMAC-SHA256 blind index. In the hub, a key that is not exactly 64 hex characters is refused rather than silently used. The older staff user tables in Site Profiles and Triage still hold names and addresses in plain form until they are dropped at the end of the cutover (R14). |
| Staff profile data | Region, specialties, leadership roles, service area, directory visibility, consents. | Stored in plain form by design, because it is not identifying without the name, and access to it is controlled. |
| Security records | Session hashes, code hashes, audit events with IP addresses. | Hashes only for secrets. IP addresses kept in plain form for audit. |
| Facility data | Sites, equipment, staffing FTE, contacts, performance. | Plain form. Facility contact names and numbers should be encrypted (R14). |
| Workforce modelling | Reporting volumes, meetings, required and effective FTE by planning unit. | Plain form. Aggregates, not personal. |
| Clinical guidance | Triage scenarios, modalities, priorities. | Plain form. Public reference content. |
Why a blind index
To sign someone in, the hub has to find their record from the address they type, but it should not be able to list everyone's address without a reason. So the hub stores two things per address: the address encrypted, and a keyed hash of the normalised address. Sign-in hashes what was typed and looks up the hash. The address itself is decrypted only at the moment an email is sent or an administrator opens the record. Each person can have several verified addresses, each with its own hash, which matters because most staff will sign in to Entra with a different address from the one they use today.
Where each app stands
| App | Sign-in | Notes |
|---|---|---|
| Site Profiles | Hub | Verifies hub tokens for its own audience. Its older password sign-in, and the portal's admin link, still issue legacy tokens from its own user table until the cutover retires them (R7, R10). |
| Service Sizing | Hub | Has never had its own sign-in. Verifies hub tokens for its own audience. |
| Triage | Hub | Reading is public. Editing needs a hub token. Its own password sign-in and a legacy shared editor credential remain until the cutover removes them (R10). |
| My HNZ Radiology | Own session | Uses the same email-code pattern, but checks members against the Site Profiles registry and keeps its own session, because hnzradiology.nz cannot share a cookie with hnzradtools.nz. Moving it onto the hub is on the roadmap (R9). |
| Identity hub admin | Hub | Admin screen at id.hnzradtools.nz/admin. |
Known gaps in this area
These are tracked with fixes in the risk register. In summary:
- The token is handed to the app in the redirect URL and kept in browser storage. A single-use exchange code and a stricter Content-Security-Policy would close this (R7).
- For a short window after a successful sign-in, the code-check path is not rate-limited (R11).
- Any app's administrator can see the whole directory and audit log (R12).
- Token signing and hashing code is written in-house on Web Crypto, and older copies remain in three apps (R18).
- Owners and some admins always get 30-day sessions, with no idle timeout (R23).
Entra ID: where it plugs in
Moving sign-in from email codes to Health NZ's Microsoft Entra ID is the natural first win after handover. The architecture already has one place where it belongs, so the change is contained to the hub.
Three things already in place make this small:
- The identity table reserves a unique Entra object ID for each person.
- Each person can hold several verified addresses. Only about a third of current users sign in with a
tewhatuora.govt.nzaddress; the rest use legacy district addresses, so matching on one address alone would miss most people. - Access tiers map directly onto Entra groups (one group per app per tier) if Health NZ later wants group-driven access.
Separately, Cloudflare Access can put Entra sign-in in front of the admin screens as an extra layer, with no application change.