Configuration
Environment variablesโ
Truly required (no default โ startup will fail or behave incorrectly without these):
| Variable | Description |
|---|---|
DB_PASS | Database password โ no default, startup fails if unset |
JWT_EXPECTED_ISSUER | Expected iss claim value โ validated on every token |
One of: CONSENT_JWKS_URI, RSA_PUBLIC_KEY_JWK, or RSA_PUBLIC_KEY_PATH | JWT key source โ application throws IllegalStateException at startup if none is set |
Optional with sensible defaults (override when your environment differs):
| Variable | Default | Description |
|---|---|---|
DB_URL | jdbc:postgresql://localhost:5432/ajfhir_consent | PostgreSQL JDBC URL |
DB_USER | ajfhir | Database username |
FHIR_BASE_URL | http://localhost:8080/fhir | HAPI FHIR server URL |
FHIR_SERVICE_TOKEN | (empty) | Service-account Bearer token for writing AuditEvent and Consent to HAPI |
AUTH_SERVER_JWKS_URI | (empty) | JWKS endpoint for Spring Security's resource server โ set to the same value as CONSENT_JWKS_URI |
JWT_EXPECTED_AUDIENCE | (empty) | Expected aud claim. If left blank, audience validation is disabled. Set this in production to prevent confused-deputy attacks. |
CONSENT_PATIENT_CLAIM | patient | JWT claim containing the FHIR Patient ID. Set to blank for Epic โ the interceptor extracts the patient ID from the FHIR request URL when this claim is absent |
CONSENT_ACTOR_CLAIM | client_id | JWT claim containing the OAuth2 client ID. Set to azp for Cerner / Keycloak |
CORS_ALLOWED_ORIGINS | (empty) | Single allowed origin for the FHIR servlet (e.g. https://myapp.example.com). For multiple origins use YAML list syntax โ a comma-separated env var will not be split correctly by Spring Boot |
FLYWAY_EXCLUDE_MIGRATIONS | V3__sandbox_seed_data | Migrations to skip. Omit or set to blank in dev to load test data |
JWT key source โ set exactly oneโ
| Variable | Description |
|---|---|
CONSENT_JWKS_URI | Dynamic JWKS endpoint โ fetches keys live, handles rotation automatically. Recommended. |
RSA_PUBLIC_KEY_JWK | Static JWK JSON string โ does not support key rotation |
RSA_PUBLIC_KEY_PATH | Path to a JWK JSON file on disk |
If none of the three key source variables are set, the application throws IllegalStateException at startup and refuses to run. This is intentional โ no key means no token can be validated.
Multi-issuer (CE feature)โ
Test against multiple auth servers simultaneously without restarting โ useful when developing against an Epic sandbox and a local Keycloak at the same time:
consent-manager:
expected-issuer: https://fhir.epic.com/interconnect-fhir-oauth
trusted-issuers:
- http://localhost:9000
trusted-issuer-jwks-uris:
- http://localhost:9000/oauth2/jwks
Opaque token introspection (CE feature)โ
Some Epic configurations issue opaque (non-JWT) access tokens. When introspection-uri is set the interceptor automatically falls back to RFC 7662 introspection when a token cannot be parsed as a JWT:
consent-manager:
introspection-uri: https://fhir.epic.com/interconnect-fhir-oauth/oauth2/introspect
introspection-client-id: ${INTROSPECTION_CLIENT_ID}
introspection-client-secret: ${INTROSPECTION_CLIENT_SECRET}
introspection-timeout-seconds: 5 # default
Consent lifecycle webhooks (CE feature)โ
Receive an HTTP POST on every consent creation, update, and revocation:
consent-manager:
webhook-urls:
- https://yourapp.example.com/hooks/consent
webhook-timeout-seconds: 5 # default
Payload fields: event, consentId, patientId, actorRef, provision, status, resources, timestamp.
Events: consent.created ยท consent.updated ยท consent.revoked
EHR-specific configurationโ
Epicโ
Epic tokens do not include the patient ID as a JWT claim โ only in the token response body. The interceptor falls back to extracting the patient ID from the FHIR request URL automatically.
CONSENT_JWKS_URI=https://fhir.epic.com/interconnect-fhir-oauth/.well-known/jwks
CONSENT_PATIENT_CLAIM= # blank โ extracted from URL
JWT_EXPECTED_ISSUER=https://fhir.epic.com/interconnect-fhir-oauth
JWT_EXPECTED_AUDIENCE=<your-epic-client-id>
Cerner / Oracle Healthโ
Cerner's iss claim ends in /oidc (e.g. https://authorization.cerner.com/tenants/abc123/oidc), while the JWKS endpoint ends in /oauth2/.well-known/jwks. These are different paths. Setting JWT_EXPECTED_ISSUER to the JWKS URI path is a common mistake that causes all token validation to fail silently.
CONSENT_JWKS_URI=https://authorization.cerner.com/tenants/<tenant>/oauth2/.well-known/jwks
CONSENT_ACTOR_CLAIM=azp
# Use the /oidc path here โ NOT the /oauth2 path
JWT_EXPECTED_ISSUER=https://authorization.cerner.com/tenants/<tenant>/oidc
Azure Health Data Servicesโ
CONSENT_JWKS_URI=https://login.microsoftonline.com/<tenant>/discovery/v2.0/keys
CONSENT_ACTOR_CLAIM=client_id
JWT_EXPECTED_ISSUER=https://login.microsoftonline.com/<tenant>/v2.0
application.yml referenceโ
consent-manager:
# HAPI FHIR server
fhir-base-url: ${FHIR_BASE_URL:http://localhost:8080/fhir}
fhir-service-token: ${FHIR_SERVICE_TOKEN:}
# JWT key source (pick ONE)
jwks-uri: ${CONSENT_JWKS_URI:}
rsa-public-key-jwk: ${RSA_PUBLIC_KEY_JWK:}
rsa-public-key-path: ${RSA_PUBLIC_KEY_PATH:}
# JWT claim mapping
patient-claim-name: ${CONSENT_PATIENT_CLAIM:patient}
actor-claim-name: ${CONSENT_ACTOR_CLAIM:client_id}
# JWT validation
expected-issuer: ${JWT_EXPECTED_ISSUER:}
expected-audience: ${JWT_EXPECTED_AUDIENCE:} # leave blank to skip (dev only)
# CE features
trusted-issuers: [] # additional trusted JWT issuers
trusted-issuer-jwks-uris: [] # JWKS URI per trusted issuer (parallel list)
introspection-uri: ${INTROSPECTION_URI:}
introspection-client-id: ${INTROSPECTION_CLIENT_ID:}
introspection-client-secret: ${INTROSPECTION_CLIENT_SECRET:}
introspection-timeout-seconds: 5
webhook-urls: []
webhook-timeout-seconds: 5
# Policy
default-policy: deny # never change to permit in production
platform-auth-server-enabled: false # always false in Community Edition
# Defaults for new consents
default-consent-period-days: 365
regulatory-basis: "GDPR Art.9 ยท HTI-1/TEFCA ยท SMART App Launch v2.2"
# CORS โ for multiple origins use yaml list, not a comma-separated env var
cors-allowed-origins: ${CORS_ALLOWED_ORIGINS:}
Flyway migrationsโ
| File | Description |
|---|---|
V1__initial_consent_schema.sql | Core tables: consent_record, consent_audit_event |
V2__user_context_and_scope_context.sql | Adds scope_context for SMART v2.2 user/ and system/ scope support |
V3__sandbox_seed_data.sql | Test data โ excluded by default via FLYWAY_EXCLUDE_MIGRATIONS |
Only set baseline-on-migrate: true on the very first deployment to a database that already has tables from a previous version. For all subsequent deployments leave it false.
Database setupโ
CREATE DATABASE ajfhir_consent OWNER ajfhir;
Flyway runs V1 and V2 on first startup, creating all tables and indexes. V3 is excluded by default. To load the sandbox seed data in development:
FLYWAY_EXCLUDE_MIGRATIONS='' docker compose up -d
Next: Consent Lifecycle โ