Add AuthN to HAPI FHIR with OAuth2 Proxy, Nginx and Keycloak - Part 4

Introduction

This is the fourth post, in a series of posts about adding support for Authentication (AuthN) to HAPI FHIR with OAuth2 Proxy, Nginx and Keycloak:

OAuth2 Proxy

OAuth2 Proxy is an open source tool that can be used to interecept requests to your application and redirect them to an Authorisation server that supports OAuth 2.0 and OpenID Connect.

I followed the recommendations in the following guides:

TLS Configuration

All traffic is routed through Nginx and TLS is terminated at Nginx (the reverse proxy).

Integration

OAuth2 Proxy forwards subrequests to Keycloak in order to authenticate requests to HAPI FHIR. Take a look at the auth_request directive section, in this post.

Config Options

OAuth2 Proxy is running behind Nginx (the reverse proxy):

  oauth2-proxy:
    container_name: oauth2-proxy

    ...

    environment:
    
      ...

      # Proxy options
      OAUTH2_PROXY_EMAIL_DOMAINS: '*'
      OAUTH2_PROXY_REDIRECT_URL: ${PROTOCOL}://${OAUTH2_PROXY_HOSTNAME}/oauth2/callback
      OAUTH2_PROXY_REVERSE_PROXY: true
      OAUTH2_PROXY_SSL_INSECURE_SKIP_VERIFY: true
      OAUTH2_PROXY_WHITELIST_DOMAINS: ${OAUTH2_PROXY_HOSTNAME}:${OAUTH2_PROXY_PORT}
      
      ...
      

We want to use the OpenID Connect (oidc) provider:

  oauth2-proxy:
    container_name: oauth2-proxy

    ...

    environment:
    
      ...

      # General Provider options
      OAUTH2_PROXY_CLIENT_ID: ${CLIENT_ID}
      OAUTH2_PROXY_CLIENT_SECRET: ${CLIENT_SECRET}
      OAUTH2_PROXY_CODE_CHALLENGE_METHOD: S256
      OAUTH2_PROXY_INSECURE_OIDC_ALLOW_UNVERIFIED_EMAIL: true
      OAUTH2_PROXY_OIDC_ISSUER_URL: ${PROTOCOL}://${KEYCLOAK_HOSTNAME}:${KEYCLOAK_PORT}/realms/${KEYCLOAK_REALM}
      OAUTH2_PROXY_PROVIDER: oidc
      OAUTH2_PROXY_PROVIDER_DISPLAY_NAME: OpenID Connect
      OAUTH2_PROXY_SCOPE: ${SCOPE}     
      
      ...

OAuth2 Proxy will check that the Issuer URL matches the Issuer URL (iss) returned by Keycloak.

You can preview tokens in the Keycloak Admin Console, for example:

