SMART on FHIR — smart-launch-api
The smart-launch-api service implements the SMART on FHIR
App Launch v2.2 EHR Launch
sequence. It is the bridge between an EHR system and Kastoria's imaging
services: an EHR launches the OHIF DICOM viewer with study context, the
service resolves the FHIR ImagingStudy to a DICOM StudyInstanceUID, and —
when configured — mints the viewer JWT that authorizes the resulting
WADO-RS and QIDO-RS
requests (see JWT Authorization).
This page documents the service from a caller's perspective: what the EHR invokes, what the browser is redirected through, and how the viewer JWT is produced.
Service base URL
https://{host}/
| Method | Path | Description |
|---|---|---|
| GET | /smart/launch |
EHR launch entry point; starts the SMART flow |
| GET | /smart/callback |
OAuth callback; completes the launch |
| GET | /smart/error |
Error page rendered for every failure path |
The SMART endpoints are browser redirect endpoints, not data APIs. Responses
are 302 redirects; a caller never receives a JSON body from them. Failures
are surfaced by redirecting to the error page (see
Error handling) — except for unknown paths, which answer
404 with {"error": "not_found", ...}.
Prerequisites
Before an EHR can launch:
- Register the EHR in the Kastoria admin console's Smart Launch API page. Each entry records:
- EHR URL — the FHIR base URL the EHR sends as
iss - Client ID — the OAuth client ID registered with that EHR
- State Secret — auto-generated at record creation; used to encrypt
the OAuth
statetoken - Active flag — a deactivated record refuses launches
- The EHR must advertise
launch-ehrin its/.well-known/smart-configurationcapabilities and expose anauthorization_endpointandtoken_endpoint. - A signing key must be configured on this service if viewer JWTs are required (see Viewer JWT issuance).
The configuration store is an allowlist: iss is unauthenticated input from
a query string, so nothing is fetched from the EHR until the configuration
record for it has been found.
The EHR launch flow
EHR ──1──▶ /smart/launch?iss=…&launch=… (browser)
◀─2── 302 → EHR authorization endpoint (browser)
EHR auth ──3──▶ user authenticates, grants launch context
◀─4── 302 → /smart/callback?code=…&state=… (browser)
service ──5──▶ token exchange + ID token verification (server-to-server)
service ──6──▶ ImagingStudy → StudyInstanceUID resolution
◀─7── 302 → OHIF viewer (+ optional #kastoriaToken) (browser)
- The EHR initiates the launch by opening
/smart/launch?iss={fhir_base_url}&launch={launch_id}— typically a new browser tab or iframe.launchis the opaque context identifier the EHR generated; the service passes it back to the EHR's authorization endpoint unchanged. - The service redirects to the EHR's authorization endpoint with an
OAuth 2.0 authorization-code request. PKCE (S256) is mandatory, and the
stateparameter is an encrypted token (JWE) carrying the PKCE verifier and EHR endpoints — the service is fully stateless. - The user authenticates with the EHR and approves the launch context.
- The EHR redirects back to
/smart/callback?code={code}&state={state}. Theredirect_uriit was given is derived from the request the service received (see Redirect URI derivation). - The service exchanges the code for tokens (
authorization_codegrant with the PKCE verifier), and verifies the returnedid_tokensignature against the EHR's JWKS endpoint when the SMART configuration publishes ajwks_uri. - The service resolves context: it reads the
patientandimagingStudyvalues the token response carried, resolves the FHIRImagingStudyID to its DICOMStudyInstanceUID(via theurn:dicom:uididentifier), checks the study belongs to the launch patient, and verifies the study exists in processed DICOM storage. - The service redirects the browser to the OHIF viewer with the resolved study — and, when enabled, a viewer JWT in the URL fragment.
Endpoint reference
GET /smart/launch
Initiates the SMART App Launch flow. Called by the EHR.
| Parameter | Required | Description |
|---|---|---|
iss |
Yes | The EHR's FHIR server base URL |
launch |
Yes | Opaque launch identifier issued by the EHR |
Example:
GET /smart/launch?iss=https://ehr.example.com/fhir&launch=abc123
The service looks up the EHR's configuration record, fetches the EHR's
/.well-known/smart-configuration, confirms the launch-ehr capability,
generates PKCE parameters and the encrypted state token, and redirects
(302) to the EHR's authorization endpoint with:
| Parameter | Value |
|---|---|
response_type |
code |
client_id |
From the EHR's configuration record |
redirect_uri |
{service}/smart/callback (see Redirect URI derivation) |
scope |
launch openid profile patient/ImagingStudy.read |
launch |
The launch identifier, passed through |
state |
The encrypted state token |
aud |
The iss value |
code_challenge / code_challenge_method |
PKCE S256 challenge |
GET /smart/callback
The OAuth redirect URI. The EHR's authorization server sends the browser here after authentication.
| Parameter | Required | Description |
|---|---|---|
code |
Yes | Authorization code from the EHR |
state |
Yes | The state token issued by /smart/launch |
error |
No | Error code, when the EHR rejected the authorization |
error_description |
No | Human-readable error from the EHR |
On success the service verifies the state token against the issuing EHR's
State Secret, exchanges the code for tokens, validates the launch context,
resolves the study, and redirects (302) to the viewer:
{viewer-base-url}/viewer?StudyInstanceUIDs={uid}
and, when a viewer JWT was minted:
{viewer-base-url}/viewer?StudyInstanceUIDs={uid}#kastoriaToken={jwt}
The JWT is URL-encoded and placed in the fragment, so it stays out of
server access logs. The appropriate information (e.g., the
studyInstanceUIDs array) needs to be presented in future requests to
DICOMweb to provide access to the studies — see
JWT Authorization for how.
GET /smart/error
The single error page every failure path redirects to. It renders whatever the redirect carried:
| Parameter | Description |
|---|---|
title |
Short failure title (e.g. EHR Not Configured) |
message |
Human-readable explanation |
detail |
Optional extra detail, safe to display |
Viewer JWT issuance
When a viewer-JWT signing key is configured for the deployment,
/smart/callback mints a viewer JWT and appends it to the viewer redirect as
the kastoriaToken fragment. The token's format, claims, and how DICOMweb
verifies it are documented in
JWT Authorization — this page covers only how the
token is produced here:
- Claim sources —
subandehrIssare taken from the EHR's verified OIDC ID token;studyInstanceUIDscarries the resolved DICOMStudyInstanceUIDof the launched study.iss,aud, andexpare set from the service's viewer-JWT configuration. - Minting precondition — a token is only minted when the EHR's ID token
was signature-verified via its JWKS endpoint (
jwks_uripresent in the SMART configuration). If the EHR publishes nojwks_uri, the ID token is decoded without verification and no viewer JWT is produced — the launch proceeds without the fragment and a warning is logged. This is deliberate: signing an unverified identity would launder trust downstream. A missing token for such an EHR is expected behavior, not a fault.
Redirect URI derivation
The redirect_uri presented to the EHR's authorization server is derived per
request as {public origin}/smart/callback, where the public origin is the
one browsers actually reach. A deployment behind a reverse proxy must present
that origin consistently — a mismatch makes the EHR reject the callback as an
unregistered redirect URI.
Error handling
The SMART endpoints are redirects, so failures are conveyed by redirecting to
/smart/error with a title, message, and optional detail — never by a
JSON error body. The titles below are the title query parameter on the
/smart/error redirect, and every title the service can produce is listed here.
They are treated as part of this API, so they are safe to assert against; the
accompanying message and detail are human-readable, may be reworded, and on some
paths carry text from your EHR.
GET /smart/launch:
| Title | Shown when |
|---|---|
Invalid Launch Request |
iss or launch is missing |
EHR Not Configured |
No usable configuration record for the iss |
EHR Deactivated |
The record exists but is deactivated |
EHR Configuration Error |
The EHR's smart-configuration could not be retrieved |
Unsupported EHR |
The EHR does not advertise the launch-ehr capability |
Launch Failed |
Any other failure |
GET /smart/callback:
| Title | Shown when |
|---|---|
Authorization Failed |
The EHR redirected back with an error parameter |
Invalid Request |
code or state is missing |
Session Expired or Invalid |
The state token is malformed, expired, unverifiable, or names an unconfigured EHR |
Token Exchange Failed |
The code was invalid, expired, or already redeemed |
ID Token Validation Failed |
The id_token failed signature verification |
Missing Patient Context |
No patient in the token response |
Missing Imaging Study Context |
No imagingStudy in the token response |
Missing Launch Context |
Neither patient nor imagingStudy |
Imaging Study Not Found |
The study does not exist, is not processed, or belongs to another patient |
Launch Failed |
Any other failure |
Error detail hygiene: the service writes its own failure text rather than surfacing
internal exceptions — the two exceptions are Authorization Failed and
Token Exchange Failed, which relay what your EHR reported. The one page whose detail
names a study or patient (Imaging Study Not Found) goes only to the user who launched it
and is deliberately kept out of logs.
Security properties
- PKCE is mandatory (S256) on every authorization request.
- The OAuth
stateis an encrypted token (JWE,dir+A256GCM) keyed by the per-EHR State Secret: tamper-evident and unreadable, expiring after 5 minutes to match the authorization-code lifetime. No server-side session storage exists. - The configuration store is the allowlist — the
iss-named EHR is not contacted until a configuration record for it exists, so the endpoint cannot be used to probe arbitrary addresses. - ID token signatures are verified via the EHR's JWKS endpoint when published — the precondition for viewer-JWT minting described in Viewer JWT issuance.
- PHI-adjacent values are never logged: token values, state tokens, and
launch context identifiers appear in logs as presence only (
present: true/false).
Testing
Merkalis can provide a standalone EHR simulator for testing the full SMART launch / EHR flow. It acts as the launching EHR — initiating the launch, driving the authorization exchange, and loading the viewer with the resolved study.
See also
- JWT Authorization — how the viewer JWT minted by this service authorizes QIDO-RS / WADO-RS requests
- WADO-RS — Retrieve Instances
- QIDO-RS — Search for Instances