Introduction

In a previous post, I wrote about the steps I followed to add Authentication (AuthN) to HAPI FHIR by utilising OAuth2 Proxy, Nginx and Keycloak

In this post, we'll look at an alternate solution that replaces OAuth2 Proxy and Nginx with APISIX.

APISIX

APISIX is an open source API Gateway that offers a wide range of features for managing and securing APIs. It supports dynamic configuration, flexible routing, and has a rich plugin ecosystem.

Docker Compose

Let's start by running APISIX in 'standalone' mode:

services:

  apisix:
    container_name: apisix
    build:
      context: ./services/apisix
      dockerfile: Dockerfile
    restart: unless-stopped
    environment:
      APISIX_STAND_ALONE: true
      APISIX_SSL_CERT: /usr/local/apisix/conf/cert/cert.pem
      APISIX_SSL_CERT_KEY: /usr/local/apisix/conf/cert/key.pem
    ports:
      - 80:9080
      - 443:9443
      
    ...
      
    volumes:
      - '${PWD}/services/apisix/conf/config-standalone.yml:/usr/local/apisix/conf/config.yaml'
      - '${PWD}/services/apisix/conf/apisix-standalone.yml:/usr/local/apisix/conf/apisix.yaml'      
      - '${PWD}/certs/cert.pem:/usr/local/apisix/conf/cert/cert.pem'
      - '${PWD}/certs/key.pem:/usr/local/apisix/conf/cert/key.pem'      

    ...
      

See: docker-compose-apisix.yml

In standalone mode only the APISIX data plane is deployed and settings are loaded from a YAML configuration file:

apisix:
  ...
  
deployment:
  role: data_plane
  role_data_plane:
    config_provider: yaml

See: config-standalone.yml

Routes are declared in a file named apisix.yaml (apisix-standalone.yml):

