Keycloak Configuration for FHIR Applications
By David Le -- Part 12 of the FhirHub Series
Keycloak is FhirHub's identity provider. It handles user authentication, token issuance, and role management. This post covers how the fhirhub realm is configured, how roles map to authorization policies, and how the .NET API transforms Keycloak's JWT claims.
Realm Setup
FhirHub uses a dedicated Keycloak realm called fhirhub. The realm is pre-configured via a JSON export that's auto-imported on first boot:
# docker-compose.yml
keycloak:
command: start-dev --import-realm
volumes:
- ./keycloak/realm-export.json:/opt/keycloak/data/import/realm-export.json:ro
The realm export contains:
- Client configurations
- Realm roles
- Default users (for development)
- Protocol mappers
- Authentication flows
Client Configuration
FhirHub registers two Keycloak clients:
fhirhub-frontend (Public Client)
The frontend client uses the Authorization Code flow with PKCE:
- Client Type: Public (no client secret)
- Flow: Authorization Code with PKCE (S256)
- Redirect URIs:
http://localhost:7002/* - Web Origins:
http://localhost:7002 - Standard Flow Enabled: Yes
- Direct Access Grants: No (browser-based only)
Public clients can't keep secrets, so PKCE provides security by generating a unique code verifier for each authorization request.
fhirhub-backend (Confidential Client)
The backend client uses Client Credentials for server-to-server communication with Keycloak's Admin API:
- Client Type: Confidential (has client secret)
- Flow: Client Credentials
- Service Account Enabled: Yes
- Purpose: User management, session queries
The backend uses this client to:
- List and manage users (admin panel)
- Query user sessions
- Invite new users
Role Hierarchy
FhirHub defines six realm roles with progressively restricted access:
admin
└── practitioner
└── nurse
└── front_desk
patient (separate hierarchy)
Each role grants specific capabilities:
admin
Full system access. Can manage users, view audit logs, delete exports. Inherits all permissions from practitioner.
practitioner
Clinical read/write access. Can create patients, write conditions and medications, order labs, manage exports. Inherits from nurse.
nurse
Clinical read access plus vitals recording. Can write vitals, manage alerts, view clinical overviews. Inherits from front_desk.
front_desk
Minimal clinical access. Can read patient demographics for scheduling and check-in purposes.
patient
Self-service access. Can read own vitals, conditions, medications, and labs. Cannot see other patients' data (enforced by PatientDataAccess policy).
Role-to-Policy Mapping
The 17 authorization policies map to roles as follows:
| Policy | admin | practitioner | nurse | front_desk | patient |
|---|---|---|---|---|---|
| CanReadPatients | x | x | x | x | x |
| CanWritePatients | x | x | |||
| CanReadVitals | x | x | x | x | |
| CanWriteVitals | x | x | x | ||
| CanReadConditions | x | x | x | x | |
| CanWriteConditions | x | x | |||
| CanReadMedications | x | x | x | x | |
| CanWriteMedications | x | x | |||
| CanReadLabs | x | x | x | x | |
| CanOrderLabs | x | x | |||
| CanReadClinicalOverviews | x | x | x | ||
| CanViewDashboard | x | x | x | ||
| CanManageAlerts | x | x | x | ||
| CanManageExports | x | x | |||
| CanDeleteExports | x | ||||
| CanManageUsers | x | ||||
| CanViewAuditLogs | x |
Claims Transformation
Keycloak embeds roles in a non-standard JWT claim path:
{
"realm_access": {
"roles": ["admin", "practitioner"]
},
"resource_access": {
"fhirhub-frontend": {
"roles": ["admin"]
}
}
}
.NET's RequireRole() expects roles as standard ClaimTypes.Role claims. The KeycloakClaimsTransformer bridges this gap:
// Registered in Program.cs
builder.Services.AddTransient<
IClaimsTransformation,
KeycloakClaimsTransformer>();
The transformer runs on every request after JWT validation, extracting roles from realm_access.roles and adding them as standard role claims that .NET's authorization system can evaluate.
Token Structure
A decoded FhirHub JWT contains:
| Claim | Example | Purpose |
|---|---|---|
iss | http://localhost:8180/realms/fhirhub | Token issuer |
sub | a1b2c3d4-... | User ID |
preferred_username | dr.smith | Display name |
realm_access.roles | ["practitioner"] | User roles |
aud | fhirhub-frontend | Intended audience |
exp | 1706547200 | Expiration time |
azp | fhirhub-frontend | Authorized party |
The API validates iss, aud, and exp on every request:
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
NameClaimType = "preferred_username",
RoleClaimType = ClaimTypes.Role
};
Keycloak Admin API Integration
FhirHub's admin panel communicates with Keycloak's Admin REST API for user management:
// Program.cs
builder.Services.Configure<KeycloakAdminOptions>(options =>
{
options.AdminApiBaseUrl = builder.Configuration["Keycloak:AdminApiBaseUrl"]
?? "http://localhost:8180";
options.Realm = builder.Configuration["Keycloak:Realm"] ?? "fhirhub";
options.BackendClientId = builder.Configuration["Keycloak:BackendClientId"]
?? "fhirhub-backend";
options.BackendClientSecret = builder.Configuration["Keycloak:BackendClientSecret"]
?? "";
});
builder.Services.AddHttpClient<IKeycloakAdminService, KeycloakAdminService>();
The KeycloakAdminService uses the backend client's service account to:
- List users in the realm
- Create new users (invite flow)
- Update user roles
- Query active sessions
- View user details
This is exposed through the UsersController in the API, protected by the CanManageUsers policy (admin only).
Development vs. Production
For development, Keycloak runs in start-dev mode which:
- Disables HTTPS requirement
- Enables the admin console without restrictions
- Uses in-memory caching
For production, you'd switch to start mode and:
- Enable HTTPS with proper certificates
- Restrict admin console access
- Configure external SMTP for email verification
- Set up session clustering for high availability
- Use stronger default passwords
What's Next
In Part 13, we'll cover HAPI FHIR server configuration -- the PostgreSQL backend, R4 version settings, CORS, and how the .NET API communicates with HAPI using the Hl7.Fhir.R4 client library.