Skip to content

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:

  1. Register the EHR in the Kastoria admin console's Smart Launch API page. Each entry records:
  2. EHR URL — the FHIR base URL the EHR sends as iss
  3. Client ID — the OAuth client ID registered with that EHR
  4. State Secret — auto-generated at record creation; used to encrypt the OAuth state token
  5. Active flag — a deactivated record refuses launches
  6. The EHR must advertise launch-ehr in its /.well-known/smart-configuration capabilities and expose an authorization_endpoint and token_endpoint.
  7. 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)
  1. The EHR initiates the launch by opening /smart/launch?iss={fhir_base_url}&launch={launch_id} — typically a new browser tab or iframe. launch is the opaque context identifier the EHR generated; the service passes it back to the EHR's authorization endpoint unchanged.
  2. The service redirects to the EHR's authorization endpoint with an OAuth 2.0 authorization-code request. PKCE (S256) is mandatory, and the state parameter is an encrypted token (JWE) carrying the PKCE verifier and EHR endpoints — the service is fully stateless.
  3. The user authenticates with the EHR and approves the launch context.
  4. The EHR redirects back to /smart/callback?code={code}&state={state}. The redirect_uri it was given is derived from the request the service received (see Redirect URI derivation).
  5. The service exchanges the code for tokens (authorization_code grant with the PKCE verifier), and verifies the returned id_token signature against the EHR's JWKS endpoint when the SMART configuration publishes a jwks_uri.
  6. The service resolves context: it reads the patient and imagingStudy values the token response carried, resolves the FHIR ImagingStudy ID to its DICOM StudyInstanceUID (via the urn:dicom:uid identifier), checks the study belongs to the launch patient, and verifies the study exists in processed DICOM storage.
  7. 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 — sub and ehrIss are taken from the EHR's verified OIDC ID token; studyInstanceUIDs carries the resolved DICOM StudyInstanceUID of the launched study. iss, aud, and exp are 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_uri present in the SMART configuration). If the EHR publishes no jwks_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 state is 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