<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:media="http://search.yahoo.com/mrss/"><channel><title><![CDATA[Rob Ferguson]]></title><description><![CDATA["True Science teaches, above all, to doubt." - Miguel de Unamuno]]></description><link>https://rob-ferguson.me/</link><image><url>https://rob-ferguson.me/favicon.png</url><title>Rob Ferguson</title><link>https://rob-ferguson.me/</link></image><generator>Ghost 5.37</generator><lastBuildDate>Tue, 14 Apr 2026 22:11:10 GMT</lastBuildDate><atom:link href="https://rob-ferguson.me/rss/" rel="self" type="application/rss+xml"/><ttl>60</ttl><item><title><![CDATA[Create a pragmatic Generative AI adoption strategy]]></title><description><![CDATA[<!--kg-card-begin: markdown--><p>Leadership advocacy, structured training, and local champions within teams are the fundamental components of a pragmatic Generative AI adoption strategy.</p>
<p>Leadership advocacy: Lead from the front, supporting and promoting the adoption of Generative AI.</p>
<p>Structured training: What is Generative AI and what can it do for me?</p>
<p>Local champions: Team</p>]]></description><link>https://rob-ferguson.me/create-a-pragmatic-generative-ai-adoption-strategy/</link><guid isPermaLink="false">6907bd1f1379090491cde1d1</guid><category><![CDATA[AI]]></category><category><![CDATA[Generative AI]]></category><category><![CDATA[Strategy]]></category><category><![CDATA[SDLC]]></category><dc:creator><![CDATA[Rob Ferguson]]></dc:creator><pubDate>Sun, 02 Nov 2025 20:49:03 GMT</pubDate><content:encoded><![CDATA[<!--kg-card-begin: markdown--><p>Leadership advocacy, structured training, and local champions within teams are the fundamental components of a pragmatic Generative AI adoption strategy.</p>
<p>Leadership advocacy: Lead from the front, supporting and promoting the adoption of Generative AI.</p>
<p>Structured training: What is Generative AI and what can it do for me?</p>
<p>Local champions: Team members who champion the role of Generative AI tools in the SDLC.</p>
<h5 id="resources">Resources</h5>
<ul>
<li>AWS docs: <a href="https://aws.amazon.com/what-is/generative-ai/?ref=rob-ferguson">What is Generative AI?</a></li>
<li>Google blog: <a href="https://blog.google/inside-google/googlers/ask-a-techspert/what-is-generative-ai/?ref=rob-ferguson">Ask a Techspert: What is generative AI?</a></li>
<li>coursera: <a href="https://www.coursera.org/learn/introduction-to-generative-ai?ref=rob-ferguson">Introduction to Generative AI</a></li>
<li>Angular dev: <a href="https://angular.dev/ai?ref=rob-ferguson">Build with AI</a></li>
<li>GitHub: <a href="https://github.com/spring-ai-community/awesome-spring-ai?ref=rob-ferguson">Awesome Spring AI</a></li>
<li>PAIR blog: <a href="https://pair.withgoogle.com/explorables/?ref=rob-ferguson">AI Explorables</a></li>
</ul>
<!--kg-card-end: markdown--><p></p><p></p><p></p>]]></content:encoded></item><item><title><![CDATA[HAPI FHIR MCP Server]]></title><description><![CDATA[<!--kg-card-begin: markdown--><h3 id="introduction">Introduction</h3>
<p><a href="https://hapifhir.io/?ref=rob-ferguson">HAPI FHIR</a> is an open source implementation of the HL7 FHIR standard for healthcare interoperability.</p>
<h4 id="model-context-protocol">Model Context Protocol</h4>
<p>The <a href="https://github.com/Robinyo/hapi-fhir-jpaserver-starter?ref=rob-ferguson">HAPI FHIR JPA Server Starter Project</a> includes a MCP (Model Context Protocol) Server.</p>
<h4 id="claude-for-desktop">Claude for Desktop</h4>
<p><a href="https://claude.ai/download?ref=rob-ferguson">Claude for Desktop</a> is a popular MCP Client.</p>
<p>We can configure Claude for Desktop</p>]]></description><link>https://rob-ferguson.me/hapi-fhir-mcp-server/</link><guid isPermaLink="false">68ed8ea91379090491cde128</guid><category><![CDATA[HAPI FHIR]]></category><category><![CDATA[MCP Server]]></category><dc:creator><![CDATA[Rob Ferguson]]></dc:creator><pubDate>Tue, 14 Oct 2025 00:18:04 GMT</pubDate><media:content url="https://rob-ferguson.me/content/images/2025/10/search-and-tools-1.png" medium="image"/><content:encoded><![CDATA[<!--kg-card-begin: markdown--><h3 id="introduction">Introduction</h3>
<img src="https://rob-ferguson.me/content/images/2025/10/search-and-tools-1.png" alt="HAPI FHIR MCP Server"><p><a href="https://hapifhir.io/?ref=rob-ferguson">HAPI FHIR</a> is an open source implementation of the HL7 FHIR standard for healthcare interoperability.</p>
<h4 id="model-context-protocol">Model Context Protocol</h4>
<p>The <a href="https://github.com/Robinyo/hapi-fhir-jpaserver-starter?ref=rob-ferguson">HAPI FHIR JPA Server Starter Project</a> includes a MCP (Model Context Protocol) Server.</p>
<h4 id="claude-for-desktop">Claude for Desktop</h4>
<p><a href="https://claude.ai/download?ref=rob-ferguson">Claude for Desktop</a> is a popular MCP Client.</p>
<p>We can configure Claude for Desktop to use HAPI FHIR&apos;s MCP server by updating its configuration file.</p>
<p>For example:</p>
<pre><code>&quot;mcpServers&quot;: {
  &quot;hapi&quot;: {
    &quot;command&quot;: &quot;npx&quot;,
      &quot;args&quot;: [
        &quot;mcp-remote@latest&quot;,
        &quot;http://localhost:8080/mcp/messages&quot;
    ]
  }
}
</code></pre>
<p>Restart Claude for Desktop and then click on the &apos;Search and tools&apos; button.</p>
<p>You should see something like:</p>
<p align="center">
  <img src="https://rob-ferguson.me/content/images/2025/10/search-and-tools.png" class="thumbnail" alt="HAPI FHIR MCP Server">
</p>
<h5 id="resources">Resources</h5>
<ul>
<li>HAPI FHIR: <a href="https://hapifhir.io/?ref=rob-ferguson">Website</a></li>
<li>HAPI FHIR: <a href="https://hapifhir.io/hapi-fhir/docs/?ref=rob-ferguson">Documentation</a></li>
<li>Google Group: <a href="https://groups.google.com/g/hapi-fhir?ref=rob-ferguson">HAPI FHIR</a></li>
<li>claude.ai: <a href="https://claude.ai/download?ref=rob-ferguson">Claude for Desktop</a></li>
<li>mcp.io: <a href="https://modelcontextprotocol.io/docs/develop/build-server?ref=rob-ferguson#testing-your-server-with-claude-for-desktop">Testing your server with Claude for Desktop</a></li>
<li>GitHub: <a href="https://github.com/wso2/fhir-mcp-server?ref=rob-ferguson">WS02 - FHIR MCP Server</a></li>
</ul>
<!--kg-card-end: markdown-->]]></content:encoded></item><item><title><![CDATA[Getting started with Open Policy Agent]]></title><description><![CDATA[<!--kg-card-begin: markdown--><h3 id="introduction">Introduction</h3>
<p>Open Policy Agent is a general-purpose policy engine that includes a high-level declarative language that you can use to define policies.</p>
<h4 id="getting-started">Getting Started</h4>
<p>The easiest way to get started with <a href="https://www.openpolicyagent.org/?ref=rob-ferguson">Open Policy Agent</a> (OPA) is to use Docker Compose.</p>
<h4 id="docker-compose">Docker Compose</h4>
<p>I created a Docker Compose configuration file, as</p>]]></description><link>https://rob-ferguson.me/getting-started-with-open-policy-agent/</link><guid isPermaLink="false">6879a45d1379090491cddfd4</guid><category><![CDATA[Open Policy Agent]]></category><category><![CDATA[OPA]]></category><category><![CDATA[Docker Compose]]></category><dc:creator><![CDATA[Rob Ferguson]]></dc:creator><pubDate>Fri, 18 Jul 2025 02:24:16 GMT</pubDate><content:encoded><![CDATA[<!--kg-card-begin: markdown--><h3 id="introduction">Introduction</h3>
<p>Open Policy Agent is a general-purpose policy engine that includes a high-level declarative language that you can use to define policies.</p>
<h4 id="getting-started">Getting Started</h4>
<p>The easiest way to get started with <a href="https://www.openpolicyagent.org/?ref=rob-ferguson">Open Policy Agent</a> (OPA) is to use Docker Compose.</p>
<h4 id="docker-compose">Docker Compose</h4>
<p>I created a Docker Compose configuration file, as follows:</p>
<pre><code>  opa:
    image: openpolicyagent/opa:1.6.0-static
    command:
      - &quot;run&quot;
      - &quot;--server&quot;
      - &quot;--addr=0.0.0.0:8181&quot;
      - &quot;--log-level=info&quot;
      - &quot;--log-format=json-pretty&quot;
    restart: unless-stopped
    ports:
      - 8181:8181
</code></pre>
<h4 id="policy-definition">Policy Definition</h4>
<p>OPA uses a declarative language called <a href="https://www.openpolicyagent.org/docs/policy-language?ref=rob-ferguson">Rego</a> to define policies.</p>
<p>For example:</p>
<pre><code>package organization.read

methods := &quot;GET HEAD&quot;

default allow := false

allow if contains(methods, input.request.method)
</code></pre>
<p>You can use Rego to specify rules for <strong>authorisation</strong> and other security aspects of your application.</p>
<h4 id="loading-policies-and-data">Loading Policies and Data</h4>
<p>The simplest way to load policies and data into OPA is to provide them via the file system (i.e., Docker volumes) and command line arguments.</p>
<p>Let&apos;s update our Docker Compose configuration file, as follows:</p>
<pre><code>  opa:
    image: openpolicyagent/opa:1.6.0-static
    command:
      - &quot;run&quot;
      - &quot;--server&quot;
      - &quot;/policies/organization-read.rego&quot;
      - &quot;/policies/organization-write.rego&quot;
      - &quot;--addr=0.0.0.0:8181&quot;
      - &quot;--log-level=info&quot;
      - &quot;--log-format=json-pretty&quot;
    restart: unless-stopped
    ports:
      - 8181:8181
    volumes:
      - &apos;${PWD}/services/opa/conf/policies:/policies&apos;
</code></pre>
<h4 id="policy-evaluation">Policy Evaluation</h4>
<p>OPA can act as a <strong>policy decision point</strong> in combination with an API Gateway (e.g., <a href="https://apisix.apache.org/?ref=rob-ferguson">APISIX</a>) acting as a <strong>policy enforcement point</strong>.</p>
<p>When an application requests access to a resource (using standard HTTP methods like GET, HEAD, POST, PUT, PATCH and DELETE), the request is intercepted by the API Gateway and routed to OPA.</p>
<p>For example:</p>
<pre><code>  - name: provider-directory-api-organization-read
    uri: /fhir/Organization*
    methods: [ &quot;GET&quot;, &quot;HEAD&quot; ]
    upstream_id: 1
    plugins:
      opa:
        host: &quot;https://opa:8282&quot;
        policy: &quot;organization/read&quot;

  - name: provider-directory-api-organization-write
    uri: /fhir/Organization*
    methods: [ &quot;POST&quot;, &quot;PUT&quot;, &quot;PATCH&quot;, &quot;DELETE&quot; ]
    upstream_id: 1
    plugins:
      opa:
        host: &quot;https://opa:8282&quot;
        policy: &quot;organization/write&quot;
</code></pre>
<p>OPA evaluates the (access control) policy and decides if the request should be &apos;allowed&apos; or &apos;denied&apos;.</p>
<p>You can obtain additional information about OPA decisions by setting your log level to &apos;debug&apos; and including the decision logs:</p>
<pre><code>  opa:
    image: openpolicyagent/opa:1.6.0-static
    command:
    
      ...
      
      - &quot;--log-level=debug&quot;
      - &quot;--set=decision_logs.console=true&quot;
