<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Yuan Ji</title>
    <description>The latest articles on DEV Community by Yuan Ji (@jiwhiz).</description>
    <link>https://dev.to/jiwhiz</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F40383%2Fb31cd1f8-9967-4ba1-97e1-e10cc1a6e593.jpeg</url>
      <title>DEV Community: Yuan Ji</title>
      <link>https://dev.to/jiwhiz</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jiwhiz"/>
    <language>en</language>
    <item>
      <title>How to Build an Elm Land Project for Production</title>
      <dc:creator>Yuan Ji</dc:creator>
      <pubDate>Sun, 13 Oct 2024 05:40:20 +0000</pubDate>
      <link>https://dev.to/jiwhiz/how-to-build-an-elm-land-project-for-production-36jo</link>
      <guid>https://dev.to/jiwhiz/how-to-build-an-elm-land-project-for-production-36jo</guid>
      <description>&lt;p&gt;After completing my demo project on &lt;a href="https://github.com/jiwhiz/keycloak-spring-security-oauth2-token-exchange-demo" rel="noopener noreferrer"&gt;Keycloak OAuth2 Token Exchange&lt;/a&gt;, I decided to expand the idea and created a new demo project called &lt;a href="https://github.com/jiwhiz/my-ai-doctor" rel="noopener noreferrer"&gt;my-ai-doctor&lt;/a&gt;. Unlike the previous demo, which focused on backend integration, this project uses Elm for the UI and Docker Compose to run all six servers locally: the Keycloak server, API server, and the SPA web servers for both MyDoctor and MyHealth.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fixing the Elm Compiler for Linux ARM 64
&lt;/h3&gt;

