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:
| Field | Endpoint used for |
|---|---|
authorization_endpoint | Authorization redirect |
token_endpoint | Code exchange, token refresh |
jwks_uri | id_token signature verification |
scopes_supported | Scope validation (optional) |
Step 3 — PKCE and state
PkceHelper.generate() produces:
codeVerifier— 96 random bytes, Base64URL-encoded (no padding)codeChallenge—BASE64URL(SHA-256(code_verifier))state— 32 random bytes, Base64URL-encoded — used as CSRF tokennonce— 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 →