</code></pre>
<p>For example:</p>
<pre><code>docker container logs opa
</code></pre>
<p>You should see something like:</p>
<pre><code>{
  &quot;current_version&quot;: &quot;1.6.0&quot;,
  &quot;level&quot;: &quot;debug&quot;,
  &quot;msg&quot;: &quot;OPA is up to date.&quot;,
  &quot;time&quot;: &quot;2025-07-18T23:00:45Z&quot;
}
{
  &quot;client_addr&quot;: &quot;172.18.0.2:34608&quot;,
  &quot;level&quot;: &quot;info&quot;,
  &quot;msg&quot;: &quot;Received request.&quot;,
  &quot;req_body&quot;: &quot;{\&quot;input\&quot;:{\&quot;request\&quot;:{\&quot;port\&quot;:9443,\&quot;query\&quot;:{\&quot;_id\&quot;:\&quot;adv-hearing-care\&quot;},\&quot;scheme\&quot;:\&quot;https\&quot;,\&quot;headers\&quot;:{\&quot;authorization\&quot;:\&quot;Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJMOFRkd19mLTRlUDJOVmZzMUVDbDJka0ZwNmFGbi1QR2xZZWlxb0FvSkowIn0.eyJleHAiOjE3NTI4ODAwNzMsImlhdCI6MTc1Mjg3OTc3MywianRpIjoidHJydGNjOmQ5OTk3MzEzLWQwNzMtZTkxYi1jMDc0LTgxNjQ1NGFlNWQ2YyIsImlzcyI6Imh0dHBzOi8va2V5Y2xvYWsuYXUubG9jYWxob3N0Ojg0NDMvcmVhbG1zL2hhcGktZmhpci1kZXYiLCJhdWQiOlsib2F1dGgyLXByb3h5IiwiYWNjb3VudCJdLCJzdWIiOiJkYTRmMDkxMi0xY2Q5LTQzMmQtOWM5ZC1hNTVkYjExMDkzZGUiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJvYXV0aDItcHJveHkiLCJhY3IiOiIxIiwiYWxsb3dlZC1vcmlnaW5zIjpbIioiXSwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbImRlZmF1bHQtcm9sZXMtaGFwaS1maGlyLWRldiIsIm9mZmxpbmVfYWNjZXNzIiwidW1hX2F1dGhvcml6YXRpb24iXX0sInJlc291cmNlX2FjY2VzcyI6eyJvYXV0aDItcHJveHkiOnsicm9sZXMiOlsidW1hX3Byb3RlY3Rpb24iXX0sImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoicHJvZmlsZSBzeXN0ZW0vT3JnYW5pemF0aW9uLnJlYWQgZW1haWwiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsImNsaWVudEhvc3QiOiIxNzIuMTguMC4xIiwicHJlZmVycmVkX3VzZXJuYW1lIjoic2VydmljZS1hY2NvdW50LW9hdXRoMi1wcm94eSIsImNsaWVudEFkZHJlc3MiOiIxNzIuMTguMC4xIiwiY2xpZW50X2lkIjoib2F1dGgyLXByb3h5In0.XAZXKtzk6lGnYYS0LcWc66MDalhWJ_p7E3-KYNse_LM9MKBgTrNqtizBdNgCeS8jgJ-366szDNv9ZvOMVgt3y-Hd2PhMMD-z93GuwcrOefcAH2Ji8ZgtgSt5VG4R7J5F_3ty1VP-Jhhq6EAMtsm1H8t-5AQjoDTaZjqZLpnVX8P-8nhkEfNZWZEILGyXWN0lcko-1lhLdIbbRYMl5cOxKM7s7FuK23G5y1JJQZRC5HWVF6Xa9zqQo-yioTpVlr2OIVE-eNA7FE17d_lDzAHQm-InNHkGEpiMcA3Wfgc-CS9slBfmIQxuU2N6Qnc1z7ZPHznAhzxcgPetCIGFjwSaTw\&quot;,\&quot;user-agent\&quot;:\&quot;curl/8.7.1\&quot;,\&quot;host\&quot;:\&quot;provider-directory.au.localhost\&quot;,\&quot;accept\&quot;:\&quot;*/*\&quot;,\&quot;content-type\&quot;:\&quot;application/fhir+json\&quot;},\&quot;method\&quot;:\&quot;GET\&quot;,\&quot;path\&quot;:\&quot;/fhir/Organization\&quot;,\&quot;host\&quot;:\&quot;provider-directory.au.localhost\&quot;},\&quot;type\&quot;:\&quot;http\&quot;,\&quot;var\&quot;:{\&quot;server_port\&quot;:\&quot;9443\&quot;,\&quot;remote_addr\&quot;:\&quot;172.18.0.1\&quot;,\&quot;timestamp\&quot;:1752879785,\&quot;remote_port\&quot;:\&quot;56920\&quot;,\&quot;server_addr\&quot;:\&quot;172.18.0.2\&quot;}}}&quot;,
  &quot;req_id&quot;: 2,
  &quot;req_method&quot;: &quot;POST&quot;,
  &quot;req_params&quot;: {},
  &quot;req_path&quot;: &quot;/v1/data/organization/read&quot;,
  &quot;time&quot;: &quot;2025-07-18T23:03:05Z&quot;
}
{
  &quot;decision_id&quot;: &quot;6ce6af4a-9ad2-4a23-ad8d-83428683ff37&quot;,
  &quot;input&quot;: {
    &quot;request&quot;: {
      &quot;headers&quot;: {
        &quot;accept&quot;: &quot;*/*&quot;,
        &quot;authorization&quot;: &quot;Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJMOFRkd19mLTRlUDJOVmZzMUVDbDJka0ZwNmFGbi1QR2xZZWlxb0FvSkowIn0.eyJleHAiOjE3NTI4ODAwNzMsImlhdCI6MTc1Mjg3OTc3MywianRpIjoidHJydGNjOmQ5OTk3MzEzLWQwNzMtZTkxYi1jMDc0LTgxNjQ1NGFlNWQ2YyIsImlzcyI6Imh0dHBzOi8va2V5Y2xvYWsuYXUubG9jYWxob3N0Ojg0NDMvcmVhbG1zL2hhcGktZmhpci1kZXYiLCJhdWQiOlsib2F1dGgyLXByb3h5IiwiYWNjb3VudCJdLCJzdWIiOiJkYTRmMDkxMi0xY2Q5LTQzMmQtOWM5ZC1hNTVkYjExMDkzZGUiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJvYXV0aDItcHJveHkiLCJhY3IiOiIxIiwiYWxsb3dlZC1vcmlnaW5zIjpbIioiXSwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbImRlZmF1bHQtcm9sZXMtaGFwaS1maGlyLWRldiIsIm9mZmxpbmVfYWNjZXNzIiwidW1hX2F1dGhvcml6YXRpb24iXX0sInJlc291cmNlX2FjY2VzcyI6eyJvYXV0aDItcHJveHkiOnsicm9sZXMiOlsidW1hX3Byb3RlY3Rpb24iXX0sImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoicHJvZmlsZSBzeXN0ZW0vT3JnYW5pemF0aW9uLnJlYWQgZW1haWwiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsImNsaWVudEhvc3QiOiIxNzIuMTguMC4xIiwicHJlZmVycmVkX3VzZXJuYW1lIjoic2VydmljZS1hY2NvdW50LW9hdXRoMi1wcm94eSIsImNsaWVudEFkZHJlc3MiOiIxNzIuMTguMC4xIiwiY2xpZW50X2lkIjoib2F1dGgyLXByb3h5In0.XAZXKtzk6lGnYYS0LcWc66MDalhWJ_p7E3-KYNse_LM9MKBgTrNqtizBdNgCeS8jgJ-366szDNv9ZvOMVgt3y-Hd2PhMMD-z93GuwcrOefcAH2Ji8ZgtgSt5VG4R7J5F_3ty1VP-Jhhq6EAMtsm1H8t-5AQjoDTaZjqZLpnVX8P-8nhkEfNZWZEILGyXWN0lcko-1lhLdIbbRYMl5cOxKM7s7FuK23G5y1JJQZRC5HWVF6Xa9zqQo-yioTpVlr2OIVE-eNA7FE17d_lDzAHQm-InNHkGEpiMcA3Wfgc-CS9slBfmIQxuU2N6Qnc1z7ZPHznAhzxcgPetCIGFjwSaTw&quot;,
        &quot;content-type&quot;: &quot;application/fhir+json&quot;,
        &quot;host&quot;: &quot;provider-directory.au.localhost&quot;,
        &quot;user-agent&quot;: &quot;curl/8.7.1&quot;
      },
      &quot;host&quot;: &quot;provider-directory.au.localhost&quot;,
      &quot;method&quot;: &quot;GET&quot;,
      &quot;path&quot;: &quot;/fhir/Organization&quot;,
      &quot;port&quot;: 9443,
      &quot;query&quot;: {
        &quot;_id&quot;: &quot;adv-hearing-care&quot;
      },
      &quot;scheme&quot;: &quot;https&quot;
    },
    &quot;type&quot;: &quot;http&quot;,
    &quot;var&quot;: {
      &quot;remote_addr&quot;: &quot;172.18.0.1&quot;,
      &quot;remote_port&quot;: &quot;56920&quot;,
      &quot;server_addr&quot;: &quot;172.18.0.2&quot;,
      &quot;server_port&quot;: &quot;9443&quot;,
      &quot;timestamp&quot;: 1752879785
    }
  },
  &quot;labels&quot;: {
    &quot;id&quot;: &quot;5db6ed80-30b9-42f1-a151-b8b63ca504e5&quot;,
    &quot;version&quot;: &quot;1.6.0&quot;
  },
  &quot;level&quot;: &quot;info&quot;,
  &quot;metrics&quot;: {
    &quot;counter_server_query_cache_hit&quot;: 0,
    &quot;timer_rego_external_resolve_ns&quot;: 459,
    &quot;timer_rego_input_parse_ns&quot;: 1074000,
    &quot;timer_rego_query_compile_ns&quot;: 337042,
    &quot;timer_rego_query_eval_ns&quot;: 1389209,
    &quot;timer_server_handler_ns&quot;: 3634125
  },
  &quot;msg&quot;: &quot;Decision Log&quot;,
  &quot;path&quot;: &quot;organization/read&quot;,
  &quot;req_id&quot;: 2,
  &quot;requested_by&quot;: &quot;172.18.0.2:34608&quot;,
  &quot;result&quot;: {
    &quot;allow&quot;: true,
    &quot;bearer_token&quot;: &quot;eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJMOFRkd19mLTRlUDJOVmZzMUVDbDJka0ZwNmFGbi1QR2xZZWlxb0FvSkowIn0.eyJleHAiOjE3NTI4ODAwNzMsImlhdCI6MTc1Mjg3OTc3MywianRpIjoidHJydGNjOmQ5OTk3MzEzLWQwNzMtZTkxYi1jMDc0LTgxNjQ1NGFlNWQ2YyIsImlzcyI6Imh0dHBzOi8va2V5Y2xvYWsuYXUubG9jYWxob3N0Ojg0NDMvcmVhbG1zL2hhcGktZmhpci1kZXYiLCJhdWQiOlsib2F1dGgyLXByb3h5IiwiYWNjb3VudCJdLCJzdWIiOiJkYTRmMDkxMi0xY2Q5LTQzMmQtOWM5ZC1hNTVkYjExMDkzZGUiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJvYXV0aDItcHJveHkiLCJhY3IiOiIxIiwiYWxsb3dlZC1vcmlnaW5zIjpbIioiXSwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbImRlZmF1bHQtcm9sZXMtaGFwaS1maGlyLWRldiIsIm9mZmxpbmVfYWNjZXNzIiwidW1hX2F1dGhvcml6YXRpb24iXX0sInJlc291cmNlX2FjY2VzcyI6eyJvYXV0aDItcHJveHkiOnsicm9sZXMiOlsidW1hX3Byb3RlY3Rpb24iXX0sImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoicHJvZmlsZSBzeXN0ZW0vT3JnYW5pemF0aW9uLnJlYWQgZW1haWwiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsImNsaWVudEhvc3QiOiIxNzIuMTguMC4xIiwicHJlZmVycmVkX3VzZXJuYW1lIjoic2VydmljZS1hY2NvdW50LW9hdXRoMi1wcm94eSIsImNsaWVudEFkZHJlc3MiOiIxNzIuMTguMC4xIiwiY2xpZW50X2lkIjoib2F1dGgyLXByb3h5In0.XAZXKtzk6lGnYYS0LcWc66MDalhWJ_p7E3-KYNse_LM9MKBgTrNqtizBdNgCeS8jgJ-366szDNv9ZvOMVgt3y-Hd2PhMMD-z93GuwcrOefcAH2Ji8ZgtgSt5VG4R7J5F_3ty1VP-Jhhq6EAMtsm1H8t-5AQjoDTaZjqZLpnVX8P-8nhkEfNZWZEILGyXWN0lcko-1lhLdIbbRYMl5cOxKM7s7FuK23G5y1JJQZRC5HWVF6Xa9zqQo-yioTpVlr2OIVE-eNA7FE17d_lDzAHQm-InNHkGEpiMcA3Wfgc-CS9slBfmIQxuU2N6Qnc1z7ZPHznAhzxcgPetCIGFjwSaTw&quot;,
    &quot;is_client&quot;: true,
    &quot;is_method&quot;: true,
    &quot;is_request_path&quot;: true,
    &quot;is_scope&quot;: true,
    &quot;methods&quot;: &quot;GET HEAD&quot;,
    &quot;path&quot;: &quot;/fhir/Organization&quot;,
    &quot;scope&quot;: &quot;system/Organization.read&quot;,
    &quot;token&quot;: {
      &quot;acr&quot;: &quot;1&quot;,
      &quot;allowed-origins&quot;: [
        &quot;*&quot;
      ],
      &quot;aud&quot;: [
        &quot;oauth2-proxy&quot;,
        &quot;account&quot;
      ],
      &quot;azp&quot;: &quot;oauth2-proxy&quot;,
      &quot;clientAddress&quot;: &quot;172.18.0.1&quot;,
      &quot;clientHost&quot;: &quot;172.18.0.1&quot;,
      &quot;client_id&quot;: &quot;oauth2-proxy&quot;,
      &quot;email_verified&quot;: false,
      &quot;exp&quot;: 1752880073,
      &quot;iat&quot;: 1752879773,
      &quot;iss&quot;: &quot;https://keycloak.au.localhost:8443/realms/hapi-fhir-dev&quot;,
      &quot;jti&quot;: &quot;trrtcc:d9997313-d073-e91b-c074-816454ae5d6c&quot;,
      &quot;preferred_username&quot;: &quot;service-account-oauth2-proxy&quot;,
      &quot;realm_access&quot;: {
        &quot;roles&quot;: [
          &quot;default-roles-hapi-fhir-dev&quot;,
          &quot;offline_access&quot;,
          &quot;uma_authorization&quot;
        ]
      },
      &quot;resource_access&quot;: {
        &quot;account&quot;: {
          &quot;roles&quot;: [
            &quot;manage-account&quot;,
            &quot;manage-account-links&quot;,
            &quot;view-profile&quot;
          ]
        },
        &quot;oauth2-proxy&quot;: {
          &quot;roles&quot;: [
            &quot;uma_protection&quot;
          ]
        }
      },
      &quot;scope&quot;: &quot;profile system/Organization.read email&quot;,
      &quot;sub&quot;: &quot;da4f0912-1cd9-432d-9c9d-a55db11093de&quot;,
      &quot;typ&quot;: &quot;Bearer&quot;
    }
  },
  &quot;time&quot;: &quot;2025-07-18T23:03:05Z&quot;,
  &quot;timestamp&quot;: &quot;2025-07-18T23:03:05.314506801Z&quot;,
  &quot;type&quot;: &quot;openpolicyagent.org/decision_logs&quot;
}
{
  &quot;client_addr&quot;: &quot;172.18.0.2:34608&quot;,
  &quot;level&quot;: &quot;info&quot;,
  &quot;msg&quot;: &quot;Sent response.&quot;,
  &quot;req_id&quot;: 2,
  &quot;req_method&quot;: &quot;POST&quot;,
  &quot;req_path&quot;: &quot;/v1/data/organization/read&quot;,
  &quot;resp_body&quot;: &quot;{\&quot;decision_id\&quot;:\&quot;6ce6af4a-9ad2-4a23-ad8d-83428683ff37\&quot;,\&quot;result\&quot;:{\&quot;allow\&quot;:true,\&quot;bearer_token\&quot;:\&quot;eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJMOFRkd19mLTRlUDJOVmZzMUVDbDJka0ZwNmFGbi1QR2xZZWlxb0FvSkowIn0.eyJleHAiOjE3NTI4ODAwNzMsImlhdCI6MTc1Mjg3OTc3MywianRpIjoidHJydGNjOmQ5OTk3MzEzLWQwNzMtZTkxYi1jMDc0LTgxNjQ1NGFlNWQ2YyIsImlzcyI6Imh0dHBzOi8va2V5Y2xvYWsuYXUubG9jYWxob3N0Ojg0NDMvcmVhbG1zL2hhcGktZmhpci1kZXYiLCJhdWQiOlsib2F1dGgyLXByb3h5IiwiYWNjb3VudCJdLCJzdWIiOiJkYTRmMDkxMi0xY2Q5LTQzMmQtOWM5ZC1hNTVkYjExMDkzZGUiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJvYXV0aDItcHJveHkiLCJhY3IiOiIxIiwiYWxsb3dlZC1vcmlnaW5zIjpbIioiXSwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbImRlZmF1bHQtcm9sZXMtaGFwaS1maGlyLWRldiIsIm9mZmxpbmVfYWNjZXNzIiwidW1hX2F1dGhvcml6YXRpb24iXX0sInJlc291cmNlX2FjY2VzcyI6eyJvYXV0aDItcHJveHkiOnsicm9sZXMiOlsidW1hX3Byb3RlY3Rpb24iXX0sImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoicHJvZmlsZSBzeXN0ZW0vT3JnYW5pemF0aW9uLnJlYWQgZW1haWwiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsImNsaWVudEhvc3QiOiIxNzIuMTguMC4xIiwicHJlZmVycmVkX3VzZXJuYW1lIjoic2VydmljZS1hY2NvdW50LW9hdXRoMi1wcm94eSIsImNsaWVudEFkZHJlc3MiOiIxNzIuMTguMC4xIiwiY2xpZW50X2lkIjoib2F1dGgyLXByb3h5In0.XAZXKtzk6lGnYYS0LcWc66MDalhWJ_p7E3-KYNse_LM9MKBgTrNqtizBdNgCeS8jgJ-366szDNv9ZvOMVgt3y-Hd2PhMMD-z93GuwcrOefcAH2Ji8ZgtgSt5VG4R7J5F_3ty1VP-Jhhq6EAMtsm1H8t-5AQjoDTaZjqZLpnVX8P-8nhkEfNZWZEILGyXWN0lcko-1lhLdIbbRYMl5cOxKM7s7FuK23G5y1JJQZRC5HWVF6Xa9zqQo-yioTpVlr2OIVE-eNA7FE17d_lDzAHQm-InNHkGEpiMcA3Wfgc-CS9slBfmIQxuU2N6Qnc1z7ZPHznAhzxcgPetCIGFjwSaTw\&quot;,\&quot;is_client\&quot;:true,\&quot;is_method\&quot;:true,\&quot;is_request_path\&quot;:true,\&quot;is_scope\&quot;:true,\&quot;methods\&quot;:\&quot;GET HEAD\&quot;,\&quot;path\&quot;:\&quot;/fhir/Organization\&quot;,\&quot;scope\&quot;:\&quot;system/Organization.read\&quot;,\&quot;token\&quot;:{\&quot;acr\&quot;:\&quot;1\&quot;,\&quot;allowed-origins\&quot;:[\&quot;*\&quot;],\&quot;aud\&quot;:[\&quot;oauth2-proxy\&quot;,\&quot;account\&quot;],\&quot;azp\&quot;:\&quot;oauth2-proxy\&quot;,\&quot;clientAddress\&quot;:\&quot;172.18.0.1\&quot;,\&quot;clientHost\&quot;:\&quot;172.18.0.1\&quot;,\&quot;client_id\&quot;:\&quot;oauth2-proxy\&quot;,\&quot;email_verified\&quot;:false,\&quot;exp\&quot;:1752880073,\&quot;iat\&quot;:1752879773,\&quot;iss\&quot;:\&quot;https://keycloak.au.localhost:8443/realms/hapi-fhir-dev\&quot;,\&quot;jti\&quot;:\&quot;trrtcc:d9997313-d073-e91b-c074-816454ae5d6c\&quot;,\&quot;preferred_username\&quot;:\&quot;service-account-oauth2-proxy\&quot;,\&quot;realm_access\&quot;:{\&quot;roles\&quot;:[\&quot;default-roles-hapi-fhir-dev\&quot;,\&quot;offline_access\&quot;,\&quot;uma_authorization\&quot;]},\&quot;resource_access\&quot;:{\&quot;account\&quot;:{\&quot;roles\&quot;:[\&quot;manage-account\&quot;,\&quot;manage-account-links\&quot;,\&quot;view-profile\&quot;]},\&quot;oauth2-proxy\&quot;:{\&quot;roles\&quot;:[\&quot;uma_protection\&quot;]}},\&quot;scope\&quot;:\&quot;profile system/Organization.read email\&quot;,\&quot;sub\&quot;:\&quot;da4f0912-1cd9-432d-9c9d-a55db11093de\&quot;,\&quot;typ\&quot;:\&quot;Bearer\&quot;}}}\n&quot;,
  &quot;resp_bytes&quot;: 2445,
  &quot;resp_duration&quot;: 8.023,
  &quot;resp_status&quot;: 200,
  &quot;time&quot;: &quot;2025-07-18T23:03:05Z&quot;
}
</code></pre>
<h5 id="source-code">Source Code</h5>
<ul>
<li>GitHub: <a href="https://github.com/Robinyo/provider-directory?ref=rob-ferguson">Australian Healthcare Provider Directory Starter Project</a></li>
</ul>
<h5 id="references">References</h5>
<h6 id="oauth-20">OAuth 2.0</h6>
<ul>
<li>IETF: <a href="https://datatracker.ietf.org/doc/html/rfc6749?ref=rob-ferguson">The OAuth 2.0 Authorization Framework</a></li>
<li>IETF: <a href="https://datatracker.ietf.org/doc/html/rfc8693?ref=rob-ferguson">OAuth 2.0 Token Exchange</a></li>
<li>IETF: <a href="https://datatracker.ietf.org/doc/html/rfc6750?ref=rob-ferguson">The OAuth 2.0 Authorization Framework: Bearer Token Usage</a></li>
<li>IETF: <a href="https://datatracker.ietf.org/doc/html/rfc8707?ref=rob-ferguson">Resource Indicators for OAuth 2.0</a></li>
<li>IETF: <a href="https://datatracker.ietf.org/doc/html/rfc7519?ref=rob-ferguson">JSON Web Token (JWT)</a></li>
<li>IETF: <a href="https://datatracker.ietf.org/doc/html/rfc9068?ref=rob-ferguson">JSON Web Token (JWT) Profile for OAuth 2.0 Access Tokens</a></li>
<li>IETF: <a href="https://datatracker.ietf.org/doc/html/rfc7591?ref=rob-ferguson">OAuth 2.0 Dynamic Client Registration Protocol</a></li>
<li>IETF: <a href="https://datatracker.ietf.org/doc/html/draft-ietf-oauth-browser-based-apps?ref=rob-ferguson">OAuth 2.0 for Browser-Based Applications</a></li>
<li>Spring docs: <a href="https://github.com/spring-projects/spring-authorization-server/issues/297?ref=rob-ferguson#issue-896744390">Implementation Guidelines for Browser-Based Applications</a></li>
</ul>
<h6 id="open-policy-agent">Open Policy Agent</h6>
<ul>
<li>Open Policy Agent docs: <a href="https://www.openpolicyagent.org/docs?ref=rob-ferguson">Introduction</a></li>
<li>Styra docs: <a href="https://docs.styra.com/opa/rego-style-guide?ref=rob-ferguson">Rego Style Guide</a></li>
<li>GitHub: <a href="https://github.com/StyraInc/awesome-opa?ref=rob-ferguson">Awesome OPA - A curated list of awesome OPA-related tools, frameworks and articles</a></li>
<li>Open Policy Agent: <a href="https://play.openpolicyagent.org/?ref=rob-ferguson">Playground</a></li>
<li>Styra Academy courses: <a href="https://academy.styra.com/courses/opa-rego?ref=rob-ferguson">OPA Policy Authoring</a></li>
<li>Open Policy Agent: <a href="https://www.openpolicyagent.org/ecosystem/by-feature/learning-rego?ref=rob-ferguson">Ecosystem</a></li>
</ul>
<!--kg-card-end: markdown-->]]></content:encoded></item><item><title><![CDATA[Use Open Policy Agent to enforce access control policies in SMART on FHIR applications]]></title><description><![CDATA[<!--kg-card-begin: markdown--><h3 id="introduction">Introduction</h3>
<p>Open Policy Agent (OPA) is a general-purpose policy engine that includes a high-level declarative language that you can use to define access control policies, enabling fine-grained access control in applications that leverage SMART on FHIR for authorisation and FHIR for data exchange.</p>
<h4 id="smart-on-fhir">SMART on FHIR</h4>
<p>Let&apos;s take</p>]]></description><link>https://rob-ferguson.me/use-open-policy-agent-to-enforce-access-control-policies-in-smart-on-fhir-applications/</link><guid isPermaLink="false">687336201379090491cddeb4</guid><category><![CDATA[Open Policy Agent]]></category><category><![CDATA[SMART on FHIR]]></category><category><![CDATA[FHIR]]></category><category><![CDATA[Access Control]]></category><category><![CDATA[Authorization]]></category><category><![CDATA[OAuth 2.0]]></category><dc:creator><![CDATA[Rob Ferguson]]></dc:creator><pubDate>Sun, 13 Jul 2025 05:07:11 GMT</pubDate><content:encoded><![CDATA[<!--kg-card-begin: markdown--><h3 id="introduction">Introduction</h3>
<p>Open Policy Agent (OPA) is a general-purpose policy engine that includes a high-level declarative language that you can use to define access control policies, enabling fine-grained access control in applications that leverage SMART on FHIR for authorisation and FHIR for data exchange.</p>
<h4 id="smart-on-fhir">SMART on FHIR</h4>
<p>Let&apos;s take a look at how OPA can be used to support SMART on FHIR authorisation.</p>
<h5 id="policy-definition">Policy Definition</h5>
<p>OPA uses a declarative language called Rego to define policies. Rego allows you to specify rules for authorisation and other security aspects of your SMART on FHIR application.</p>
<p>For example, you could define a policy that uses request attributes (e.g., method and path), data elements in a Bearer Token (e.g., scope) and other constraints:</p>
<pre><code>package organization.read

methods := &quot;GET HEAD&quot;
path := &quot;/fhir/Organization&quot;
scope := &quot;system/Organization.read&quot;

default allow := false

allow if {
    is_method
    is_request_path
    is_scope
}

is_method if contains(methods, input.request.method)
is_request_path if input.request.path == path
is_scope if contains(token.scope, scope)

bearer_token := t if {
    v := input.request.headers.authorization
    startswith(v, &quot;Bearer &quot;)
    t := substring(v, count(&quot;Bearer &quot;), -1)
}

token := payload if {
    [_, payload, _] := io.jwt.decode(bearer_token)
}
</code></pre>
<p>See: <a href="https://github.com/Robinyo/provider-directory/blob/main/backend/services/opa/conf/policies/organization-read.rego?ref=rob-ferguson">organization-read.rego</a></p>
<h5 id="policy-evaluation">Policy Evaluation</h5>
<p>OPA can act as a <strong>policy decision point</strong> in combination with an API Gateway acting as a <strong>policy enforcement point</strong>.</p>
<p>When a SMART on FHIR application requests access to a FHIR resource (using standard HTTP methods like GET, HEAD, POST, PUT, PATCH and DELETE), the request is intercepted by the API Gateway and routed to OPA.</p>
<p>For example:</p>
<pre><code>  - name: provider-directory-api-organization-read
    uri: /fhir/Organization*
    methods: [ &quot;GET&quot;, &quot;HEAD&quot; ]
    upstream_id: 1
    plugins:
      opa:
        host: &quot;https://opa:8282&quot;
        policy: &quot;organization/read&quot;

  - name: provider-directory-api-organization-write
    uri: /fhir/Organization*
    methods: [ &quot;POST&quot;, &quot;PUT&quot;, &quot;PATCH&quot;, &quot;DELETE&quot; ]
    upstream_id: 1
    plugins:
      opa:
        host: &quot;https://opa:8282&quot;
        policy: &quot;organization/write&quot;
</code></pre>
<p>See: <a href="https://github.com/Robinyo/provider-directory/blob/main/backend/services/apisix/conf/apisix-standalone.yml?ref=rob-ferguson">api-gateway-configuration.yml</a></p>
<p>OPA evaluates the (access control) policy and decides if the request should be &apos;allowed&apos; or &apos;denied&apos;.</p>
<h4 id="benefits-of-using-open-policy-agent">Benefits of using Open Policy Agent</h4>
<p><strong>Fine-grained Access Control</strong></p>
<p>OPA enables granular control over FHIR resource access, allowing you to specify exactly which FHIR resources an application can access and under what conditions.</p>
<p><strong>Centralised Policy Management</strong></p>
<p>OPA provides a central point for managing and enforcing policies, making it easier to maintain a consistent approach to access control across your application portfolio.</p>
<p><strong>Improved Security</strong></p>
<p>By decoupling policy enforcement from application logic, OPA helps reduce the risk of vulnerabilities and improves the overall security posture of your organisation.</p>
<p><strong>Auditability</strong></p>
<p>OPA policies are code, making them version-controlled and auditable.</p>
<p><strong>Flexibility and Extensibility</strong></p>
<p>OPA&apos;s Rego language is flexible and extensible, allowing you to define complex access control rules based on your specific needs.</p>
<h4 id="example-use-case">Example Use Case</h4>
<p>Imagine a SMART on FHIR application that needs to access a patient&apos;s medication information.</p>
<p>Using OPA, you could define a policy that allows access to medication information only if the application is launched by a clinician and if the patient has consented to data sharing.</p>
<p>When the application requests access to medication data, OPA would evaluate the request against the defined policy. If the conditions are met, OPA would grant access, otherwise, access would be denied.</p>
<p>In essence, OPA provides a powerful way to enhance the security and flexibility of SMART on FHIR applications by enabling fine-grained, policy-based access control, making it a valuable tool for organisations looking to leverage the benefits of both SMART on FHIR and a general-purpose policy engine.</p>
<h5 id="source-code">Source Code</h5>
<ul>
<li>GitHub: <a href="https://github.com/Robinyo/provider-directory?ref=rob-ferguson">Australian Healthcare Provider Directory Starter Project</a></li>
</ul>
<h5 id="references">References</h5>
<h6 id="oauth-20">OAuth 2.0</h6>
<ul>
<li>IETF: <a href="https://datatracker.ietf.org/doc/html/rfc6749?ref=rob-ferguson">The OAuth 2.0 Authorization Framework</a></li>
<li>IETF: <a href="https://datatracker.ietf.org/doc/html/rfc8693?ref=rob-ferguson">OAuth 2.0 Token Exchange</a></li>
<li>IETF: <a href="https://datatracker.ietf.org/doc/html/rfc6750?ref=rob-ferguson">The OAuth 2.0 Authorization Framework: Bearer Token Usage</a></li>
<li>IETF: <a href="https://datatracker.ietf.org/doc/html/rfc8707?ref=rob-ferguson">Resource Indicators for OAuth 2.0</a></li>
<li>IETF: <a href="https://datatracker.ietf.org/doc/html/rfc7519?ref=rob-ferguson">JSON Web Token (JWT)</a></li>
<li>IETF: <a href="https://datatracker.ietf.org/doc/html/rfc9068?ref=rob-ferguson">JSON Web Token (JWT) Profile for OAuth 2.0 Access Tokens</a></li>
<li>IETF: <a href="https://datatracker.ietf.org/doc/html/rfc7591?ref=rob-ferguson">OAuth 2.0 Dynamic Client Registration Protocol</a></li>
<li>IETF: <a href="https://datatracker.ietf.org/doc/html/draft-ietf-oauth-browser-based-apps?ref=rob-ferguson">OAuth 2.0 for Browser-Based Applications</a></li>
<li>Spring docs: <a href="https://github.com/spring-projects/spring-authorization-server/issues/297?ref=rob-ferguson#issue-896744390">Implementation Guidelines for Browser-Based Applications</a></li>
</ul>
<h6 id="hl7">HL7</h6>
<ul>
<li>HL7: <a href="https://www.hl7.org/fhir/implementationguide.html?ref=rob-ferguson">Implementation Guide</a></li>
<li>HL7: <a href="https://hl7.org/fhir/packages.html?ref=rob-ferguson">FHIR NPM Packages</a></li>
<li>AU Core: <a href="https://hl7.org.au/fhir/core/history.html?ref=rob-ferguson">Publication (Version) History</a></li>
<li>AU Core FHIR Implementation Guide: <a href="https://hl7.org.au/fhir/core/1.0.0-preview/index.html?ref=rob-ferguson">AU Core - 1.0.0-preview</a></li>
<li>AU Core FHIR Implementation Guide: <a href="https://confluence.hl7.org/display/HAFWG/AU+Core+FHIR+IG+Testing+FAQs?ref=rob-ferguson">Testing FAQs</a></li>
<li>Sparked AU Core Test Data: <a href="https://github.com/hl7au/au-fhir-test-data/blob/master/Postman/Sparked%20AUCore%20Test%20Data.postman_collection.json?ref=rob-ferguson">Postman collection</a></li>
<li>HL7 AU: <a href="https://hl7.org.au/fhir/pd/pd2/index.html?ref=rob-ferguson">Australian Provider Directory Implementation Guide</a></li>
</ul>
<h6 id="smart-on-fhir">SMART on FHIR</h6>
<ul>
<li>HL7: <a href="https://build.fhir.org/ig/HL7/smart-app-launch/?ref=rob-ferguson">SMART App Launch</a></li>
<li>SMART Health IT: <a href="https://docs.smarthealthit.org/?ref=rob-ferguson">SMART on FHIR</a></li>
</ul>
<h6 id="smart-on-fhirstandalone-launch">SMART on FHIR - Standalone Launch</h6>
<ul>
<li>Project Alvearie: <a href="https://alvearie.io/blog/smart-keycloak/?ref=rob-ferguson">SMART App Launch</a></li>
<li>Project Alvearie: <a href="https://github.com/Alvearie/keycloak-extensions-for-fhir?ref=rob-ferguson">Keycloak extensions for FHIR</a></li>
<li>Keycloak extensions for FHIR: <a href="https://github.com/Alvearie/keycloak-extensions-for-fhir/issues/64?ref=rob-ferguson">Upgrade to the Quarkus-based distribution</a></li>
<li>Keycloak discussion: <a href="https://github.com/keycloak/keycloak/discussions/10303?ref=rob-ferguson">Fine grained scope consent management</a></li>
</ul>
<h6 id="smart-on-fhirehr-launch">SMART on FHIR - EHR Launch</h6>
<ul>
<li>GitHub: <a href="https://github.com/zedwerks/keycloak-smart-fhir?ref=rob-ferguson">Zedwerks - Keycloak extensions for FHIR</a></li>
</ul>
<h6 id="keycloak">Keycloak</h6>
<ul>
<li>Keycloak docs: <a href="https://www.keycloak.org/server/configuration-production?ref=rob-ferguson">Configuring Keycloak for production</a></li>
<li>Keycloak docs: <a href="https://www.keycloak.org/server/enabletls?ref=rob-ferguson">Configuring TLS</a></li>
<li>Keycloak docs: <a href="https://www.keycloak.org/server/keycloak-truststore?ref=rob-ferguson">Configuring trusted certificates</a></li>
<li>Keycloak docs: <a href="https://www.keycloak.org/server/hostname?ref=rob-ferguson">Configuring the hostname</a></li>
<li>Keycloak docs: <a href="https://www.keycloak.org/server/reverseproxy?ref=rob-ferguson">Using a reverse proxy</a></li>
<li>Keycloak docs: <a href="https://www.keycloak.org/server/containers?ref=rob-ferguson">Running Keycloak in a container</a></li>
<li>Keycloak docs: <a href="https://www.keycloak.org/migration/migrating-to-quarkus?ref=rob-ferguson">Migrating to the Quarkus distribution</a></li>
<li>Keycloak docs: <a href="https://www.keycloak.org/docs/latest/upgrading/?ref=rob-ferguson">Upgrading Guide - 26.1.0</a></li>
<li>Keycloak docs: <a href="https://www.keycloak.org/docs/latest/authorization_services/index.html?ref=rob-ferguson">Authorization Services Guide</a></li>
</ul>
<h6 id="keycloak-based-development">Keycloak-based  Development</h6>
<ul>
<li>GitHub: <a href="https://github.com/thomasdarimont/keycloak-project-example?ref=rob-ferguson">Keycloak Project Example</a></li>
<li>GitHub: <a href="https://github.com/thomasdarimont/awesome-keycloak?ref=rob-ferguson">Awesome Keycloak</a></li>
</ul>
<h6 id="keycloak-support">Keycloak Support</h6>
<ul>
<li>Google Group: <a href="https://groups.google.com/g/keycloak-user?ref=rob-ferguson">Keycloak User</a></li>
<li>Google Group: <a href="https://groups.google.com/g/keycloak-dev?ref=rob-ferguson">Keycloak Dev</a></li>
</ul>
<h6 id="apisix">APISIX</h6>
<ul>
<li>APISIX docs: <a href="https://apisix.apache.org/docs/apisix/deployment-modes/?ref=rob-ferguson#standalone">Deployment modes</a></li>
<li>APISIX docs: <a href="https://apisix.apache.org/docs/apisix/ssl-protocol/?ref=rob-ferguson">SSL Protocol</a></li>
<li>APISIX docs: <a href="https://apisix.apache.org/docs/apisix/certificate/?ref=rob-ferguson">Certificate</a></li>
<li>APISIX docs: <a href="https://apisix.apache.org/docs/apisix/plugins/openid-connect/?ref=rob-ferguson">Plugins - OpenID Connect</a></li>
</ul>
<h6 id="open-policy-agent">Open Policy Agent</h6>
<ul>
<li>Open Policy Agent docs: <a href="https://www.openpolicyagent.org/docs?ref=rob-ferguson">Introduction</a></li>
<li>Styra docs: <a href="https://docs.styra.com/opa/rego-style-guide?ref=rob-ferguson">Rego Style Guide</a></li>
<li>GitHub: <a href="https://github.com/StyraInc/awesome-opa?ref=rob-ferguson">Awesome OPA - A curated list of awesome OPA-related tools, frameworks and articles</a></li>
<li>Open Policy Agent: <a href="https://play.openpolicyagent.org/?ref=rob-ferguson">Playground</a></li>
<li>Styra Academy courses: <a href="https://academy.styra.com/courses/opa-rego?ref=rob-ferguson">OPA Policy Authoring</a></li>
<li>Open Policy Agent: <a href="https://www.openpolicyagent.org/ecosystem/by-feature/learning-rego?ref=rob-ferguson">Ecosystem</a></li>
</ul>
<h6 id="provider-directory">Provider Directory</h6>
<ul>
<li>HAPI FHIR: <a href="https://hapifhir.io/?ref=rob-ferguson">Website</a></li>
<li>HAPI FHIR: <a href="https://hapifhir.io/hapi-fhir/docs/?ref=rob-ferguson">Documentation</a></li>
<li>Google Group: <a href="https://groups.google.com/g/hapi-fhir?ref=rob-ferguson">HAPI FHIR</a></li>
</ul>
<!--kg-card-end: markdown-->]]></content:encoded></item><item><title><![CDATA[Healthcare Provider Directory Access Control]]></title><description><![CDATA[<!--kg-card-begin: markdown--><h3 id="introduction">Introduction</h3>
<p>In the Australian context, a healthcare provider can be defined as an individual or organisation involved in the delivery of healthcare services.</p>
<p>This broad definition encompasses a range of individuals, including doctors, nurses, and other health professionals, as well as organisations like hospitals, clinics, and aged care facilities.</p>
<h4 id="individual-healthcare-providers">Individual</h4>]]></description><link>https://rob-ferguson.me/healthcare-provider-directory-access-control/</link><guid isPermaLink="false">68534181352fee04fcab1eef</guid><category><![CDATA[Healthcare]]></category><category><![CDATA[Provider Directory]]></category><category><![CDATA[Access Control]]></category><category><![CDATA[Authorization]]></category><category><![CDATA[FHIR]]></category><category><![CDATA[Keycloak]]></category><category><![CDATA[APISIX]]></category><category><![CDATA[Open Policy Agent]]></category><category><![CDATA[Rego]]></category><dc:creator><![CDATA[Rob Ferguson]]></dc:creator><pubDate>Thu, 19 Jun 2025 00:39:22 GMT</pubDate><content:encoded><![CDATA[<!--kg-card-begin: markdown--><h3 id="introduction">Introduction</h3>
<p>In the Australian context, a healthcare provider can be defined as an individual or organisation involved in the delivery of healthcare services.</p>
<p>This broad definition encompasses a range of individuals, including doctors, nurses, and other health professionals, as well as organisations like hospitals, clinics, and aged care facilities.</p>
<h4 id="individual-healthcare-providers">Individual Healthcare Providers</h4>
<p>These are individuals who provide, have provided, or are intending to provide healthcare services. This includes registered health professionals such as medical practitioners, nurses, dentists, pharmacists, and allied health professionals.</p>
<p><strong>Note:</strong> Individual healthcare providers are also referred to as healthcare practitioners.</p>
<h4 id="organisational-healthcare-providers">Organisational Healthcare Providers</h4>
<p>These are organisations that deliver healthcare services. Examples include hospitals, day procedure centers, aged care facilities, and pathology or radiology services.</p>
<h4 id="healthcare-identifiers">Healthcare Identifiers</h4>
<p>Healthcare Provider Identifiers (HPI-I and HPI-O) are used to uniquely identify individuals and organisations involved in the delivery of healthcare services.</p>
<h3 id="healthcare-provider-directory">Healthcare Provider Directory</h3>
<p>A healthcare provider directory is a repository of information (where data is stored and maintained) about healthcare providers.</p>
<h4 id="fhir-resources">FHIR Resources</h4>
<p>There are a number of provider-related FHIR resources.</p>
<p>For example:</p>
<ul>
<li>Practitioner</li>
<li>Practitioner Role</li>
<li>Organization</li>
<li>Healthcare Service</li>
<li>Location</li>
</ul>
<p><strong>Note:</strong> As per the FHIR specifcation, the spelling of FHIR resource names (like &quot;Organization&quot;) follows the American English standard.</p>
<h4 id="fhir-operations">FHIR Operations</h4>
<p>FHIR operations are interactions defined by the FHIR standard for manipulating healthcare data. They follow a RESTful paradigm, allowing for Create, Read, Update, Delete (CRUD) and Search actions on FHIR resources.</p>
<p>For example, to read Organisation information:</p>
<pre><code>GET /Organization/{id}
</code></pre>
<h4 id="smart-on-fhir">SMART on FHIR</h4>
<p>SMART on FHIR defines OAuth 2.0 <a href="https://build.fhir.org/ig/HL7/smart-app-launch/scopes-and-launch-context.html?ref=rob-ferguson">scopes</a> that allow applications to request a specific set of access rights. The OAuth 2.0 client conveys this information to the authorization server in the form of a &apos;scope&apos; request parameter.</p>
<p>The SMART on FHIR specification defines the structure of scopes, for example:</p>
<pre><code>system/Organization.read
system/Organization.write
</code></pre>
<p>To enable an OAuth 2.0 client to read all the values from the Organization resource the client would include the <code>system/Organization.read</code> scope in its request to the authorization server.</p>
<p>A resource context prefixes each SMART on FHIR scope:</p>
<pre><code>user
patient
system
</code></pre>
<p>This value represents one of three possible scenarios: <code>user</code> access to the resource is constrained by the user&apos;s access permissions; <code>patient</code> access to the resource is restricted to the in-context patient; <code>system</code> access is confined to system-based authorization workflows (e.g., the Client Credentials grant).</p>
<p>The following modification rights are defined:</p>
<pre><code>read
write
</code></pre>
<p>Where read includes &quot;search&quot; and &quot;history&#x201D;. And, write includes create&quot;, &quot;update and &quot;delete&quot;.</p>
<p><strong>Note:</strong> Keycloak does not support wildcard scopes, clients must explicitly request each scope they require.</p>
<h3 id="access-control">Access Control</h3>
<p>How do we define <strong>coarse-grained</strong> access control?</p>
<p>Coarse-grained access control is when access to resources is granted or denied based on broad, general criteria, often at the role (RBAC) level.</p>
<p>For example:</p>
<ul>
<li>Responsible Officer (RO)</li>
<li>Organisation Maintenance Officer (OMO)</li>
<li>Authorised Employee</li>
<li>Individual Health Care Provider</li>
<li>Contracted Service Provider (CSP)</li>
<li>General Supporting Organisation (GSO)</li>
</ul>
<p>How do we define <strong>fine-grained</strong> access control?</p>
<p>Fine-grained access control is when access to resources is granted or denied based on multiple conditions and may combine different access control mechanisms (ABAC, RBAC, ReBAC, UBAC).</p>
<p><strong>In the Australian Healthcare context, support for fine-grained access control is often required.</strong></p>
<p>For example, a Practitioner must be granted the Organisation Maintenance Officer (OMO) role and have a <a href="https://rob-ferguson.me/parties-roles-and-relationships/">membership</a> relationship with an Organisation in order to maintain Healthcare Service information on an Organisation&apos;s behalf.</p>
<blockquote>
<p>We need an Authorisation Service that can provide fine-grained access control for FHIR resources in a Healthcare Provider Directory.</p>
</blockquote>
<h3 id="authorisation-service">Authorisation Service</h3>
<p>The Authorisation Service is comprised of the following components:</p>
<ul>
<li>OAuth 2.0 Authorization Server that supports the <strong>Client Credentials</strong> grant and the <strong>Token Exchange</strong> grant</li>
<li>Policy Engine</li>
</ul>
<p><a href="https://www.keycloak.org/?ref=rob-ferguson">Keycloak</a> supports the Client Credentials grant and the Token Exchange grant.</p>
<p>Policy decisions (evaluation) are performed by the general purpose Policy Engine (i.e., <a href="https://www.openpolicyagent.org/docs?ref=rob-ferguson">Open Policy Agent</a>).</p>
<p>Policies are enforced by the API Gateway (<a href="https://apisix.apache.org/docs/?ref=rob-ferguson">APISIX</a>).</p>
<p>Policies are authored in <a href="https://www.openpolicyagent.org/docs/policy-language?ref=rob-ferguson">Rego</a>.</p>
<p>Reference data (e.g., Roles, Permissions, Relationships) is loaded when the Policy Engine is healthy.</p>
<p>Policies are loaded when the Policy Engine is healthy and all reference data has been loaded.</p>
<h5 id="source-code">Source Code</h5>
<ul>
<li>GitHub: <a href="https://github.com/Robinyo/provider-directory?ref=rob-ferguson">Australian Healthcare Provider Directory Starter Project</a></li>
</ul>
<h3 id="whats-next">What&apos;s Next</h3>
<p>In the next post, we&apos;ll take a look at <strong>Open Policy Agent</strong>. A general-purpose policy engine that you can use for policy decisions (evaluation) in API gateways, microservices and more.</p>
<h5 id="references">References</h5>
<h6 id="oauth-20">OAuth 2.0</h6>
<ul>
<li>IETF: <a href="https://datatracker.ietf.org/doc/html/rfc6749?ref=rob-ferguson">The OAuth 2.0 Authorization Framework</a></li>
<li>IETF: <a href="https://datatracker.ietf.org/doc/html/rfc8693?ref=rob-ferguson">OAuth 2.0 Token Exchange</a></li>
<li>IETF: <a href="https://datatracker.ietf.org/doc/html/rfc6750?ref=rob-ferguson">The OAuth 2.0 Authorization Framework: Bearer Token Usage</a></li>
<li>IETF: <a href="https://datatracker.ietf.org/doc/html/rfc8707?ref=rob-ferguson">Resource Indicators for OAuth 2.0</a></li>
<li>IETF: <a href="https://datatracker.ietf.org/doc/html/rfc7519?ref=rob-ferguson">JSON Web Token (JWT)</a></li>
<li>IETF: <a href="https://datatracker.ietf.org/doc/html/rfc9068?ref=rob-ferguson">JSON Web Token (JWT) Profile for OAuth 2.0 Access Tokens</a></li>
<li>IETF: <a href="https://datatracker.ietf.org/doc/html/rfc7591?ref=rob-ferguson">OAuth 2.0 Dynamic Client Registration Protocol</a></li>
<li>IETF: <a href="https://datatracker.ietf.org/doc/html/draft-ietf-oauth-browser-based-apps?ref=rob-ferguson">OAuth 2.0 for Browser-Based Applications</a></li>
<li>Spring docs: <a href="https://github.com/spring-projects/spring-authorization-server/issues/297?ref=rob-ferguson#issue-896744390">Implementation Guidelines for Browser-Based Applications</a></li>
</ul>
<h6 id="hl7">HL7</h6>
<ul>
<li>HL7: <a href="https://www.hl7.org/fhir/implementationguide.html?ref=rob-ferguson">Implementation Guide</a></li>
<li>HL7: <a href="https://hl7.org/fhir/packages.html?ref=rob-ferguson">FHIR NPM Packages</a></li>
<li>AU Core: <a href="https://hl7.org.au/fhir/core/history.html?ref=rob-ferguson">Publication (Version) History</a></li>
<li>AU Core FHIR Implementation Guide: <a href="https://hl7.org.au/fhir/core/1.0.0-preview/index.html?ref=rob-ferguson">AU Core - 1.0.0-preview</a></li>
<li>AU Core FHIR Implementation Guide: <a href="https://confluence.hl7.org/display/HAFWG/AU+Core+FHIR+IG+Testing+FAQs?ref=rob-ferguson">Testing FAQs</a></li>
<li>Sparked AU Core Test Data: <a href="https://github.com/hl7au/au-fhir-test-data/blob/master/Postman/Sparked%20AUCore%20Test%20Data.postman_collection.json?ref=rob-ferguson">Postman collection</a></li>
<li>HL7 AU: <a href="https://hl7.org.au/fhir/pd/pd2/index.html?ref=rob-ferguson">Australian Provider Directory Implementation Guide</a></li>
</ul>
<h6 id="keycloak">Keycloak</h6>
<ul>
<li>Keycloak docs: <a href="https://www.keycloak.org/server/configuration-production?ref=rob-ferguson">Configuring Keycloak for production</a></li>
<li>Keycloak docs: <a href="https://www.keycloak.org/server/enabletls?ref=rob-ferguson">Configuring TLS</a></li>
<li>Keycloak docs: <a href="https://www.keycloak.org/server/keycloak-truststore?ref=rob-ferguson">Configuring trusted certificates</a></li>
<li>Keycloak docs: <a href="https://www.keycloak.org/server/hostname?ref=rob-ferguson">Configuring the hostname</a></li>
<li>Keycloak docs: <a href="https://www.keycloak.org/server/reverseproxy?ref=rob-ferguson">Using a reverse proxy</a></li>
<li>Keycloak docs: <a href="https://www.keycloak.org/server/containers?ref=rob-ferguson">Running Keycloak in a container</a></li>
<li>Keycloak docs: <a href="https://www.keycloak.org/migration/migrating-to-quarkus?ref=rob-ferguson">Migrating to the Quarkus distribution</a></li>
<li>Keycloak docs: <a href="https://www.keycloak.org/docs/latest/upgrading/?ref=rob-ferguson">Upgrading Guide - 26.1.0</a></li>
<li>Keycloak docs: <a href="https://www.keycloak.org/docs/latest/authorization_services/index.html?ref=rob-ferguson">Authorization Services Guide</a></li>
</ul>
<h6 id="keycloak-based-development">Keycloak-based  Development</h6>
<ul>
<li>GitHub: <a href="https://github.com/thomasdarimont/keycloak-project-example?ref=rob-ferguson">Keycloak Project Example</a></li>
<li>GitHub: <a href="https://github.com/thomasdarimont/awesome-keycloak?ref=rob-ferguson">Awesome Keycloak</a></li>
</ul>
<h6 id="keycloak-support">Keycloak Support</h6>
<ul>
<li>Google Group: <a href="https://groups.google.com/g/keycloak-user?ref=rob-ferguson">Keycloak User</a></li>
<li>Google Group: <a href="https://groups.google.com/g/keycloak-dev?ref=rob-ferguson">Keycloak Dev</a></li>
</ul>
<h6 id="apisix">APISIX</h6>
<ul>
<li>APISIX docs: <a href="https://apisix.apache.org/docs/apisix/deployment-modes/?ref=rob-ferguson#standalone">Deployment modes</a></li>
<li>APISIX docs: <a href="https://apisix.apache.org/docs/apisix/ssl-protocol/?ref=rob-ferguson">SSL Protocol</a></li>
<li>APISIX docs: <a href="https://apisix.apache.org/docs/apisix/certificate/?ref=rob-ferguson">Certificate</a></li>
<li>APISIX docs: <a href="https://apisix.apache.org/docs/apisix/plugins/openid-connect/?ref=rob-ferguson">Plugins - OpenID Connect</a></li>
</ul>
<h6 id="provider-directory">Provider Directory</h6>
<ul>
<li>HAPI FHIR: <a href="https://hapifhir.io/?ref=rob-ferguson">Website</a></li>
<li>HAPI FHIR: <a href="https://hapifhir.io/hapi-fhir/docs/?ref=rob-ferguson">Documentation</a></li>
<li>Google Group: <a href="https://groups.google.com/g/hapi-fhir?ref=rob-ferguson">HAPI FHIR</a></li>
</ul>
<!--kg-card-end: markdown-->]]></content:encoded></item><item><title><![CDATA[Secure HAPI FHIR data at rest]]></title><description><![CDATA[<!--kg-card-begin: markdown--><h3 id="introduction">Introduction</h3>
<p><a href="https://hapifhir.io/?ref=rob-ferguson">HAPI FHIR</a> is an open source implementation of the HL7 FHIR standard for healthcare interoperability.</p>
<p>The easiest way to get started with HAPI FHIR is to use the <strong>HAPI FHIR JPA Server Starter Project</strong>.</p>
<p>In previous posts, we have worked to augment the HAPI FHIR JPA Server Starter Project</p>]]></description><link>https://rob-ferguson.me/secure-hapi-fhir-data-at-rest/</link><guid isPermaLink="false">683ec714352fee04fcab1e84</guid><category><![CDATA[pg_tde]]></category><category><![CDATA[TDE]]></category><category><![CDATA[FHIR]]></category><category><![CDATA[HAPI FHIR]]></category><category><![CDATA[Percona Distribution for PostgreSQL]]></category><category><![CDATA[PostgreSQL]]></category><category><![CDATA[data at rest]]></category><dc:creator><![CDATA[Rob Ferguson]]></dc:creator><pubDate>Tue, 03 Jun 2025 10:12:22 GMT</pubDate><content:encoded><![CDATA[<!--kg-card-begin: markdown--><h3 id="introduction">Introduction</h3>
<p><a href="https://hapifhir.io/?ref=rob-ferguson">HAPI FHIR</a> is an open source implementation of the HL7 FHIR standard for healthcare interoperability.</p>
<p>The easiest way to get started with HAPI FHIR is to use the <strong>HAPI FHIR JPA Server Starter Project</strong>.</p>
<p>In previous posts, we have worked to augment the HAPI FHIR JPA Server Starter Project in order to demonstrate secure access to FHIR resources.</p>
<p>Including:</p>
<ul>
<li>Support for OpenID Connect and OAuth 2.0 (e.g., SMART on FHIR)</li>
<li>Secure HAPI FHIR data in transit</li>
</ul>
<p>See: <a href="https://github.com/Robinyo/hapi-fhir-au?ref=rob-ferguson">HAPI FHIR AU Starter Project</a></p>
<p>In this post, we&apos;ll use the Docker image of the <strong>Percona Distribution for PostgreSQL</strong> to encrypt data at rest.</p>
<h3 id="percona-distribution-for-postgresql">Percona Distribution for PostgreSQL</h3>
<p>The <code>pg_tde</code> extension enables encryption at the table level. The encryption is transparent and does not require any changes to applications.</p>
<h4 id="secure-data-at-rest">Secure data at rest</h4>
<h5 id="enable-encryption">Enable encryption</h5>
<p>The Docker image of the Percona Distribution for PostgreSQL includes the <code>pg_tde</code> extension that provides data encryption:</p>
<pre><code>  postgres:
    container_name: postgres  
    image: percona/percona-distribution-postgresql:17.5
    
    ...

    environment:
      ENABLE_PG_TDE: 1
      
      ...

