Skip to main content

Data Flow

EHR Launch โ€” step by stepโ€‹

1. Clinician browser โ†’ GET /portal (Auth Server :9000)
2. Auth Server โ†’ GET /fhir/Patient (HAPI :8080) โ€” fetch patient list
3. Clinician selects patient โ†’ POST /portal/launch
4. Auth Server โ†’ INSERT launch_contexts (atomic, 5-min expiry)
5. Browser โ†’ redirect to SMART Client /launch?iss=...&launch=TOKEN
6. SMART Client โ†’ GET /.well-known/smart-configuration (HAPI)
7. HAPI โ†’ proxy โ†’ Auth Server /.well-known/smart-configuration
8. SMART Client โ†’ GET /oauth2/authorize?code_challenge=...&launch=TOKEN
9. Auth Server โ†’ redirect to /login (or IdP if idp profile active)
10. Clinician logs in
11. Auth Server โ†’ resolve launch token โ†’ patient + encounter
12. Auth Server โ†’ issue access_token (JWT, RS256, SMART extras in claims)
13. Browser โ†’ redirect to SMART Client /callback?code=...
14. SMART Client โ†’ POST /oauth2/token (code + code_verifier PKCE)
15. Auth Server โ†’ token response: access_token + patient + encounter + id_token
16. SMART Client โ†’ GET /fhir/Patient/{id} (Bearer token)
17. HAPI โ†’ SmartScopeInterceptor verifies RS256 + scope
18. AuditService โ†’ write FHIR AuditEvent (async)

Token response formatโ€‹

{
"access_token": "eyJ...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "launch openid patient/Patient.rs",
"refresh_token": "eyJ...",
"patient": "ePatient-123",
"encounter": "eEncounter-456",
"need_patient_banner": true,
"id_token": "eyJ..."
}

patient, encounter, and need_patient_banner are top-level JSON fields โ€” not just JWT claims. This is what most implementations get wrong.

Scope enforcementโ€‹

Every FHIR request passes through two interceptors in sequence:

  1. SmartScopeInterceptor โ€” verifies RS256 JWT signature via RemoteJWKSet, extracts scope claim, checks resource type + HTTP method against granted scopes
  2. ConsentEnforcementInterceptor (v1.1.0) โ€” checks FHIR Consent record for this patient + client combination