&lt;p&gt;The first challenge was building the Elm Land project and deploying it to Docker containers. Initially, I used a standard Dockerfile to build my Elm UI project, but soon hit a roadblock:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; &amp;gt; [build 8/9] RUN npm install:                                                                                                                             
8.709 npm error code 1                                                                                                                                     
8.709 npm error path /app/node_modules/elm                                                                                                                 
8.709 npm error command failed                                                                                                                             
8.709 npm error command sh -c node install.js                                                                                                              
8.709 npm error -- ERROR -----------------------------------------------------------------------
8.709 npm error
8.709 npm error I am detecting that your computer (linux_arm64) may not be compatible with any
8.709 npm error of the official pre-built binaries.
8.709 npm error
8.709 npm error I recommend against using the npm installer for your situation. Check out the
8.709 npm error alternative installers at https://github.com/elm/compiler/releases/tag/0.19.1
8.709 npm error to see if there is something that will work better for you.
8.709 npm error
8.709 npm error From there I recommend asking for guidance on Slack or Discourse to find someone
8.709 npm error who can help with your specific situation.
8.709 npm error
8.709 npm error --------------------------------------------------------------------------------
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After some investigation, I found that the latest Elm compiler (version 0.19.1) &lt;a href="https://github.com/elm/compiler/issues/2283" rel="noopener noreferrer"&gt;does not support linux_arm64&lt;/a&gt;. This meant I could build and run Elm projects on my Apple Mac Mini Pro natively, but not inside Docker. Fortunately, &lt;a href="https://github.com/lydell" rel="noopener noreferrer"&gt;Simon Lydell&lt;/a&gt; and &lt;a href="https://github.com/supermario" rel="noopener noreferrer"&gt;Mario Rogic&lt;/a&gt; created a new version of the &lt;a href="https://www.npmjs.com/package/@lydell/elm" rel="noopener noreferrer"&gt;elm npm package&lt;/a&gt;, which supports Linux ARM 64. Based on &lt;a href="https://github.com/ryan-haskell/vite-plugin-elm-watch/issues/3" rel="noopener noreferrer"&gt;this discussion&lt;/a&gt;, the solution was to override the elm package in my &lt;code&gt;package.json&lt;/code&gt; file by adding these lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"overrides": {
  "elm": "npm:@lydell/elm"
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This replaces the Elm npm module in the locally downloaded &lt;code&gt;node_modules&lt;/code&gt; directory with the &lt;code&gt;@lydell/elm&lt;/code&gt; module, allowing the unofficial Elm compiler to work correctly on the Linux ARM64 platform.&lt;/p&gt;

&lt;h3&gt;
  
  
  Passing Environment Variables to UI Code
&lt;/h3&gt;

&lt;p&gt;When developing the AI Doctor system locally, the UI SPA connects to the backend API at &lt;code&gt;http://api.mydoctor:8081/api/v1/records&lt;/code&gt;. However, when running the entire demo inside Docker, the API is accessible via port 80 with the help of an Nginx reverse proxy: &lt;code&gt;http://api.mydoctor/api/v1/records&lt;/code&gt;. The challenge was how to dynamically change the backend API URL in the Elm code.&lt;/p&gt;

&lt;p&gt;The Elm Land documentation briefly mentions &lt;a href="https://elm.land/reference/elm-land-json.html#app-env" rel="noopener noreferrer"&gt;environment variables&lt;/a&gt;, but without detailed examples. I found a helpful blog, &lt;a href="https://seanrmurphy.medium.com/deploy-and-elm-frontend-to-cloudflare-pages-f8859db1ed51" rel="noopener noreferrer"&gt;Deploying an Elm Frontend to Cloudflare Pages&lt;/a&gt;, which provided some hints on how to pass environment variables to an Elm Land app. After studying the Elm Land documentation again (&lt;a href="https://elm.land/guide/working-with-js.html#sending-initial-data" rel="noopener noreferrer"&gt;here&lt;/a&gt; and &lt;a href="https://elm.land/reference/elm-land-json.html#app-env" rel="noopener noreferrer"&gt;here&lt;/a&gt;), I realized it was quite straightforward to pass different values as environment variables to Elm code.&lt;/p&gt;

&lt;p&gt;In the &lt;code&gt;interop.js&lt;/code&gt; file, we can pick environment variables and pass them to Elm code as &lt;a href="https://guide.elm-lang.org/interop/flags" rel="noopener noreferrer"&gt;&lt;code&gt;Flags&lt;/code&gt;&lt;/a&gt;. For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;flags&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;apiBaseUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;API_BASE_URL&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, we pass the backend API base URL as a field in &lt;code&gt;flags&lt;/code&gt;, which is then saved into &lt;code&gt;Shared.Model&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;First, I needed to customize the Shared module:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;elm-land customize shared
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This command will move three files from &lt;code&gt;.elm-land&lt;/code&gt; into &lt;code&gt;src&lt;/code&gt; folder: &lt;code&gt;Shared.elm&lt;/code&gt;, &lt;code&gt;Shared/Model.elm&lt;/code&gt; and &lt;code&gt;Shared/Msg.elm&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Next, I added the field &lt;code&gt;apiBaseUrl&lt;/code&gt; to &lt;code&gt;Shared.Model.Model&lt;/code&gt;. Here is the code for &lt;code&gt;Shared/Model.elm&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elm"&gt;&lt;code&gt;&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="kt"&gt;Shared&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="kt"&gt;Model&lt;/span&gt; &lt;span class="k"&gt;exposing&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="k"&gt;alias&lt;/span&gt; &lt;span class="kt"&gt;Model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;apiBaseUrl&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In &lt;code&gt;Shared.elm&lt;/code&gt;, I decoded the flags and passed &lt;code&gt;apiBaseUrl&lt;/code&gt; to &lt;code&gt;Shared.Model.Model&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elm"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- FLAGS&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="k"&gt;alias&lt;/span&gt; &lt;span class="kt"&gt;Flags&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;apiBaseUrl&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;decoder&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="kt"&gt;Decode&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="kt"&gt;Decoder&lt;/span&gt; &lt;span class="kt"&gt;Flags&lt;/span&gt;
&lt;span class="n"&gt;decoder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="kt"&gt;Json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="kt"&gt;Decode&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;map&lt;/span&gt; &lt;span class="kt"&gt;Flags&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="kt"&gt;Decode&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;field&lt;/span&gt; &lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;apiBaseUrl"&lt;/span&gt; &lt;span class="kt"&gt;Json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="kt"&gt;Decode&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;-- INIT&lt;/span&gt;
&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="k"&gt;alias&lt;/span&gt; &lt;span class="kt"&gt;Model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="kt"&gt;Shared&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="kt"&gt;Model&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="kt"&gt;Model&lt;/span&gt;

&lt;span class="n"&gt;init&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Result&lt;/span&gt; &lt;span class="kt"&gt;Json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="kt"&gt;Decode&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="kt"&gt;Error&lt;/span&gt; &lt;span class="kt"&gt;Flags&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;Route&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="kt"&gt;Model&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;Effect&lt;/span&gt; &lt;span class="kt"&gt;Msg&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;init&lt;/span&gt; &lt;span class="n"&gt;flagsResult&lt;/span&gt; &lt;span class="n"&gt;route&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;apiBaseUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;flagsResult&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt;
          &lt;span class="kt"&gt;Ok&lt;/span&gt; &lt;span class="n"&gt;flags&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;apiBaseUrl&lt;/span&gt;
          &lt;span class="kt"&gt;Err&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;Effect&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;none&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;Shared.Model.Model&lt;/code&gt; (or &lt;code&gt;Shared.Model&lt;/code&gt;) is then passed to each page, so the &lt;code&gt;apiBaseUrl&lt;/code&gt; can be accessed whenever we need to call the backend API. See example code at &lt;a href="https://github.com/jiwhiz/my-ai-doctor/blob/main/mydoctor/mydoctor-ui/src/Pages/Test.elm" rel="noopener noreferrer"&gt;&lt;code&gt;Pages/Test.elm&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Setting Environment Variables
&lt;/h3&gt;

&lt;p&gt;To set environment variables like &lt;code&gt;API_BASE_URL&lt;/code&gt;, you first need to declare them in the &lt;code&gt;elm-land.json&lt;/code&gt; file under &lt;a href="https://elm.land/reference/elm-land-json.html#app-env" rel="noopener noreferrer"&gt;app.env&lt;/a&gt;, as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"app"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"env"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"API_BASE_URL"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;For security reasons, all environment variables are hidden from your Elm Land application by default.&lt;br&gt;
When you want to share an environment variable with your app, the app.env field is the one spot check&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;See my &lt;a href="https://github.com/jiwhiz/my-ai-doctor/blob/main/mydoctor/mydoctor-ui/elm-land.json" rel="noopener noreferrer"&gt;&lt;code&gt;elm-land.json&lt;/code&gt;&lt;/a&gt; file, and I also declared &lt;code&gt;AUTH_SERVER_URL&lt;/code&gt; for Keycloak server address and &lt;code&gt;WS_BASE_URL&lt;/code&gt; for backend chatbot WebSocket address.&lt;/p&gt;

&lt;p&gt;Then, you can pass &lt;code&gt;API_BASE_URL&lt;/code&gt; in the command line when developing locally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;API_BASE_URL=http://api.mydoctor:8081/api/v1 elm-land server
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For production builds, use the &lt;code&gt;ENV&lt;/code&gt; directive in the &lt;code&gt;Dockerfile&lt;/code&gt; like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; API_BASE_URL="http://api.mydoctor/api/v1"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;See my &lt;a href="https://github.com/jiwhiz/my-ai-doctor/blob/main/mydoctor/mydoctor-ui/Dockerfile" rel="noopener noreferrer"&gt;&lt;code&gt;Dockerfile&lt;/code&gt;&lt;/a&gt; for mydoctor-ui app.&lt;/p&gt;

&lt;p&gt;You can find all source code at &lt;a href="https://github.com/jiwhiz/my-ai-doctor" rel="noopener noreferrer"&gt;https://github.com/jiwhiz/my-ai-doctor&lt;/a&gt;. This is still a work in progress. Please let me know if you find this blog is helpful.&lt;/p&gt;

</description>
      <category>elmland</category>
      <category>docker</category>
      <category>env</category>
    </item>
    <item>
      <title>Using ChatGPT o1 to write UI code with Elm</title>
      <dc:creator>Yuan Ji</dc:creator>
      <pubDate>Tue, 17 Sep 2024 03:37:25 +0000</pubDate>
      <link>https://dev.to/jiwhiz/using-chatgpt-o1-to-write-ui-code-with-elm-2cb7</link>
      <guid>https://dev.to/jiwhiz/using-chatgpt-o1-to-write-ui-code-with-elm-2cb7</guid>
      <description>&lt;p&gt;After I finished my &lt;a href="https://github.com/jiwhiz/keycloak-spring-security-oauth2-token-exchange-demo" rel="noopener noreferrer"&gt;Oauth 2 Token Exchange demo project&lt;/a&gt;, I suddenly had a desire to write the front end UI with &lt;a href="https://elm-lang.org/" rel="noopener noreferrer"&gt;Elm language&lt;/a&gt;. Elm is my favorite front end language: elegant, functional, strongly typed.&lt;/p&gt;

&lt;p&gt;But I haven't touched Elm for over a year, so it would take a while to remember how to start. Fortunately, or coincidentally, &lt;a href="https://openai.com/index/introducing-openai-o1-preview/" rel="noopener noreferrer"&gt;ChatGPT o1 just came out&lt;/a&gt;, and I wanted to see if it is as good as people say.&lt;/p&gt;

&lt;p&gt;So I wrote a detailed prompt and asked ChatGPT o1 to give me a simple web application using the Elm language and the elm-land framework, also using keycloak to do login with port. I love the &lt;a href="https://elm.land/" rel="noopener noreferrer"&gt;Elm Land framework&lt;/a&gt;, and it is still well maintained today. &lt;/p&gt;

&lt;p&gt;It gave me step-by-step instructions on how to setup the &lt;code&gt;mydoctor-elm&lt;/code&gt; project, exactly the same as what Elm Land document says, but using my project name instead. Excellent job!&lt;/p&gt;

&lt;p&gt;Then it gave me &lt;code&gt;Main.elm&lt;/code&gt; code with port implementation to connect with &lt;code&gt;keycloak.js&lt;/code&gt;.  I copied to VS Code, and, to my surprise, it passed Elm compiler's type check!&lt;/p&gt;

&lt;p&gt;Unfortunately the javascript portion of the code was not correct; it resembled interop examples from the &lt;a href="https://guide.elm-lang.org/interop/ports" rel="noopener noreferrer"&gt;official Elm documentation&lt;/a&gt; or online examples. The issue is that, as a framework, Elm Land has its own way to handle port with Javascript; the structure of the JS code is quite different. I think it is because there isn't enough training data (meaning not many open-source projects using Elm Land with ports) for ChatGPT to figure out how to write Javascript code within Elm Land constraints. Besides, its code also didn't handle keycloak login well.&lt;/p&gt;

&lt;p&gt;So I read the &lt;a href="https://elm.land/guide/working-with-js.html" rel="noopener noreferrer"&gt;Elm Land documentation&lt;/a&gt; again and wrote my own code. I have to say Elm Land documentation is really good! I quickly finished &lt;code&gt;interop.js&lt;/code&gt; and got it working. During the coding process, I asked ChatGPT serval questions about how to handle promise, how to use 'check-sso' for silent authentication, and it gave me correct answers, because those are very common questions.&lt;/p&gt;

&lt;p&gt;I posted my &lt;code&gt;interop.js&lt;/code&gt; code here so next time when someone ask ChatGPT how to connect keycloak.js with port in Elm Land project, they can get some code that works:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Keycloak&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;keycloak-js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;keycloak&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Keycloak&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://auth.mydoctor:8080&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;realm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mydoctor-demo&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;clientId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mydoctor-elm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;


&lt;span class="c1"&gt;// This is called BEFORE your Elm app starts up&lt;/span&gt;
&lt;span class="c1"&gt;// &lt;/span&gt;
&lt;span class="c1"&gt;// The value returned here will be passed as flags &lt;/span&gt;
&lt;span class="c1"&gt;// into your `Shared.init` function.&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;flags&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// This is called AFTER your Elm app starts up&lt;/span&gt;
&lt;span class="c1"&gt;//&lt;/span&gt;
&lt;span class="c1"&gt;// Here you can work with `app.ports` to send messages&lt;/span&gt;
&lt;span class="c1"&gt;// to your Elm application, or subscribe to incoming&lt;/span&gt;
&lt;span class="c1"&gt;// messages from Elm&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;onReady&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="c1"&gt;// Initialize Keycloak&lt;/span&gt;
    &lt;span class="nx"&gt;keycloak&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;onLoad&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;check-sso&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;silentCheckSsoRedirectUri&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;origin&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/assets/silent-check-sso.html&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;keycloak&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;authenticated&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ports&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onLoginSuccess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;keycloak&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ports&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ports&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;login&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ports&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;logout&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

        &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ports&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;login&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;keycloak&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;login&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;keycloak&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;authenticated&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ports&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onLoginSuccess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;keycloak&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;})&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Failed to login Keycloak&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="p"&gt;});&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;

        &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ports&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;logout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Call logout()&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nx"&gt;keycloak&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;logout&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;After logout: &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;keycloak&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;authenticated&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="p"&gt;})&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Failed to logout Keycloak&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="p"&gt;});&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The full &lt;code&gt;mydoctor-elm&lt;/code&gt; project can be found at &lt;a href="https://github.com/jiwhiz/keycloak-spring-security-oauth2-token-exchange-demo/tree/main/mydoctor/mydoctor-elm" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Overall, after this simple experiment, I was very impressed that its Elm code passed type checking on the first try. However, for situations where answers are not easily found online, it still generates incorrect code, so developers can not fully trust it. For simple tasks, I don't need to search online and read many pages to find the answer, just ask ChatGPT, and the results are pretty good. So ChatGPT o1 is a better coding assistant, my productivity improved a lot. What is your experience with ChatGPT o1 as a developer?&lt;/p&gt;

