Skip to main content

EHR Launch Flow

The EHR launch mode is triggered when a clinician clicks "Launch App" from within the Epic EHR. Epic opens the application in a new tab, sending iss and launch as query parameters.

Step 1 — Receive the launch request

SmartLaunchController.launch() handles GET /launch:

@GetMapping("/launch")
public String launch(
@RequestParam String iss,
@RequestParam String launch,
HttpSession session) {

// 1. Cache the ISS for this session
session.setAttribute("iss", iss);

// 2. Discover the authorization endpoint
SmartConfiguration config = discoveryService.discover(iss);

// 3. Generate PKCE parameters
PkceParameters pkce = pkceHelper.generate();
session.setAttribute("pkce", pkce);

// 4. Build the authorization URL
String authUrl = SmartAuthRequestBuilder.builder()
.authorizationEndpoint(config.authorizationEndpoint())
.clientId(smartProperties.getClientId())
.redirectUri(smartProperties.getRedirectUri())
.scopes(smartProperties.getScopes())
.launchToken(launch)
.iss(iss)
.codeChallenge(pkce.codeChallenge())
.state(pkce.state())
.build();

return "redirect:" + authUrl;
}

Step 2 — SMART discovery

SmartDiscoveryService.discover(iss) calls {iss}/.well-known/smart-configuration and returns a typed SmartConfiguration object. The result is cached in Caffeine for 10 minutes per ISS.

Key fields extracted from the discovery document:

FieldEndpoint used for
authorization_endpointAuthorization redirect
token_endpointCode exchange, token refresh
jwks_uriid_token signature verification
scopes_supportedScope validation (optional)

Step 3 — PKCE and state

PkceHelper.generate() produces:

  • codeVerifier — 96 random bytes, Base64URL-encoded (no padding)
  • codeChallengeBASE64URL(SHA-256(code_verifier))
  • state — 32 random bytes, Base64URL-encoded — used as CSRF token
  • nonce — 32 random bytes — included in authorization request, verified in id_token

These are stored in PkceParameters in the HTTP session.

Step 4 — Authorization redirect

The authorization URL includes all SMART-required parameters:

https://fhir.epic.com/interconnect-fhir-oauth/oauth2/authorize
?response_type=code
&client_id=<your-client-id>
&redirect_uri=https://your-app.com/callback
&scope=launch+openid+fhirUser+patient/Patient.rs+patient/Condition.rs
&launch=<launch-token>
&aud=<iss>
&code_challenge=<s256-challenge>
&code_challenge_method=S256
&state=<random-state>
&nonce=<random-nonce>

Note the aud parameter — Epic requires this to match iss exactly. The client sets aud=iss automatically.


Next: Standalone Launch →