Introduction
This is the second post, in a series of posts about adding support for Authentication (AuthN) to HAPI FHIR with OAuth2 Proxy, Nginx and Keycloak:
- Add AuthN to HAPI FHIR with OAuth2 Proxy, Nginx and Keycloak - Part 1
- Add AuthN to HAPI FHIR with OAuth2 Proxy, Nginx and Keycloak - Part 2
Keycloak
Keycloak is an open source Identity and Access Management solution that supports:
- Single Sign On (SSO)
- OpenID Connect (OIDC), OAuth 2.0 and SAML 2.0
- LDAP and Active Directory
- User Federation, Identity Brokering and Social Login
- Centralised User Management
Update the Docker Compose configuration file, as follows:
keycloak:
container_name: keycloak
build:
context: ./services/keycloak
dockerfile: Dockerfile
# restart: unless-stopped
healthcheck:
test: "bash /opt/keycloak/health-check.sh"
interval: 5s
timeout: 10s
retries: 12
command:
[
'start-dev',
'--log-level=INFO,io.vertx.ext.web.impl.RouterImpl:TRACE',
]
environment:
KC_HOSTNAME: ${KEYCLOAK_HOSTNAME:-localhost}
KC_HOSTNAME_PORT: ${KEYCLOAK_PORT:-5001}
KC_HOSTNAME_STRICT_BACKCHANNEL: false
KC_HTTP_ENABLED: true
KC_HOSTNAME_STRICT_HTTPS: false
KC_HEALTH_ENABLED: true
KC_BOOTSTRAP_ADMIN_USERNAME: ${KEYCLOAK_ADMIN:-admin}
KC_BOOTSTRAP_ADMIN_PASSWORD: ${KEYCLOAK_ADMIN_PASSWORD:-secret}
KC_DB: postgres
KC_DB_URL: jdbc:postgresql://postgres:5432/${POSTGRES_DB:-hapi-fhir}
KC_DB_USERNAME: ${POSTGRES_USER:-admin}
KC_DB_PASSWORD: ${POSTGRES_PASSWORD:-secret}
ports:
- ${KEYCLOAK_PORT:-5001}:8080
volumes:
- .:/import
- ./services/keycloak/health-check.sh:/opt/keycloak/health-check.sh
depends_on:
postgres:
condition: service_healthy
See: docker-compose.yml
The updated .env
file:
PROTOCOL=http
POSTGRES_DB=hapi-fhir
POSTGRES_USER=admin
POSTGRES_PASSWORD=secret
KEYCLOAK_HOSTNAME=localhost
# KEYCLOAK_HOSTNAME=host.docker.internal
KEYCLOAK_PORT=5001
KEYCLOAK_REALM=hapi-fhir-dev
KEYCLOAK_ADMIN=admin
KEYCLOAK_ADMIN_PASSWORD=secret
See: .env
Note: Docker will look for your .env
file in the same directory as your Docker Compose configuration file.
Start the services:
docker compose up
Sign in to the Admin Console
Navigate to the Admin Console:
http://localhost:5001
And sign in using the bootstrap Admin username (admin) and password (secret):
The Keycloak Admin Console's welcome page:
As noted in the OAuth2 Proxy OAuth Provider Configuration Guide for Keycloak OIDC, I followed the steps in Keycloak's Getting Started with Docker guide to create: a realm; a user; and a client.
Create a realm
Click the (dropdown) arrow next to Keycloak (master), then click the 'Create realm' button:
Enter a Realm name (hapi-fhir-dev) in the Realm name field then click the 'Create' button:
Create a user
Initially, a new realm will have no users:
Click the 'Create new user' button:
- Leave the 'Required user actions' field blank
- Enable the 'Email verified' option
Enter a:
- Username: hey@rob-ferguson.me
- Email: hey@rob-ferguson.me
- First (Given) name: Rob
- Last (Family) name: Ferguson
Then click the 'Create' button.
This user needs a password to log in. To set an initial password, click the 'Credentials' tab and complete the 'Set password' form:
Toggle 'Temporary' to 'Off' so that the user does not need to update their password the first time they sign in.
Sign in to the Account Console
You can log in to the Account Console to verify that this user is configured correctly.
Navigate to the Account Console:
http://localhost:5001/realms/hapi-fhir-dev/account
Log in with username and the password you created:
As a user in the Account Console, you can manage your account including modifying your profile, adding two-factor authentication, and including identity provider accounts.
Create a client
In the Realm that you created (hapi-fhir-dev) select 'Clients' and then click the 'Create client' button:
Select:
- Client type: OpenID Connect
Enter a:
- Client ID: oauth2-proxy
- Name: OAuth2 Proxy
Then click the 'Next' button.
Enable the 'Client authentication' option and deselect the 'Direct access grants' option:
Then click the 'Next' button.
Enter a Valid redirect URI (http://localhost:4180/oauth2/callback):
Then click the 'Save' button.
You can find your 'Client Secret' on the 'Credentials' tab:
I followed the steps in the OAuth2 Proxy OAuth Provider Configuration Guide for Keycloak OIDC to create a dedicated audience mapper:
For example:
Stop the services:
docker compose stop
OAuth2 Proxy
Update the Docker Compose configuration file, as follows:
redis:
container_name: redis
build:
context: ./services/redis
dockerfile: Dockerfile
command: --save 60 1 --loglevel warning
# restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "redis-cli ping | grep PONG"]
start_period: 20s
interval: 30s
retries: 5
timeout: 3s
volumes:
- cache:/data
oauth2-proxy:
container_name: oauth2-proxy
build:
context: ./services/oauth2-proxy
dockerfile: Dockerfile
# restart: unless-stopped
command:
[
'--standard-logging=true',
'--auth-logging=true',
'--request-logging=true',
'--skip-auth-preflight=true',
]
environment:
# https://oauth2-proxy.github.io/oauth2-proxy/configuration/providers/openid_connect
# https://oauth2-proxy.github.io/oauth2-proxy/configuration/providers/keycloak_oidc
# https://zitadel.com/docs/examples/identity-proxy/oauth2-proxy
# Provider config
OAUTH2_PROXY_SKIP_OIDC_DISCOVERY: true
# OAUTH2_PROXY_PROVIDER: oidc
OAUTH2_PROXY_PROVIDER: keycloak-oidc
OAUTH2_PROXY_PROVIDER_DISPLAY_NAME: Keycloak
# OAUTH2_PROXY_USER_ID_CLAIM: sub
OAUTH2_PROXY_REDIRECT_URL: ${PROTOCOL}://${OAUTH2_PROXY_HOSTNAME}:${OAUTH2_PROXY_PORT}/oauth2/callback
OAUTH2_PROXY_OIDC_ISSUER_URL: ${PROTOCOL}://${KEYCLOAK_HOSTNAME}:${KEYCLOAK_PORT}/realms/${KEYCLOAK_REALM}
OAUTH2_PROXY_UPSTREAMS: http://hapi-fhir
# OAUTH2_PROXY_EMAIL_DOMAIN: "*"
OAUTH2_PROXY_EMAIL_DOMAINS: "*"
# OAUTH2_PROXY_CODE_CHALLENGE_METHOD: S256
# OAuth2 client configuration
OAUTH2_PROXY_CLIENT_ID: ${CLIENT_ID}
OAUTH2_PROXY_CLIENT_SECRET: ${CLIENT_SECRET}
OAUTH2_PROXY_PASS_ACCESS_TOKEN: true
# Cookie configuration
OAUTH2_PROXY_COOKIE_NAME: ${COOKIE_NAME}
OAUTH2_PROXY_COOKIE_SECRET: ${COOKIE_SECRET}
# OAUTH2_PROXY_COOKIE_DOMAINS: ${OAUTH2_PROXY_HOSTNAME}:${OAUTH2_PROXY_PORT}
# OAUTH2_PROXY_COOKIE_DOMAINS: ${OAUTH2_PROXY_HOSTNAME}
OAUTH2_PROXY_COOKIE_DOMAINS: .${OAUTH2_PROXY_HOSTNAME}:${OAUTH2_PROXY_PORT}
OAUTH2_PROXY_COOKIE_SAMESITE: lax
OAUTH2_PROXY_COOKIE_HTTPONLY: true
OAUTH2_PROXY_COOKIE_SECURE: false
OAUTH2_PROXY_COOKIE_EXPIRE: 10m
OAUTH2_PROXY_COOKIE_REFRESH: 5m
OAUTH2_PROXY_COOKIE_CSRF_PER_REQUEST: true
OAUTH2_PROXY_HTTP_ADDRESS: 0.0.0.0:${OAUTH2_PROXY_PORT}
OAUTH2_PROXY_WHITELIST_DOMAINS: .${OAUTH2_PROXY_HOSTNAME}:${OAUTH2_PROXY_PORT}
OAUTH2_PROXY_INSECURE_OIDC_ALLOW_UNVERIFIED_EMAIL: true
OAUTH2_PROXY_SSL_UPSTREAM_INSECURE_SKIP_VERIFY: true
OAUTH2_PROXY_SSL_INSECURE_SKIP_VERIFY: true
OAUTH2_PROXY_ERRORS_TO_INFO_LOG: true
OAUTH2_PROXY_SHOW_DEBUG_ON_ERROR: true
# Session storage
OAUTH2_PROXY_REDIS_CONNECTION_URL: redis://redis
OAUTH2_PROXY_SESSION_STORE_TYPE: redis
ports:
- ${OAUTH2_PROXY_PORT:-4180}:4180
depends_on:
redis:
condition: service_healthy
keycloak:
condition: service_healthy
volumes:
postgres_data:
driver: local
cache:
driver: local
See: docker-compose.yml
The updated .env
file:
PROTOCOL=http
# Postgres
POSTGRES_DB=hapi-fhir
POSTGRES_USER=admin
POSTGRES_PASSWORD=secret
# Cookie
COOKIE_NAME=oauth2-proxy
COOKIE_SECRET=uzVUu9BdSpOXqPeMaGoTYuTHazRXWoUCajyLUfWlnv8=
# Keycloak
KEYCLOAK_HOSTNAME=127.0.0.1
KEYCLOAK_PORT=5001
KEYCLOAK_REALM=hapi-fhir-dev
KEYCLOAK_ADMIN=admin
KEYCLOAK_ADMIN_PASSWORD=secret
# OAuth Client
CLIENT_ID=oauth2-proxy
CLIENT_SECRET=aHkRec1BYkfaKgMg164JmvKu8u9iWNHM
# OAuth2 Proxy
OAUTH2_PROXY_HOSTNAME=127.0.0.1
OAUTH2_PROXY_PORT=4180
See: .env
Start the services:
docker compose up
Navigate to the secure HAPI FHIR Welcome page:
http://localhost:4180
Click the 'Sign in with Keycloak' button:
OAuth2 Proxy will redirect you to Keycloak so you can sign in:
Hmmm, that's not right:
Check the OAuth2 Proxy logs:
docker logs --tail 100 oauth2-proxy
Error redeeming code during OAuth2 callback: token exchange failed:
oauth2-proxy | [2024/12/31 05:31:19] [oauthproxy.go:1024] No valid authentication in request. Initiating login.
oauth2-proxy | 172.18.0.1:59698 - b9004406-829a-418c-8719-2d593884c0b8 - - [2024/12/31 05:31:21] localhost:4180 GET - "/oauth2/start?rd=%2F" HTTP/1.1 "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36" 302 418 0.001
keycloak | 2024-12-31 05:31:21,394 TRACE [io.vertx.ext.web.impl.RouterImpl] (vert.x-eventloop-thread-3) Router: 1136801768 accepting request GET http://localhost:5001/realms/hapi-fhir-dev/protocol/openid-connect/auth?approval_prompt=force&client_id=oauth2-proxy&code_challenge=9uxM9ZONIXfegF1Mb6AXQjUUjF68GPYq6xYxutu1yGM&code_challenge_method=S256&redirect_uri=http%3A%2F%2Flocalhost%3A4180%2Foauth2%2Fcallback&response_type=code&scope=openid+email+profile&state=MJTOr9fZLifKnfMqpYuCrYN1PuvGRGbTebXQqO2nQ2c%3A%2F
oauth2-proxy | [2024/12/31 05:31:21] [oauthproxy.go:895] Error redeeming code during OAuth2 callback: token exchange failed: Post "http://localhost:5001/realms/hapi-fhir-dev/protocol/openid-connect/token": dial tcp [::1]:5001: connect: connection refused
oauth2-proxy | 172.18.0.1:59698 - 7504b8b1-4c94-4687-89d0-151417ea375e - - [2024/12/31 05:31:21] localhost:4180 GET - "/oauth2/callback?state=MJTOr9fZLifKnfMqpYuCrYN1PuvGRGbTebXQqO2nQ2c%3A%2F&session_state=32f7b7f7-bdf4-4263-81c1-6d3c2fc6795e&iss=http%3A%2F%2Flocalhost%3A5001%2Frealms%2Fhapi-fhir-dev&code=3a88f5f0-e094-4796-96ac-4b31914764c9.32f7b7f7-bdf4-4263-81c1-6d3c2fc6795e.38ca69e4-14e5-4f40-9ece-5e1dc7c46f69" HTTP/1.1 "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36" 500 2771 0.002
OAuth2 Proxy - Open Issues
I have raised the following issues:
- Keycloak OIDC - Error redeeming code during OAuth2 callback: token exchange failed
- Keycloak Health Check
- OIDC Discovery fails
- AuthFailure Invalid authentication via OAuth2: unable to obtain CSRF cookie