</code></pre>
<p>See: <a href="https://github.com/Robinyo/hapi-fhir-au/blob/main/backend/docker-compose-hapi-fhir-enable-tls.yml?ref=rob-ferguson">docker-compose.yml</a></p>
<p><code>ENABLE_PG_TDE: 1</code> adds <code>pg_tde</code> to the <code>shared_preload_libraries</code> entry in the <code>postgresql.conf</code> file and enables the custom storage manager.</p>
<p>Connect to the container:</p>
<pre><code>docker exec -it postgres bash
</code></pre>
<p>Start an interactive <code>psql</code> session:</p>
<pre><code>psql -U admin -d hapi-fhir
</code></pre>
<p>Create the <code>pg_tde</code> extension in the database you want to encrypt:</p>
<pre><code>\c hapi-fhir;
CREATE EXTENSION pg_tde;
</code></pre>
<p>Check the list of installed extensions:</p>
<pre><code>hapi-fhir=# \dx
                 List of installed extensions
  Name   | Version |   Schema   |         Description          
---------+---------+------------+------------------------------
 pg_tde  | 1.0-rc  | public     | pg_tde access method
 plpgsql | 1.0     | pg_catalog | PL/pgSQL procedural language
(2 rows)
</code></pre>
<p>Configure a key provider:</p>
<pre><code>SELECT pg_tde_add_database_key_provider_file(&apos;file-vault&apos;, &apos;/tmp/pg_tde_test_001_basic.per&apos;);
</code></pre>
<p>You should see something like:</p>
<pre><code> pg_tde_add_database_key_provider_file 
---------------------------------------
                                     1
(1 row)
</code></pre>
<p><strong>Note:</strong> This sample key provider configuration is meant for development and testing purposes only, not production.</p>
<p>Set a principal key:</p>
<pre><code>SELECT pg_tde_set_key_using_database_key_provider(&apos;test-db-key&apos;, &apos;file-vault&apos;);
</code></pre>
<p>You should see something like:</p>
<pre><code> pg_tde_set_key_using_database_key_provider 
--------------------------------------------
 
(1 row)
</code></pre>
<p>Alter a HAPI FHIR table to enable encryption:</p>
<pre><code>ALTER TABLE hfj_resource SET ACCESS METHOD tde_heap;
</code></pre>
<p>You should see something like:</p>
<pre><code>ALTER TABLE
</code></pre>
<p>Check to see if the table is encrypted:</p>
<pre><code>select pg_tde_is_encrypted(&apos;hfj_resource&apos;);
</code></pre>
<p>You should see something like:</p>
<pre><code> pg_tde_is_encrypted 
---------------------
 t