</description>
      <category>chatgpto1</category>
      <category>elm</category>
      <category>keycloak</category>
      <category>elmland</category>
    </item>
    <item>
      <title>OAuth 2 Token Exchange with Spring Security and Keycloak</title>
      <dc:creator>Yuan Ji</dc:creator>
      <pubDate>Sat, 14 Sep 2024 07:07:45 +0000</pubDate>
      <link>https://dev.to/jiwhiz/oauth-2-token-exchange-with-spring-security-and-keycloak-1a6i</link>
      <guid>https://dev.to/jiwhiz/oauth-2-token-exchange-with-spring-security-and-keycloak-1a6i</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;In today's interconnected digital landscape, companies often collaborate to provide seamless services to their users. In this post, we’ll explore a scenario involving two hypothetical companies: &lt;strong&gt;MyDoctor&lt;/strong&gt; and &lt;strong&gt;MyHealth&lt;/strong&gt;. We’ll demonstrate how &lt;strong&gt;MyHealth&lt;/strong&gt; users can log in to &lt;strong&gt;MyDoctor&lt;/strong&gt; using their &lt;strong&gt;MyHealth&lt;/strong&gt; credentials, and how &lt;strong&gt;MyDoctor&lt;/strong&gt;'s backend can securely call &lt;strong&gt;MyHealth&lt;/strong&gt;'s APIs on behalf of the user. To achieve this, we’ll leverage &lt;a href="https://datatracker.ietf.org/doc/html/rfc8693" rel="noopener noreferrer"&gt;OAuth 2 Token Exchange (RFC8693)&lt;/a&gt; with &lt;a href="https://spring.io/projects/spring-security" rel="noopener noreferrer"&gt;Spring Security&lt;/a&gt; and &lt;a href="https://www.keycloak.org/" rel="noopener noreferrer"&gt;Keycloak&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;All the code can be found in my GitHub project &lt;a href="https://github.com/jiwhiz/keycloak-spring-security-oauth2-token-exchange-demo" rel="noopener noreferrer"&gt;here&lt;/a&gt;. &lt;/p&gt;

