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:

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:

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:

oauth2-proxy-keycloak-sign-in

Hmmm, that's not right:

oauth2-proxy-keycloak-sign-in-error

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:

Source Code: