Architecture Overview
Component map
┌──────────── ──────────────────────────────────────┐
│ Immunisation App :8084 │
│ │
│ SmartLaunchController /launch /callback │
│ │ │
│ SmartLaunchService │
│ ├─ discover(iss) → Caffeine cache │
│ ├─ PKCE S256 (96-byte verifier) │
│ ├─ exchangeCode() → token + SMART extras │
│ ├─ id_token RS256 validation │
│ └─ refreshIfNeeded() (120s buffer) │
│ │ │
│ SmartSessionFilter ← every protected route │
│ TokenRefreshFilter ← /api/** only │
│ │ │
│ ImmunizationFhirService @Cacheable │
│ ├─ getVaccinationHistory() Immunization │
│ ├─ getRecommendations() ImmunizationRec. │
│ └─ getPatient() Patient │
│ │ │
│ VdsNcService │
│ └─ generateQrBase64() WHO VDS-NC payload │
│ │ │
│ ImmunizationController → Thymeleaf views │
│ dashboard / history / recommendations / │
│ certificate │
│ │
│ ImmunizationApiController /api/** → JSON │
└──────────────────────────────────────────────────┘
│ │
Auth Server :9000 HAPI FHIR :8080
PKCE, tokens, JWKS Immunization, Patient
ImmunizationRecommendation
Package structure
com.ajfhir.immunization
├── ImmunizationApplication.java @SpringBootApplication
├── config/
│ ├── ImmunizationConfig.java Security, FhirContext, filter chain
│ └── ImmunizationProperties.java @ConfigurationProperties("immunization")
├── model/
│ ├── SmartLaunchContext.java Session state (Serializable)
│ ├── VaccinationRecord.java Record wrapping FHIR Immunization
│ └── VaccineRecommendation.java Record wrapping ImmunizationRecommendation entry
├── service/
│ ├── SmartLaunchService.java Full SMART v2.2 handshake
│ ├── ImmunizationFhirService.java HAPI FHIR queries + caching
│ └── VdsNcService.java WHO VDS-NC payload + QR generation
├── controller/
│ ├── SmartLaunchController.java GET /launch, GET /callback
│ ├── ImmunizationController.java UI views (dashboard, history, etc.)
│ └── ImmunizationApiController.java REST JSON API /api/**
└── security/
├── SmartSessionFilter.java Session guard on protected routes
└── TokenRefreshFilter.java Proactive refresh on /api/**
Request lifecycle
Phase 1 — EHR launch (GET /launch?iss=...&launch=...)
SmartLaunchController.launch() receives the iss (FHIR base URL) and launch token. It creates a new SmartLaunchContext, calls SmartLaunchService.discover(iss) which fetches /.well-known/smart-configuration (cached by ISS), generates a 96-byte PKCE code verifier with S256 challenge, stores the context in the HTTP session, and issues a 302 redirect to the auth server's authorization endpoint.
Phase 2 — Callback (GET /callback?code=...&state=...)
SmartLaunchController.callback() reads the SmartLaunchContext from session, validates the CSRF state using constant-time comparison, then calls SmartLaunchService.exchangeCode(). The token exchange sends the code + PKCE verifier to the token endpoint and receives the complete SMART token response including patient, encounter, need_patient_banner, and id_token. The id_token is RS256-validated against the auth server's JWKS. The context is updated in session. 302 redirect to /dashboard.
Phase 3 — Protected routes
SmartSessionFilter intercepts every non-public request. If a valid SmartLaunchContext exists in session, it sets a Spring Security Authentication and the request proceeds. TokenRefreshFilter intercepts /api/** specifically and calls SmartLaunchService.refreshIfNeeded() — if less than 120 seconds remain on the access token, a proactive refresh runs before the controller executes.
Phase 4 — FHIR data
ImmunizationFhirService builds a HAPI IGenericClient configured with a BearerTokenAuthInterceptor for the current access token. Results are cached by patientId for 600 seconds. All four pages — dashboard, history, schedule, certificate — read from the same cache entries.
Next: Data Model →