Safeguard
Developer Tools•

SMART app launch: EHR vs standalone flow and its security checks

How EHR and standalone SMART launches differ, and the checks both need: PKCE with S256, a high entropy state, the aud parameter and fixed redirect URIs.

Safeguard Security Team
3 min read

A SMART app starts either from inside the EHR, which passes an iss and launch parameter, or standalone, where the app asks for context through scopes. Both then use an authorization code flow. Every app must support PKCE with S256, validate state, send aud, and use pre-registered redirect URIs.

Two ways to start

EHR launch. The EHR opens the app's registered launch URL with two parameters. iss is the EHR's FHIR endpoint. launch is an opaque identifier for this launch's context, which the app echoes back during authorization.

Standalone launch. The user starts the app outside the EHR, for example from a phone home screen. There is no launch request. The app asks for context through scopes such as launch/patient.

Discovery

The app appends .well-known/smart-configuration to the FHIR base URL and sends a GET. The JSON response provides the authorization and token endpoints.

The authorization request

The browser goes to the authorization endpoint with response_type set to code, client_id, redirect_uri, scope, state, aud, code_challenge and code_challenge_method. The launch parameter is required for EHR launch and omitted for standalone. Add openid and fhirUser if the app needs user identity.

The four checks that stop common attacks

PKCE. The spec states "All SMART apps SHALL support Proof Key for Code Exchange (PKCE)." Servers must support S256 and must not support plain. The app sends a challenge, then proves it holds the verifier at token exchange.

State. The app generates an unpredictable value per session with at least 122 bits of entropy, and validates it on return, tied to the user's current session. This guards against cross site request forgery and session fixation.

aud. This is the URL of the FHIR server the app wants to reach. It stops a genuine token from being sent to a counterfeit server. In an EHR launch it matches iss, and the resource server checks that a token's audience is itself.

Redirect URI. Register fixed, fully specified URIs in advance and match one exactly at request time. The app must not pass values received at its redirect URL on to other arbitrary URLs.

After the code arrives

The app POSTs to the token endpoint with grant_type authorization_code, the code, the same redirect_uri and the code_verifier. Public apps include client_id. Confidential apps authenticate. The response includes access_token, token_type, scope, and usually expires_in, and may add a refresh token, id token and context such as patient. The server must send Cache-Control: no-store.

The app calls the API with an Authorization: Bearer header. A refresh request may ask for the same or a narrower scope, never broader.

What to test in your own implementation

  • Does the server reject plain code challenge methods?
  • Does a reused or tampered state value fail?
  • Does a token for one FHIR base fail on another?
  • Are redirect URIs compared exactly, with no prefix matching?

For scope design, see SMART on FHIR scopes explained.

Quick check

Why does SMART need aud if the token already has scopes?

Scopes say what the token allows, aud says where it may be used. Without aud, a counterfeit server could receive a real token and replay it.

Sources

Last reviewed: 19 October 2026.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.