Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

SMART App Launch

FerroEHR can act as the resource server in a SMART App Launch setup: a clinical app is launched with an OAuth2/OIDC token from your authorization server (Keycloak or any standards-compliant IdP), and the CDR advertises that server’s endpoints, understands SMART resource scopes in the token, and binds the launch context (the selected patient/EHR) to what the token may touch. FerroEHR never issues tokens, registers clients, or serves the OAuth2 endpoints itself — those remain your authorization server’s job.

Support is off by default. A stock server serves no discovery document and runs no scope gate, so the wire is byte-identical to a non-SMART deployment until you opt in.

Enabling it

Turn it on and tell the server where your authorization server lives:

export FERROEHR__SMART__ENABLED=true
export FERROEHR__SMART__ENDPOINTS__AUTHORIZATION_ENDPOINT=https://as.example/auth
export FERROEHR__SMART__ENDPOINTS__TOKEN_ENDPOINT=https://as.example/token

SMART scopes ride only Bearer tokens, so pair this with OIDC bearer authentication (see Security & multi-tenancy). If SMART is enabled without any bearer mechanism, the server logs a warning at boot: it can serve discovery, but the scope gate will never see a scope.

The full key set lives in the [smart] section of ferroehr.toml; each key can be overridden with the shown FERROEHR__SMART__* environment variable (__ separates nested fields):

KeyDefaultMeaning
FERROEHR__SMART__ENABLEDfalseMaster switch. Off = no discovery document (404) and an inert scope gate.
FERROEHR__SMART__PLATFORM_BASE_URLunsetBase the discovery document hangs off. Unset = the REST root (/ferroehr/rest). A leading path is honoured (/gateway/v1/gateway/v1/.well-known/smart-configuration).
FERROEHR__SMART__EHR_ID_CLAIMehrIdToken claim carrying the launch context’s openEHR EHR id.
FERROEHR__SMART__PATIENT_CLAIMpatientFallback launch-context claim when the EHR-id claim is absent.
FERROEHR__SMART__REQUIRE_SMART_SCOPESfalseFail-closed switch — see Advisory vs required below.
FERROEHR__SMART__EPISODE__ENABLEDfalseAdvertise + accept episode launch context (experimental; advisory only, no episode filtering).
FERROEHR__SMART__LAUNCH_BASE64_JSONfalseAdvertise the base64-JSON launch-parameter capability (experimental; consumed by the app, not the CDR).
FERROEHR__SMART__ENDPOINTS__ISSUERunsetAdvertised token issuer. Unset = falls back to the configured OIDC bearer issuer.
FERROEHR__SMART__ENDPOINTS__JWKS_URIunsetAdvertised jwks_uri.
FERROEHR__SMART__ENDPOINTS__AUTHORIZATION_ENDPOINTunsetAdvertised OAuth2 authorization endpoint.
FERROEHR__SMART__ENDPOINTS__TOKEN_ENDPOINTunsetAdvertised OAuth2 token endpoint.
FERROEHR__SMART__ENDPOINTS__REGISTRATION_ENDPOINTunsetAdvertised dynamic-client registration endpoint.
FERROEHR__SMART__ENDPOINTS__INTROSPECTION_ENDPOINTunsetAdvertised token introspection endpoint.
FERROEHR__SMART__ENDPOINTS__REVOCATION_ENDPOINTunsetAdvertised token revocation endpoint.
FERROEHR__SMART__ENDPOINTS__MANAGEMENT_ENDPOINTunsetAdvertised user management endpoint.
FERROEHR__SMART__ENDPOINTS__TOKEN_ENDPOINT_AUTH_METHODS_SUPPORTED[]Advertised client auth methods (e.g. client_secret_basic, private_key_jwt).
FERROEHR__SMART__ENDPOINTS__GRANT_TYPES_SUPPORTED[]Advertised grant types. implicit and the password grant are deprecated in SMART and rejected at boot.
FERROEHR__SMART__ENDPOINTS__RESPONSE_TYPES_SUPPORTED[]Advertised response types (e.g. code).
FERROEHR__SMART__ENDPOINTS__CODE_CHALLENGE_METHODS_SUPPORTED[]Advertised PKCE methods (e.g. S256).
FERROEHR__SMART__ENDPOINTS__SCOPES_SUPPORTED[]Advertised scopes. Empty = a default list reflecting what the CDR enforces.

Every unset optional endpoint is simply omitted from the discovery document — the server advertises your values verbatim and validates none of them beyond the deprecated-grant check.

The discovery document

When enabled, the server serves the standard SMART configuration document, unauthenticated, at:

