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