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:
- SmartScopeInterceptor โ verifies RS256 JWT signature via
RemoteJWKSet, extractsscopeclaim, checks resource type + HTTP method against granted scopes - ConsentEnforcementInterceptor (v1.1.0) โ checks FHIR Consent record for this patient + client combination