Class EhrLaunchProvider

java.lang.Object
org.fhirfrog.frog.smart.EhrLaunchProvider
All Implemented Interfaces:
SmartAuthProvider

public final class EhrLaunchProvider extends Object implements SmartAuthProvider
SmartAuthProvider strategy for the SMART App Launch 2.0 EHR launch flow - the case where an EHR (or an EHR simulator such as ehr.smartforms.io) launches an app with an opaque or base64-JSON launch token, rather than the app being started standalone the way FormLoginPkceProvider drives it.

Exposed via three static factories rather than one constructor, because "drive the whole interactive walk" and "an authorization code/token was already obtained out-of-band" are genuinely different call shapes that share the same launch-context decoding logic:

  • preAuthorizedToken(String) - zero HTTP calls; wrap an already-obtained token response (e.g. captured from a manual browser walk through an EHR launcher, or a debug proxy) and just decode its launch context.
  • #preAuthorizedCode(String, String, String, String, String, String) - one HTTP call (the token POST); exchange an authorization code obtained out-of-band (e.g. captured from the redirect URL right after a human clicks through a login/consent screen once).
  • #fullWalk(String, String, String, String, String, String, AuthorizePageHandler) - the full walk: discovery, authorize, an optional pluggable interactive-step handler (see AuthorizePageHandler), then token exchange.

Why aud is sent on the authorize request here but not by FormLoginPkceProvider: SMART App Launch's EHR-launch flow mandates aud=<iss> on the authorize request (audience-binding the auth request to the resource server). This is a real behavioural difference from FormLoginPkceProvider, not an oversight to copy - that class drives a standalone launch against a target that doesn't enforce it.

Deviation from a browser-driven login: fullWalk is deliberately a best-effort, explicitly-bounded path, not a generic "solve any login page" abstraction - see AuthorizePageHandler's Javadoc. preAuthorizedToken(String) and #preAuthorizedCode(String, String, String, String, String, String) are the primary, always-reliable paths for targets with an interactive authorize screen this module can't drive; no headless-browser dependency is used or proposed here.

The obtained token (and its launch context) is cached for the lifetime of a provider instance, the same as FormLoginPkceProvider: obtainAccessToken() only runs its flow once, on first call. Construct a new instance to force a fresh run.

  • Method Details

    • preAuthorizedToken

      public static EhrLaunchProvider preAuthorizedToken(String tokenResponseJson)
      Mode 1a: wrap an already-obtained token response and just decode its launch context. Makes no HTTP calls at all - the cleanest, always-works path when a human (or an existing tool) has already driven the real browser through an EHR launcher once and captured the resulting token response.
      Parameters:
      tokenResponseJson - the full JSON body returned by the SMART token endpoint
    • preAuthorizedCode

      public static EhrLaunchProvider preAuthorizedCode(String iss, String code, String clientId, String clientSecret, String redirectUri, String codeVerifier, String tokenEndpointOverride)
      Mode 1b: exchange an authorization code obtained out-of-band (e.g. captured from the redirect URL immediately after a human clicks through a login/consent screen once - codes are typically short-lived, so this needs to be scripted) for a token. Exercises this module's own token-exchange logic end-to-end without ever needing to render or drive a login page.
      Parameters:
      iss - the SMART server's FHIR base URL (resource server / issuer), used for discovery; may be null if tokenEndpointOverride is given
      code - the authorization code to exchange
      clientId - the OAuth2 client id the code was issued to
      clientSecret - the client secret, for a confidential client - sent as HTTP Basic auth on the token request; null for a public/PKCE-only client
      redirectUri - the redirect URI the code was issued for
      codeVerifier - the PKCE code verifier used to obtain the code, or null if PKCE wasn't used for it
      tokenEndpointOverride - the token endpoint to POST to directly, or null to discover it from iss's smart-configuration
    • fullWalk

      public static EhrLaunchProvider fullWalk(String iss, String launch, String clientId, String clientSecret, String redirectUri, String scopes, String codeVerifier, AuthorizePageHandler interactiveHandler)
      Mode 2: drive the full EHR-launch walk - discovery, authorize, an optional pluggable interactive-step handler, then token exchange. See AuthorizePageHandler's Javadoc for what it can and can't help with.
      Parameters:
      iss - the SMART server's FHIR base URL (resource server / issuer)
      launch - the EHR-supplied launch token (opaque or base64-JSON)
      clientId - the OAuth2 client id
      clientSecret - the client secret, for a confidential client - sent as HTTP Basic auth on the token request; null for a public/PKCE-only client
      redirectUri - the redirect URI registered for that client
      scopes - space-separated scope string; must include launch
      codeVerifier - a PKCE code verifier, or null to generate a random one the same way FormLoginPkceProvider does
      interactiveHandler - handles an interactive (200 HTML) authorize-page response, or null if the target auto-approves and never shows one - in which case an interactive response fails fast rather than hanging
    • standaloneLaunch

      public static EhrLaunchProvider standaloneLaunch(String iss, String clientId, String clientSecret, String redirectUri, String scopes, String codeVerifier, AuthorizePageHandler interactiveHandler)
      Mode 2b: drive a standalone launch - like fullWalk(java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String, org.fhirfrog.frog.smart.AuthorizePageHandler), but with no launch token at all. For servers advertising the context-standalone-patient capability (patient context is negotiated during authorize itself, e.g. via a patient-picker or a pre-selected default for the client, rather than being handed to the app up front by an EHR). Aidbox is a concrete example: its smart-configuration lists both launch-ehr and context-standalone-patient, and unlike a true EHR-launch, nothing has to mint a launch id for this path first.
      Parameters:
      iss - the SMART server's FHIR base URL (resource server / issuer)
      clientId - the OAuth2 client id
      clientSecret - the client secret, for a confidential client - sent as HTTP Basic auth on the token request; null for a public/PKCE-only client
      redirectUri - the redirect URI registered for that client
      scopes - space-separated scope string (typically includes launch/patient rather than launch)
      codeVerifier - a PKCE code verifier, or null to generate a random one
      interactiveHandler - handles an interactive (200 HTML) authorize-page response, or null if the target auto-approves
    • obtainAccessToken

      public String obtainAccessToken()
      Description copied from interface: SmartAuthProvider
      Obtain an access token, running whatever flow this strategy implements.
      Specified by:
      obtainAccessToken in interface SmartAuthProvider
      Returns:
      the raw access token string (e.g. a JWT), suitable for use as Authorization: Bearer <token>
    • launchContext

      public Optional<LaunchContext> launchContext()
      The SMART launch context (patient/encounter/fhirContext/etc.) associated with the obtained token, for strategies that support SMART launch context - see EhrLaunchProvider, whose EHR-launch flow returns launch-context alongside the access token.

      A default method rather than an addition to the interface's required contract, so existing strategies that don't carry launch context (FormLoginPkceProvider, StaticTokenProvider) are unaffected and simply return Optional.empty().

      Triggers obtainAccessToken() if the flow hasn't run yet, since the launch context is decoded from the same token-endpoint response as the access token.

      Specified by:
      launchContext in interface SmartAuthProvider
      Returns:
      the launch context, or Optional.empty() if this strategy doesn't have one