&lt;h2&gt;
  
  
  Business Scenario
&lt;/h2&gt;

&lt;p&gt;Let’s start by outlining the scenario:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;MyHealth&lt;/strong&gt;: A service where users can view their health records.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;MyDoctor&lt;/strong&gt;: A service where users can register, log in, and talk to AI doctors to get medical advice.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now, imagine &lt;strong&gt;MyHealth&lt;/strong&gt; users want to access &lt;strong&gt;MyDoctor&lt;/strong&gt; without creating a new account. Additionally, for &lt;strong&gt;MyDoctor&lt;/strong&gt;'s AI bot to give better advice, the backend needs to securely access &lt;strong&gt;MyHealth&lt;/strong&gt;'s APIs to retrieve user health records or other user-specific data.&lt;/p&gt;

&lt;p&gt;In the past, this could have been done using &lt;a href="https://en.wikipedia.org/wiki/API_key" rel="noopener noreferrer"&gt;API key&lt;/a&gt;, where &lt;strong&gt;MyHealth&lt;/strong&gt; would assign a unique secret key to &lt;strong&gt;MyDoctor&lt;/strong&gt; for accessing &lt;strong&gt;MyHealth&lt;/strong&gt; API. However, this approach only authenticates the application, meaning &lt;strong&gt;MyDoctor&lt;/strong&gt; would have full access to all of &lt;strong&gt;MyHealth&lt;/strong&gt;’s data, which raises trust and security concerns.&lt;/p&gt;

&lt;p&gt;To avoid this, &lt;strong&gt;MyHealth&lt;/strong&gt; needs to authenticate the actual user and apply proper access controls. So, how can &lt;strong&gt;MyDoctor&lt;/strong&gt; impersonate the current logged-in user and access &lt;strong&gt;MyHealth&lt;/strong&gt;’s APIs? We can use OAuth 2’s Token Exchange extension, which allows one token (e.g., from MyDoctor) to be exchanged for another token (e.g., from MyHealth) that has the correct permissions for that user.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understanding OAuth 2 Token Exchange
&lt;/h2&gt;

&lt;p&gt;Token Exchange &lt;a href="https://datatracker.ietf.org/doc/html/rfc8693" rel="noopener noreferrer"&gt;RFC 8693&lt;/a&gt; is an extension to the standard OAuth 2.0 that allows a client to exchange an existing token for another token. This is particularly useful in scenarios where:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A client needs to access resources on behalf of the user from multiple APIs.&lt;/li&gt;
&lt;li&gt;There is a need to exchange one token (e.g., an ID token or access token) for another token (e.g., an access token with specific permissions or scope).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In this scenario:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;MyHealth&lt;/strong&gt; user logs into &lt;strong&gt;MyDoctor&lt;/strong&gt; via Keycloak’s identity provider linking.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;MyDoctor&lt;/strong&gt; backend exchanges the received token for one that allows it to access &lt;strong&gt;MyHealth&lt;/strong&gt;'s APIs.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Keycloak Setup and Linking Identity Providers
&lt;/h2&gt;

&lt;p&gt;To enable &lt;strong&gt;MyHealth&lt;/strong&gt; users to log in to &lt;strong&gt;MyDoctor&lt;/strong&gt; via their &lt;strong&gt;MyHealth&lt;/strong&gt; account, we need to configure &lt;strong&gt;Keycloak&lt;/strong&gt; to act as the identity provider (IdP). Since Token Exchange is a preview feature, you have to start the Keycloak server with &lt;code&gt;--features=preview&lt;/code&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  Setting up Keycloak for MyHealth
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;Create a new realm &lt;code&gt;myhealth-demo&lt;/code&gt; for the &lt;strong&gt;MyHealth&lt;/strong&gt; keycloak server.&lt;/li&gt;
&lt;li&gt;Under the &lt;code&gt;myhealth-demo&lt;/code&gt; realm, create a new client &lt;code&gt;myhealth-ui&lt;/code&gt; for &lt;strong&gt;MyHealth&lt;/strong&gt;'s frontend. Add the client role &lt;code&gt;view-health-record&lt;/code&gt;. Since it is a SPA web application, disable &lt;code&gt;Client authentication&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Add a user John Doe, login id &lt;code&gt;john&lt;/code&gt;, password &lt;code&gt;john&lt;/code&gt; with client role &lt;code&gt;view-health-record&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Create a new client &lt;code&gt;mydoctor-api-server&lt;/code&gt; for the &lt;strong&gt;MyDoctor&lt;/strong&gt; backend API server to access &lt;strong&gt;MyHealth&lt;/strong&gt; API endpoints.&lt;/li&gt;
&lt;li&gt;Create another client &lt;code&gt;mydoctor-auth&lt;/code&gt; for &lt;strong&gt;MyDoctor&lt;/strong&gt; keycloak server to link &lt;strong&gt;MyHealth&lt;/strong&gt; keycloak server as an identity provider.&lt;/li&gt;
&lt;/ol&gt;

&lt;h4&gt;
  
  
  Setting up Keycloak for MyDoctor
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;Create a new realm &lt;code&gt;mydoctor-demo&lt;/code&gt; for the &lt;strong&gt;MyDoctor&lt;/strong&gt; keycloak server.&lt;/li&gt;
&lt;li&gt;Under the &lt;code&gt;mydoctor-demo&lt;/code&gt; realm, create a new client &lt;code&gt;mydoctor-ui&lt;/code&gt; for &lt;strong&gt;MyDoctor&lt;/strong&gt;'s frontend. Add client role &lt;code&gt;edit-appointment&lt;/code&gt; and &lt;code&gt;view-appointment&lt;/code&gt;. Disable &lt;code&gt;Client authentication&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Add a user &lt;code&gt;doctor&lt;/code&gt; with the client role &lt;code&gt;edit-appointment&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Add another client &lt;code&gt;mydoctor-api&lt;/code&gt; for the backend API server to do token exchange requests.&lt;/li&gt;
&lt;li&gt;In &lt;code&gt;Realm settings&lt;/code&gt;, select &lt;code&gt;User registration&lt;/code&gt; tab, click button &lt;code&gt;Assign role&lt;/code&gt; to add role &lt;code&gt;view-appointment&lt;/code&gt;, so any new user will automatically have this role.&lt;/li&gt;
&lt;/ol&gt;

