Files
bd-fhir-national/ops/keycloak-setup.md
2026-03-16 00:02:58 +06:00

304 lines
8.7 KiB
Markdown

# Keycloak Setup — BD FHIR National
**Realm:** `hris`
**Keycloak URL:** `https://auth.dghs.gov.bd`
**Audience:** DGHS Identity and Access Management team
---
## Overview
This document covers the Keycloak configuration required for BD FHIR National
deployment. It assumes the `hris` realm and `mci-api` role already exist
(pre-existing national HRIS configuration). Only the additions for FHIR
deployment are documented here.
---
## Part 1 — Create `fhir-admin` realm role
The `fhir-admin` role grants access to:
- `DELETE /admin/terminology/cache` — terminology cache flush
- `GET /admin/terminology/cache/stats` — cache statistics
This role is **not** assigned to vendor clients. It is assigned only to the
ICD-11 version upgrade pipeline service account and DGHS system administrators.
### Steps (Keycloak Admin Console)
1. Log in to `https://auth.dghs.gov.bd/admin/master/console`
2. Select realm: **hris**
3. Navigate to: **Realm roles****Create role**
4. Fill in:
- **Role name:** `fhir-admin`
- **Description:** `BD FHIR server administrative operations — cache management and system configuration`
5. Click **Save**
### Steps (Keycloak Admin REST API — for automation)
```bash
# Get admin token
ADMIN_TOKEN=$(curl -s -X POST \
"https://auth.dghs.gov.bd/realms/master/protocol/openid-connect/token" \
-d "grant_type=password" \
-d "client_id=admin-cli" \
-d "username=${KEYCLOAK_ADMIN_USER}" \
-d "password=${KEYCLOAK_ADMIN_PASSWORD}" \
| jq -r '.access_token')
# Create fhir-admin role
curl -s -X POST \
"https://auth.dghs.gov.bd/admin/realms/hris/roles" \
-H "Authorization: Bearer ${ADMIN_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"name": "fhir-admin",
"description": "BD FHIR server administrative operations"
}'
```
---
## Part 2 — Create `fhir-admin` service account client
The version upgrade pipeline authenticates with a dedicated client.
This client must never be shared with vendor systems.
### Steps (Admin Console)
1. Navigate to: **Clients****Create client**
2. **Client type:** OpenID Connect
3. **Client ID:** `fhir-admin-pipeline`
4. Click **Next**
5. **Client authentication:** ON (confidential client)
6. **Service accounts roles:** ON
7. **Standard flow:** OFF (machine-to-machine only)
8. Click **Save**
### Assign fhir-admin role to service account
1. Navigate to: **Clients**`fhir-admin-pipeline`**Service accounts roles**
2. Click **Assign role**
3. Filter by: **Filter by realm roles**
4. Select: `fhir-admin`
5. Click **Assign**
### Retrieve client secret
1. Navigate to: **Clients**`fhir-admin-pipeline`**Credentials**
2. Copy **Client secret** — store in your secrets vault
3. This secret is used in `ops/version-upgrade-integration.md`
---
## Part 3 — Configure vendor clients
Each vendor organisation requires one Keycloak client. This section documents
the **template** for creating a vendor client. Repeat for each vendor.
### Naming convention
```
fhir-vendor-{organisation-id}
```
Where `{organisation-id}` is the DGHS facility code, e.g.:
- `fhir-vendor-DGHS-FAC-001` for Dhaka Medical College Hospital
- `fhir-vendor-DGHS-FAC-002` for Square Hospital
### Steps (Admin Console)
1. Navigate to: **Clients****Create client**
2. **Client type:** OpenID Connect
3. **Client ID:** `fhir-vendor-{organisation-id}`
4. Click **Next**
5. **Client authentication:** ON
6. **Service accounts roles:** ON
7. **Standard flow:** OFF
8. Click **Save**
### Assign mci-api role
1. Navigate to: **Clients**`fhir-vendor-{org-id}`**Service accounts roles**
2. Click **Assign role**
3. Select: `mci-api`
4. Click **Assign**
### Add sending_facility user attribute
The `sending_facility` claim is a custom token mapper that injects the vendor's
DGHS facility code into every token issued to this client. The
`KeycloakJwtInterceptor` reads this claim for audit logging.
**Without this mapper, audit logs will show `client_id` as the facility
identifier instead of the DGHS facility code. This degrades audit quality
and generates WARN logs in HAPI on every submission.**
#### Create user attribute on service account
1. Navigate to: **Clients**`fhir-vendor-{org-id}`**Service accounts**
2. Click the service account user link (e.g., `service-account-fhir-vendor-xxx`)
3. Navigate to: **Attributes** tab
4. Click **Add attribute**
5. Key: `sending_facility`
6. Value: `{DGHS facility code}` (e.g., `DGHS-FAC-001`)
7. Click **Save**
#### Create token mapper
1. Navigate to: **Clients**`fhir-vendor-{org-id}`**Client scopes**
2. Click the dedicated scope link (e.g., `fhir-vendor-xxx-dedicated`)
3. Navigate to: **Mappers****Add mapper****By configuration**
4. Select: **User Attribute**
5. Fill in:
- **Name:** `sending-facility-mapper`
- **User Attribute:** `sending_facility`
- **Token Claim Name:** `sending_facility`
- **Claim JSON Type:** String
- **Add to access token:** ON
- **Add to ID token:** OFF
- **Add to userinfo:** OFF
6. Click **Save**
#### Verify token contains sending_facility
```bash
# Get vendor token
TOKEN=$(curl -s -X POST \
"https://auth.dghs.gov.bd/realms/hris/protocol/openid-connect/token" \
-d "grant_type=client_credentials" \
-d "client_id=fhir-vendor-{org-id}" \
-d "client_secret={secret}" \
| jq -r '.access_token')
# Decode and check claims (base64 decode middle segment)
echo $TOKEN | cut -d. -f2 | base64 -d 2>/dev/null | jq '{
iss,
sub,
azp,
exp,
sending_facility,
realm_access: .realm_access.roles
}'
# Expected output:
# {
# "iss": "https://auth.dghs.gov.bd/realms/hris",
# "sub": "...",
# "azp": "fhir-vendor-{org-id}",
# "exp": ...,
# "sending_facility": "DGHS-FAC-001",
# "realm_access": ["mci-api", "offline_access"]
# }
```
---
## Part 4 — Token validation verification
After creating a client, verify the full token validation chain works
before onboarding the vendor.
### Test 1 — Valid token accepted
```bash
TOKEN=$(curl -s -X POST \
"https://auth.dghs.gov.bd/realms/hris/protocol/openid-connect/token" \
-d "grant_type=client_credentials" \
-d "client_id=fhir-vendor-{org-id}" \
-d "client_secret={secret}" \
| jq -r '.access_token')
curl -s -o /dev/null -w "%{http_code}" \
-H "Authorization: Bearer ${TOKEN}" \
https://fhir.dghs.gov.bd/fhir/Patient
# Expected: 200 (empty bundle) or 404 — NOT 401
```
### Test 2 — Missing token rejected
```bash
curl -s -o /dev/null -w "%{http_code}" \
https://fhir.dghs.gov.bd/fhir/Patient
# Expected: 401
```
### Test 3 — Expired token rejected
```bash
# Use a deliberately expired token (exp in the past)
# Easiest: wait for a token to expire (default Keycloak token lifetime: 5 minutes)
# Then attempt a request with the expired token.
# Expected: 401
```
### Test 4 — Wrong realm rejected
```bash
# Get a token from a different realm (if available) or forge iss claim
# Expected: 401
```
### Test 5 — mci-api role required
```bash
# Create a test client WITHOUT mci-api role
# Get token for that client
# Attempt FHIR request
# Expected: 401
```
### Test 6 — fhir-admin endpoint requires fhir-admin role
```bash
# Use a vendor token (mci-api only, no fhir-admin)
VENDOR_TOKEN=...
curl -s -w "\n%{http_code}" \
-X DELETE \
-H "Authorization: Bearer ${VENDOR_TOKEN}" \
https://fhir.dghs.gov.bd/admin/terminology/cache
# Expected: 403
# Use fhir-admin token
ADMIN_TOKEN=$(curl -s -X POST \
"https://auth.dghs.gov.bd/realms/hris/protocol/openid-connect/token" \
-d "grant_type=client_credentials" \
-d "client_id=fhir-admin-pipeline" \
-d "client_secret={admin_secret}" \
| jq -r '.access_token')
curl -s -w "\n%{http_code}" \
-X DELETE \
-H "Authorization: Bearer ${ADMIN_TOKEN}" \
https://fhir.dghs.gov.bd/admin/terminology/cache
# Expected: 200 with flush summary JSON
```
---
## Part 5 — Token lifetime configuration
Keycloak default access token lifetime is 5 minutes. For machine-to-machine
FHIR submissions, this is appropriate — vendor systems must refresh tokens
before expiry. Do not increase the token lifetime to accommodate vendors who
are not refreshing tokens correctly. Token refresh is the vendor's
responsibility, not a server-side workaround.
**Recommended settings for vendor clients:**
| Setting | Value | Rationale |
|---------|-------|-----------|
| Access Token Lifespan | 5 minutes | Short-lived — minimises window for token replay |
| Refresh Token Max Reuse | 0 | One-time use refresh tokens |
| Client Session Idle | 30 minutes | Vendor batch jobs may pause between submissions |
| Client Session Max | 8 hours | Maximum session for a single batch run |
Configure at: **Realm Settings****Tokens** for defaults,
or per-client at: **Clients**`{client}`**Advanced****Advanced settings**.