(1 row)
</code></pre>
<p>End your interactive <code>psql</code> session:</p>
<pre><code>\q
</code></pre>
<h5 id="source-code">Source Code</h5>
<ul>
<li>GitHub: <a href="https://github.com/Robinyo/hapi-fhir-au?ref=rob-ferguson">HAPI FHIR AU Starter Project</a></li>
</ul>
<h5 id="resources">Resources</h5>
<ul>
<li>Percona Distribution for PostgreSQL: <a href="https://docs.percona.com/postgresql/17/docker.html?ref=rob-ferguson">Docker</a></li>
<li>Percona Community Forum: <a href="https://forums.percona.com/c/postgresql/pg-tde-transparent-data-encryption-tde/82?ref=rob-ferguson">pg_tde</a></li>
<li>GitHub: <a href="https://github.com/percona/postgres/blob/TDE_REL_17_STABLE/contrib/pg_tde/t/001_basic.pl?ref=rob-ferguson">Percona PostgreSQL - Test Case 001</a></li>
</ul>
<!--kg-card-end: markdown-->]]></content:encoded></item><item><title><![CDATA[Secure HAPI FHIR data in transit]]></title><description><![CDATA[<!--kg-card-begin: markdown--><h3 id="introduction">Introduction</h3>
<p><a href="https://hapifhir.io/?ref=rob-ferguson">HAPI FHIR</a> is an open source implementation of the HL7 FHIR standard for healthcare interoperability.</p>
<p>The easiest way to get started with HAPI FHIR is to use the <strong>HAPI FHIR JPA Server Starter Project</strong>.</p>
<p>In previous posts, we have worked to augment the HAPI FHIR JPA Server Starter Project</p>]]></description><link>https://rob-ferguson.me/secure-hapi-fhir-data-in-transit/</link><guid isPermaLink="false">683eb859352fee04fcab1d9b</guid><category><![CDATA[TLS]]></category><category><![CDATA[SSL]]></category><category><![CDATA[FHIR]]></category><category><![CDATA[HAPI FHIR]]></category><category><![CDATA[PostgreSQL]]></category><category><![CDATA[PGSSLMODE]]></category><category><![CDATA[data in transit]]></category><dc:creator><![CDATA[Rob Ferguson]]></dc:creator><pubDate>Tue, 03 Jun 2025 09:49:52 GMT</pubDate><content:encoded><![CDATA[<!--kg-card-begin: markdown--><h3 id="introduction">Introduction</h3>
<p><a href="https://hapifhir.io/?ref=rob-ferguson">HAPI FHIR</a> is an open source implementation of the HL7 FHIR standard for healthcare interoperability.</p>
<p>The easiest way to get started with HAPI FHIR is to use the <strong>HAPI FHIR JPA Server Starter Project</strong>.</p>
<p>In previous posts, we have worked to augment the HAPI FHIR JPA Server Starter Project in order to demonstrate secure access to FHIR resources.</p>
<p>Including:</p>
<ul>
<li>Support for OpenID Connect and OAuth 2.0 (e.g., SMART on FHIR)</li>
</ul>
<p>See: <a href="https://github.com/Robinyo/hapi-fhir-au?ref=rob-ferguson">HAPI FHIR AU Starter Project</a></p>
<p>In this post, we&apos;ll enable TLS in HAPI FHIR&apos;s embedded web server and ensure that all PostgreSQL clients use encrypted connections.</p>
<h4 id="hapi-fhir">HAPI FHIR</h4>
<h5 id="enable-tls">Enable TLS</h5>
<p>HAPI FHIR relies on the underlying web server for TLS encryption.</p>
<p>All embedded web servers supported by Spring Boot can be configured to secure connections with TLS (SSL) by using the <code>server.ssl.*</code> properties.</p>
<p>For example:</p>
<pre><code>services:

  hapi-fhir:
    container_name: hapi-fhir
    build:
      context: ./services/hapi-fhir
      dockerfile: Dockerfile
    restart: unless-stopped
    ports:
      - 6443:8443      
    environment:
      SERVER_SSL_ENABLED: true
      SERVER_SSL_KEY_STORE_TYPE: PKCS12
      SERVER_SSL_KEY_STORE: file:/keystore/keystore.p12
      SERVER_SSL_KEY_STORE_PASSWORD: secret
      SERVER_SSL_KEY_ALIAS: tomcat
      SERVER_PORT: 8443
      SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/${HAPI_FHIR_DB}
      SPRING_DATASOURCE_USERNAME: ${POSTGRES_USER}
      SPRING_DATASOURCE_PASSWORD: ${POSTGRES_PASSWORD}
      SPRING_DATASOURCE_DRIVER_CLASS_NAME: org.postgresql.Driver
      SPRING_JPA_PROPERTIES_HIBERNATE_DIALECT: ca.uhn.fhir.jpa.model.dialect.HapiFhirPostgresDialect
      SPRING_JPA_PROPERTIES_SEARCH_ENABLED: false
    env_file:
      - ./.env
    volumes:
      - &apos;${PWD}/certs/keystore.p12:/keystore/keystore.p12&apos;
    configs:
      - source: hapi
        target: /app/config/application.yaml
    depends_on:
      postgres:
        condition: service_healthy
      keycloak.au.localhost:
        condition: service_healthy
    networks:
      - hapi_fhir_network

  ...

configs:
  hapi:
    file: ./hapi.application-enable-tls.yaml

</code></pre>
<p>See: <a href="https://github.com/Robinyo/hapi-fhir-au/blob/main/backend/docker-compose-hapi-fhir-enable-tls.yml?ref=rob-ferguson">docker-compose.yml</a></p>
<p>Access to a keystore file containing the server certificate and private key is also required. You can use <code>openssl</code> to create a PKCS12 keystore.</p>
<p>For example:</p>
<pre><code>openssl pkcs12 -export -in cert.pem -inkey key.pem -out keystore.p12 -name tomcat -password pass:secret
</code></pre>
<h4 id="postgresql">PostgreSQL</h4>
<h5 id="enable-tls">Enable TLS</h5>
<p>Support for encrypted connections is enabled by setting the <code>ssl</code> parameter to <code>on</code>. The server will listen for both normal and secure connections on the same port.</p>
<p>Connecting clients can be required to use encrypted connections by setting the environment variable <code>PGSSLMODE</code> to <code>require</code>.</p>
<p>For example:</p>
<pre><code>services:

  postgres:
    container_name: postgres
    
    ...
    
    command: &gt;
      -c ssl=on 
      -c ssl_cert_file=/var/lib/postgresql/server.crt 
      -c ssl_key_file=/var/lib/postgresql/server.key
    environment:
      POSTGRES_DB: ${POSTGRES_DB}
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      PGSSLMODE: require
    env_file:
      - ./.env      
    volumes:
      - &apos;${PWD}/certs/cert.pem:/var/lib/postgresql/server.crt&apos;
      - &apos;${PWD}/certs/key.pem:/var/lib/postgresql/server.key&apos;
      - postgres_data:/var/lib/postgresql/data
      
    ...

</code></pre>
<p>PostgreSQL also requires access to the files containing the server certificate and private key.</p>
<p><strong>Note:</strong> On Unix and macOS systems the cert and key file permissions must disallow any access to world or group.</p>
<p>For example:</p>
<pre><code>sudo chmod 600 *.pem
</code></pre>
<p>We can check that the connections to PostgreSQL are secure by running the following query:</p>
<pre><code>select pg_ssl.pid, pg_ssl.ssl, pg_ssl.version,
       pg_sa.backend_type, pg_sa.usename, pg_sa.client_addr
       from pg_stat_ssl pg_ssl
       join pg_stat_activity pg_sa
       on pg_ssl.pid = pg_sa.pid;
</code></pre>
<p>In pgAdmin:</p>
<p align="center">
  <img src="https://rob-ferguson.me/content/images/2025/06/pgdmin-checking-connections.png" class="thumbnail">
</p>
<p>We can obtain HAPI FHIR&apos;s IP Address using the following command:</p>
<pre><code>docker inspect -f &apos;{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}&apos; hapi-fhir
</code></pre>
<p>You should see something like:</p>
<pre><code>172.18.0.6
</code></pre>
<h5 id="source-code">Source Code</h5>
<ul>
<li>GitHub: <a href="https://github.com/Robinyo/hapi-fhir-au?ref=rob-ferguson">HAPI FHIR AU Starter Project</a></li>
</ul>
<h5 id="resources">Resources</h5>
<ul>
<li>HAPI FHIR: <a href="https://hapifhir.io/?ref=rob-ferguson">Website</a></li>
<li>HAPI FHIR: <a href="https://hapifhir.io/hapi-fhir/docs/?ref=rob-ferguson">Documentation</a></li>
<li>HAPI FHIR AU docs: <a href="https://github.com/Robinyo/hapi-fhir-au/blob/main/docs/developer/mkcert/README.md?ref=rob-ferguson">Working with mkcert</a></li>
<li>PostgreSQL: <a href="https://www.postgresql.org/docs/current/index.html?ref=rob-ferguson">Documentation</a></li>
</ul>
<!--kg-card-end: markdown-->]]></content:encoded></item><item><title><![CDATA[Getting started with the APISIX authz-keycloak plugin]]></title><description><![CDATA[<!--kg-card-begin: markdown--><h3 id="introduction">Introduction</h3>
<p>In a previous <a href="https://rob-ferguson.me/add-authz-to-hapi-fhir-with-apisix-and-keycloak/">post</a>, I wrote about how to use Keycloak and the APISIX <code>openid_connect</code> plugin to add support for SMART on FHIR to HAPI FHIR.</p>
<p>In this post we are going to look at how to use Keycloak and the APISIX <code>authz-keycloak</code> plugin to add support for</p>]]></description><link>https://rob-ferguson.me/getting-started-with-the-apisix-authz-keycloak-plugin/</link><guid isPermaLink="false">683388c8352fee04fcab1c5e</guid><category><![CDATA[SMART on FHIR]]></category><category><![CDATA[Authorization]]></category><category><![CDATA[authz-keycloak]]></category><category><![CDATA[Plugin]]></category><category><![CDATA[APISIX]]></category><category><![CDATA[FHIR]]></category><category><![CDATA[HAPI FHIR]]></category><category><![CDATA[Keycloak]]></category><category><![CDATA[AuthZ]]></category><dc:creator><![CDATA[Rob Ferguson]]></dc:creator><pubDate>Sun, 25 May 2025 22:36:36 GMT</pubDate><content:encoded><![CDATA[<!--kg-card-begin: markdown--><h3 id="introduction">Introduction</h3>
<p>In a previous <a href="https://rob-ferguson.me/add-authz-to-hapi-fhir-with-apisix-and-keycloak/">post</a>, I wrote about how to use Keycloak and the APISIX <code>openid_connect</code> plugin to add support for SMART on FHIR to HAPI FHIR.</p>
<p>In this post we are going to look at how to use Keycloak and the APISIX <code>authz-keycloak</code> plugin to add support for SMART on FHIR to HAPI FHIR.</p>
<h4 id="keycloak-authorization-services">Keycloak Authorization Services</h4>
<h5 id="fine-grained-authorisation">Fine-grained Authorisation</h5>
<p>You must allow the &apos;Authorization&apos; capability config setting in order to enable support for fine-grained authorisation.</p>
<p align="center">
  <img src="https://rob-ferguson.me/content/images/2025/05/keycloak-capability-config-authorization.png" class="thumbnail">