&lt;h4&gt;
  
  
  Adding MyHealth as an Identity Provider:
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;In the &lt;strong&gt;MyDoctor&lt;/strong&gt; keycloak server under &lt;code&gt;mydoctor-demo&lt;/code&gt; realm, navigate to &lt;strong&gt;Identity Providers&lt;/strong&gt; and select &lt;strong&gt;OpenID Connect&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Enter the details of the &lt;strong&gt;MyHealth&lt;/strong&gt; Keycloak server, using the well-known OpenID configuration URL &lt;code&gt;http://auth.myhealth:8090/realms/myhealth-demo/.well-known/openid-configuration&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Enter client ID and secret of &lt;code&gt;mydoctor-auth&lt;/code&gt; client from &lt;strong&gt;MyHealth&lt;/strong&gt; keycloak server.&lt;/li&gt;
&lt;li&gt;Turn on &lt;code&gt;Store tokens&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Follow the Keycloak documentation &lt;a href="https://www.keycloak.org/docs/latest/securing_apps/#internal-token-to-external-token-exchange" rel="noopener noreferrer"&gt;7.3. Internal token to external token exchange&lt;/a&gt;, to enable permissions for token exchange, and create a client policy.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It is very tedious to setup Keycloak servers correctly, however, you can find the step-by-step instructions in my Github project to start two Keycloak servers with docker and take a look at all the settings. Just login to Keycloak servers using &lt;code&gt;admin&lt;/code&gt; as username and password at &lt;a href="http://mydoctor:8080/admin/master/console/#/mydoctor-demo" rel="noopener noreferrer"&gt;http://mydoctor:8080&lt;/a&gt; and &lt;a href="http://myhealth:8090/admin/master/console/#/myhealth-demo" rel="noopener noreferrer"&gt;http://myhealth:8090&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementing OAuth 2 Token Exchange in Spring Security
&lt;/h2&gt;

&lt;p&gt;Token Exchange has been &lt;a href="https://spring.io/blog/2024/03/19/token-exchange-support-in-spring-security-6-3-0-m3" rel="noopener noreferrer"&gt;supported in Spring Security since version 6.3&lt;/a&gt;. Below, we will demonstrate how &lt;strong&gt;MyDoctor&lt;/strong&gt;’s backend can use this feature to retrieve the  health records of a logged-in &lt;strong&gt;MyHealth&lt;/strong&gt; user.&lt;/p&gt;

&lt;h4&gt;
  
  
  Configure MyHealth API Server App:
&lt;/h4&gt;

&lt;p&gt;The &lt;strong&gt;MyHealth&lt;/strong&gt; backend API &lt;code&gt;myhealth-api&lt;/code&gt; is an OAuth 2 resource server. Add the following dependency in &lt;code&gt;build.gradle&lt;/code&gt;:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight gradle"&gt;&lt;code&gt;

    &lt;span class="n"&gt;implementation&lt;/span&gt; &lt;span class="s1"&gt;'org.springframework.boot:spring-boot-starter-oauth2-resource-server'&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Config spring security in &lt;code&gt;ProjectConfig&lt;/code&gt;:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;

&lt;span class="nd"&gt;@Configuration&lt;/span&gt;
&lt;span class="nd"&gt;@EnableWebSecurity&lt;/span&gt;
&lt;span class="nd"&gt;@EnableMethodSecurity&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;securedEnabled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProjectConfig&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nd"&gt;@Bean&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;SecurityFilterChain&lt;/span&gt; &lt;span class="nf"&gt;filterChain&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;HttpSecurity&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;throws&lt;/span&gt; &lt;span class="nc"&gt;Exception&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// @formatter:off&lt;/span&gt;
        &lt;span class="n"&gt;http&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;csrf&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;AbstractHttpConfigurer:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;disable&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;cors&lt;/span&gt;&lt;span class="o"&gt;((&lt;/span&gt;&lt;span class="n"&gt;cors&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;cors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;configurationSource&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;corsConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;CorsConfiguration&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
                &lt;span class="n"&gt;corsConfig&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setAllowedOrigins&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"http://myhealth:4210"&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
                &lt;span class="n"&gt;corsConfig&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setAllowedMethods&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
                    &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"GET"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"POST"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"OPTIONS"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"PUT"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"DELETE"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;);&lt;/span&gt;
                &lt;span class="n"&gt;corsConfig&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setAllowedHeaders&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"*"&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
                &lt;span class="n"&gt;corsConfig&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setAllowCredentials&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;corsConfig&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
            &lt;span class="o"&gt;}))&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;authorizeHttpRequests&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;authorize&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;authorize&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;requestMatchers&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;HttpMethod&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;OPTIONS&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"/**"&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;permitAll&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;requestMatchers&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/api/records"&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;hasAuthority&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ROLE_view_health_record"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;anyRequest&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;authenticated&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;oauth2ResourceServer&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;oauth2ResourceServer&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;oauth2ResourceServer&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;jwt&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="n"&gt;jwt&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;jwt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;jwtAuthenticationConverter&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;KeycloakJwtAuthenticationConverter&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"myhealth-ui"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"mydoctor-api-server"&lt;/span&gt;&lt;span class="o"&gt;))))&lt;/span&gt;
            &lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;;&lt;/span&gt;
        &lt;span class="c1"&gt;// @formatter:on&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Bean&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;JwtDecoder&lt;/span&gt; &lt;span class="nf"&gt;jwtDecoder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OAuth2ResourceServerProperties&lt;/span&gt; &lt;span class="n"&gt;oAuth2ResourceServerProperties&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;NimbusJwtDecoder&lt;/span&gt; &lt;span class="n"&gt;jwtDecoder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;NimbusJwtDecoder&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withJwkSetUri&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;oAuth2ResourceServerProperties&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getJwt&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getJwkSetUri&lt;/span&gt;&lt;span class="o"&gt;()).&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;jwtDecoder&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setJwtValidator&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;JwtValidators&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;createDefaultWithIssuer&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;oAuth2ResourceServerProperties&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getJwt&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getIssuerUri&lt;/span&gt;&lt;span class="o"&gt;()));&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;jwtDecoder&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;KeycloakJwtAuthenticationConverter&lt;/code&gt; is the &lt;a href="https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/core/convert/converter/Converter.html" rel="noopener noreferrer"&gt;Spring Converter&lt;/a&gt; to translate OAuth 2 access token claims of &lt;code&gt;resource_access&lt;/code&gt; to Spring Security &lt;code&gt;GrantedAuthority&lt;/code&gt;. I copied the code from StackOverflow discussion &lt;a href="https://stackoverflow.com/questions/72226464/how-configure-the-jwtauthenticationconverter-for-a-specific-claim-structure/72301501#72301501" rel="noopener noreferrer"&gt;How configure the JwtAuthenticationConverter for a specific claim structure?&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And here is the config for resource server in application.yml:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;

