Skip to main content

Fire Arrow Server 1.14.0

· 7 min read

Fire Arrow Server 1.14.0 has been released.

  • (breaking) The web UI now signs in through a server-side confidential OAuth client; every login profile must be given a client secret and have its identity provider redirect URI updated before upgrading, or the server will not start
  • (feature) A new $drop-privilege operation lets a privileged caller mint a short-lived token scoped down to a single identity, never granting more access than the caller already has

Web UI Sign-In Now Uses a Confidential Backend-for-Frontend

Before 1.14.0, the administration web UI signed in as a public OAuth client running entirely in the browser. Access and refresh tokens were obtained in the browser and kept in localStorage, and every FHIR and admin API call sent them from the browser as a bearer token. This is the pattern the current IETF best-practice guidance for browser-based apps explicitly recommends against for applications that handle personal data — and Fire Arrow handles patient data.

Starting with 1.14.0, sign-in goes through a Backend-for-Frontend (BFF) that runs inside the server. The server itself is now the OAuth client and holds the client secret. The browser never receives an access or refresh token: it holds only a session cookie, and all API calls are proxied through /bff/fhir/** and /bff/admin-api/**, which attach the bearer token server-side. Refresh happens server-side, and signing out revokes the tokens at the identity provider rather than only clearing browser state.

This is a security improvement, but it is also a breaking configuration change: an existing web UI OAuth client that was registered as a public/single-page client will no longer work, and a profile without a client secret is rejected at startup.

Breaking Change

Each web UI login profile under fire-arrow.admin.web-ui-auth.profiles must now be a confidential OAuth client. If any profile is missing client-secret, the server fails to start with an error naming the offending profile. Before upgrading, add a client secret to every profile and reconfigure the client at your identity provider.

There are two parts to the migration: the identity provider, and application.yaml.

1. At your identity provider

  • Convert the web UI application registration from a public/SPA client to a confidential (web) client, and generate a client secret.
  • Update the registered redirect URI to the BFF callback. The path is now /bff/login/oauth2/code/<profile>, where <profile> is the profile's profile value. For a profile named patient-login on host fhir.example.com, register:
https://fhir.example.com/bff/login/oauth2/code/patient-login

The previous browser callback (for example /admin/login/callback) is no longer used and can be removed.

  • If you rely on single sign-out, register a post-logout redirect URI as well, for example https://fhir.example.com/admin/login.

2. In application.yaml

Add client-secret to every profile. The offline_access scope is no longer required in the browser — refresh is handled server-side — so the scope list can be simplified. A profile that previously looked like this:

fire-arrow:
admin:
web-ui-auth:
profiles:
- profile: patient-login
name: Patient
authentication-system: oidc_example
client-id: "${OIDC_PATIENT_CLIENT_ID}"
scopes: "openid offline_access"

becomes:

fire-arrow:
admin:
web-ui-auth:
profiles:
- profile: patient-login
name: Patient
authentication-system: oidc_example
client-id: "${OIDC_PATIENT_CLIENT_ID}"
client-secret: "${OIDC_PATIENT_CLIENT_SECRET}"
scopes: "openid profile"

The new per-profile keys are:

KeyRequiredDefaultPurpose
client-secretYesThe confidential client secret. Supply it through an environment variable or secret store; do not commit it.
client-authentication-methodNoclient_secret_basicHow the server authenticates to the identity provider's token endpoint. Set to private_key_jwt for private-key JWT client authentication.
post-logout-redirect-uriNoWhere the identity provider returns the browser after single sign-out.

Content Security Policy for external origins

1.14.0 also adds a configurable security-headers block for the admin UI under fire-arrow.admin.security-headers, covering HSTS and a Content Security Policy. The defaults are deployment-agnostic and require no change for a standard setup.

If your identity provider, object storage, or another dependency is served from a different origin than the server, add those origins to the CSP so the login redirect and API calls are not blocked:

fire-arrow:
admin:
security-headers:
csp:
additional-connect-src: ["https://login.example-idp.com"]
additional-frame-src: []
additional-script-src: []
hsts:
enabled: true
max-age-seconds: 31536000
include-sub-domains: false

include-sub-domains now defaults to false so that HSTS is safe to enable on deployments that share a parent domain with unrelated subdomains.

What else to expect on upgrade

The BFF stores its sessions and authorized-client records in the server's PostgreSQL database. The required tables are created automatically by Flyway on the first startup after upgrade; no manual database step is needed beyond the DDL permissions the server already uses for migrations.

Deployments that run with authentication disabled (AUTH_ENABLED=false) are unaffected: the BFF does not activate, and the UI continues to render without a login.

Delegated Access with $drop-privilege

1.14.0 introduces a server-level FHIR operation, $drop-privilege, for services that hold broad access but need to act within the permissions of a single subject. A typical example is a backend Device identity with access across many organizations that, before handing work to a less-trusted component, wants a token that can only see and touch one Patient.

A JWT-authenticated caller issues:

POST [base]/$drop-privilege

with a Parameters body:

ParameterRequiredDescription
fhir_identity_referenceYesThe target identity to scope the token to, e.g. Patient/123.
expires_atNoRequested expiry instant. Capped as described below.

The response is an RFC 8693-style Parameters resource containing access_token (a short-lived fa_ token), token_type (Bearer), expires_in, fhir_identity_reference, and on_behalf_of (the original caller's reference).

The key guarantee is that a dropped-privilege token can never widen access. At use time, every request made with it is authorized as the intersection of two rule sets:

  • the target identity's rules (for example, Patient/123's patient-compartment rules), and
  • the original caller's rules (the on-behalf-of identity's rules).

If the target's rules would allow something the caller's rules deny, the request is denied. The token is therefore always a subset of what the caller could already do.

Who can call it

$drop-privilege is a JWT-only operation. Callers presenting an API token (fa_/fo_) receive 403 Forbidden, and the JWT must carry an exp claim. Minting is gated in two steps:

  1. The caller must have an authorization rule granting the drop-privilege operation on the target resource type.
  2. The caller must additionally be authorized to update the specific target resource instance under its own rules.

An authorization rule granting the operation looks like the other operation rules in application.yaml:

- resourceType: "Patient"
operation: "drop-privilege"
validator: "LegitimateInterest"

The operation requires the API token provider to be enabled (API_TOKENS_ENABLED=true), since the minted token is a Fire Arrow fa_ token.

Token lifetime

The issued token's lifetime is the minimum of the requested expiry, the caller's JWT exp claim, and the configured maximum. If the JWT is close to expiring, the dropped token is correspondingly short; the caller refreshes its JWT and mints again. The relevant settings live under the API token provider and are only needed if you use the operation:

KeyDefaultPurpose
drop-privilege-default-expiration-minutes5Lifetime when the caller does not request one.
drop-privilege-max-expiration-minutes15Upper bound on any dropped token's lifetime.
drop-privilege-cleanup-after-days14How long expired dropped tokens are retained for audit before deletion.

The supporting database columns are added automatically by Flyway on the first startup after upgrade; no operator action is required.