Skip to main content

Configuration

Environment variablesโ€‹

Truly required (no default โ€” startup will fail or behave incorrectly without these):

VariableDescription
DB_PASSDatabase password โ€” no default, startup fails if unset
JWT_EXPECTED_ISSUERExpected iss claim value โ€” validated on every token
One of: CONSENT_JWKS_URI, RSA_PUBLIC_KEY_JWK, or RSA_PUBLIC_KEY_PATHJWT key source โ€” application throws IllegalStateException at startup if none is set

Optional with sensible defaults (override when your environment differs):

VariableDefaultDescription
DB_URLjdbc:postgresql://localhost:5432/ajfhir_consentPostgreSQL JDBC URL
DB_USERajfhirDatabase username
FHIR_BASE_URLhttp://localhost:8080/fhirHAPI 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_CLAIMpatientJWT 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_CLAIMclient_idJWT 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_MIGRATIONSV3__sandbox_seed_dataMigrations to skip. Omit or set to blank in dev to load test data

JWT key source โ€” set exactly oneโ€‹

VariableDescription
CONSENT_JWKS_URIDynamic JWKS endpoint โ€” fetches keys live, handles rotation automatically. Recommended.
RSA_PUBLIC_KEY_JWKStatic JWK JSON string โ€” does not support key rotation
RSA_PUBLIC_KEY_PATHPath to a JWK JSON file on disk
danger

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

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 issuer format

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โ€‹

FileDescription
V1__initial_consent_schema.sqlCore tables: consent_record, consent_audit_event
V2__user_context_and_scope_context.sqlAdds scope_context for SMART v2.2 user/ and system/ scope support
V3__sandbox_seed_data.sqlTest data โ€” excluded by default via FLYWAY_EXCLUDE_MIGRATIONS
baseline-on-migrate

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 โ†’