&lt;span class="na"&gt;spring&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;security&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;oauth2&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;resourceserver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;jwt&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;issuer-uri&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://auth.myhealth:8090/realms/myhealth-demo&lt;/span&gt;
          &lt;span class="na"&gt;jwk-set-uri&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://auth.myhealth:8090/realms/myhealth-demo/protocol/openid-connect/certs&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h4&gt;
  
  
  Configure &lt;strong&gt;MyDoctor&lt;/strong&gt; API Server App:
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;MyDoctor&lt;/strong&gt; API backend needs to call &lt;strong&gt;MyHealth&lt;/strong&gt; API, so it acts as both an OAuth 2 Resource Server and an OAuth 2 Client. Add these dependencies in&lt;code&gt;build.gradle&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight gradle"&gt;&lt;code&gt;

    &lt;span class="n"&gt;implementation&lt;/span&gt; &lt;span class="s1"&gt;'org.springframework.boot:spring-boot-starter-oauth2-resource-server'&lt;/span&gt;
    &lt;span class="n"&gt;implementation&lt;/span&gt; &lt;span class="s1"&gt;'org.springframework.boot:spring-boot-starter-oauth2-client'&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;In application.yml, configure the two clients &lt;code&gt;myhealth-client&lt;/code&gt; and &lt;code&gt;mydoctor-client&lt;/code&gt;:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;

&lt;span class="na"&gt;spring&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;security&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;oauth2&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;resourceserver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;jwt&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;issuer-uri&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://auth.mydoctor:8080/realms/mydoctor-demo&lt;/span&gt;
          &lt;span class="na"&gt;jwk-set-uri&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://auth.mydoctor:8080/realms/mydoctor-demo/protocol/openid-connect/certs&lt;/span&gt;
      &lt;span class="na"&gt;client&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;registration&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;myhealth-client&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;client-id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mydoctor-api-server&lt;/span&gt;
            &lt;span class="na"&gt;authorization-grant-type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;urn:ietf:params:oauth:grant-type:jwt-bearer&lt;/span&gt;
            &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;health-auth-provider&lt;/span&gt;
          &lt;span class="na"&gt;mydoctor-client&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;client-id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mydoctor-api&lt;/span&gt;
            &lt;span class="na"&gt;client-secret&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nvYxjQFYGdNI8zj5Nb3Jz25ezWgN1cE8&lt;/span&gt;
            &lt;span class="na"&gt;client-authentication-method&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;client_secret_basic&lt;/span&gt;
            &lt;span class="na"&gt;authorization-grant-type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;urn:ietf:params:oauth:grant-type:token-exchange&lt;/span&gt;
            &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;doctor-auth-provider&lt;/span&gt;
        &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;doctor-auth-provider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;token-uri&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://auth.mydoctor:8080/realms/mydoctor-demo/protocol/openid-connect/token&lt;/span&gt;
          &lt;span class="na"&gt;health-auth-provider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;token-uri&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://auth.myhealth:8090/realms/myhealth-demo/protocol/openid-connect/token&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;You can see those two clients have two different providers.&lt;/p&gt;

&lt;p&gt;When &lt;strong&gt;MyDoctor&lt;/strong&gt; UI calls backend API endpoint of &lt;code&gt;http://api.mydoctor:8081/api/records&lt;/code&gt;, backend API server actually calls &lt;strong&gt;MyHealth&lt;/strong&gt; API endpoint &lt;code&gt;http://api.myhealth:8082/api/record&lt;/code&gt; using WebClient:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;

&lt;span class="nd"&gt;@RestController&lt;/span&gt;
&lt;span class="nd"&gt;@RequestMapping&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/api"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="nd"&gt;@RequiredArgsConstructor&lt;/span&gt;
&lt;span class="nd"&gt;@Slf4j&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RecordController&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;WebClient&lt;/span&gt; &lt;span class="n"&gt;webClient&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;@GetMapping&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/records"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Message&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;getHealthRecords&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
        &lt;span class="nd"&gt;@RegisteredOAuth2AuthorizedClient&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"mydoctor-client"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="nc"&gt;OAuth2AuthorizedClient&lt;/span&gt; &lt;span class="n"&gt;doctorAuthClient&lt;/span&gt;
    &lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;doctorAuthClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getAccessToken&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getTokenValue&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;debug&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Exchanged access token is:\n {}\n"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;webClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;uri&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"http://api.myhealth:8082/api/records"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;((&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setBearerAuth&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;retrieve&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;bodyToMono&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ParameterizedTypeReference&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Message&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;()&lt;/span&gt; &lt;span class="o"&gt;{})&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;block&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;debug&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Return result from MyHealth API Server: {}"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;We have to impersonate current user as &lt;strong&gt;MyHealth&lt;/strong&gt; user, so we add bearer token to WebClient http request header. The token is coming from injected &lt;code&gt;doctorAuthClient&lt;/code&gt;, which is resolved by &lt;code&gt;OAuth2AuthorizedClientArgumentResolver&lt;/code&gt; using client registration ID &lt;code&gt;mydoctor-client&lt;/code&gt;. This client will call &lt;strong&gt;MyDoctor&lt;/strong&gt; keycloak server to do Token Exchange, passing current login user access token from &lt;strong&gt;MyDoctor&lt;/strong&gt; keycloak, and get a new access token from &lt;strong&gt;MyHealth&lt;/strong&gt; keycloak server.&lt;/p&gt;