routes:
  - uri: /*
    host: hapi-fhir.au.localhost
    upstream:
      nodes:
        "hapi-fhir:8080": 1
      type: roundrobin

    ...
    
#END 

See: apisix-standalone.yml

APISIX will reload the file if it detects any changes.

Enable TLS

You can enable TLS and ciphers via the the ssl settings in config.yaml (config-standalone.yml):

apisix:
  node_listen: 9080
  enable_admin: false
  enable_ipv6: false

  ssl:
    enable: true
    listen_port: 9443
    ssl_protocols: "TLSv1.2 TLSv1.3"
    ssl_ciphers: "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384"

deployment:
  role: data_plane
  role_data_plane:
    config_provider: yaml

See: config-standalone.yml

And certs via the the ssls settings in apisix.yaml (apisix-standalone.yml):

routes:
  - uri: /*
    host: hapi-fhir.au.localhost
    upstream:
      nodes:
        "hapi-fhir:8080": 1
      type: roundrobin

    ...

ssls:
  -
    cert: |
      -----BEGIN CERTIFICATE-----
      MIIEYzCCAsugAwIBAgIRAI09tHJ9h1MZ3Yjn/hxbCZowDQYJKoZIhvcNAQELBQAw
      gY8xHjAcBgNVBAoTFW1rY2VydCBkZXZlbG9wbWVudCBDQTEyMDAGA1UECwwpcm9i
      QFJvYnMtTWFjQm9vay1Qcm8ubG9jYWwgKFJvYiBGZXJndXNvbikxOTA3BgNVBAMM
      MG1rY2VydCByb2JAUm9icy1NYWNCb29rLVByby5sb2NhbCAoUm9iIEZlcmd1c29u
      KTAeFw0yNTAxMDcyMDA2NDhaFw0yNzA0MDcyMTA2NDhaMF0xJzAlBgNVBAoTHm1r
      Y2VydCBkZXZlbG9wbWVudCBjZXJ0aWZpY2F0ZTEyMDAGA1UECwwpcm9iQFJvYnMt
      TWFjQm9vay1Qcm8ubG9jYWwgKFJvYiBGZXJndXNvbikwggEiMA0GCSqGSIb3DQEB
      AQUAA4IBDwAwggEKAoIBAQCuWQhFkxQRb10Yxb94upW9LQ8KVXKs+4ujd3YH/OPX
      C9dsJM4qu9lSUUTjhEFTfexO9uYSpS15vZeOAkXVDjMSBLeXVRq+cg2Q1K3nGPa3
      rPrAmjavCbvf/9Pi8r459v9kjWuKLXspoY2v7biJz/az2JYTwF7s0QKUcbz0K3tk
      Ke3DBBIBfaIuBGUbidIT7p6vZY9EW0an2YzhN/F3PuQn8Xl/rC2NUrwIba1KgmUk
      29XT2MaC2pq6g/D7sJcITF1soHFqhRkuT53J4NRc5Q9v134LTSqEppu2RibwZ2wL
      oa/NaCOB8VXihT9HrjN0AViH9n27oAuf59uVZU5aPK5ZAgMBAAGjazBpMA4GA1Ud
      DwEB/wQEAwIFoDATBgNVHSUEDDAKBggrBgEFBQcDATAfBgNVHSMEGDAWgBQsq7oj
      ij5wNK5tI2xSxRrN6iU0KzAhBgNVHREEGjAYghZoYXBpLWZoaXIuYXUubG9jYWxo
      b3N0MA0GCSqGSIb3DQEBCwUAA4IBgQCTPN8SExEx3zKuz8AcqvGn3DutM4CVo6VJ
      q3btlOppHP7EfU1vQ1YUg3t41vt04OTUJYEb7jlHc9ZqMU2/b8YSJ6hTlDvs16j7
      b7F/GiZclRyO4SL8HBdDhleOlz9Z0dVwex0Joz6WkwnXPv7djwOMG4o9GQqGbFzP
      cxjHKi/GsebPaGOy5/liuCqC7/UNebCyu4on73WHLj3YcjSf9uLsp9Vaq8NtCx5k
      IlmI5ocp9cdZ5vq2/zkwdTSrVtVWs6ZNrt6JhUGSkG6BoKaatzQAaO9hwq0tId3P
      SRjD4fYydWbEMusCxFcf6l7Jix96IaSG60TMLE5nh02QF9rtP4PRZ7aGaj6SuHK4
      yyApQj8TtHFAqWU6HDbK4x6jQE00vX7GU8Nnw5FxLD4Ns8/lrccxT1fItB9R+1ld
      QJ9WIwXytm10XNJVvJhLqOltw4PwWnc/4N88xA0oZD99qMUcDkspRDgUd3G2PmzV
      BRbCSva9ET7UbprFfCDT9mVXxbnjz2s=
      -----END CERTIFICATE-----
    key: |
      -----BEGIN PRIVATE KEY-----
      MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCuWQhFkxQRb10Y
      xb94upW9LQ8KVXKs+4ujd3YH/OPXC9dsJM4qu9lSUUTjhEFTfexO9uYSpS15vZeO
      AkXVDjMSBLeXVRq+cg2Q1K3nGPa3rPrAmjavCbvf/9Pi8r459v9kjWuKLXspoY2v
      7biJz/az2JYTwF7s0QKUcbz0K3tkKe3DBBIBfaIuBGUbidIT7p6vZY9EW0an2Yzh
      N/F3PuQn8Xl/rC2NUrwIba1KgmUk29XT2MaC2pq6g/D7sJcITF1soHFqhRkuT53J
      4NRc5Q9v134LTSqEppu2RibwZ2wLoa/NaCOB8VXihT9HrjN0AViH9n27oAuf59uV
      ZU5aPK5ZAgMBAAECggEANsiRGdOSXbwhg7Q3awcuIAh1jmi1JPfRs+bIts/XA+6b
      nUafZbwrGHui6t7W7BZIV7OrLbaraHKTmbLLIJxandHPooTCZ49NBfJeRpyIgcSf
      8j9C6ZNkboljmg9uiKY9L+pkHUIXTkiOTfajouIvAeoPlls/OKigZ+apWgwDtMAX
      SdQ7Yd2pc9LjJAYN7GYoAAR7hn4fbR6p8dITPL4+wne5gQtutltUZxttElaGpWLz
      84wu5kIqx3IzmwUD2WuCOymy+3kJVzis4HKeYtwW7ENbmpUmTFkXAJTrVGgRqSSb
      kD6oCREwESkpI1+t50HepTifDIHuLyWU1ap+za4OgQKBgQDTdZn5jk3h5F1WkNbZ
      iZCLy70zWflE2GJR0bPCqZA5DuEo5+5zQe10r4xXfOLbcspg04h+zjC3/sTkA0RV
      RtoXJi8S1FYeajL+xgpiqWyltrFQAELP5yRi/ClZuf4TO7lUyzpklwPmrZnzb8AI
      lV9g7IaRzvskYfkCT6ay6y3HCQKBgQDTEkjAeL4pUIw68qkZ2d2HLtY5ZNL8FsVs
      aEOQSvXFDunm8OmTiTdsFsjX04VLnTz2d9cEwdCyMYvrbX6B/upN2ZgkKCpR8b9P
      TaMEeQY+4VYd9SbcVLFuhZQOpb4EM4rDWN9jGk0BYiEbrXRZQIU6HTSSF+sLjZis
      py2G24+w0QKBgBHrm3rstmj4Y3icmbih0eAnCge6DkfpVpu8e9F5cUGEo0xGK40U
      /zyuS+R2LvuOBNyj0KN+cd6F9sWkCTx43q6ri726xPma4mt4+RRXa1+31dsDyqW3
      3vuMhyyVeJTEsPYgqvgvXCNGfw+EXu/bSNP794OP2PTCYMnzWhs7lwuRAoGAYVCk
      yljhFBtXDDalUI3qXVFy47Ngs2msTHcl73kgJ2Lg5OFeT++L5gH7R8b2Rg6Q9PH7
      6O2TUxUU9c7d7QGi9ZHFW6ZJHM7g7adV6dIC1yr9kYJeEGfcBqD/ymEQYs+AwuBO
      3lpZ9rFPonsukZf11P1yJ4lvjTwTkEbj7rF8ZoECgYB+n5qGTY2KSm6vW3RSPDv/
      U1EOATvg//2QY4kknaBfrRmZuYS+EdVfCHkuHcLoDts3hAew3DVdUV93IFEFxh6+
      v9sFzlZr90aEFHZhd8MSTAD20XcvxLHMsYnbG3O1kNgCuVX0KosW5mR061akNasW
      0j9vW0tRX0bJhGDbt9BoJA==
      -----END PRIVATE KEY-----
    snis:
      - "hapi-fhir.au.localhost"
#END

See: apisix-standalone.yml

OpenID Connect Plugin

APISIX can intercept requests to your application and redirect them to an Authorisation server that supports OAuth 2.0 and OpenID Connect.

For example:

routes:
  - uri: /*
    host: hapi-fhir.au.localhost
    upstream:
      nodes:
        "hapi-fhir:8080": 1
      type: roundrobin
    plugins:
      openid-connect:
        bearer_only: false
        client_id: ${{CLIENT_ID}}
        client_secret: ${{CLIENT_SECRET}}
        discovery: ${{PROTOCOL}}://${{KEYCLOAK_HOSTNAME}}:${{KEYCLOAK_PORT}}/realms/${{KEYCLOAK_REALM}}/.well-known/openid-configuration
        realm: ${{KEYCLOAK_REALM}}
        redirect_uri: ${{PROTOCOL}}://${{HAPI_FHIR_HOSTNAME}}/oauth2/callback
        scope: ${{SCOPE}}
        session:
          secret: ${{COOKIE_SECRET}}

    ...

See: apisix-standalone.yml

HAPI FHIR AU with Auth Starter Project

Follow the steps in the HAPI FHIR AU with Auth Starter Project's Quick Start guide to enable secure access to HAPI FHIR.

Navigate to:

https://hapi-fhir.au.localhost

You should see something like:

Sign in to your Keycloak Account

Enter your username (hey@rob-ferguson.me) and password (secret), then click the 'Sign In' button to sign in using the OpenID Connect (OIDC) Authorization Code flow.

Note: I followed the steps in Keycloak's Getting Started with Docker guide to create: a realm; a user; and a client. Keycloak will import the hapi-fhir-dev realm (i.e., development-realm.json) when it starts up.

Your connection is secure:

Navigate to the OpenAPI UI for the HAPI FHIR R4 Server:

https://hapi-fhir.au.localhost/fhir

You should see something like:

Note: You can override the default FHIR Server Base URL, for example:

hapi:
  fhir:
    server_address: https://hapi-fhir.au.localhost/fhir

See: hapi.application.yaml

To stop the services:

docker compose -f docker-compose-apisix.yml stop

To remove the services:

docker compose -f docker-compose-apisix.yml down

To remove the data volume:

docker volume rm backend_postgres_data
Source Code

What's Next

In the next post, we'll take a look at adding support for SMART on FHIR to HAPI FHIR.

Source Code
References
System Hardening
HL7
SMART on FHIR
SMART on FHIR - Standalone Launch
SMART on FHIR - EHR Launch
Keycloak
Keycloak-based Development
Keycloak Support
APISIX
HAPI FHIR