The complete OAuth2 Proxy configuration:

  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',
      ]
    environment:

      # General Provider options
      OAUTH2_PROXY_CLIENT_ID: ${CLIENT_ID}
      OAUTH2_PROXY_CLIENT_SECRET: ${CLIENT_SECRET}
      OAUTH2_PROXY_CODE_CHALLENGE_METHOD: S256
      OAUTH2_PROXY_INSECURE_OIDC_ALLOW_UNVERIFIED_EMAIL: true
      OAUTH2_PROXY_OIDC_ISSUER_URL: ${PROTOCOL}://${KEYCLOAK_HOSTNAME}:${KEYCLOAK_PORT}/realms/${KEYCLOAK_REALM}
      OAUTH2_PROXY_PROVIDER: oidc
      OAUTH2_PROXY_PROVIDER_DISPLAY_NAME: OpenID Connect
      OAUTH2_PROXY_SCOPE: ${SCOPE}

      # Cookie Options
      OAUTH2_PROXY_COOKIE_CSRF_PER_REQUEST: true
      OAUTH2_PROXY_COOKIE_EXPIRE: 30m
      OAUTH2_PROXY_COOKIE_HTTPONLY: true
      OAUTH2_PROXY_COOKIE_NAME: ${COOKIE_NAME}
      OAUTH2_PROXY_COOKIE_REFRESH: 25m
      OAUTH2_PROXY_COOKIE_SAMESITE: lax
      OAUTH2_PROXY_COOKIE_SECRET: ${COOKIE_SECRET}
      OAUTH2_PROXY_COOKIE_SECURE: true

      # Header options
      OAUTH2_PROXY_PASS_ACCESS_TOKEN: true
      OAUTH2_PROXY_PASS_AUTHORIZATION_HEADER: true
      OAUTH2_PROXY_PROXY_HEADERS: xforwarded
      OAUTH2_PROXY_SET_AUTHORIZATION_HEADER: true
      OAUTH2_PROXY_SET_XAUTHREQUEST: true

      # Logging options
      OAUTH2_PROXY_ERRORS_TO_INFO_LOG: true
      OAUTH2_PROXY_REQUEST_LOGGING: true
      OAUTH2_PROXY_SILENCE_PING_LOGGING: true

      # Page Template options
      OAUTH2_PROXY_SHOW_DEBUG_ON_ERROR: true

      # Proxy options
      OAUTH2_PROXY_EMAIL_DOMAINS: '*'
      OAUTH2_PROXY_REDIRECT_URL: ${PROTOCOL}://${OAUTH2_PROXY_HOSTNAME}/oauth2/callback
      OAUTH2_PROXY_REVERSE_PROXY: true
      # OAUTH2_PROXY_SKIP_PROVIDER_BUTTON: true
      OAUTH2_PROXY_SSL_INSECURE_SKIP_VERIFY: true
      OAUTH2_PROXY_WHITELIST_DOMAINS: ${OAUTH2_PROXY_HOSTNAME}:${OAUTH2_PROXY_PORT}

      # Server options
      OAUTH2_PROXY_HTTP_ADDRESS: 0.0.0.0:${OAUTH2_PROXY_PORT}

      # Session options
      OAUTH2_PROXY_REDIS_CONNECTION_URL: redis://redis
      OAUTH2_PROXY_SESSION_STORE_TYPE: redis

      # Upstreams configuration
      OAUTH2_PROXY_SSL_UPSTREAM_INSECURE_SKIP_VERIFY: true
      OAUTH2_PROXY_UPSTREAMS: http://hapi-fhir:8080/

    env_file: ./.env
    ports:
      - 4180:4180
    depends_on:
      redis:
        condition: service_healthy
      keycloak.au.localhost:
        condition: service_healthy
    networks:
      - hapi_fhir_network

See: docker-compose.yml

The .env file:

PROTOCOL=https
# Postgres
POSTGRES_DB=hapi-fhir
POSTGRES_USER=admin
POSTGRES_PASSWORD=secret
# Keycloak
KEYCLOAK_HOSTNAME=keycloak.au.localhost
KEYCLOAK_PORT=8443
KEYCLOAK_REALM=hapi-fhir-dev
KEYCLOAK_ADMIN=admin
KEYCLOAK_ADMIN_PASSWORD=secret
# OAuth Client
CLIENT_ID=oauth2-proxy
CLIENT_SECRET=aHkRec1BYkfaKgMg164JmvKu8u9iWNHM
SCOPE=openid
# OAuth2 Proxy
OAUTH2_PROXY_HOSTNAME=hapi-fhir.au.localhost
OAUTH2_PROXY_PORT=4180
COOKIE_NAME=SESSION
COOKIE_SECRET=uzVUu9BdSpOXqPeMaGoTYuTHazRXWoUCajyLUfWlnv8=

See: .env

Note: Docker will look for your .env file in the same directory as your Docker Compose configuration file.

Source Code
References
System Hardening
OAuth 2.0
Keycloak
Nginx
OAuth2 Proxy