&lt;p&gt;All the tricky parts are in Spring Security configuration:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;

    &lt;span class="nc"&gt;OAuth2AuthorizedClientProvider&lt;/span&gt; &lt;span class="nf"&gt;tokenExchange&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
        &lt;span class="nc"&gt;ClientRegistrationRepository&lt;/span&gt; &lt;span class="n"&gt;clientRegistrationRepository&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
        &lt;span class="nc"&gt;OAuth2AuthorizedClientRepository&lt;/span&gt; &lt;span class="n"&gt;authorizedClientRepository&lt;/span&gt;
    &lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;Function&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;OAuth2AuthorizationContext&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;OAuth2Token&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;subjectResolver&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getPrincipal&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nc"&gt;JwtAuthenticationToken&lt;/span&gt; &lt;span class="n"&gt;jwtAuthenticationToken&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                &lt;span class="nc"&gt;Jwt&lt;/span&gt; &lt;span class="n"&gt;jwt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jwtAuthenticationToken&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getToken&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
                &lt;span class="nc"&gt;OAuth2AccessToken&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;OAuth2AccessToken&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;TokenType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;BEARER&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;jwt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getTokenValue&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;jwt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getIssuedAt&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;jwt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getExpiresAt&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
                &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;debug&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Get Access Token for current user from JwtAuthenticationToken: {}"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getTokenValue&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
            &lt;span class="o"&gt;}&lt;/span&gt;

            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;RuntimeException&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Cannot resolve subject token with context principal "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getPrincipal&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="o"&gt;};&lt;/span&gt;

        &lt;span class="nc"&gt;Converter&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;TokenExchangeGrantRequest&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;RequestEntity&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;?&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;requestEntityConverter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TokenExchangeGrantRequestEntityConverter&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="nd"&gt;@Override&lt;/span&gt;
            &lt;span class="kd"&gt;protected&lt;/span&gt; &lt;span class="nc"&gt;MultiValueMap&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;createParameters&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;TokenExchangeGrantRequest&lt;/span&gt; &lt;span class="n"&gt;grantRequest&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                &lt;span class="nc"&gt;MultiValueMap&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;parameters&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;super&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;createParameters&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;grantRequest&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
                &lt;span class="n"&gt;parameters&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"requested_issuer"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"myhealth-keycloak-oidc"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;parameters&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
            &lt;span class="o"&gt;}&lt;/span&gt;
        &lt;span class="o"&gt;};&lt;/span&gt;
        &lt;span class="nc"&gt;DefaultTokenExchangeTokenResponseClient&lt;/span&gt; &lt;span class="n"&gt;accessTokenResponseClient&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;DefaultTokenExchangeTokenResponseClient&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;accessTokenResponseClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setRequestEntityConverter&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;requestEntityConverter&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="nc"&gt;TokenExchangeOAuth2AuthorizedClientProvider&lt;/span&gt; &lt;span class="n"&gt;authorizedClientProvider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
                &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;TokenExchangeOAuth2AuthorizedClientProvider&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;authorizedClientProvider&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setSubjectTokenResolver&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;subjectResolver&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;authorizedClientProvider&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setAccessTokenResponseClient&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;accessTokenResponseClient&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;authorizedClientProvider&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Bean&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;OAuth2AuthorizedClientManager&lt;/span&gt; &lt;span class="nf"&gt;authorizedClientManager&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
        &lt;span class="nc"&gt;ClientRegistrationRepository&lt;/span&gt; &lt;span class="n"&gt;clientRegistrationRepository&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
        &lt;span class="nc"&gt;OAuth2AuthorizedClientService&lt;/span&gt; &lt;span class="n"&gt;clientService&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
        &lt;span class="nc"&gt;OAuth2AuthorizedClientRepository&lt;/span&gt; &lt;span class="n"&gt;authorizedClientRepository&lt;/span&gt;
    &lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;OAuth2AuthorizedClientProvider&lt;/span&gt; &lt;span class="n"&gt;authorizedClientProvider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
                &lt;span class="nc"&gt;OAuth2AuthorizedClientProviderBuilder&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
                        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;refreshToken&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
                        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;clientCredentials&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
                        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tokenExchange&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;clientRegistrationRepository&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;authorizedClientRepository&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
                        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

        &lt;span class="nc"&gt;AuthorizedClientServiceOAuth2AuthorizedClientManager&lt;/span&gt; &lt;span class="n"&gt;authorizedClientManager&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
                &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;AuthorizedClientServiceOAuth2AuthorizedClientManager&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;clientRegistrationRepository&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;clientService&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="n"&gt;authorizedClientManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setAuthorizedClientProvider&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;authorizedClientProvider&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;authorizedClientManager&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Bean&lt;/span&gt;
    &lt;span class="nc"&gt;WebClient&lt;/span&gt; &lt;span class="nf"&gt;webClient&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OAuth2AuthorizedClientManager&lt;/span&gt; &lt;span class="n"&gt;authorizedClientManager&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;ServletOAuth2AuthorizedClientExchangeFilterFunction&lt;/span&gt; &lt;span class="n"&gt;oauth2Client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
                &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ServletOAuth2AuthorizedClientExchangeFilterFunction&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;authorizedClientManager&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;oauth2Client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setDefaultClientRegistrationId&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"myhealth-client"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;WebClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;apply&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;oauth2Client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;oauth2Configuration&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;It took me several days to get it working! I got the idea from Spring Security project &lt;a href="https://github.com/spring-projects/spring-security/issues/15408" rel="noopener noreferrer"&gt;issue discussion&lt;/a&gt;, and &lt;a href="https://www.keycloak.org/docs/latest/securing_apps/#_token-exchange" rel="noopener noreferrer"&gt;Keycloak documentation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;First, using &lt;code&gt;subjectResolver&lt;/code&gt; to get JWT token from current security context, because we are using &lt;code&gt;KeycloakJwtAuthenticationConverter&lt;/code&gt; to store &lt;code&gt;JwtAuthenticationToken&lt;/code&gt; object as &lt;code&gt;Principal&lt;/code&gt;. Pass it to &lt;code&gt;TokenExchangeOAuth2AuthorizedClientProvider&lt;/code&gt;, so this client provider can call &lt;strong&gt;MyDoctor&lt;/strong&gt; keycloak server with current login user’s access token.&lt;/p&gt;

&lt;p&gt;Second, Keycloak server implements RFC8693 Token Exchange a little differently:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Token exchange in Keycloak is a very loose implementation of the OAuth Token Exchange specification at the IETF. We have extended it a little, ignored some of it, and loosely interpreted other parts of the specification.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The Keycloak token exchange request parameters include &lt;code&gt;requested_issuer&lt;/code&gt;, which is the alias of the ID provider in &lt;strong&gt;MyDoctor&lt;/strong&gt; configuration, the &lt;code&gt;MyHealth Keycloak&lt;/code&gt;, and its alias is &lt;code&gt;myhealth-keycloak-oidc&lt;/code&gt;. Since &lt;code&gt;requested_issuer&lt;/code&gt; is not &lt;a href="https://datatracker.ietf.org/doc/html/rfc8693#name-request" rel="noopener noreferrer"&gt;the standard RFC8693 request parameters&lt;/a&gt;, Spring Security doesn’t support it as well. Fortunately I can hack &lt;code&gt;TokenExchangeOAuth2AuthorizedClientProvider&lt;/code&gt; with a customized &lt;code&gt;OAuth2AccessTokenResponseClient&lt;/code&gt; to set &lt;code&gt;requested_issuer&lt;/code&gt; parameter inside &lt;code&gt;TokenExchangeGrantRequestEntityConverter&lt;/code&gt;. See above code in &lt;code&gt;tokenExchange()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Third, config &lt;code&gt;OAuth2AuthorizedClientManager&lt;/code&gt; with &lt;code&gt;OAuth2AuthorizedClientProvider&lt;/code&gt; from  &lt;code&gt;tokenExchange()&lt;/code&gt;, so our &lt;code&gt;doctorAuthClient&lt;/code&gt; above can call &lt;strong&gt;MyDoctor&lt;/strong&gt; Keycloak server to do token exchange as client ID &lt;code&gt;mydoctor-api&lt;/code&gt;!&lt;/p&gt;