GET /ferroehr/rest/.well-known/smart-configuration

(relative to the REST root — the configured base path without its /openehr/v1 tail — or to PLATFORM_BASE_URL when set). A launching app reads it to find your authorization server. It looks like:

{
  "issuer": "https://as.example/realms/ferroehr",
  "authorization_endpoint": "https://as.example/auth",
  "token_endpoint": "https://as.example/token",
  "capabilities": ["context-openehr-ehr", "openehr-permission-v1"],
  "scopes_supported": [
    "openid", "profile", "offline_access",
    "launch", "launch/patient",
    "patient/composition-*.cruds", "patient/aql-*.rs",
    "user/composition-*.cruds", "user/template-*.cruds", "user/aql-*.cruds",
    "system/composition-*.cruds", "system/aql-*.cruds"
  ],
  "response_types_supported": ["code"],
  "services": [
    { "type": "org.openehr.rest", "baseUrl": "/ferroehr/rest/openehr/v1" }
  ]
}
  • capabilities always contains context-openehr-ehr and openehr-permission-v1, plus context-openehr-episode and launch-base64-json when those switches are on.
  • services names the openEHR REST service with its base URL, plus the FHIR façade (org.fhir.rest) when the FHIR routes are enabled.
  • scopes_supported is the default list above unless you configure your own, which is then emitted verbatim.

With SMART disabled the path is not mounted at all (404).

The scope grammar

SMART resource scopes have the form <compartment>/<resource>.<permissions>:

  • Compartmentpatient (the launch context’s EHR only), user (what the user may see), or system (a backend service, no user).
  • Resource — one of three families: composition-<template-id>, template-<template-id>, or aql-<stored-query-name>.
  • Permissions — any combination of c create, r read, u update, d delete, s search/execute (order-free, e.g. .crud, .rs).

Resource ids accept wildcards: * matches within one ::-delimited namespace segment, ** matches across namespaces, and a bare * or ** matches every id. The permission tail is split at the last dot, so template ids and query names keep their internal dots and versions.

ScopeGrants
patient/composition-*.crudCreate, read, update, and delete any composition — but only in the launched patient’s EHR.
patient/aql-*.rsRead and execute any stored query, scoped to the launched patient’s EHR.
patient/composition-MyHospital::Template.v0.rRead compositions of exactly that template, in the launched patient’s EHR.
user/composition-MyHospital::*.rRead compositions of any template in the MyHospital namespace (not sub-namespaces).
user/template-*.crudsFull access to template definitions.
system/aql-org.openehr::bloodpressure.v1.rsA backend service may read and execute that one stored query, across all EHRs.

Scopes the server does not recognise are ignored (never granted, never fatal); the identity scopes (openid, profile, offline_access, …) and the launch/launch/patient context scopes pass through untouched.

Launch context: binding to one EHR

When an app is launched for a patient, your authorization server puts the resolved openEHR EHR id in the token — by default in an ehrId claim, with the standard SMART patient claim as fallback (both claim names are configurable). Any operation permitted only by a patient/… scope is then bound to that one EHR: requests against any other EHR are refused, and a token holding only patient-compartment scopes but no launch-context claim is refused outright. user/ and system/ scopes carry no such binding.

How scopes compose with RBAC and ABAC

The SMART gate is one more layer in the authorization chain, evaluated after authentication, the per-EHR EHR_ACCESS gate, RBAC, and ABAC (see Security & multi-tenancy). Every active layer must allow the request; SMART never overrides a denial from another layer. A scope denial is a 403 Forbidden.

In this release the scope gate enforces the composition family — composition reads and writes are checked against …/composition-….… scopes and the patient-compartment binding. The template and aql scope forms are parsed and advertised so authorization servers can issue them; those families, like the EHR, EHR_STATUS, CONTRIBUTION, and DIRECTORY operations (for which SMART defines no resource type), remain governed by the RBAC/ABAC layers.

Advisory vs required

  • Advisory (default, REQUIRE_SMART_SCOPES=false) — the gate enforces only when the token actually carries SMART resource scopes for the resource family in question. A non-SMART token (or a Basic-auth caller, which has no scopes) is unaffected. Once a token does carry, say, composition scopes, at least one of them must match the operation or the request is refused.
  • Required (REQUIRE_SMART_SCOPES=true) — fail-closed: a Bearer token with no matching SMART resource scope for a scope-governed operation is denied, matching deployments where every app is a SMART app.

Note

Episode context is experimental: enabling it advertises the capability and accepts launch/episode, but the server applies no episode-scoped filtering — openEHR has no first-class episode resource yet.