</p>
<h4 id="apisix">APISIX</h4>
<h5 id="authz-keycloak-plugin">AuthZ Keycloak plugin</h5>
<p>The <code>authz-keycloak</code> plugin enables APISIX to leverage the fine-grained authorisation policies and access control mechanisms supported by Keycloak.</p>
<p>See: <a href="https://www.keycloak.org/docs/latest/authorization_services/?ref=rob-ferguson">Keycloak - Authorization Services Guide</a></p>
<h5 id="static-permissions">Static permissions</h5>
<p>The <code>authz-keycloak</code> plugin can be configured to support different authorisation scenarios.</p>
<p>For example, you can configure Keycloak to use scope-based permissions associated with a client scope policy and configure the <code>authz-keycloak</code> plugin to use static permissions:</p>
<ul>
<li>Resource: <strong>Patient</strong></li>
<li>Authorisation Scope: <strong>Patient.read</strong></li>
<li>Client Scope: <strong>system/Patient.read</strong></li>
<li>Client Scope Policy: patient-read-client-scope-policy</li>
<li>Scope-based Permission: patient-read-scope-permission</li>
</ul>
<pre><code>  permissions: [ &quot;Patient#Patient.read&quot; ]
</code></pre>
<h5 id="create-an-authorisation-scope-in-keycloak">Create an Authorisation Scope in Keycloak</h5>
<p>Create an Authorisation Scope (Patient.read) in Keycloak:</p>
<p align="center">
  <img src="https://rob-ferguson.me/content/images/2025/05/keycloak-create-authorization-scope.png" class="thumbnail">
</p>
<h5 id="create-a-resource-in-keycloak">Create a Resource in Keycloak</h5>
<p>Create a Resource (Patient) in Keycloak:</p>
<p align="center">
  <img src="https://rob-ferguson.me/content/images/2025/05/keycloak-create-resource.png" class="thumbnail">
</p>
<h5 id="create-a-client-scope-in-keycloak">Create a Client Scope in Keycloak</h5>
<p>Create a Client Scope (system/Patient.read) in Keycloak:</p>
<p align="center">
  <img src="https://rob-ferguson.me/content/images/2025/05/keycloak-create-client-scope.png" class="thumbnail">
</p>
<h5 id="create-a-client-scope-policy-in-keycloak">Create a Client Scope Policy in Keycloak</h5>
<p>Create a Client Scope Policy (patient-read-client-scope-policy) in Keycloak:</p>
<p align="center">
  <img src="https://rob-ferguson.me/content/images/2025/05/keycloak-create-client-scope-policy.png" class="thumbnail">
</p>
<h5 id="create-a-scope-based-permission-in-keycloak">Create a Scope-based Permission in Keycloak</h5>
<p>Create a Scope-based Permission (patient-read-scope-permission) in Keycloak:</p>
<p align="center">
  <img src="https://rob-ferguson.me/content/images/2025/05/keycloak-create-client-scope-based-permission.png" class="thumbnail">
</p>
<h5 id="configure-apisix">Configure APISIX</h5>
<p>Create a route as follows:</p>
<pre><code>
  ...

  - name: hapi-fhir-api
    host: hapi-fhir.au.localhost
    uri: /fhir/Patient*
    methods: [ &quot;GET&quot; ]
    upstream_id: 1
    plugins:
      authz-keycloak:
        lazy_load_paths: false
        ssl_verify: true
        client_id: ${{CLIENT_ID}}
        client_secret: ${{CLIENT_SECRET}}
        discovery: ${{PROTOCOL}}://${{KEYCLOAK_HOSTNAME}}:${{KEYCLOAK_PORT}}/realms/${{KEYCLOAK_REALM}}/.well-known/openid-configuration
        permissions: [ &quot;Patient#Patient.read&quot; ]
</code></pre>
<p>See: <a href="https://github.com/Robinyo/hapi-fhir-au/blob/main/backend/services/apisix/conf/apisix-standalone.yml?ref=rob-ferguson">apisix-standalone.yml</a></p>
<p>Each entry in the &apos;permissions&apos; attribute must be formatted as expected by the token endpoint&apos;s permission parameter.</p>
<p>For example: &quot;Resource#Authorisation Scope&quot;.</p>
<p>See: <a href="https://www.keycloak.org/docs/latest/authorization_services/index.html?ref=rob-ferguson#_service_obtaining_permissions">Keycloak - Authorization Services Guide: Obtaining permissions</a></p>
<h4 id="call-the-hapi-fhir-api">Call the HAPI FHIR API</h4>
<h5 id="request-a-token">Request a token</h5>
<p>To access the API, you must request an access token. You will need to POST to the token URL.</p>
<p>For example (<code>scope=system/Patient.read</code>):</p>
<pre><code>ACCESS_TOKEN=$(curl -s -X POST https://keycloak.au.localhost:8443/realms/hapi-fhir-dev/protocol/openid-connect/token \
  -H &apos;content-type: application/x-www-form-urlencoded&apos; \
  -d grant_type=client_credentials \
  -d client_id=oauth2-proxy \
  -d client_secret=aHkRec1BYkfaKgMg164JmvKu8u9iWNHM \
  -d scope=system/Patient.read | (jq -r &apos;.access_token&apos;))
                 
# echo &quot;$ACCESS_TOKEN&quot;                 
</code></pre>
<p><strong>Note:</strong> You can use <a href="https://jwt.io/?ref=rob-ferguson">jwt.io</a> to decode the access token.</p>
<h5 id="introspect-a-token">Introspect a token</h5>
<p>To introspect an Access Token you will need to POST to the introspect URL.</p>
<p>For example:</p>
<pre><code>curl -X POST &quot;https://keycloak.au.localhost:8443/realms/hapi-fhir-dev/protocol/openid-connect/token/introspect&quot; \
  -H &apos;content-type: application/x-www-form-urlencoded&apos; \
  -d client_id=oauth2-proxy \
  -d client_secret=aHkRec1BYkfaKgMg164JmvKu8u9iWNHM \
  -d &quot;token_type_hint=access_token&amp;token=$ACCESS_TOKEN&quot;
</code></pre>
<h5 id="call-the-api">Call the API</h5>
<p>To call the HAPI FHIR API, an application must pass the access token as a Bearer token in the Authorization header of the HTTP request.</p>
<p>For example:</p>
<pre><code>curl -X GET https://hapi-fhir.au.localhost/fhir/Patient?_id=baratz-toni \
  -H &apos;Content-Type: application/fhir+json&apos; \
  -H &quot;Authorization: Bearer $ACCESS_TOKEN&quot;
</code></pre>
<p>You should see something like:</p>
<pre><code>{
  &quot;resourceType&quot;: &quot;Bundle&quot;,
  &quot;id&quot;: &quot;9d80c83a-0b06-4b78-bce2-21e6666348d8&quot;,
  &quot;meta&quot;: {
    &quot;lastUpdated&quot;: &quot;2025-05-23T05:43:01.959+00:00&quot;
  },
  &quot;type&quot;: &quot;searchset&quot;,
  &quot;total&quot;: 1,
  &quot;link&quot;: [ {
    &quot;relation&quot;: &quot;self&quot;,
    &quot;url&quot;: &quot;https://hapi-fhir.au.localhost/fhir/Patient?_id=baratz-toni&quot;
  } ],
  &quot;entry&quot;: [ {
    &quot;fullUrl&quot;: &quot;https://hapi-fhir.au.localhost/fhir/Patient/baratz-toni&quot;,
    &quot;resource&quot;: {
      &quot;resourceType&quot;: &quot;Patient&quot;,
      &quot;id&quot;: &quot;baratz-toni&quot;,
      &quot;meta&quot;: {
        &quot;versionId&quot;: &quot;1&quot;,
        &quot;lastUpdated&quot;: &quot;2025-05-23T05:42:36.551+00:00&quot;,
        &quot;source&quot;: &quot;#rKTCeZfmnReSjm8X&quot;,
        &quot;profile&quot;: [ &quot;http://hl7.org.au/fhir/core/StructureDefinition/au-core-patient&quot; ]
      },
      &quot;extension&quot;: [ {
        &quot;url&quot;: &quot;http://hl7.org.au/fhir/StructureDefinition/indigenous-status&quot;,
        &quot;valueCoding&quot;: {
          &quot;system&quot;: &quot;https://healthterminologies.gov.au/fhir/CodeSystem/australian-indigenous-status-1&quot;,
          &quot;code&quot;: &quot;1&quot;,
          &quot;display&quot;: &quot;Aboriginal but not Torres Strait Islander origin&quot;
        }
      }, {
        &quot;url&quot;: &quot;http://hl7.org/fhir/StructureDefinition/individual-genderIdentity&quot;,
        &quot;extension&quot;: [ {
          &quot;url&quot;: &quot;value&quot;,
          &quot;valueCodeableConcept&quot;: {
            &quot;coding&quot;: [ {
              &quot;system&quot;: &quot;http://snomed.info/sct&quot;,
              &quot;code&quot;: &quot;446141000124107&quot;,
              &quot;display&quot;: &quot;Identifies as female gender&quot;
            } ]
          }
        } ]
      }, {
        &quot;url&quot;: &quot;http://hl7.org/fhir/StructureDefinition/individual-pronouns&quot;,
        &quot;extension&quot;: [ {
          &quot;url&quot;: &quot;value&quot;,
          &quot;valueCodeableConcept&quot;: {
            &quot;coding&quot;: [ {
              &quot;system&quot;: &quot;http://loinc.org&quot;,
              &quot;code&quot;: &quot;LA29519-8&quot;,
              &quot;display&quot;: &quot;she/her/her/hers/herself&quot;
            } ]
          }
        } ]
      }, {
        &quot;url&quot;: &quot;http://hl7.org/fhir/StructureDefinition/individual-recordedSexOrGender&quot;,
        &quot;extension&quot;: [ {
          &quot;url&quot;: &quot;type&quot;,
          &quot;valueCodeableConcept&quot;: {
            &quot;coding&quot;: [ {
              &quot;system&quot;: &quot;http://snomed.info/sct&quot;,
              &quot;code&quot;: &quot;1515311000168102&quot;,
              &quot;display&quot;: &quot;Biological sex at birth&quot;
            } ]
          }
        }, {
          &quot;url&quot;: &quot;value&quot;,
          &quot;valueCodeableConcept&quot;: {
            &quot;coding&quot;: [ {
              &quot;system&quot;: &quot;http://snomed.info/sct&quot;,
              &quot;code&quot;: &quot;248152002&quot;,
              &quot;display&quot;: &quot;Female&quot;
            } ]
          }
        } ]
      } ],
      &quot;identifier&quot;: [ {
        &quot;extension&quot;: [ {
          &quot;url&quot;: &quot;http://hl7.org.au/fhir/StructureDefinition/ihi-status&quot;,
          &quot;valueCoding&quot;: {
            &quot;system&quot;: &quot;https://healthterminologies.gov.au/fhir/CodeSystem/ihi-status-1&quot;,
            &quot;code&quot;: &quot;active&quot;
          }
        }, {
          &quot;url&quot;: &quot;http://hl7.org.au/fhir/StructureDefinition/ihi-record-status&quot;,
          &quot;valueCoding&quot;: {
            &quot;system&quot;: &quot;https://healthterminologies.gov.au/fhir/CodeSystem/ihi-record-status-1&quot;,
            &quot;code&quot;: &quot;verified&quot;,
            &quot;display&quot;: &quot;verified&quot;
          }
        } ],
        &quot;type&quot;: {
          &quot;coding&quot;: [ {
            &quot;system&quot;: &quot;http://terminology.hl7.org/CodeSystem/v2-0203&quot;,
            &quot;code&quot;: &quot;NI&quot;
          } ],
          &quot;text&quot;: &quot;IHI&quot;
        },
        &quot;system&quot;: &quot;http://ns.electronichealth.net.au/id/hi/ihi/1.0&quot;,
        &quot;value&quot;: &quot;8003608000311662&quot;
      }, {
        &quot;type&quot;: {
          &quot;coding&quot;: [ {
            &quot;system&quot;: &quot;http://terminology.hl7.org/CodeSystem/v2-0203&quot;,
            &quot;code&quot;: &quot;MC&quot;
          } ],
          &quot;text&quot;: &quot;Medicare Number&quot;
        },
        &quot;system&quot;: &quot;http://ns.electronichealth.net.au/id/medicare-number&quot;,
        &quot;value&quot;: &quot;69518252411&quot;
      } ],
      &quot;name&quot;: [ {
        &quot;use&quot;: &quot;official&quot;,
        &quot;family&quot;: &quot;BARATZ&quot;,
        &quot;given&quot;: [ &quot;Toni&quot; ]
      } ],
      &quot;telecom&quot;: [ {
        &quot;system&quot;: &quot;phone&quot;,
        &quot;value&quot;: &quot;0870101270&quot;,
        &quot;use&quot;: &quot;home&quot;
      }, {
        &quot;system&quot;: &quot;phone&quot;,
        &quot;value&quot;: &quot;0491570156&quot;,
        &quot;use&quot;: &quot;mobile&quot;
      }, {
        &quot;system&quot;: &quot;phone&quot;,
        &quot;value&quot;: &quot;0870108006&quot;,
        &quot;use&quot;: &quot;work&quot;
      } ],
      &quot;gender&quot;: &quot;female&quot;,
      &quot;birthDate&quot;: &quot;1978-06-16&quot;,
      &quot;address&quot;: [ {
        &quot;line&quot;: [ &quot;24 Law Cir&quot; ],
        &quot;city&quot;: &quot;Bassendean&quot;,
        &quot;state&quot;: &quot;WA&quot;,
        &quot;postalCode&quot;: &quot;6054&quot;,
        &quot;country&quot;: &quot;AU&quot;
      } ]
    },
    &quot;search&quot;: {
      &quot;mode&quot;: &quot;match&quot;
    }
  } ]
}             
</code></pre>
<h5 id="test-data">Test Data</h5>
<p>See: <a href="https://rob-ferguson.me/hapi-fhir-and-au-core-test-data/">HAPI FHIR and AU Core Test Data</a></p>
<h5 id="source-code">Source Code</h5>
<ul>
<li>GitHub: <a href="https://github.com/Robinyo/hapi-fhir-au/?ref=rob-ferguson">HAPI FHIR AU with Auth Starter Project</a></li>
</ul>
<h5 id="references">References</h5>
<h6 id="system-hardening">System Hardening</h6>
<ul>
<li>Australian Signals Directorate: <a href="https://www.cyber.gov.au/resources-business-and-government/maintaining-devices-and-systems/system-hardening-and-administration/web-hardening/implementing-certificates-tls-https-and-opportunistic-tls?ref=rob-ferguson">Implementing Certificates, TLS, HTTPS and Opportunistic TLS</a></li>
<li>Cloudflare docs: <a href="https://developers.cloudflare.com/ssl/edge-certificates/additional-options/cipher-suites/recommendations/?ref=rob-ferguson">Cipher suites recommendations</a></li>
</ul>
<h6 id="hl7">HL7</h6>
<ul>
<li>HL7: <a href="https://www.hl7.org/fhir/implementationguide.html?ref=rob-ferguson">Implementation Guide</a></li>
<li>HL7: <a href="https://hl7.org/fhir/packages.html?ref=rob-ferguson">FHIR NPM Packages</a></li>
<li>AU Core: <a href="https://hl7.org.au/fhir/core/history.html?ref=rob-ferguson">Publication (Version) History</a></li>
<li>AU Core FHIR Implementation Guide: <a href="https://hl7.org.au/fhir/core/1.0.0-preview/index.html?ref=rob-ferguson">AU Core - 1.0.0-preview</a></li>
<li>AU Core FHIR Implementation Guide: <a href="https://confluence.hl7.org/display/HAFWG/AU+Core+FHIR+IG+Testing+FAQs?ref=rob-ferguson">Testing FAQs</a></li>
<li>Sparked AU Core Test Data: <a href="https://github.com/hl7au/au-fhir-test-data/blob/master/Postman/Sparked%20AUCore%20Test%20Data.postman_collection.json?ref=rob-ferguson">Postman collection</a></li>
</ul>
<h6 id="smart-on-fhir">SMART on FHIR</h6>
<ul>
<li>HL7: <a href="https://build.fhir.org/ig/HL7/smart-app-launch/?ref=rob-ferguson">SMART App Launch</a></li>
<li>SMART Health IT: <a href="https://docs.smarthealthit.org/?ref=rob-ferguson">SMART on FHIR</a></li>
</ul>
<h6 id="smart-on-fhirstandalone-launch">SMART on FHIR - Standalone Launch</h6>
<ul>
<li>Project Alvearie: <a href="https://alvearie.io/blog/smart-keycloak/?ref=rob-ferguson">SMART App Launch</a></li>
<li>Project Alvearie: <a href="https://github.com/Alvearie/keycloak-extensions-for-fhir?ref=rob-ferguson">Keycloak extensions for FHIR</a></li>
<li>Keycloak extensions for FHIR: <a href="https://github.com/Alvearie/keycloak-extensions-for-fhir/issues/64?ref=rob-ferguson">Upgrade to the Quarkus-based distribution</a></li>
<li>Keycloak discussion: <a href="https://github.com/keycloak/keycloak/discussions/10303?ref=rob-ferguson">Fine grained scope consent management</a></li>
</ul>
<h6 id="smart-on-fhirehr-launch">SMART on FHIR - EHR Launch</h6>
<ul>
<li>GitHub: <a href="https://github.com/zedwerks/keycloak-smart-fhir?ref=rob-ferguson">Zedwerks - Keycloak extensions for FHIR</a></li>
</ul>
<h6 id="keycloak">Keycloak</h6>
<ul>
<li>Keycloak docs: <a href="https://www.keycloak.org/server/configuration-production?ref=rob-ferguson">Configuring Keycloak for production</a></li>
<li>Keycloak docs: <a href="https://www.keycloak.org/server/enabletls?ref=rob-ferguson">Configuring TLS</a></li>
<li>Keycloak docs: <a href="https://www.keycloak.org/server/keycloak-truststore?ref=rob-ferguson">Configuring trusted certificates</a></li>
<li>Keycloak docs: <a href="https://www.keycloak.org/server/hostname?ref=rob-ferguson">Configuring the hostname</a></li>
<li>Keycloak docs: <a href="https://www.keycloak.org/server/reverseproxy?ref=rob-ferguson">Using a reverse proxy</a></li>
<li>Keycloak docs: <a href="https://www.keycloak.org/server/containers?ref=rob-ferguson">Running Keycloak in a container</a></li>
<li>Keycloak docs: <a href="https://www.keycloak.org/migration/migrating-to-quarkus?ref=rob-ferguson">Migrating to the Quarkus distribution</a></li>
<li>Keycloak docs: <a href="https://www.keycloak.org/docs/latest/upgrading/?ref=rob-ferguson">Upgrading Guide - 26.1.0</a></li>
<li>Keycloak docs: <a href="https://www.keycloak.org/docs/latest/authorization_services/index.html?ref=rob-ferguson">Authorization Services Guide</a></li>
</ul>
<h6 id="keycloak-based-development">Keycloak-based  Development</h6>
<ul>
<li>GitHub: <a href="https://github.com/thomasdarimont/keycloak-project-example?ref=rob-ferguson">Keycloak Project Example</a></li>
<li>GitHub: <a href="https://github.com/thomasdarimont/awesome-keycloak?ref=rob-ferguson">Awesome Keycloak</a></li>
</ul>
<h6 id="keycloak-support">Keycloak Support</h6>
<ul>
<li>Google Group: <a href="https://groups.google.com/g/keycloak-user?ref=rob-ferguson">Keycloak User</a></li>
<li>Google Group: <a href="https://groups.google.com/g/keycloak-dev?ref=rob-ferguson">Keycloak Dev</a></li>
</ul>
<h6 id="apisix">APISIX</h6>
<ul>
<li>APISIX docs: <a href="https://apisix.apache.org/docs/apisix/deployment-modes/?ref=rob-ferguson#standalone">Deployment modes</a></li>
<li>APISIX docs: <a href="https://apisix.apache.org/docs/apisix/ssl-protocol/?ref=rob-ferguson">SSL Protocol</a></li>
<li>APISIX docs: <a href="https://apisix.apache.org/docs/apisix/certificate/?ref=rob-ferguson">Certificate</a></li>
<li>APISIX docs: <a href="https://apisix.apache.org/docs/apisix/plugins/openid-connect/?ref=rob-ferguson">Plugins - OpenID Connect</a></li>
</ul>
<h6 id="hapi-fhir">HAPI FHIR</h6>
<ul>
<li>HAPI FHIR: <a href="https://hapifhir.io/?ref=rob-ferguson">Website</a></li>
<li>HAPI FHIR: <a href="https://hapifhir.io/hapi-fhir/docs/?ref=rob-ferguson">Documentation</a></li>
<li>Google Group: <a href="https://groups.google.com/g/hapi-fhir?ref=rob-ferguson">HAPI FHIR</a></li>
</ul>
<!--kg-card-end: markdown-->]]></content:encoded></item><item><title><![CDATA[Add support for SMART on FHIR to HAPI FHIR with APISIX and Keycloak]]></title><description><![CDATA[<!--kg-card-begin: markdown--><h3 id="introduction">Introduction</h3>
<p>In a previous <a href="https://rob-ferguson.me/add-authn-to-hapi-fhir-with-apisix-and-keycloak/">post</a>, I wrote about the steps I followed to add Authentication (AuthN) to HAPI FHIR by utilising APISIX and Keycloak.</p>
<p>In this post we are going to look at adding support for SMART on FHIR to HAPI FHIR.</p>
<h4 id="smart-on-fhir">SMART on FHIR</h4>
<p>SMART on FHIR (Substitutable Medical</p>]]></description><link>https://rob-ferguson.me/add-authz-to-hapi-fhir-with-apisix-and-keycloak/</link><guid isPermaLink="false">68300e4e352fee04fcab1b4b</guid><category><![CDATA[SMART on FHIR]]></category><category><![CDATA[Authorization]]></category><category><![CDATA[openid-connect]]></category><category><![CDATA[Plugin]]></category><category><![CDATA[APISIX]]></category><category><![CDATA[FHIR]]></category><category><![CDATA[HAPI FHIR]]></category><category><![CDATA[Keycloak]]></category><category><![CDATA[AuthZ]]></category><dc:creator><![CDATA[Rob Ferguson]]></dc:creator><pubDate>Fri, 23 May 2025 07:14:38 GMT</pubDate><media:content url="https://rob-ferguson.me/content/images/2025/05/asf_logo_wide_small-1.png" medium="image"/><content:encoded><![CDATA[<!--kg-card-begin: markdown--><h3 id="introduction">Introduction</h3>
<img src="https://rob-ferguson.me/content/images/2025/05/asf_logo_wide_small-1.png" alt="Add support for SMART on FHIR to HAPI FHIR with APISIX and Keycloak"><p>In a previous <a href="https://rob-ferguson.me/add-authn-to-hapi-fhir-with-apisix-and-keycloak/">post</a>, I wrote about the steps I followed to add Authentication (AuthN) to HAPI FHIR by utilising APISIX and Keycloak.</p>
<p>In this post we are going to look at adding support for SMART on FHIR to HAPI FHIR.</p>
<h4 id="smart-on-fhir">SMART on FHIR</h4>
<p>SMART on FHIR (Substitutable Medical Applications and Reusable Technologies on FHIR) is a healthcare standard that promotes interoperability between client applications and FHIR-enabled systems.</p>
<h5 id="smart-on-fhir-scopes">SMART on FHIR Scopes</h5>
<p>SMART on FHIR defines OAuth 2.0 <a href="https://build.fhir.org/ig/HL7/smart-app-launch/scopes-and-launch-context.html?ref=rob-ferguson">scopes</a> that allow client applications to request a specific set of access rights. The client conveys this information to the authorization server in the form of a &apos;scope&apos; request parameter.</p>
<p>For example:</p>
<ul>
<li><code>system/Patient.read</code></li>
<li><code>system/Patient.write</code></li>
</ul>
<h4 id="apisix">APISIX</h4>
<h5 id="openid-connect-plugin">OpenID Connect Plugin</h5>
<p>We can configure the OpenID Connect plugin&apos;s <code>required_scopes</code> attribute to require one or more scopes.</p>
<p>For example:</p>
<pre><code>
  ...

  - name: hapi-fhir-api
    uri: /fhir/Patient*
    methods: [ &quot;GET&quot; ]    
    upstream_id: 1
    plugins:
      openid-connect:
        bearer_only: true
        client_id: ${{CLIENT_ID}}
        client_secret: ${{CLIENT_SECRET}}
        discovery: ${{PROTOCOL}}://${{KEYCLOAK_HOSTNAME}}:${{KEYCLOAK_PORT}}/realms/${{KEYCLOAK_REALM}}/.well-known/openid-configuration
        required_scopes: [ &quot;system/Patient.read&quot; ]
</code></pre>
<p>See: <a href="https://github.com/Robinyo/hapi-fhir-au/blob/main/backend/services/apisix/conf/apisix-standalone.yml?ref=rob-ferguson">apisix-standalone.yml</a></p>
<p>APISIX will call the token introspection endpoint and check that the Access Token includes the required scopes.</p>
<h4 id="keycloak">Keycloak</h4>
<h5 id="request-a-token">Request a token</h5>
<p>To access the HAPI FHIR API, you must first request an access token. You will need to POST to the token URL.</p>
<p>For example (<code>scope=system/Patient.read</code>):</p>
<pre><code>ACCESS_TOKEN=$(curl -s -X POST https://keycloak.au.localhost:8443/realms/hapi-fhir-dev/protocol/openid-connect/token \
  -H &apos;content-type: application/x-www-form-urlencoded&apos; \
  -d grant_type=client_credentials \
  -d client_id=oauth2-proxy \
  -d client_secret=aHkRec1BYkfaKgMg164JmvKu8u9iWNHM \
  -d scope=system/Patient.read | (jq -r &apos;.access_token&apos;))
                 
# echo &quot;$ACCESS_TOKEN&quot;                 
</code></pre>
<p><strong>Note:</strong> You can use <a href="https://jwt.io/?ref=rob-ferguson">jwt.io</a> to decode the access token.</p>
<h5 id="introspect-a-token">Introspect a token</h5>
<p>To introspect an Access Token you will need to POST to the introspect URL.</p>
<p>For example:</p>
<pre><code>curl -X POST &quot;https://keycloak.au.localhost:8443/realms/hapi-fhir-dev/protocol/openid-connect/token/introspect&quot; \
  -H &apos;content-type: application/x-www-form-urlencoded&apos; \
  -d client_id=oauth2-proxy \
  -d client_secret=aHkRec1BYkfaKgMg164JmvKu8u9iWNHM \
  -d &quot;token_type_hint=access_token&amp;token=$ACCESS_TOKEN&quot;
</code></pre>
<h5 id="client-scopes">Client Scopes</h5>
<p>In Keycloak, an OAuth 2.0 scope is mapped to a <strong>client scope</strong>.</p>
<p>See: <a href="https://rob-ferguson.me/add-authz-to-hapi-fhir-part-2/">Keycloak - Client Scopes</a></p>
<h4 id="hapi-fhir">HAPI FHIR</h4>
<h5 id="call-the-api">Call the API</h5>
<p>To call the HAPI FHIR API, a client application must pass an access token as a Bearer token in the Authorization header of your HTTP request.</p>
<p>For example:</p>
<pre><code>curl -X GET https://hapi-fhir.au.localhost/fhir/Patient?_id=baratz-toni \
  -H &apos;Content-Type: application/fhir+json&apos; \
  -H &quot;Authorization: Bearer $ACCESS_TOKEN&quot;
</code></pre>
<p>You should see something like:</p>
<pre><code>{
  &quot;resourceType&quot;: &quot;Bundle&quot;,
  &quot;id&quot;: &quot;9d80c83a-0b06-4b78-bce2-21e6666348d8&quot;,
  &quot;meta&quot;: {
    &quot;lastUpdated&quot;: &quot;2025-05-23T05:43:01.959+00:00&quot;
  },
  &quot;type&quot;: &quot;searchset&quot;,
  &quot;total&quot;: 1,
  &quot;link&quot;: [ {
    &quot;relation&quot;: &quot;self&quot;,
    &quot;url&quot;: &quot;https://hapi-fhir.au.localhost/fhir/Patient?_id=baratz-toni&quot;
  } ],
  &quot;entry&quot;: [ {
    &quot;fullUrl&quot;: &quot;https://hapi-fhir.au.localhost/fhir/Patient/baratz-toni&quot;,
    &quot;resource&quot;: {
      &quot;resourceType&quot;: &quot;Patient&quot;,
      &quot;id&quot;: &quot;baratz-toni&quot;,
      &quot;meta&quot;: {
        &quot;versionId&quot;: &quot;1&quot;,
        &quot;lastUpdated&quot;: &quot;2025-05-23T05:42:36.551+00:00&quot;,
        &quot;source&quot;: &quot;#rKTCeZfmnReSjm8X&quot;,
        &quot;profile&quot;: [ &quot;http://hl7.org.au/fhir/core/StructureDefinition/au-core-patient&quot; ]
      },
      &quot;extension&quot;: [ {
        &quot;url&quot;: &quot;http://hl7.org.au/fhir/StructureDefinition/indigenous-status&quot;,
        &quot;valueCoding&quot;: {
          &quot;system&quot;: &quot;https://healthterminologies.gov.au/fhir/CodeSystem/australian-indigenous-status-1&quot;,
          &quot;code&quot;: &quot;1&quot;,
          &quot;display&quot;: &quot;Aboriginal but not Torres Strait Islander origin&quot;
        }
      }, {
        &quot;url&quot;: &quot;http://hl7.org/fhir/StructureDefinition/individual-genderIdentity&quot;,
        &quot;extension&quot;: [ {
          &quot;url&quot;: &quot;value&quot;,
          &quot;valueCodeableConcept&quot;: {
            &quot;coding&quot;: [ {
              &quot;system&quot;: &quot;http://snomed.info/sct&quot;,
              &quot;code&quot;: &quot;446141000124107&quot;,
              &quot;display&quot;: &quot;Identifies as female gender&quot;
            } ]
          }
        } ]
      }, {
        &quot;url&quot;: &quot;http://hl7.org/fhir/StructureDefinition/individual-pronouns&quot;,
        &quot;extension&quot;: [ {
          &quot;url&quot;: &quot;value&quot;,
          &quot;valueCodeableConcept&quot;: {
            &quot;coding&quot;: [ {
              &quot;system&quot;: &quot;http://loinc.org&quot;,
              &quot;code&quot;: &quot;LA29519-8&quot;,
              &quot;display&quot;: &quot;she/her/her/hers/herself&quot;
            } ]
          }
        } ]
      }, {
        &quot;url&quot;: &quot;http://hl7.org/fhir/StructureDefinition/individual-recordedSexOrGender&quot;,
        &quot;extension&quot;: [ {
          &quot;url&quot;: &quot;type&quot;,
          &quot;valueCodeableConcept&quot;: {
            &quot;coding&quot;: [ {
              &quot;system&quot;: &quot;http://snomed.info/sct&quot;,
              &quot;code&quot;: &quot;1515311000168102&quot;,
              &quot;display&quot;: &quot;Biological sex at birth&quot;
            } ]
          }
        }, {
          &quot;url&quot;: &quot;value&quot;,
          &quot;valueCodeableConcept&quot;: {
            &quot;coding&quot;: [ {
              &quot;system&quot;: &quot;http://snomed.info/sct&quot;,
              &quot;code&quot;: &quot;248152002&quot;,
              &quot;display&quot;: &quot;Female&quot;
            } ]
          }
        } ]
      } ],
      &quot;identifier&quot;: [ {
        &quot;extension&quot;: [ {
          &quot;url&quot;: &quot;http://hl7.org.au/fhir/StructureDefinition/ihi-status&quot;,
          &quot;valueCoding&quot;: {
            &quot;system&quot;: &quot;https://healthterminologies.gov.au/fhir/CodeSystem/ihi-status-1&quot;,
            &quot;code&quot;: &quot;active&quot;
          }
        }, {
          &quot;url&quot;: &quot;http://hl7.org.au/fhir/StructureDefinition/ihi-record-status&quot;,
          &quot;valueCoding&quot;: {
            &quot;system&quot;: &quot;https://healthterminologies.gov.au/fhir/CodeSystem/ihi-record-status-1&quot;,
            &quot;code&quot;: &quot;verified&quot;,
            &quot;display&quot;: &quot;verified&quot;
          }
        } ],
        &quot;type&quot;: {
          &quot;coding&quot;: [ {
            &quot;system&quot;: &quot;http://terminology.hl7.org/CodeSystem/v2-0203&quot;,
            &quot;code&quot;: &quot;NI&quot;
          } ],
          &quot;text&quot;: &quot;IHI&quot;
        },
        &quot;system&quot;: &quot;http://ns.electronichealth.net.au/id/hi/ihi/1.0&quot;,
        &quot;value&quot;: &quot;8003608000311662&quot;
      }, {
        &quot;type&quot;: {
          &quot;coding&quot;: [ {
            &quot;system&quot;: &quot;http://terminology.hl7.org/CodeSystem/v2-0203&quot;,
            &quot;code&quot;: &quot;MC&quot;
          } ],
          &quot;text&quot;: &quot;Medicare Number&quot;
        },
        &quot;system&quot;: &quot;http://ns.electronichealth.net.au/id/medicare-number&quot;,
        &quot;value&quot;: &quot;69518252411&quot;
      } ],
      &quot;name&quot;: [ {
        &quot;use&quot;: &quot;official&quot;,
        &quot;family&quot;: &quot;BARATZ&quot;,
        &quot;given&quot;: [ &quot;Toni&quot; ]
      } ],
      &quot;telecom&quot;: [ {
        &quot;system&quot;: &quot;phone&quot;,
        &quot;value&quot;: &quot;0870101270&quot;,
        &quot;use&quot;: &quot;home&quot;
      }, {
        &quot;system&quot;: &quot;phone&quot;,
        &quot;value&quot;: &quot;0491570156&quot;,
        &quot;use&quot;: &quot;mobile&quot;
      }, {
        &quot;system&quot;: &quot;phone&quot;,
        &quot;value&quot;: &quot;0870108006&quot;,
        &quot;use&quot;: &quot;work&quot;
      } ],
      &quot;gender&quot;: &quot;female&quot;,
      &quot;birthDate&quot;: &quot;1978-06-16&quot;,
      &quot;address&quot;: [ {
        &quot;line&quot;: [ &quot;24 Law Cir&quot; ],
        &quot;city&quot;: &quot;Bassendean&quot;,
        &quot;state&quot;: &quot;WA&quot;,
        &quot;postalCode&quot;: &quot;6054&quot;,
        &quot;country&quot;: &quot;AU&quot;
      } ]
    },
    &quot;search&quot;: {
      &quot;mode&quot;: &quot;match&quot;
    }
  } ]
}             
</code></pre>
<h5 id="source-code">Source Code</h5>
<ul>
<li>GitHub: <a href="https://github.com/Robinyo/hapi-fhir-au/?ref=rob-ferguson">HAPI FHIR AU with Auth Starter Project</a></li>
</ul>
<h3 id="whats-next">What&apos;s Next</h3>
<p>In the next <a href="https://rob-ferguson.me/getting-started-with-the-apisix-authz-keycloak-plugin/">post</a>, we&apos;ll take a look at APISIX&apos;s <code>authz-keycloak</code> plugin.</p>
<h5 id="references">References</h5>
<h6 id="system-hardening">System Hardening</h6>
<ul>
<li>Australian Signals Directorate: <a href="https://www.cyber.gov.au/resources-business-and-government/maintaining-devices-and-systems/system-hardening-and-administration/web-hardening/implementing-certificates-tls-https-and-opportunistic-tls?ref=rob-ferguson">Implementing Certificates, TLS, HTTPS and Opportunistic TLS</a></li>
<li>Cloudflare docs: <a href="https://developers.cloudflare.com/ssl/edge-certificates/additional-options/cipher-suites/recommendations/?ref=rob-ferguson">Cipher suites recommendations</a></li>
</ul>
<h6 id="hl7">HL7</h6>
<ul>
<li>HL7: <a href="https://www.hl7.org/fhir/implementationguide.html?ref=rob-ferguson">Implementation Guide</a></li>
<li>HL7: <a href="https://hl7.org/fhir/packages.html?ref=rob-ferguson">FHIR NPM Packages</a></li>
<li>AU Core: <a href="https://hl7.org.au/fhir/core/history.html?ref=rob-ferguson">Publication (Version) History</a></li>
<li>AU Core FHIR Implementation Guide: <a href="https://hl7.org.au/fhir/core/1.0.0-preview/index.html?ref=rob-ferguson">AU Core - 1.0.0-preview</a></li>
<li>AU Core FHIR Implementation Guide: <a href="https://confluence.hl7.org/display/HAFWG/AU+Core+FHIR+IG+Testing+FAQs?ref=rob-ferguson">Testing FAQs</a></li>
<li>Sparked AU Core Test Data <a href="https://github.com/hl7au/au-fhir-test-data/blob/master/Postman/Sparked%20AUCore%20Test%20Data.postman_collection.json?ref=rob-ferguson">Postman collection</a></li>
</ul>
<h6 id="smart-on-fhir">SMART on FHIR</h6>
<ul>
<li>HL7: <a href="https://build.fhir.org/ig/HL7/smart-app-launch/?ref=rob-ferguson">SMART App Launch</a></li>
<li>SMART Health IT: <a href="https://docs.smarthealthit.org/?ref=rob-ferguson">SMART on FHIR</a></li>
</ul>
<h6 id="smart-on-fhirstandalone-launch">SMART on FHIR - Standalone Launch</h6>
<ul>
<li>Project Alvearie: <a href="https://alvearie.io/blog/smart-keycloak/?ref=rob-ferguson">SMART App Launch</a></li>
<li>Project Alvearie: <a href="https://github.com/Alvearie/keycloak-extensions-for-fhir?ref=rob-ferguson">Keycloak extensions for FHIR</a></li>
<li>Keycloak extensions for FHIR: <a href="https://github.com/Alvearie/keycloak-extensions-for-fhir/issues/64?ref=rob-ferguson">Upgrade to the Quarkus-based distribution</a></li>
<li>Keycloak discussion: <a href="https://github.com/keycloak/keycloak/discussions/10303?ref=rob-ferguson">Fine grained scope consent management</a></li>
</ul>
<h6 id="smart-on-fhirehr-launch">SMART on FHIR - EHR Launch</h6>
<ul>
<li>GitHub: <a href="https://github.com/zedwerks/keycloak-smart-fhir?ref=rob-ferguson">Zedwerks - Keycloak extensions for FHIR</a></li>
</ul>
<h6 id="keycloak">Keycloak</h6>
<ul>
<li>Keycloak docs: <a href="https://www.keycloak.org/server/configuration-production?ref=rob-ferguson">Configuring Keycloak for production</a></li>
<li>Keycloak docs: <a href="https://www.keycloak.org/server/enabletls?ref=rob-ferguson">Configuring TLS</a></li>
<li>Keycloak docs: <a href="https://www.keycloak.org/server/keycloak-truststore?ref=rob-ferguson">Configuring trusted certificates</a></li>
<li>Keycloak docs: <a href="https://www.keycloak.org/server/hostname?ref=rob-ferguson">Configuring the hostname</a></li>
<li>Keycloak docs: <a href="https://www.keycloak.org/server/reverseproxy?ref=rob-ferguson">Using a reverse proxy</a></li>
<li>Keycloak docs: <a href="https://www.keycloak.org/server/containers?ref=rob-ferguson">Running Keycloak in a container</a></li>
<li>Keycloak docs: <a href="https://www.keycloak.org/migration/migrating-to-quarkus?ref=rob-ferguson">Migrating to the Quarkus distribution</a></li>
<li>Keycloak docs: <a href="https://www.keycloak.org/docs/latest/upgrading/?ref=rob-ferguson">Upgrading Guide - 26.1.0</a></li>
<li>Keycloak docs: <a href="https://www.keycloak.org/docs/latest/authorization_services/index.html?ref=rob-ferguson">Authorization Services Guide</a></li>
</ul>
<h6 id="keycloak-based-development">Keycloak-based  Development</h6>
<ul>
<li>GitHub: <a href="https://github.com/thomasdarimont/keycloak-project-example?ref=rob-ferguson">Keycloak Project Example</a></li>
<li>GitHub: <a href="https://github.com/thomasdarimont/awesome-keycloak?ref=rob-ferguson">Awesome Keycloak</a></li>
</ul>
<h6 id="keycloak-support">Keycloak Support</h6>
<ul>
<li>Google Group: <a href="https://groups.google.com/g/keycloak-user?ref=rob-ferguson">Keycloak User</a></li>
<li>Google Group: <a href="https://groups.google.com/g/keycloak-dev?ref=rob-ferguson">Keycloak Dev</a></li>
</ul>
<h6 id="apisix">APISIX</h6>
<ul>
<li>APISIX docs: <a href="https://apisix.apache.org/docs/apisix/deployment-modes/?ref=rob-ferguson#standalone">Deployment modes</a></li>
<li>APISIX docs: <a href="https://apisix.apache.org/docs/apisix/ssl-protocol/?ref=rob-ferguson">SSL Protocol</a></li>
<li>APISIX docs: <a href="https://apisix.apache.org/docs/apisix/certificate/?ref=rob-ferguson">Certificate</a></li>
<li>APISIX docs: <a href="https://apisix.apache.org/docs/apisix/plugins/openid-connect/?ref=rob-ferguson">Plugins - OpenID Connect</a></li>
</ul>
<h6 id="hapi-fhir">HAPI FHIR</h6>
<ul>
<li>HAPI FHIR: <a href="https://hapifhir.io/?ref=rob-ferguson">Website</a></li>
<li>HAPI FHIR: <a href="https://hapifhir.io/hapi-fhir/docs/?ref=rob-ferguson">Documentation</a></li>
<li>Google Group: <a href="https://groups.google.com/g/hapi-fhir?ref=rob-ferguson">HAPI FHIR</a></li>
</ul>
<!--kg-card-end: markdown-->]]></content:encoded></item><item><title><![CDATA[Add AuthN to HAPI FHIR with APISIX and Keycloak]]></title><description><![CDATA[<!--kg-card-begin: markdown--><h3 id="introduction">Introduction</h3>
<p>In a previous <a href="https://rob-ferguson.me/add-authn-to-hapi-fhir-with-oauth2-proxy-nginx-and-keycloak-part-1/">post</a>, I wrote about the steps I followed to add Authentication (AuthN) to HAPI FHIR by utilising OAuth2 Proxy, Nginx and Keycloak</p>
<p>In this post, we&apos;ll look at an alternate solution that replaces OAuth2 Proxy and Nginx with APISIX.</p>
<h4 id="apisix">APISIX</h4>
<p><a href="https://apisix.apache.org/?ref=rob-ferguson">APISIX</a> is an open</p>]]></description><link>https://rob-ferguson.me/add-authn-to-hapi-fhir-with-apisix-and-keycloak/</link><guid isPermaLink="false">682d6f12352fee04fcab19d1</guid><category><![CDATA[APISIX]]></category><category><![CDATA[Docker Compose]]></category><category><![CDATA[FHIR]]></category><category><![CDATA[HAPI FHIR]]></category><category><![CDATA[Authentication]]></category><category><![CDATA[Keycloak]]></category><category><![CDATA[AuthN]]></category><dc:creator><![CDATA[Rob Ferguson]]></dc:creator><pubDate>Wed, 21 May 2025 07:55:23 GMT</pubDate><media:content url="https://rob-ferguson.me/content/images/2025/05/asf_logo_wide_small.png" medium="image"/><content:encoded><![CDATA[<!--kg-card-begin: markdown--><h3 id="introduction">Introduction</h3>
<img src="https://rob-ferguson.me/content/images/2025/05/asf_logo_wide_small.png" alt="Add AuthN to HAPI FHIR with APISIX and Keycloak"><p>In a previous <a href="https://rob-ferguson.me/add-authn-to-hapi-fhir-with-oauth2-proxy-nginx-and-keycloak-part-1/">post</a>, I wrote about the steps I followed to add Authentication (AuthN) to HAPI FHIR by utilising OAuth2 Proxy, Nginx and Keycloak</p>
<p>In this post, we&apos;ll look at an alternate solution that replaces OAuth2 Proxy and Nginx with APISIX.</p>
<h4 id="apisix">APISIX</h4>
<p><a href="https://apisix.apache.org/?ref=rob-ferguson">APISIX</a> 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.</p>
<h5 id="docker-compose">Docker Compose</h5>
<p>Let&apos;s start by running APISIX in &apos;standalone&apos; mode:</p>
<pre><code>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:
      - &apos;${PWD}/services/apisix/conf/config-standalone.yml:/usr/local/apisix/conf/config.yaml&apos;
      - &apos;${PWD}/services/apisix/conf/apisix-standalone.yml:/usr/local/apisix/conf/apisix.yaml&apos;      
      - &apos;${PWD}/certs/cert.pem:/usr/local/apisix/conf/cert/cert.pem&apos;
      - &apos;${PWD}/certs/key.pem:/usr/local/apisix/conf/cert/key.pem&apos;      

    ...
      
</code></pre>
<p>See: <a href="https://github.com/Robinyo/hapi-fhir-au/blob/main/backend/docker-compose-apisix.yml?ref=rob-ferguson">docker-compose-apisix.yml</a></p>
<p>In standalone mode only the APISIX data plane is deployed and settings are loaded from a YAML configuration file:</p>
<pre><code>apisix:
  ...
  
deployment:
  role: data_plane
  role_data_plane:
    config_provider: yaml
</code></pre>
<p>See: <a href="https://github.com/Robinyo/hapi-fhir-au/blob/main/backend/services/apisix/conf/config-standalone.yml?ref=rob-ferguson">config-standalone.yml</a></p>
<p>Routes are declared in a file named <code>apisix.yaml (apisix-standalone.yml)</code>:</p>
<pre><code>routes:
  - uri: /*
    host: hapi-fhir.au.localhost
    upstream:
      nodes:
        &quot;hapi-fhir:8080&quot;: 1
      type: roundrobin

    ...
    
#END 
</code></pre>
<p>See: <a href="https://github.com/Robinyo/hapi-fhir-au/blob/main/backend/services/apisix/conf/apisix-standalone.yml?ref=rob-ferguson">apisix-standalone.yml</a></p>
<p>APISIX will reload the file if it detects any changes.</p>
<h5 id="enable-tls">Enable TLS</h5>
<p>You can enable TLS and ciphers via the the <code>ssl</code> settings in <code>config.yaml (config-standalone.yml)</code>:</p>
<pre><code>apisix:
  node_listen: 9080
  enable_admin: false
  enable_ipv6: false

  ssl:
    enable: true
    listen_port: 9443
    ssl_protocols: &quot;TLSv1.2 TLSv1.3&quot;
    ssl_ciphers: &quot;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&quot;

deployment:
  role: data_plane
  role_data_plane:
    config_provider: yaml
</code></pre>
<p>See: <a href="https://github.com/Robinyo/hapi-fhir-au/blob/main/backend/services/apisix/conf/config-standalone.yml?ref=rob-ferguson">config-standalone.yml</a></p>
<p>And certs via the the <code>ssls</code> settings in <code>apisix.yaml (apisix-standalone.yml)</code>:</p>
<pre><code>routes:
  - uri: /*
    host: hapi-fhir.au.localhost
    upstream:
      nodes:
        &quot;hapi-fhir:8080&quot;: 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:
      - &quot;hapi-fhir.au.localhost&quot;
#END
</code></pre>
<p>See: <a href="https://github.com/Robinyo/hapi-fhir-au/blob/main/backend/services/apisix/conf/apisix-standalone.yml?ref=rob-ferguson">apisix-standalone.yml</a></p>
<h5 id="openid-connect-plugin">OpenID Connect Plugin</h5>
<p>APISIX can intercept requests to your application and redirect them to an Authorisation server that supports OAuth 2.0 and OpenID Connect.</p>
<p>For example:</p>
<pre><code>routes:
  - uri: /*
    host: hapi-fhir.au.localhost
    upstream:
      nodes:
        &quot;hapi-fhir:8080&quot;: 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}}

    ...

</code></pre>
<p>See: <a href="https://github.com/Robinyo/hapi-fhir-au/blob/main/backend/services/apisix/conf/apisix-standalone.yml?ref=rob-ferguson">apisix-standalone.yml</a></p>
<h4 id="hapi-fhir-au-with-auth-starter-project">HAPI FHIR AU with Auth Starter Project</h4>
<p>Follow the steps in the HAPI FHIR AU with Auth Starter Project&apos;s <a href="https://github.com/Robinyo/hapi-fhir-au/?ref=rob-ferguson">Quick Start</a> guide to enable <strong>secure</strong> access to HAPI FHIR.</p>
<p>Navigate to:</p>
<pre><code>https://hapi-fhir.au.localhost
</code></pre>
<p>You should see something like:</p>
<p align="center">
  <img src="https://rob-ferguson.me/content/images/2025/01/keycloak-sign-in.png" alt="Add AuthN to HAPI FHIR with APISIX and Keycloak" class="thumbnail">
</p>
<p>Enter your username (<a href="mailto:hey@rob-ferguson.me?ref=rob-ferguson">hey@rob-ferguson.me</a>) and password (secret), then click the &apos;Sign In&apos; button to sign in using the OpenID Connect (OIDC) Authorization Code flow.</p>
<p><strong>Note:</strong> I followed the steps in Keycloak&apos;s <a href="https://www.keycloak.org/getting-started/getting-started-docker?ref=rob-ferguson">Getting Started with Docker</a> guide to create: a realm; a user; and a client. Keycloak will import the <code>hapi-fhir-dev</code> realm (i.e., development-realm.json) when it starts up.</p>
<p align="center">
  <img src="https://rob-ferguson.me/content/images/2025/01/hapi-fhir-welcome-page-3.png" class="thumbnail" alt="Add AuthN to HAPI FHIR with APISIX and Keycloak">
</p>
<p>Your connection is secure:</p>
<p align="center">
  <img src="https://rob-ferguson.me/content/images/2025/01/your-connection-is-secure.png" class="thumbnail" alt="Add AuthN to HAPI FHIR with APISIX and Keycloak">
</p>
<p>Navigate to the OpenAPI UI for the HAPI FHIR R4 Server:</p>
<pre><code>https://hapi-fhir.au.localhost/fhir
</code></pre>
<p>You should see something like:</p>
<p align="center">
  <img src="https://rob-ferguson.me/content/images/2025/01/hapi-fhir-openapi-ui.png" class="thumbnail" alt="Add AuthN to HAPI FHIR with APISIX and Keycloak">
</p>
<p><strong>Note:</strong> You can override the default FHIR Server Base URL, for example:</p>
<pre><code>hapi:
  fhir:
    server_address: https://hapi-fhir.au.localhost/fhir
</code></pre>
<p>See: <a href="https://github.com/Robinyo/hapi-fhir-au/blob/main/backend/hapi.application.yaml?ref=rob-ferguson">hapi.application.yaml</a></p>
<p>To stop the services:</p>
<pre><code>docker compose -f docker-compose-apisix.yml stop
</code></pre>
<p>To remove the services:</p>
<pre><code>docker compose -f docker-compose-apisix.yml down
</code></pre>
<p>To remove the data volume:</p>
<pre><code>docker volume rm backend_postgres_data
</code></pre>
<h5 id="source-code">Source Code</h5>
<ul>
<li>GitHub: <a href="https://github.com/Robinyo/hapi-fhir-au/?ref=rob-ferguson">HAPI FHIR AU with Auth Starter Project</a></li>
</ul>
<h3 id="whats-next">What&apos;s Next</h3>
<p>In the next <a href="https://rob-ferguson.me/add-authz-to-hapi-fhir-with-apisix-and-keycloak/">post</a>, we&apos;ll take a look at adding support for SMART on FHIR to HAPI FHIR.</p>
<h5 id="source-code">Source Code</h5>
<ul>
<li>GitHub: <a href="https://github.com/Robinyo/hapi-fhir-au/?ref=rob-ferguson">HAPI FHIR AU with Auth Starter Project</a></li>
</ul>
<h5 id="references">References</h5>
<h6 id="system-hardening">System Hardening</h6>
<ul>
<li>Australian Signals Directorate: <a href="https://www.cyber.gov.au/resources-business-and-government/maintaining-devices-and-systems/system-hardening-and-administration/web-hardening/implementing-certificates-tls-https-and-opportunistic-tls?ref=rob-ferguson">Implementing Certificates, TLS, HTTPS and Opportunistic TLS</a></li>
<li>Cloudflare docs: <a href="https://developers.cloudflare.com/ssl/edge-certificates/additional-options/cipher-suites/recommendations/?ref=rob-ferguson">Cipher suites recommendations</a></li>
</ul>
<h6 id="hl7">HL7</h6>
<ul>
<li>HL7: <a href="https://www.hl7.org/fhir/implementationguide.html?ref=rob-ferguson">Implementation Guide</a></li>
<li>HL7: <a href="https://hl7.org/fhir/packages.html?ref=rob-ferguson">FHIR NPM Packages</a></li>
<li>AU Core: <a href="https://hl7.org.au/fhir/core/history.html?ref=rob-ferguson">Publication (Version) History</a></li>
<li>AU Core FHIR Implementation Guide: <a href="https://hl7.org.au/fhir/core/1.0.0-preview/index.html?ref=rob-ferguson">AU Core - 1.0.0-preview</a></li>
<li>AU Core FHIR Implementation Guide: <a href="https://confluence.hl7.org/display/HAFWG/AU+Core+FHIR+IG+Testing+FAQs?ref=rob-ferguson">Testing FAQs</a></li>
<li>Sparked AU Core Test Data <a href="https://github.com/hl7au/au-fhir-test-data/blob/master/Postman/Sparked%20AUCore%20Test%20Data.postman_collection.json?ref=rob-ferguson">Postman collection</a></li>
</ul>
<h6 id="smart-on-fhir">SMART on FHIR</h6>
<ul>
<li>HL7: <a href="https://build.fhir.org/ig/HL7/smart-app-launch/?ref=rob-ferguson">SMART App Launch</a></li>
<li>SMART Health IT: <a href="https://docs.smarthealthit.org/?ref=rob-ferguson">SMART on FHIR</a></li>
</ul>
<h6 id="smart-on-fhirstandalone-launch">SMART on FHIR - Standalone Launch</h6>
<ul>
<li>Project Alvearie: <a href="https://alvearie.io/blog/smart-keycloak/?ref=rob-ferguson">SMART App Launch</a></li>
<li>Project Alvearie: <a href="https://github.com/Alvearie/keycloak-extensions-for-fhir?ref=rob-ferguson">Keycloak extensions for FHIR</a></li>
<li>Keycloak extensions for FHIR: <a href="https://github.com/Alvearie/keycloak-extensions-for-fhir/issues/64?ref=rob-ferguson">Upgrade to the Quarkus-based distribution</a></li>
<li>Keycloak discussion: <a href="https://github.com/keycloak/keycloak/discussions/10303?ref=rob-ferguson">Fine grained scope consent management</a></li>
</ul>
<h6 id="smart-on-fhirehr-launch">SMART on FHIR - EHR Launch</h6>
<ul>
<li>GitHub: <a href="https://github.com/zedwerks/keycloak-smart-fhir?ref=rob-ferguson">Zedwerks - Keycloak extensions for FHIR</a></li>
</ul>
<h6 id="keycloak">Keycloak</h6>
<ul>
<li>Keycloak docs: <a href="https://www.keycloak.org/server/configuration-production?ref=rob-ferguson">Configuring Keycloak for production</a></li>
<li>Keycloak docs: <a href="https://www.keycloak.org/server/enabletls?ref=rob-ferguson">Configuring TLS</a></li>
<li>Keycloak docs: <a href="https://www.keycloak.org/server/keycloak-truststore?ref=rob-ferguson">Configuring trusted certificates</a></li>
<li>Keycloak docs: <a href="https://www.keycloak.org/server/hostname?ref=rob-ferguson">Configuring the hostname</a></li>
<li>Keycloak docs: <a href="https://www.keycloak.org/server/reverseproxy?ref=rob-ferguson">Using a reverse proxy</a></li>
<li>Keycloak docs: <a href="https://www.keycloak.org/server/containers?ref=rob-ferguson">Running Keycloak in a container</a></li>
<li>Keycloak docs: <a href="https://www.keycloak.org/migration/migrating-to-quarkus?ref=rob-ferguson">Migrating to the Quarkus distribution</a></li>
<li>Keycloak docs: <a href="https://www.keycloak.org/docs/latest/upgrading/?ref=rob-ferguson">Upgrading Guide - 26.1.0</a></li>
<li>Keycloak docs: <a href="https://www.keycloak.org/docs/latest/authorization_services/index.html?ref=rob-ferguson">Authorization Services Guide</a></li>
</ul>
<h6 id="keycloak-based-development">Keycloak-based  Development</h6>
<ul>
<li>GitHub: <a href="https://github.com/thomasdarimont/keycloak-project-example?ref=rob-ferguson">Keycloak Project Example</a></li>
<li>GitHub: <a href="https://github.com/thomasdarimont/awesome-keycloak?ref=rob-ferguson">Awesome Keycloak</a></li>
</ul>
<h6 id="keycloak-support">Keycloak Support</h6>
<ul>
<li>Google Group: <a href="https://groups.google.com/g/keycloak-user?ref=rob-ferguson">Keycloak User</a></li>
<li>Google Group: <a href="https://groups.google.com/g/keycloak-dev?ref=rob-ferguson">Keycloak Dev</a></li>
</ul>
<h6 id="apisix">APISIX</h6>
<ul>
<li>APISIX docs: <a href="https://apisix.apache.org/docs/apisix/deployment-modes/?ref=rob-ferguson#standalone">Deployment modes</a></li>
<li>APISIX docs: <a href="https://apisix.apache.org/docs/apisix/ssl-protocol/?ref=rob-ferguson">SSL Protocol</a></li>
<li>APISIX docs: <a href="https://apisix.apache.org/docs/apisix/certificate/?ref=rob-ferguson">Certificate</a></li>
<li>APISIX docs: <a href="https://apisix.apache.org/docs/apisix/plugins/openid-connect/?ref=rob-ferguson">Plugins - OpenID Connect</a></li>
</ul>
<h6 id="hapi-fhir">HAPI FHIR</h6>
<ul>
<li>HAPI FHIR: <a href="https://hapifhir.io/?ref=rob-ferguson">Website</a></li>
<li>HAPI FHIR: <a href="https://hapifhir.io/hapi-fhir/docs/?ref=rob-ferguson">Documentation</a></li>
<li>Google Group: <a href="https://groups.google.com/g/hapi-fhir?ref=rob-ferguson">HAPI FHIR</a></li>
</ul>
<!--kg-card-end: markdown-->]]></content:encoded></item><item><title><![CDATA[Rediscovering Eclipse Papyrus]]></title><description><![CDATA[<!--kg-card-begin: markdown--><p>Papyrus is an Eclipse-based modelling environment that includes diagram editors for UML 2 and SysML as well as CSS stylesheet support.</p>
<p>In this post, we&apos;ll install Papyrus and then create a new modelling project and a <a href="http://en.wikipedia.org/wiki/Object_Modeling_in_Color?ref=rob-ferguson" target="_blank">colourful</a> class diagram.</p>
<h3 id="install-eclipse">Install Eclipse</h3>
<p>First, we need to <a href="https://www.eclipse.org/downloads/packages/?ref=rob-ferguson" target="_blank">download</a> the Eclipse</p>]]></description><link>https://rob-ferguson.me/rediscovering-eclipse-papyrus/</link><guid isPermaLink="false">681a9adc352fee04fcab17b8</guid><category><![CDATA[Eclipse Papyrus]]></category><category><![CDATA[UML]]></category><category><![CDATA[SysML]]></category><category><![CDATA[Tutorial]]></category><dc:creator><![CDATA[Rob Ferguson]]></dc:creator><pubDate>Fri, 09 May 2025 23:10:39 GMT</pubDate><media:content url="https://rob-ferguson.me/content/images/2025/05/uml-modeling.png" medium="image"/><content:encoded><![CDATA[<!--kg-card-begin: markdown--><img src="https://rob-ferguson.me/content/images/2025/05/uml-modeling.png" alt="Rediscovering Eclipse Papyrus"><p>Papyrus is an Eclipse-based modelling environment that includes diagram editors for UML 2 and SysML as well as CSS stylesheet support.</p>
<p>In this post, we&apos;ll install Papyrus and then create a new modelling project and a <a href="http://en.wikipedia.org/wiki/Object_Modeling_in_Color?ref=rob-ferguson" target="_blank">colourful</a> class diagram.</p>
<h3 id="install-eclipse">Install Eclipse</h3>
<p>First, we need to <a href="https://www.eclipse.org/downloads/packages/?ref=rob-ferguson" target="_blank">download</a> the Eclipse Modeling Tools package:</p>
<p align="center">
  <img src="https://rob-ferguson.me/content/images/2025/05/emt-package.png" class="thumbnail" alt="Rediscovering Eclipse Papyrus">
</p>
<p>I downloaded the Eclipse Modeling Tools package (2025&#x2011;03 R) for macOS AArch64:</p>
<pre><code>eclipse-modeling-2025-03-R-macosx-cocoa-aarch64.dmg
</code></pre>
<p>Launch the disk image file (.dmg) and then drag the <code>Eclipse</code> icon into the Applications folder:</p>
<p align="center">
  <img src="https://rob-ferguson.me/content/images/2025/05/eclipse-dmg.png" class="thumbnail" alt="Rediscovering Eclipse Papyrus">
</p>
<h3 id="install-papyrus">Install Papyrus</h3>
<p>Launch Eclipse:</p>
<p align="center">
  <img src="https://rob-ferguson.me/content/images/2025/05/eclipse-splashscreen.png" width="340px" class="thumbnail" alt="Rediscovering Eclipse Papyrus">
</p>
<p>Then go to &apos;Help&apos; =&gt; &apos;Install New Software...&apos;:</p>
<p><img src="https://rob-ferguson.me/content/images/2025/05/eclipse-install-new-software.png" alt="Rediscovering Eclipse Papyrus" loading="lazy"></p>
<p>Click the &apos;Add...&apos; button and add the Eclipse Papyrus-Desktop Update Site:</p>
<pre><code>https://download.eclipse.org/modeling/mdt/papyrus/papyrus-desktop/updates/nightly/master
</code></pre>
<p>For example:</p>
<p align="center">
  <img src="https://rob-ferguson.me/content/images/2025/05/papyrus-update-site.png" class="thumbnail" alt="Rediscovering Eclipse Papyrus">
</p>
<p>Select the options you want to install then click the &apos;Next&apos; button:</p>
<p align="center">
  <img src="https://rob-ferguson.me/content/images/2025/05/eclipse-papyrus-install-options.png" class="thumbnail" alt="Rediscovering Eclipse Papyrus">
</p>
<p>Review the items to be installed then click the &apos;Next&apos; button:</p>
<p align="center">
  <img src="https://rob-ferguson.me/content/images/2025/05/eclipse-papyrus-install-items-1.png" class="thumbnail" alt="Rediscovering Eclipse Papyrus">
</p>
<p>Accept the terms of the license agreement then click the &apos;Finish&apos; button.</p>
<p>When prompted, accept the confirmations and then restart Eclipse.</p>
<h3 id="create-a-new-modelling-project">Create a new Modelling Project</h3>
<p>You can create a new modelling project by choosing &apos;New Papyrus Project&apos; on the Eclipse Welcome page:</p>
<p align="center">
  <img src="https://rob-ferguson.me/content/images/2025/05/eclipse-papyrus-welcome.png" class="thumbnail" alt="Rediscovering Eclipse Papyrus">
</p>
<p>Or by selecting &apos;File&apos; =&gt; &apos;New&apos; =&gt; &apos;Other...&apos; and then choosing &apos;Papyrus&apos; =&gt; &apos;Papyrus Project&apos;:</p>
<p align="center">
  <img src="https://rob-ferguson.me/content/images/2025/05/new-project.png" class="thumbnail" alt="Rediscovering Eclipse Papyrus">
</p>
<p>Select the architecture context(s) and viewpoints to apply to the Papyrus model then click the &apos;Next&apos; button:</p>
<p align="center">
  <img src="https://rob-ferguson.me/content/images/2025/05/architecture-context.png" class="thumbnail" alt="Rediscovering Eclipse Papyrus">
</p>
<p>Enter a Project name and a Model file name then click the &apos;Finish&apos; button:</p>
<p align="center">
  <img src="https://rob-ferguson.me/content/images/2025/05/new-project-name.png" class="thumbnail" alt="Rediscovering Eclipse Papyrus">
</p>
<p>You should see something like:</p>
<p align="center">
  <img src="https://rob-ferguson.me/content/images/2025/05/eclipse-payrus-perspective.png" class="thumbnail" alt="Rediscovering Eclipse Papyrus">
</p>
<p>Let&apos;s start by creating a new Class Diagram, right click on the Model Explorer:</p>
<p align="center">
  <img src="https://rob-ferguson.me/content/images/2025/05/model-explorer-right-click.png" class="thumbnail" alt="Rediscovering Eclipse Papyrus">
</p>
<p>Then choose &apos;New Diagram&apos; =&gt; &apos;Class Diagram&apos; and enter a diagram name then click the &apos;OK&apos; button:</p>
<p align="center">
  <img src="https://rob-ferguson.me/content/images/2025/05/diagram-name.png" class="thumbnail" alt="Rediscovering Eclipse Papyrus">
</p>
<p>The diagram editor includes a palette of nodes and edges:</p>
<p align="center">
  <img src="https://rob-ferguson.me/content/images/2025/05/diagram-editor.png" class="thumbnail" alt="Rediscovering Eclipse Papyrus">
</p>
<p>Let&apos;s start by adding a &apos;Class&apos; node to the diagram, select the &apos;Class&apos; node and then drag and drop it onto the diagram:</p>
<p>
  <img src="https://rob-ferguson.me/content/images/2025/05/class1-2.png" width="140px" class="thumbnail" alt="Rediscovering Eclipse Papyrus">
</p>
<p>Let&apos;s change the new classes name (the diagram editor supports in-place editing or you can use the &apos;Properties&apos; tab):</p>
<p>
  <img src="https://rob-ferguson.me/content/images/2025/05/party-place-thing.png" width="160px" class="thumbnail" alt="Rediscovering Eclipse Papyrus">
</p>
<p>And, right-click to &apos;Format&apos; =&gt; &apos;Fill Color&apos;:</p>
<p>
  <img src="https://rob-ferguson.me/content/images/2025/05/fill-colour.png" width="160px" class="thumbnail" alt="Rediscovering Eclipse Papyrus">
</p>
<p>And, right-click to &apos;Format&apos; =&gt; &apos;Show/Hide Compartments&apos;:</p>
<p align="center">
  <img src="https://rob-ferguson.me/content/images/2025/05/show-hide-compartments.png" class="thumbnail" alt="Rediscovering Eclipse Papyrus">
</p>
<p>Uncheck each option and you should see something like:</p>
<p>
  <img src="https://rob-ferguson.me/content/images/2025/05/ppt-hide-compartments.png" width="160px" class="thumbnail" alt="Rediscovering Eclipse Papyrus">
</p>
<p>Now, before we add a few more &apos;Class&apos; nodes let&apos;s use the &apos;Properties&apos; tab to help us layout the diagram:</p>
<p align="center">
  <img src="https://rob-ferguson.me/content/images/2025/05/rulers-and-grid.png" class="thumbnail" alt="Rediscovering Eclipse Papyrus">
</p>
<p>See, they&apos;re all lined up:</p>
<p>
  <img src="https://rob-ferguson.me/content/images/2025/05/snap-to-grid.png" width="480px" class="thumbnail" alt="Rediscovering Eclipse Papyrus">
</p>
<h5 id="references">References</h5>
<ul>
<li>Eclipse Dev: <a href="https://eclipse.dev/papyrus/?ref=rob-ferguson">Eclipse Papyrus</a></li>
<li>Eclipse Dev: <a href="https://eclipse.dev/papyrus/documentation.html?ref=rob-ferguson">Eclipse Papyrus - Documentation</a></li>
<li>Eclipse Dev: <a href="https://eclipse.dev/papyrus/download.html?ref=rob-ferguson#desktop">Eclipse Papyrus - Downloads</a></li>
</ul>
<!--kg-card-end: markdown-->]]></content:encoded></item><item><title><![CDATA[Serving your Angular frontend from your Spring Boot backend]]></title><description><![CDATA[<!--kg-card-begin: markdown--><h3 id="introduction">Introduction</h3>
<blockquote>
<p>What if your Angular frontend relied on your Spring Boot backend to handle all of its authentication and authorisation responsibilities and API interactions?</p>
</blockquote>
<p>The Backend for Frontend (BFF) design pattern is an architectural approach that a browser-based application can use to handle all of its authentication and authorisation responsibilities</p>]]></description><link>https://rob-ferguson.me/serving-your-angular-frontend-from-your-spring-boot-backend/</link><guid isPermaLink="false">679a98d9352fee04fcab16a2</guid><category><![CDATA[Angular]]></category><category><![CDATA[Spring Boot]]></category><category><![CDATA[Backend for Frontend]]></category><category><![CDATA[BFF]]></category><category><![CDATA[OpenID Connect]]></category><category><![CDATA[OAuth 2.0]]></category><dc:creator><![CDATA[Rob Ferguson]]></dc:creator><pubDate>Thu, 30 Jan 2025 01:11:53 GMT</pubDate><media:content url="https://rob-ferguson.me/content/images/2025/01/web-ui-1.png" medium="image"/><content:encoded><![CDATA[<!--kg-card-begin: markdown--><h3 id="introduction">Introduction</h3>
<blockquote>
<img src="https://rob-ferguson.me/content/images/2025/01/web-ui-1.png" alt="Serving your Angular frontend from your Spring Boot backend"><p>What if your Angular frontend relied on your Spring Boot backend to handle all of its authentication and authorisation responsibilities and API interactions?</p>
</blockquote>
<p>The Backend for Frontend (BFF) design pattern is an architectural approach that a browser-based application can use to handle all of its authentication and authorisation responsibilities and API interactions, for example:</p>
<ul>
<li>
<p>The BFF interacts with an OAuth 2.0 Authorization Server as a confidential OAuth 2.0 Client.</p>
</li>
<li>
<p>The BFF manages OAuth 2.0 access and refresh tokens in the context of a cookie-based session, avoiding the direct exposure of any tokens to the browser-based application.</p>
</li>
<li>
<p>The BFF forwards all requests to a OAuth 2.0 Resource Server, augmenting them with the correct access token before forwarding them to the resource server.</p>
</li>
</ul>
<p>The <a href="https://datatracker.ietf.org/doc/html/draft-ietf-oauth-browser-based-apps?ref=rob-ferguson">OAuth 2.0 for Browser-Based Applications</a>  specification outlines the threats, attack consequences, security considerations and best practices that must be taken into account when developing browser-based applications. It also discusses how different architectural approaches can help address some of these challenges.</p>
<p>In this post we are going to look at some options for working with Angular and Spring Boot.</p>
<p><strong>Note:</strong> At the time of writing, Angular is at version <code>19.x</code> and Spring Boot is at version <code>3.4.2</code>.</p>
<h3 id="spring-boot">Spring Boot</h3>
<h4 id="static-content">Static Content</h4>
<p><a href="https://docs.spring.io/spring-boot/reference/web/servlet.html?ref=rob-ferguson#web.servlet.spring-mvc.static-content">Static content</a> can be served from your Spring Boot application if you place it in the right location. By default, Spring Boot serves static content from resources in the classpath at <code>/static</code>.</p>
<p>For example:</p>
<pre><code> &#x251C;&#x2500;&#x2500; /serendipity
     &#x2514;&#x2500;&#x2500; /modules
        &#x2514;&#x2500;&#x2500; /web-bff
            &#x2514;&#x2500;&#x2500; /src
                &#x2514;&#x2500;&#x2500; /main
                    &#x2514;&#x2500;&#x2500; /java
                    &#x2514;&#x2500;&#x2500; /resources
                        &#x2514;&#x2500;&#x2500; /static
                            &#x251C;&#x2500;&#x2500; 3rdpartylicenses.txt
                            &#x251C;&#x2500;&#x2500; favicon.ico
                            &#x251C;&#x2500;&#x2500; index.html
                            &#x251C;&#x2500;&#x2500; main-2STXNAPZ.js
                            &#x251C;&#x2500;&#x2500; polyfills-EQXJKH7W.js
                            &#x251C;&#x2500;&#x2500; prerendered-routes.json
                            &#x251C;&#x2500;&#x2500; styles-5INURTSO.css
                &#x2514;&#x2500;&#x2500; /test
</code></pre>
<p>The <code>index.html</code> resource is special because, if it exists, it is used as a &apos;welcome page&apos;, which means it is served up as the root (&apos;/&apos;) resource (that is, at <code>http://localhost:8080/</code>).</p>
<p>For example:</p>
<p align="center">
  <img src="https://rob-ferguson.me/content/images/2025/01/web-ui.png" class="thumbnail" alt="Serving your Angular frontend from your Spring Boot backend">
</p>
<h4 id="managing-routes">Managing Routes</h4>
<p>We also need to forward any requests, where the requested path doesn&#x2019;t look like a static file, to Angular.</p>
<p>For example:</p>
<pre><code>@Controller
public class AngularForwardController {

    @GetMapping(&quot;{path:^(?!api|public)[^\\.]*}/**&quot;)
    public String handleForward() {
        return &quot;forward:/&quot;;
    }

}
</code></pre>
<p>Spring Boot handles static files and API requests. Angular handles everything else in the browser.</p>
<h4 id="maven-plugins">Maven Plugins</h4>
<h5 id="maven-resources-plugin">Maven Resources Plugin</h5>
<p>You can use the <a href="https://maven.apache.org/plugins/maven-resources-plugin/?ref=rob-ferguson">maven-resources-plugin</a> to copy your resources:</p>
<pre><code>	&lt;build&gt;
		&lt;plugins&gt;

			&lt;plugin&gt;
				&lt;artifactId&gt;maven-resources-plugin&lt;/artifactId&gt;
				&lt;executions&gt;
					&lt;execution&gt;
						&lt;id&gt;copy-resources&lt;/id&gt;
						&lt;phase&gt;validate&lt;/phase&gt;
						&lt;goals&gt;
							&lt;goal&gt;copy-resources&lt;/goal&gt;
						&lt;/goals&gt;
						&lt;configuration&gt;
							&lt;outputDirectory&gt;./target/classes/static/&lt;/outputDirectory&gt;
							&lt;resources&gt;
								&lt;resource&gt;
									&lt;directory&gt;../../../frontend/dist/web-ui&lt;/directory&gt;
								&lt;/resource&gt;
							&lt;/resources&gt;
						&lt;/configuration&gt;
					&lt;/execution&gt;
				&lt;/executions&gt;
			&lt;/plugin&gt;
            
            ...

		&lt;/plugins&gt;
	&lt;/build&gt;
</code></pre>
<h5 id="frontend-maven-plugin">Frontend Maven Plugin</h5>
<p>Alternatively, you can place your Angular source code in your <a href="https://auth0.com/blog/the-backend-for-frontend-pattern-bff/?ref=rob-ferguson">Backend for Frontend</a>&apos;s <code>/webapp</code> directory:</p>
<p>For example:</p>
<pre><code> &#x251C;&#x2500;&#x2500; /serendipity
     &#x2514;&#x2500;&#x2500; /modules
        &#x2514;&#x2500;&#x2500; /web-bff        
            &#x2514;&#x2500;&#x2500; /src
                &#x2514;&#x2500;&#x2500; /main
                    &#x2514;&#x2500;&#x2500; /java
                    &#x2514;&#x2500;&#x2500; /resources
                    &#x2514;&#x2500;&#x2500; /webapp
                        &#x2514;&#x2500;&#x2500; /app
                        &#x2514;&#x2500;&#x2500; /environments
                        &#x251C;&#x2500;&#x2500; favicon.ico
                        &#x251C;&#x2500;&#x2500; index.html
                        &#x251C;&#x2500;&#x2500; main.ts
                        &#x251C;&#x2500;&#x2500; styles.scss                        
                &#x2514;&#x2500;&#x2500; /test
</code></pre>
<p><strong>Note:</strong> Spring Boot will ignore the <code>src/main/webapp</code> directory if your application is packaged as a fat (aka uber) jar.</p>
<p>And use the <a href="https://github.com/eirslett/frontend-maven-plugin?ref=rob-ferguson">frontend-maven-plugin</a> to run your Angular build:</p>
<pre><code>    &lt;build&gt;
        &lt;plugins&gt;

            &lt;plugin&gt;
                &lt;groupId&gt;com.github.eirslett&lt;/groupId&gt;
                &lt;artifactId&gt;frontend-maven-plugin&lt;/artifactId&gt;
                &lt;version&gt;${frontend.maven.plugin.version}&lt;/version&gt;
                &lt;executions&gt;
                    &lt;execution&gt;
                        &lt;id&gt;nodeAndNpmSetup&lt;/id&gt;
                        &lt;goals&gt;
                            &lt;goal&gt;install-node-and-npm&lt;/goal&gt;
                        &lt;/goals&gt;
                    &lt;/execution&gt;
                    &lt;execution&gt;
                        &lt;id&gt;npmInstall&lt;/id&gt;
                        &lt;goals&gt;
                            &lt;goal&gt;npm&lt;/goal&gt;
                        &lt;/goals&gt;
                        &lt;configuration&gt;
                            &lt;arguments&gt;install&lt;/arguments&gt;
                        &lt;/configuration&gt;
                    &lt;/execution&gt;
                    &lt;execution&gt;
                        &lt;id&gt;npmRunBuild&lt;/id&gt;
                        &lt;goals&gt;
                            &lt;goal&gt;npm&lt;/goal&gt;
                        &lt;/goals&gt;
                        &lt;configuration&gt;
                            &lt;arguments&gt;run build&lt;/arguments&gt;
                        &lt;/configuration&gt;
                    &lt;/execution&gt;
                &lt;/executions&gt;
                &lt;configuration&gt;
                    &lt;nodeVersion&gt;${node.version}&lt;/nodeVersion&gt;
                &lt;/configuration&gt;
            &lt;/plugin&gt;
            
            ...

        &lt;/plugins&gt;
    &lt;/build&gt;
</code></pre>
<p>Don&apos;t forget to update the <code>outputPath</code> in your <code>angular.json</code>:</p>
<pre><code>        ...
        
        &quot;build&quot;: {
          &quot;builder&quot;: &quot;@angular-devkit/build-angular:application&quot;,
          &quot;options&quot;: {
            &quot;outputPath&quot;: {
              &quot;base&quot;: &quot;./target/classes/static&quot;,
              &quot;browser&quot;: &quot;&quot;
            },
            
        ...
</code></pre>
<h5 id="references">References</h5>
<h6 id="oauth-20">OAuth 2.0</h6>
<ul>
<li>IETF: <a href="https://datatracker.ietf.org/doc/html/draft-ietf-oauth-browser-based-apps?ref=rob-ferguson">OAuth 2.0 for Browser-Based Applications</a></li>
<li>IETF: <a href="https://www.rfc-editor.org/rfc/rfc9700.html?ref=rob-ferguson">Best Current Practice for OAuth 2.0 Security</a></li>
<li>Spring docs: <a href="https://github.com/spring-projects/spring-authorization-server/issues/297?ref=rob-ferguson#issue-896744390">Implementation Guidelines for Browser-Based Applications</a></li>
</ul>
<h6 id="spring">Spring</h6>
<ul>
<li>Spring Boot docs: <a href="https://docs.spring.io/spring-boot/reference/web/servlet.html?ref=rob-ferguson#web.servlet.spring-mvc.static-content">Static Content</a></li>
<li>Spring Projects: <a href="https://spring.io/projects/spring-cloud-gateway?ref=rob-ferguson">Spring Cloud Gateway</a></li>
</ul>
<!--kg-card-end: markdown-->]]></content:encoded></item><item><title><![CDATA[Add AuthZ to HAPI FHIR - Part 2]]></title><description><![CDATA[<!--kg-card-begin: markdown--><h3 id="introduction">Introduction</h3>
<p>This is the second post, in a series of posts about adding support for (fine grained) Authorization (AuthZ) in HAPI FHIR.</p>
<ul>
<li><a href="https://rob-ferguson.me/add-authz-to-hapi-fhir-1/">Add AuthZ to HAPI FHIR - Part 1</a></li>
<li>Add AuthZ to HAPI FHIR - Part 2</li>
</ul>
<p>In the first <a href="https://rob-ferguson.me/add-authz-to-hapi-fhir-1/">post</a>, we identified some options to enable Authorization in</p>]]></description><link>https://rob-ferguson.me/add-authz-to-hapi-fhir-part-2/</link><guid isPermaLink="false">678aff42352fee04fcab1494</guid><category><![CDATA[FHIR]]></category><category><![CDATA[HAPI FHIR]]></category><category><![CDATA[Authorization]]></category><category><![CDATA[SMART on FHIR]]></category><category><![CDATA[Keycloak]]></category><category><![CDATA[AuthZ]]></category><dc:creator><![CDATA[Rob Ferguson]]></dc:creator><pubDate>Sat, 18 Jan 2025 02:00:00 GMT</pubDate><content:encoded><![CDATA[<!--kg-card-begin: markdown--><h3 id="introduction">Introduction</h3>
<p>This is the second post, in a series of posts about adding support for (fine grained) Authorization (AuthZ) in HAPI FHIR.</p>
<ul>
<li><a href="https://rob-ferguson.me/add-authz-to-hapi-fhir-1/">Add AuthZ to HAPI FHIR - Part 1</a></li>
<li>Add AuthZ to HAPI FHIR - Part 2</li>
</ul>
<p>In the first <a href="https://rob-ferguson.me/add-authz-to-hapi-fhir-1/">post</a>, we identified some options to enable Authorization in HAPI FHIR.</p>
<p>In this post we are going to look at Option 1 in more detail.</p>
<h3 id="option-1">Option 1</h3>
<p>There are three components to this solution: an <strong>Identity Provider</strong> that authenticates the user; an <strong>Authorization Server</strong> that decides what a given user can access; and an <strong>Access Gateway</strong> which enforces those permissions.</p>
<ul>
<li>Identity Provider: <a href="https://www.keycloak.org/?ref=rob-ferguson">Keycloak</a></li>
<li>Authorization Server: <a href="https://www.keycloak.org/?ref=rob-ferguson">Keycloak</a></li>
<li>Access Gateway: <a href="https://github.com/google/fhir-gateway/?ref=rob-ferguson">FHIR Info Gateway</a></li>
</ul>
<h3 id="keycloak">Keycloak</h3>
<h4 id="smart-on-fhir-scopes">SMART on FHIR Scopes</h4>
<p>SMART on FHIR defines OAuth 2.0 <a href="https://build.fhir.org/ig/HL7/smart-app-launch/scopes-and-launch-context.html?ref=rob-ferguson">scopes</a> that allow client applications to request a specific set of access rights. The client conveys this information to the authorization server in the form of a &apos;scope&apos; request parameter.</p>
<p>The SMART on FHIR specification defines the structure of scopes, for example:</p>
<ul>
<li><code>user/CarePlan.read</code></li>
<li><code>patient/MedicationOrder.read</code></li>
<li><code>system/Observation.write</code></li>
</ul>
<p>To enable a user to read all the values from the CarePlan resource the client would include the <code>user/CarePlan.read</code> scope in its request to the authorization server.</p>
<p>A resource context prefixes each SMART on FHIR scope:</p>
<ul>
<li><code>user</code></li>
<li><code>patient</code></li>
<li><code>system</code></li>
</ul>
<p>This value represents one of three possible scenarios: <code>user</code> access to the resource is constrained by the user&apos;s access permissions; <code>patient</code> access to the resource is restricted to the in-context patient; <code>system</code> access is confined to system-based authorization workflows.</p>
<p>The following modification rights are defined:</p>
<ul>
<li><code>read</code></li>
<li><code>write</code></li>
</ul>
<p>Where <code>read</code> includes &quot;search&quot; and &quot;history&#x201D;. And, <code>write</code> includes create&quot;, &quot;update and &quot;delete&quot;.</p>
<p><strong>Keycloak does not support <a href="https://build.fhir.org/ig/HL7/smart-app-launch/scopes-and-launch-context.html?ref=rob-ferguson#wildcard-scopes">wildcard</a> scopes, clients must explicitly request each scope they require.</strong></p>
<p>Also see:</p>
<ul>
<li>Oracle Health Millennium Platform: <a href="https://docs.oracle.com/en/industries/health/millennium-platform-apis/fhir-authorization-framework/?ref=rob-ferguson#scopes">Scopes</a></li>
<li>okta forum: <a href="https://devforum.okta.com/t/creating-wildcard-custom-scopes-for-oauth2/27418?ref=rob-ferguson">SMART on FHIR wildcard scopes</a></li>
</ul>
<h4 id="client-scopes">Client Scopes</h4>
<p>In Keycloak, an OAuth 2.0 scope is mapped to a <strong>client scope</strong>.</p>
<p>Navigate to the Keycloak Admin Console:</p>
<pre><code>https://keycloak.au.localhost:8443
</code></pre>
<p>In the <code>hapi-fhir-dev</code> realm, select &apos;Client scopes&apos;, then click the &apos;Create client scope&apos; button:</p>
<p align="center">
  <img src="https://rob-ferguson.me/content/images/2025/01/keycloak-client-scopes.png" class="thumbnail">
</p>
<p>Enter the details for the <code>patient/AllergyIntolerance.read</code> scope:</p>
<p align="center">
  <img src="https://rob-ferguson.me/content/images/2025/01/keycloak-create-client-scope-2.png" class="thumbnail">
</p>
<p>Then click the &apos;Save&apos; button.</p>
<p>Select &apos;Clients&apos; and <code>oauth2-proxy</code> and then choose the &apos;Client scopes&apos; tab, click the &apos;Add client scope&apos; button:</p>
<p align="center">
  <img src="https://rob-ferguson.me/content/images/2025/01/keycloak-client-client-details.png" class="thumbnail">
</p>
<p>Add the <code>patient/AllergyIntolerance.read</code> client scope to the client:</p>
<p align="center">
  <img src="https://rob-ferguson.me/content/images/2025/01/keycloak-client-add-client-scopes-1.png" class="thumbnail">
</p>
<p><strong>Note:</strong> The <code>patient/AllergyIntolerance.read</code> scope is an optional scope.</p>
<p>If we enable the &apos;Consent required&apos; setting:</p>
<p align="center">
  <img src="https://rob-ferguson.me/content/images/2025/01/keycloak-client-login-settings-consent-required.png" class="thumbnail">
</p>
<p>An add the <code>patient/AllergyIntolerance.read</code> client scope to the scope request parameter (via the project&apos;s <code>.env</code> file):</p>
<pre><code>...

SCOPE=&quot;openid patient/AllergyIntolerance.read&quot;
</code></pre>
<p>See: <a href="https://github.com/Robinyo/hapi-fhir-au/blob/main/backend/.env?ref=rob-ferguson">.env</a></p>
<p>Then we can check that we have successfully configured the <code>patient/AllergyIntolerance.read</code> client scope:</p>
<p align="center">
  <img src="https://rob-ferguson.me/content/images/2025/01/keycloak-grant-access.png" class="thumbnail">
</p>
<h4 id="call-the-fhir-api">Call the FHIR API</h4>
<h5 id="oauth-20-client-credentials-grant">OAuth 2.0 Client Credentials Grant</h5>
<p>You must allow the &apos;Service account roles&apos; capability config setting in order to enable support for the OAuth 2.0 <strong>Client Credentials Grant</strong>:</p>
<p align="center">
  <img src="https://rob-ferguson.me/content/images/2025/01/hapi-fhir-service-account-roles-1.png" class="thumbnail">
</p>
<h5 id="request-a-token">Request a token</h5>
<p>To access the API, you must request an access token. You will need to POST to the token URL.</p>
<p>For example (<code>scope=system/Patient.read</code>):</p>
<pre><code>ACCESS_TOKEN=$(curl -s -X POST https://keycloak.au.localhost:8443/realms/hapi-fhir-dev/protocol/openid-connect/token \
  -H &apos;content-type: application/x-www-form-urlencoded&apos; \
  -d grant_type=client_credentials \
  -d client_id=oauth2-proxy \
  -d client_secret=aHkRec1BYkfaKgMg164JmvKu8u9iWNHM \
  -d scope=system/Patient.read | (jq -r &apos;.access_token&apos;))
                 
# echo &quot;$ACCESS_TOKEN&quot;                 
</code></pre>
<p>If you <a href="https://jwt.io/?ref=rob-ferguson">decode</a> the access token you should see something like:</p>
<pre><code>{
  &quot;exp&quot;: 1737495977,
  &quot;iat&quot;: 1737495677,
  &quot;jti&quot;: &quot;d6ba5750-c49a-419e-9d7a-80bb8eda8f5e&quot;,
  &quot;iss&quot;: &quot;https://keycloak.au.localhost:8443/realms/hapi-fhir-dev&quot;,
  &quot;aud&quot;: [
    &quot;oauth2-proxy&quot;,
    &quot;account&quot;
  ],
  &quot;sub&quot;: &quot;da4f0912-1cd9-432d-9c9d-a55db11093de&quot;,
  &quot;typ&quot;: &quot;Bearer&quot;,
  &quot;azp&quot;: &quot;oauth2-proxy&quot;,
  &quot;acr&quot;: &quot;1&quot;,
  &quot;allowed-origins&quot;: [
    &quot;*&quot;
  ],
  &quot;realm_access&quot;: {
    &quot;roles&quot;: [
      &quot;default-roles-hapi-fhir-dev&quot;,
      &quot;offline_access&quot;,
      &quot;uma_authorization&quot;
    ]
  },
  &quot;resource_access&quot;: {
    &quot;oauth2-proxy&quot;: {
      &quot;roles&quot;: [
        &quot;uma_protection&quot;
      ]
    },
    &quot;account&quot;: {
      &quot;roles&quot;: [
        &quot;manage-account&quot;,
        &quot;manage-account-links&quot;,
        &quot;view-profile&quot;
      ]
    }
  },
  &quot;scope&quot;: &quot;system/Patient.read profile email&quot;,
  &quot;email_verified&quot;: false,
  &quot;clientHost&quot;: &quot;172.18.0.1&quot;,
  &quot;preferred_username&quot;: &quot;service-account-oauth2-proxy&quot;,
  &quot;clientAddress&quot;: &quot;172.18.0.1&quot;,
  &quot;client_id&quot;: &quot;oauth2-proxy&quot;
}
</code></pre>
<h5 id="call-the-api">Call the API</h5>
<p>To call the API, an application must pass the access token as a Bearer token in the Authorization header of your HTTP request.</p>
<p>For example:</p>
<pre><code>curl -X GET https://hapi-fhir.au.localhost/fhir/Patient?_id=baratz-toni \
  -H &apos;Content-Type: application/fhir+json&apos; \
  -H &quot;Authorization: Bearer $ACCESS_TOKEN&quot;
</code></pre>
<h5 id="source-code">Source Code</h5>
<ul>
<li>GitHub: <a href="https://github.com/Robinyo/hapi-fhir-au/?ref=rob-ferguson">HAPI FHIR AU with Auth Starter Project</a></li>
</ul>
<h5 id="references">References</h5>
<h6 id="oauth-20">OAuth 2.0</h6>
<ul>
<li>IETF: <a href="https://datatracker.ietf.org/doc/html/draft-ietf-oauth-browser-based-apps?ref=rob-ferguson">OAuth 2.0 for Browser-Based Applications</a></li>
<li>Spring docs: <a href="https://github.com/spring-projects/spring-authorization-server/issues/297?ref=rob-ferguson#issue-896744390">Implementation Guidelines for Browser-Based Applications</a></li>
</ul>
<h6 id="smart-on-fhir">SMART on FHIR</h6>
<ul>
<li>HL7: <a href="https://build.fhir.org/ig/HL7/smart-app-launch/?ref=rob-ferguson">SMART App Launch</a></li>
<li>HL7: <a href="https://build.fhir.org/ig/HL7/smart-app-launch/backend-services.html?ref=rob-ferguson">Backend Services</a></li>
<li>SMART Health IT: <a href="https://docs.smarthealthit.org/?ref=rob-ferguson">SMART on FHIR</a></li>
</ul>
<h6 id="keycloak">Keycloak</h6>
<ul>
<li>Keycloak docs: <a href="https://www.keycloak.org/migration/migrating-to-quarkus?ref=rob-ferguson">Migrating to the Quarkus distribution</a></li>
<li>Keycloak docs: <a href="https://www.keycloak.org/docs/latest/upgrading/?ref=rob-ferguson">Upgrading Guide - 26.1.0</a></li>
<li>Keycloak docs: <a href="https://www.keycloak.org/docs/26.1.0/server_admin/?ref=rob-ferguson">Server Administration Guide</a></li>
<li>Google Group: <a href="https://groups.google.com/g/keycloak-dev?ref=rob-ferguson">Keycloak Dev</a></li>
<li>Google Group: <a href="https://groups.google.com/g/keycloak-user?ref=rob-ferguson">Keycloak User</a></li>
</ul>
<h6 id="fhir-info-gateway">FHIR Info Gateway</h6>
<ul>
<li>GitHub: <a href="https://github.com/google/fhir-gateway/blob/main/doc/docs/design.md?ref=rob-ferguson#introduction">FHIR Info Gateway</a></li>
</ul>
<h6 id="clinical-information-systems">Clinical Information Systems</h6>
<ul>
<li>Oracle Health Millennium Platform: <a href="https://docs.oracle.com/en/industries/health/millennium-platform-apis/fhir-authorization-framework/?ref=rob-ferguson#authorization">Authorization Framework</a></li>
</ul>
<!--kg-card-end: markdown--><p></p><p></p>]]></content:encoded></item><item><title><![CDATA[Add AuthZ to HAPI FHIR - Part 1]]></title><description><![CDATA[<!--kg-card-begin: markdown--><h3 id="introduction">Introduction</h3>
<p>In previous <a href="https://rob-ferguson.me/add-authn-to-hapi-fhir-with-oauth2-proxy-nginx-and-keycloak-part-1/">posts</a>, I wrote about adding support for Authentication (AuthN) to HAPI FHIR.</p>
<p>In this post, we will take a look at some options to enable (fine grained) Authorization (AuthZ) in HAPI FHIR.</p>
<h3 id="option-1">Option 1</h3>
<p>There are three components to this solution: an <strong>Identity Provider</strong> that authenticates the</p>]]></description><link>https://rob-ferguson.me/add-authz-to-hapi-fhir-1/</link><guid isPermaLink="false">67897a80352fee04fcab12e2</guid><category><![CDATA[FHIR]]></category><category><![CDATA[HAPI FHIR]]></category><category><![CDATA[Authorization]]></category><category><![CDATA[SMART on FHIR]]></category><category><![CDATA[Keycloak]]></category><category><![CDATA[AuthZ]]></category><dc:creator><![CDATA[Rob Ferguson]]></dc:creator><pubDate>Thu, 16 Jan 2025 23:00:56 GMT</pubDate><content:encoded><![CDATA[<!--kg-card-begin: markdown--><h3 id="introduction">Introduction</h3>
<p>In previous <a href="https://rob-ferguson.me/add-authn-to-hapi-fhir-with-oauth2-proxy-nginx-and-keycloak-part-1/">posts</a>, I wrote about adding support for Authentication (AuthN) to HAPI FHIR.</p>
<p>In this post, we will take a look at some options to enable (fine grained) Authorization (AuthZ) in HAPI FHIR.</p>
<h3 id="option-1">Option 1</h3>
<p>There are three components to this solution: an <strong>Identity Provider</strong> that authenticates the user; an <strong>Authorization Server</strong> that decides what a given user can access; and an <strong>Access Gateway</strong> which enforces those permissions.</p>
<ul>
<li>Identity Provider: <a href="https://www.keycloak.org/?ref=rob-ferguson">Keycloak</a></li>
<li>Authorization Server: <a href="https://www.keycloak.org/?ref=rob-ferguson">Keycloak</a></li>
<li>Access Gateway: <a href="https://github.com/google/fhir-gateway/?ref=rob-ferguson">FHIR Info Gateway</a></li>
</ul>
<h4 id="authorization-server">Authorization Server</h4>
<p>The <a href="https://github.com/Robinyo/hapi-fhir-au/?ref=rob-ferguson">HAPI FHIR AU with Auth Starter Project</a> already uses Keycloak for Authentication (AuthN).</p>
<p>We will need to extend Keycloak and add support for <a href="https://build.fhir.org/ig/HL7/smart-app-launch/?ref=rob-ferguson">SMART on FHIR</a>.</p>
<p><strong><a href="https://build.fhir.org/ig/HL7/smart-app-launch/app-launch.html?ref=rob-ferguson#launch-app-standalone-launch">Standalone Launch</a></strong></p>
<p>See:</p>
<ul>
<li>Project Alvearie: <a href="https://alvearie.io/blog/smart-keycloak/?ref=rob-ferguson">SMART App Launch</a></li>
<li>Project Alvearie: <a href="https://github.com/Alvearie/keycloak-extensions-for-fhir?ref=rob-ferguson">Keycloak extensions for FHIR</a></li>
<li>Keycloak extensions for FHIR: <a href="https://github.com/Alvearie/keycloak-extensions-for-fhir/issues/64?ref=rob-ferguson">Upgrade to the Quarkus-based distribution</a></li>
<li>Keycloak discussion: <a href="https://github.com/keycloak/keycloak/discussions/10303?ref=rob-ferguson">Fine grained scope consent management</a></li>
</ul>
<p><strong><a href="https://build.fhir.org/ig/HL7/smart-app-launch/app-launch.html?ref=rob-ferguson#launch-app-ehr-launch">EHR Launch</a></strong></p>
<p>See:</p>
<ul>
<li>GitHub: <a href="https://github.com/zedwerks/keycloak-smart-fhir?ref=rob-ferguson">Zedwerks - Keycloak extensions for FHIR</a></li>
</ul>
<h4 id="access-gateway">Access Gateway</h4>
<p>The FHIR Information Gateway sits in front of a FHIR store (e.g., a HAPI FHIR server) and controls access to FHIR resources.</p>
<p>See:</p>
<ul>
<li>FHIR Info Gateway: <a href="https://github.com/google/fhir-gateway/blob/main/doc/docs/design.md?ref=rob-ferguson#introduction">Introduction</a></li>
</ul>
<h3 id="option-2">Option 2</h3>
<p>Three components as per option 1.</p>
<p>AuthN and AuthZ: Keycloak</p>
<h4 id="access-gateway">Access Gateway</h4>
<p>A severless implementation of an Access Gateway.</p>
<p><a href="https://knative.dev/docs/?ref=rob-ferguson">Knative</a> and <a href="https://www.openfaas.com/?ref=rob-ferguson">OpenFaaS</a> are open-source environments for creating and hosting serverless functions.</p>
<p>See:</p>
<ul>
<li>GitHub: <a href="https://github.com/oktadev/okta-smartfhir-docs?ref=rob-ferguson">Okta SMART on FHIR docs</a></li>
<li>GitHub: <a href="https://github.com/oktadev/okta-smartfhir-demo?ref=rob-ferguson">Okta SMART on FHIR demo</a></li>
</ul>
<h3 id="option-3">Option 3</h3>
<p>Three components as per option 1.</p>
<p>AuthN and AuthZ: Keycloak</p>
<h4 id="access-gateway">Access Gateway</h4>
<p>An Access Gateway implementation with support for Relationship-Based<br>
Access Control (ReBAC).</p>
<p>See:</p>
<ul>
<li>GitHub: <a href="https://github.com/dancinnamon-okta/secured-fhir-proxy?ref=rob-ferguson">Secured FHIR Proxy</a></li>
<li>OpenFGA: <a href="https://openfga.dev/?ref=rob-ferguson">Relationship-based access control</a></li>
</ul>
<h3 id="option-4">Option 4</h3>
<p>Three components as per option 1.</p>
<p>AuthN and AuthZ: Keycloak</p>
<h4 id="access-gateway">Access Gateway</h4>
<p>An Access Gateway implementation based on Spring Cloud Gateway.</p>
<p>See:</p>
<ul>
<li>Spring blog: <a href="https://spring.io/blog/2022/10/11/embracing-virtual-threads?ref=rob-ferguson">Embracing Virtual Threads</a></li>
<li>Spring blog: <a href="https://spring.io/blog/2024/04/30/spring-tips-spring-cloud-gateway-for-spring-mvc?ref=rob-ferguson">Spring Tips: Spring Cloud Gateway for Spring MVC</a></li>
<li>Spring docs: <a href="https://docs.spring.io/spring-cloud-gateway/reference/spring-cloud-gateway-server-mvc/how-it-works.html?ref=rob-ferguson">Spring Cloud Gateway Server MVC</a></li>
</ul>
<h3 id="whats-next">What&apos;s Next</h3>
<p>In the next <a href="https://rob-ferguson.me/add-authz-to-hapi-fhir-part-2/">post</a>, we&apos;ll take a look Option 1 in more detail.</p>
<h5 id="source-code">Source Code</h5>
<ul>
<li>GitHub: <a href="https://github.com/Robinyo/hapi-fhir-au/?ref=rob-ferguson">HAPI FHIR AU with Auth Starter Project</a></li>
</ul>
<h5 id="references">References</h5>
<h6 id="oauth-20">OAuth 2.0</h6>
<ul>
<li>IETF: <a href="https://datatracker.ietf.org/doc/html/draft-ietf-oauth-browser-based-apps?ref=rob-ferguson">OAuth 2.0 for Browser-Based Applications</a></li>
<li>Spring docs: <a href="https://github.com/spring-projects/spring-authorization-server/issues/297?ref=rob-ferguson#issue-896744390">Implementation Guidelines for Browser-Based Applications</a></li>
</ul>
<h6 id="smart-on-fhir">SMART on FHIR</h6>
<ul>
<li>HL7: <a href="https://build.fhir.org/ig/HL7/smart-app-launch/?ref=rob-ferguson">SMART App Launch</a></li>
<li>HL7: <a href="https://build.fhir.org/ig/HL7/smart-app-launch/backend-services.html?ref=rob-ferguson">Backend Services</a></li>
<li>SMART Health IT: <a href="https://docs.smarthealthit.org/?ref=rob-ferguson">SMART on FHIR</a></li>
</ul>
<h6 id="keycloak">Keycloak</h6>
<ul>
<li>Keycloak docs: <a href="https://www.keycloak.org/migration/migrating-to-quarkus?ref=rob-ferguson">Migrating to the Quarkus distribution</a></li>
<li>Keycloak docs: <a href="https://www.keycloak.org/docs/latest/upgrading/?ref=rob-ferguson">Upgrading Guide - 26.1.0</a></li>
<li>Keycloak docs: <a href="https://www.keycloak.org/docs/latest/authorization_services/index.html?ref=rob-ferguson">Authorization Services Guide</a></li>
</ul>
<h6 id="fhir-info-gateway">FHIR Info Gateway</h6>
<ul>
<li>GitHub: <a href="https://github.com/google/fhir-gateway/blob/main/doc/docs/design.md?ref=rob-ferguson#introduction">FHIR Info Gateway</a></li>
</ul>
<!--kg-card-end: markdown-->]]></content:encoded></item><item><title><![CDATA[Add AuthN to HAPI FHIR with OAuth2 Proxy, Nginx and Keycloak - Part 4]]></title><description><![CDATA[<!--kg-card-begin: markdown--><h3 id="introduction">Introduction</h3>
<p>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:</p>
<ul>
<li><a href="https://rob-ferguson.me/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 1</a></li>
<li><a href="https://rob-ferguson.me/add-authn-to-hapi-fhir-with-oauth2-proxy-nginx-and-keycloak-part-2/">Add AuthN to HAPI FHIR with OAuth2 Proxy, Nginx and</a></li></ul>]]></description><link>https://rob-ferguson.me/add-authn-to-hapi-fhir-with-oauth2-proxy-nginx-and-keycloak-part-4/</link><guid isPermaLink="false">678378d8352fee04fcab11ba</guid><category><![CDATA[FHIR]]></category><category><![CDATA[HAPI FHIR]]></category><category><![CDATA[Authentication]]></category><category><![CDATA[OAuth2 Proxy]]></category><category><![CDATA[Keycloak]]></category><category><![CDATA[Nginx]]></category><category><![CDATA[AuthN]]></category><dc:creator><![CDATA[Rob Ferguson]]></dc:creator><pubDate>Sun, 12 Jan 2025 08:43:27 GMT</pubDate><media:content url="https://rob-ferguson.me/content/images/2025/01/oauth2-proxy-sign-in-1.png" medium="image"/><content:encoded><![CDATA[<!--kg-card-begin: markdown--><h3 id="introduction">Introduction</h3>
<img src="https://rob-ferguson.me/content/images/2025/01/oauth2-proxy-sign-in-1.png" alt="Add AuthN to HAPI FHIR with OAuth2 Proxy, Nginx and Keycloak - Part 4"><p>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:</p>
<ul>
<li><a href="https://rob-ferguson.me/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 1</a></li>
<li><a href="https://rob-ferguson.me/add-authn-to-hapi-fhir-with-oauth2-proxy-nginx-and-keycloak-part-2/">Add AuthN to HAPI FHIR with OAuth2 Proxy, Nginx and Keycloak - Part 2</a></li>
<li><a href="https://rob-ferguson.me/add-authn-to-hapi-fhir-with-oauth2-proxy-nginx-and-keycloak-part-3/">Add AuthN to HAPI FHIR with OAuth2 Proxy, Nginx and Keycloak - Part 3</a></li>
<li>Add AuthN to HAPI FHIR with OAuth2 Proxy, Nginx and Keycloak - Part 4</li>
</ul>
<h4 id="oauth2-proxy">OAuth2 Proxy</h4>
<p><a href="https://github.com/oauth2-proxy/oauth2-proxy?ref=rob-ferguson">OAuth2 Proxy</a> is an open source tool that can be used to intercept requests to your application and redirect them to an Authorisation server that supports OAuth 2.0 and OpenID Connect.</p>
<p>I followed the recommendations in the following guides:</p>
<ul>
<li>OAuth2 Proxy docs: <a href="https://oauth2-proxy.github.io/oauth2-proxy/configuration/tls/?ref=rob-ferguson">TLS Configuration</a></li>
<li>OAuth2 Proxy docs: <a href="https://oauth2-proxy.github.io/oauth2-proxy/configuration/integration?ref=rob-ferguson">Integration</a></li>
</ul>
<h5 id="tls-configuration">TLS Configuration</h5>
<p>All traffic is routed through Nginx and TLS is terminated at Nginx (the reverse proxy).</p>
<h5 id="integration">Integration</h5>
<p>OAuth2 Proxy forwards subrequests to Keycloak in order to authenticate requests to HAPI FHIR. Take a look at the <code>auth_request</code> directive section, in this <a href="https://rob-ferguson.me/add-authn-to-hapi-fhir-with-oauth2-proxy-nginx-and-keycloak-part-3/">post</a>.</p>
<h5 id="config-options">Config Options</h5>
<p>OAuth2 Proxy is running behind Nginx (the reverse proxy):</p>
<pre><code>  oauth2-proxy:
    container_name: oauth2-proxy

    ...

    environment:
    
      ...

      # Proxy options
      OAUTH2_PROXY_EMAIL_DOMAINS: &apos;*&apos;
      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}
      
      ...
      
</code></pre>
<p>We want to use the OpenID Connect (oidc) provider:</p>
<pre><code>  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}     
      
      ...

</code></pre>
<p>OAuth2 Proxy will check that the Issuer URL matches the Issuer URL (iss) returned by Keycloak.</p>
<p>You can preview tokens in the Keycloak Admin Console, for example:</p>
<p align="center">
  <img src="https://rob-ferguson.me/content/images/2025/01/keycloak-generated-id-token.png" class="thumbnail" alt="Add AuthN to HAPI FHIR with OAuth2 Proxy, Nginx and Keycloak - Part 4">
</p>
<p>The complete OAuth2 Proxy configuration:</p>
<pre><code>  oauth2-proxy:
    container_name: oauth2-proxy
    build:
      context: ./services/oauth2-proxy
      dockerfile: Dockerfile
    restart: unless-stopped
    command:
      [
        &apos;--standard-logging=true&apos;,
        &apos;--auth-logging=true&apos;,
        &apos;--request-logging=true&apos;,
      ]
    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: &apos;*&apos;
      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
</code></pre>
<p>See: <a href="https://github.com/Robinyo/hapi-fhir-au/blob/main/backend/docker-compose.yml?ref=rob-ferguson">docker-compose.yml</a></p>
<p>The <code>.env</code> file:</p>
<pre><code>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=
</code></pre>
<p>See: <a href="https://github.com/Robinyo/hapi-fhir-au/blob/main/backend/.env?ref=rob-ferguson">.env</a></p>
<p><strong>Note:</strong> Docker will look for your <code>.env</code> file in the same directory as your Docker Compose configuration file.</p>
<h5 id="source-code">Source Code</h5>
<ul>
<li>GitHub: <a href="https://github.com/Robinyo/hapi-fhir-au/?ref=rob-ferguson">HAPI FHIR AU with Auth Starter Project</a></li>
</ul>
<h5 id="references">References</h5>
<h6 id="system-hardening">System Hardening</h6>
<ul>
<li>ASD: <a href="https://www.cyber.gov.au/resources-business-and-government/maintaining-devices-and-systems/system-hardening-and-administration/web-hardening/implementing-certificates-tls-https-and-opportunistic-tls?ref=rob-ferguson">Implementing Certificates, TLS, HTTPS and Opportunistic TLS</a></li>
<li>Cloudflare docs: <a href="https://developers.cloudflare.com/ssl/edge-certificates/additional-options/cipher-suites/recommendations/?ref=rob-ferguson">Cipher suites recommendations</a></li>
</ul>
<h6 id="oauth-20">OAuth 2.0</h6>
<ul>
<li>IETF: <a href="https://datatracker.ietf.org/doc/html/draft-ietf-oauth-browser-based-apps?ref=rob-ferguson">OAuth 2.0 for Browser-Based Applications</a></li>
<li>Spring docs: <a href="https://github.com/spring-projects/spring-authorization-server/issues/297?ref=rob-ferguson#issue-896744390">Implementation Guidelines for Browser-Based Applications</a></li>
<li>okta Developer blog: <a href="https://developer.okta.com/blog/2022/06/16/oauth-java?ref=rob-ferguson">OAuth for Java Developers</a></li>
<li>OAuth.com: <a href="https://www.oauth.com/playground/?_gl=1*1fwid4n*_gcl_au*MjEyMTY2MzU4NS4xNzM1MDI2MjQ4*_ga*MTk3OTgwNDIxNS4xNzM1MDI2MjQ4*_ga_QKMSDV5369*MTczNjAyMjIyMS42LjEuMTczNjAyMjkyOS41Ny4wLjA.&amp;ref=rob-ferguson">OAuth 2.0 Playground</a></li>
</ul>
<h6 id="keycloak">Keycloak</h6>
<ul>
<li>Keycloak guides: <a href="https://www.keycloak.org/server/configuration-production?ref=rob-ferguson">Configuring Keycloak for production</a></li>
<li>Keycloak guides: <a href="https://www.keycloak.org/server/enabletls?ref=rob-ferguson">Configuring TLS</a></li>
<li>Keycloak guides: <a href="https://www.keycloak.org/server/keycloak-truststore?ref=rob-ferguson">Configuring trusted certificates</a></li>
<li>Keycloak guides: <a href="https://www.keycloak.org/server/hostname?ref=rob-ferguson">Configuring the hostname</a></li>
<li>Keycloak guides: <a href="https://www.keycloak.org/server/reverseproxy?ref=rob-ferguson">Using a reverse proxy</a></li>
<li>Keycloak guides: <a href="https://www.keycloak.org/server/containers?ref=rob-ferguson">Running Keycloak in a container</a></li>
</ul>
<h6 id="nginx">Nginx</h6>
<ul>
<li>Nginx docs: <a href="https://docs.nginx.com/nginx/admin-guide/security-controls/terminating-ssl-http/?ref=rob-ferguson">NGINX SSL Termination</a></li>
<li>Nginx docs: <a href="https://docs.nginx.com/nginx/admin-guide/security-controls/configuring-subrequest-authentication/?ref=rob-ferguson">Authentication Based on Subrequest Result</a></li>
</ul>
<h6 id="oauth2-proxy">OAuth2 Proxy</h6>
<ul>
<li>OAuth2 Proxy docs: <a href="https://oauth2-proxy.github.io/oauth2-proxy/configuration/integration?ref=rob-ferguson">Integration</a></li>
<li>OAuth2 Proxy docs: <a href="https://oauth2-proxy.github.io/oauth2-proxy/configuration/tls/?ref=rob-ferguson">TLS Configuration</a></li>
</ul>
<!--kg-card-end: markdown-->]]></content:encoded></item></channel></rss>