EHR/EMR-12 min read

SMART on FHIR app launch: EHR launch vs standalone, iss, aud and PKCE

Implement SMART App Launch 2.2.0 against Epic and other EHRs — EHR launch versus standalone, iss and aud, the launch token, PKCE S256, and why the Epic sandbox is not production.

Ala Ben Aicha

SMART on FHIR app launch: EHR launch vs standalone, iss, aud and PKCE

Direct answer

EHR launch passes iss and an opaque launch token; standalone does not. Authorize with aud equal to the FHIR base and PKCE S256. Treat Epic sandbox versus production as a test matrix, not an SLA.

Launch-mode mapping

Pin SMART App Launch 2.2.0 (STU 2.2, package hl7.fhir.uv.smart-app-launch#2.2.0, generated 30 April 2024, on FHIR R4). The IG supports four use cases: patient standalone, patient portal launch, provider standalone, provider portal launch. This page is the launch and token handshake, not a resource-by-resource FHIR tutorial — that remains the FHIR integration guide. Vendor flavour versus Cerner/Oracle Health is the Epic vs Cerner comparison.

Mode How the app starts Launch parameters Authorize extras Token context
EHR launch User is already in a chart or portal; EHR opens the registered launch URL iss = FHIR base URL; launch = opaque handle Scope launch plus launch={token}; aud equals that iss EHR supplies patient / encounter from the current session
Standalone launch User opens the app outside the EHR None Scope launch/patient (and/or launch/encounter); aud = the FHIR base you intend to call Authorization server collects context (often a patient picker)

Discovery is GET {iss}/.well-known/smart-configuration (EHR launch) or GET {fhir-base}/.well-known/smart-configuration (standalone). You need authorization_endpoint, token_endpoint, and code_challenge_methods_supported including S256.

Authorize (minimum):

Parameter EHR launch Standalone
response_type code code
client_id registered registered
redirect_uri exact registered URI exact registered URI
scope launch + clinical scopes + optional openid fhirUser launch/patient + clinical scopes
state unpredictable, bound to the session same
aud FHIR resource base (same as iss) FHIR resource base you will call
launch echo the opaque token omit
code_challenge / code_challenge_method S256 PKCE S256 PKCE

All SMART apps SHALL support PKCE. Servers SHALL support S256 and SHALL NOT support plain. Token exchange POSTs grant_type=authorization_code, the code, the same redirect_uri, and code_verifier. Public apps cannot protect a client secret; confidential apps should prefer asymmetric client authentication when the EHR offers it.

aud exists so a genuine bearer token is not sent to a counterfeit resource server. The resource server must check that the token was issued for its FHIR base. If aud ≠ the server you call, expect a rejected token even when the code exchange succeeded.

Identifier traps

Trap What breaks Fix
iss host ≠ aud host Token works in discovery, 401 on Patient read EHR launch: aud is the launch iss. Do not rewrite the hostname to a “pretty” proxy
Launch token not echoed EHR cannot bind the OAuth request to the chart Scope launch and query launch= with the same opaque value
Sandbox client ID in production Epic (and others) issue two client IDs Non-production ID against the sandbox; production ID only after “ready for production”
PKCE verifier lost across the redirect invalid_grant Store code_verifier in app-specific storage keyed by state, not in a world-readable cookie
redirect_uri trailing-slash drift Authorization denied Byte-for-byte match with registration
Following a reference to another FHIR host with the same bearer Token theft / wrong audience New authorization for that aud; do not forward the token

Epic-specific testing topic, not a delivery promise: Epic on FHIR documents a Current Sandbox at https://fhir.epic.com/interconnect-fhir-oauth/ and separate production community-member bases. Backend OAuth is a JWT to the token endpoint (SMART Backend Services, with Epic differences). The sandbox auto-maps your client to a user; a live Epic community member requires an ECSA to map the client ID to an audit user before a backend token is issued. Capability, scopes, and patient selection differ between sandbox and that member. Put those deltas on the test matrix. Do not invent an “8-week App Orchard” SLA — App Orchard is retired, and no duration here is a quote.

Test fixtures

De-identified. These URLs and tokens are shapes, not credentials.

EHR launch URL the EHR opens:

https://app.example.org/launch?iss=https%3A%2F%2Fehr.example.org%2Ffhir&launch=xyz123

Authorize (line breaks for reading):

https://ehr.example.org/authorize?
  response_type=code&
  client_id=growth-chart-example&
  redirect_uri=https%3A%2F%2Fapp.example.org%2Fafter-auth&
  launch=xyz123&
  scope=launch%20patient%2FPatient.rs%20patient%2FObservation.rs%20openid%20fhirUser&
  state=98wrghuwuogerg97&
  aud=https%3A%2F%2Fehr.example.org%2Ffhir&
  code_challenge=YPXe7B8ghKrj8PsT4L6ltupgI12NQJ5vblB07F4rGaw&
  code_challenge_method=S256

Standalone omits launch and uses scope=launch/patient patient/Patient.rs ... with the same aud discipline.

Minimum tests:

  1. EHR launch: iss captured, launch echoed, aud == iss, PKCE round-trip, patient on the token response.
  2. Standalone: patient picker, launch/patient granted, no leftover launch parameter.
  3. Wrong aud (token requested for server A, call server B) — must fail.
  4. Replay of an authorization code — must fail.
  5. Epic (or analogue): sandbox client ID against sandbox; production client ID against a member FHIR base; document the first scope that exists in one and not the other.

Never commit real client secrets, JWKs, or access tokens. Rotate anything that has been in a screenshot.

Failure modes and rollback

Failure Detection Rollback
User stuck on authorize Missing state round-trip or blocked third-party cookies in an EHR iframe Break out of the iframe if the EHR requires it; never drop state
Token for the wrong patient patient on the token ≠ chart context Trust token context, not a Patient id the app cached from the last launch
Refresh after logout online_access vs offline_access confusion EHR launch usually wants online access; do not request offline unless the product actually needs unattended access
Sandbox-only scopes in a customer build First production 403 Scope matrix: sandbox × one non-prod member × prod; ship the intersection
Confidential secret in a public SPA Extractable client secret Public + PKCE, or a BFF that holds the secret

Disable a misbehaving app by revoking the client at the EHR and rotating keys. There is no “undo launch” other than rejecting the token and sending the user back to the chart.

Related service

If you need an Epic-facing SMART launch (provider embedded, patient standalone, or the test matrix against sandbox vs a member), that is Epic integration. For a scoped launch spike, use contact with project intent.

This page is not an Epic Showroom listing, not Vendor Services membership, not a security assessment, and not a timeline. Backend-services JWT vs patient vs provider SMART is the Epic FHIR for HealthTech startups decision page.

SMART on FHIROAuth 2.0PKCEEpicApp LaunchFHIREHRInteroperability

Related reading and services

Let's Continue the Conversation

Have questions about this topic? I'd love to hear from you.

Get in Touch

🍪 Do you like cookies?

Allow analytics cookies to help understand site visits and enquiries? Optional analytics stays off until you accept.

Learn More