&lt;p&gt;Last, build &lt;code&gt;WebClient&lt;/code&gt; with &lt;code&gt;ServletOAuth2AuthorizedClientExchangeFilterFunction&lt;/code&gt;, passing client registration ID &lt;code&gt;myhealth-client&lt;/code&gt;, so this &lt;code&gt;WebClient&lt;/code&gt; will be able to call &lt;strong&gt;MyHealth&lt;/strong&gt; API as client ID &lt;code&gt;mydoctor-api-server&lt;/code&gt;, but we use the access token exchanged from &lt;strong&gt;MyDoctor&lt;/strong&gt; Keycloak server as bearer token.&lt;/p&gt;

&lt;p&gt;You can open &lt;strong&gt;MyDoctor&lt;/strong&gt; UI at &lt;code&gt;http://mydoctor:4200&lt;/code&gt;, &lt;br&gt;
&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbu0apim7ri9bzhmvksae.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbu0apim7ri9bzhmvksae.png" alt="MyDoctor Website"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;and click &lt;code&gt;Login&lt;/code&gt; button, to redirect to &lt;strong&gt;MyDoctor&lt;/strong&gt; Keycloak Login Page:&lt;br&gt;
&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fayso7sf9rlr5mzqjc1fs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fayso7sf9rlr5mzqjc1fs.png" alt="MyDoctor Keycloak Login"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then click &lt;code&gt;MyHealth Keycloak&lt;/code&gt; button to login with &lt;strong&gt;MyHealth&lt;/strong&gt; Keycloak server:&lt;br&gt;
&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffp5ivo9i55petuss1j26.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffp5ivo9i55petuss1j26.png" alt="MyHealth Keycloak Login"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can see the host changed from &lt;code&gt;auth.mydoctor:8080&lt;/code&gt; to &lt;code&gt;auth.myhealth:8090&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Login with &lt;strong&gt;MyHealth&lt;/strong&gt; user John Doe (username and password as &lt;code&gt;john&lt;/code&gt;), you can see in &lt;strong&gt;MyDoctor&lt;/strong&gt; Keycloak server admin console, under &lt;code&gt;mydoctor-demo&lt;/code&gt; realm, a new user &lt;code&gt;john&lt;/code&gt; was added, and this user has links to another identity provider &lt;code&gt;Myhealth-keycloak-oidc&lt;/code&gt;.&lt;br&gt;
&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1fz3mx3vktwt4hio50o2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1fz3mx3vktwt4hio50o2.png" alt="MyDoctor Keycloak User"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In &lt;strong&gt;MyDoctor&lt;/strong&gt; UI, after login, you can click button &lt;code&gt;call http://api.mydoctor:8081/api/records&lt;/code&gt;, so UI will call &lt;strong&gt;MyDoctor&lt;/strong&gt; backend, and backend server actually will first call &lt;strong&gt;MyDoctor&lt;/strong&gt; Keycloak server to do Token Exchange, then call &lt;strong&gt;MyHealth&lt;/strong&gt; API server to get user health records.&lt;br&gt;
&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzt68aurk2xxvsblnyryc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzt68aurk2xxvsblnyryc.png" alt="MyDoctor UI"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;From &lt;code&gt;mydoctor-api&lt;/code&gt; server log, we can find out the access token used by &lt;strong&gt;MyDoctor&lt;/strong&gt; UI is &lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"exp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1726293365&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"iat"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1726293065&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"auth_time"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1726293065&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"jti"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"c01fc98f-f959-42ba-a25b-7e8e1a29dc12"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"iss"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http://auth.mydoctor:8080/realms/mydoctor-demo"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"aud"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"broker"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"account"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sub"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"5adba68d-c9cc-487d-a9e2-62bfae549483"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"typ"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bearer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"azp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mydoctor-ui"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"40cd230f-a5af-4308-84dc-e5f856bc0a0e"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"acr"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"allowed-origins"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"http://mydoctor:4200"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"realm_access"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"roles"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"default-roles-mydoctor-demo"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"offline_access"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"uma_authorization"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"resource_access"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"mydoctor-ui"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"roles"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"view-appointment"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"broker"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"roles"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"read-token"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"account"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"roles"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"manage-account"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"manage-account-links"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"view-profile"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scope"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"openid email profile"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"email_verified"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"John Doe"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"preferred_username"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"john"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"given_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"John"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"family_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Doe"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"john.doe@demo.com"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;


&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;And after token exchange, the access token used by &lt;strong&gt;MyDoctor&lt;/strong&gt; API server:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"exp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1726293365&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"iat"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1726293065&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"auth_time"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1726293065&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"jti"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"6c221b25-9b49-45e1-975a-88448104050d"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"iss"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http://auth.myhealth:8090/realms/myhealth-demo"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"aud"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"myhealth-ui"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"account"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sub"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1b0d98b6-ea18-4dbd-9d6e-a83e621c07c2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"typ"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bearer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"azp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mydoctor-auth"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"531b04de-1ac5-4b04-8125-4c8ca10872bf"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"acr"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"allowed-origins"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"http://auth.mydoctor:8080/*"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"realm_access"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"roles"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"default-roles-myhealth-demo"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"offline_access"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"uma_authorization"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"resource_access"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"myhealth-ui"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"roles"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"view-health-record"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"account"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"roles"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"manage-account"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"manage-account-links"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"view-profile"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scope"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"openid profile email"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"email_verified"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"John Doe"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"preferred_username"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"john"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"given_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"John"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"family_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Doe"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"john.doe@demo.com"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;


&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Before exchange, the access token has the role of &lt;code&gt;view-appointment&lt;/code&gt; for &lt;code&gt;mydoctor-ui&lt;/code&gt;, and after exchange, it has the role of &lt;code&gt;view-health-record&lt;/code&gt; for &lt;code&gt;myhealth-ui&lt;/code&gt;. Quite amazing!&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Recently, I designed and implemented an integration allowing users to log in to Company A using Company B’s account and access Company B’s APIs on the user’s behalf. By combining Keycloak’s identity provider linking and the OAuth 2 Token Exchange protocol, we can provide a seamless experience while maintaining secure API calls between different systems.&lt;/p&gt;

&lt;p&gt;However, as of September 2024, Token Exchange is still a preview feature in Keycloak, and Spring Security has only recently added support for it. I hope this guide helps others attempting to implement this functionality in their projects. &lt;/p&gt;

</description>
      <category>oauth2</category>
      <category>tokenexchange</category>
      <category>springsecurity</category>
      <category>keycloak</category>
    </item>
  </channel>
</rss>
