<?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: Mohammed Ammer</title>
    <description>The latest articles on DEV Community by Mohammed Ammer (@mohammedalics).</description>
    <link>https://dev.to/mohammedalics</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%2F929555%2F62e70b3c-f635-417b-9c65-e4e2455f82ae.jpeg</url>
      <title>DEV Community: Mohammed Ammer</title>
      <link>https://dev.to/mohammedalics</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mohammedalics"/>
    <language>en</language>
    <item>
      <title>Optimizing Keycloak Caches: Best Practices for Embedded and External Infinispan</title>
      <dc:creator>Mohammed Ammer</dc:creator>
      <pubDate>Mon, 16 Sep 2024 15:14:36 +0000</pubDate>
      <link>https://dev.to/mohammedalics/optimizing-keycloak-caches-best-practices-for-embedded-and-external-infinispan-l9e</link>
      <guid>https://dev.to/mohammedalics/optimizing-keycloak-caches-best-practices-for-embedded-and-external-infinispan-l9e</guid>
      <description>&lt;p&gt;Although setting up Keycloak is relatively straightforward, regardless of your infrastructure's complexity, optimizing its performance for your specific workload can be challenging.&lt;/p&gt;

&lt;p&gt;One common approach is to use an external Infinispan with a database persistence to store sessions outside of Keycloak, &lt;a href="https://github.com/keycloak/keycloak/discussions/28271" rel="noopener noreferrer"&gt;at least until version 26 makes the user session persistence feature&lt;/a&gt; (introduced in Keycloak version 25) a permanent part of Keycloak, moving beyond its previous preview status.&lt;/p&gt;

&lt;p&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%2Fjg4yucxyrewe5poczm9k.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%2Fjg4yucxyrewe5poczm9k.png" alt="Basic Keycloak Setup with External Infinispan Cluster and Database persistence"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  In-Memory Cache
&lt;/h1&gt;

&lt;p&gt;The biggest challenge many encounter with "in-memory" caches is managing memory usage. It's crucial for applications to have defined memory limits; otherwise, you risk facing endless issues, including memory overconsumption and frequent application crashes.&lt;/p&gt;

&lt;p&gt;At this point, we can identify two types of caches:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;External Infinispan Cache:&lt;/strong&gt; This operates independently of Keycloak but is used by Keycloak as a remote cache.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Embedded Infinispan Cache:&lt;/strong&gt; Managed within Keycloak, this cache allows data (e.g. sessions) to be shared across Keycloak nodes/containers, reducing the need for frequent reads from the external cache.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's start with the external Infinispan cache, as it's crucial for safeguarding sessions against any unexpected issues in the Keycloak cluster.&lt;/p&gt;

&lt;h2&gt;
  
  
  External Infinispan Cache
&lt;/h2&gt;

&lt;p&gt;As mentioned earlier, using an external Infinispan cache solves part of the problem, but since it still stores data in memory, there's a risk of data loss. Adding persistent storage (e.g., a database) ensures the data remains safe.&lt;/p&gt;

&lt;h3&gt;
  
  
  Limit the memory size
&lt;/h3&gt;

&lt;p&gt;With data persisted in the database, you can set a smaller memory limit for the Infinispan cache configuration to suit your cost-efficient resources. However, you must carefully assess the database capacity that matches your load, as less memory for cache means increased demand on the database. &lt;br&gt;
See &lt;a href="https://infinispan.org/docs/15.0.x/titles/configuring/configuring.html#configuring-eviction-total-entries_configuring-memory-usage" rel="noopener noreferrer"&gt;Configuring maximum count eviction&lt;/a&gt; from Infinispan Documentation&lt;/p&gt;

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

&lt;span class="nt"&gt;&amp;lt;distributed-cache&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;memory&lt;/span&gt; &lt;span class="na"&gt;max-count=&lt;/span&gt;&lt;span class="s"&gt;"10000"&lt;/span&gt; &lt;span class="na"&gt;when-full=&lt;/span&gt;&lt;span class="s"&gt;"REMOVE"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/distributed-cache&amp;gt;&lt;/span&gt;


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

&lt;/div&gt;
&lt;h3&gt;
  
  
  Avoid cache Preload
&lt;/h3&gt;

&lt;p&gt;When an Infinispan instance starts, it preloads sessions from the database into the cache, even if the cache is not currently in use, to meet the &lt;code&gt;max-count&lt;/code&gt; configured for each cache. To enable "lazy loading"—where Infinispan loads data only when needed—set the preload option to false. Additionally, ensure that the shared flag is set to true.&lt;br&gt;
See &lt;a href="https://infinispan.org/docs/stable/titles/configuring/configuring.html#configuring-single-file-stores_persistence" rel="noopener noreferrer"&gt;Configuring Persistence Store&lt;/a&gt; from Infinispan Documentation&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;

&lt;span class="nt"&gt;&amp;lt;distributed-cache&lt;/span&gt;  &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"sessions"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;persistence&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;string-keyed-jdbc-store&lt;/span&gt;
            &lt;span class="na"&gt;xmlns=&lt;/span&gt;&lt;span class="s"&gt;"urn:infinispan:config:store:jdbc:15.0"&lt;/span&gt; &lt;span class="na"&gt;preload=&lt;/span&gt;&lt;span class="s"&gt;"false"&lt;/span&gt; &lt;span class="na"&gt;shared=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt; &lt;span class="na"&gt;dialect=&lt;/span&gt;&lt;span class="s"&gt;"POSTGRES"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;string-keyed-table&lt;/span&gt; &lt;span class="na"&gt;prefix=&lt;/span&gt;&lt;span class="s"&gt;"EXT"&lt;/span&gt; &lt;span class="na"&gt;create-on-start=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt; &lt;span class="na"&gt;drop-on-exit=&lt;/span&gt;&lt;span class="s"&gt;"false"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;id-column&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"id"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"VARCHAR(255)"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;data-column&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"data"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"BYTEA"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;timestamp-column&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"timestamp"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"BIGINT"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;segment-column&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"segment"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"INT"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;/string-keyed-table&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/string-keyed-jdbc-store&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/persistence&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/distributed-cache&amp;gt;&lt;/span&gt;


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

&lt;/div&gt;
&lt;h3&gt;
  
  
  Disable the statistics
&lt;/h3&gt;

&lt;p&gt;Unfortunately, querying &lt;code&gt;statistics&lt;/code&gt; in Infinispan can lead to numerous executions of the following query:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;

&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="nv"&gt;"kc_sessions"&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="nb"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="nb"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;


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

&lt;/div&gt;

&lt;p&gt;As the amount of data in your &lt;code&gt;sessions&lt;/code&gt; table increases, the performance of this query degrades. This is because the query is highly CPU-intensive for the database. If you're using AWS RDS, this could result in exhausting your CPU credits, causing your cluster to become unable to handle incoming traffic effectively.&lt;/p&gt;

&lt;p&gt;Does this mean you won’t be able to monitor metrics for the &lt;code&gt;sessions&lt;/code&gt; table? Not at all.&lt;/p&gt;

&lt;p&gt;If you enable Keycloak metrics, you can still track the number of entries in the external Infinispan cache database through the &lt;code&gt;vendor_cache_store_number_of_persisted_entries&lt;/code&gt; metric.&lt;/p&gt;

&lt;p&gt;To disable statistics &lt;/p&gt;

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

&lt;span class="nt"&gt;&amp;lt;distributed-cache&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"sessions"&lt;/span&gt; &lt;span class="na"&gt;statistics=&lt;/span&gt;&lt;span class="s"&gt;"false"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/distributed-cache&amp;gt;&lt;/span&gt;



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

&lt;/div&gt;
&lt;h3&gt;
  
  
  Limit owners
&lt;/h3&gt;

&lt;p&gt;To conserve memory in your Infinispan instance, you can configure it to have the minimum number of owners for the sessions. As long as your database can handle I/O operations within an acceptable timeframe, this approach can be effective. &lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;

&lt;span class="nt"&gt;&amp;lt;distributed-cache&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"sessions"&lt;/span&gt; &lt;span class="na"&gt;owners=&lt;/span&gt;&lt;span class="s"&gt;"2"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/distributed-cache&amp;gt;&lt;/span&gt;



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

&lt;/div&gt;
&lt;h3&gt;
  
  
  State transfer
&lt;/h3&gt;

&lt;p&gt;State transfer is the process by which data is moved between nodes in a distributed cache cluster. This mechanism is essential for ensuring that all nodes in the cluster have a consistent view of the cache's data, especially when nodes are added, removed, or rebalanced.&lt;/p&gt;

&lt;p&gt;As long as the preload is set to &lt;code&gt;false&lt;/code&gt;, the initial state transfer doesn't really matter as no preload to be considered anyways. &lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;

&lt;span class="nt"&gt;&amp;lt;distributed-cache&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"sessions"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;state-transfer&lt;/span&gt; &lt;span class="na"&gt;timeout=&lt;/span&gt;&lt;span class="s"&gt;"60000"&lt;/span&gt; &lt;span class="na"&gt;await-initial-transfer=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/distributed-cache&amp;gt;&lt;/span&gt;


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

&lt;/div&gt;
&lt;h2&gt;
  
  
  Embedded Infinispan Cache
&lt;/h2&gt;

&lt;p&gt;The embedded Infinispan cache in Keycloak is an internal, local cache used to store session and configuration data to improve performance and reduce database load; however, improper configuration can lead to memory issues for Keycloak.&lt;/p&gt;
&lt;h3&gt;
  
  
  Limit the memory size
&lt;/h3&gt;

&lt;p&gt;By default, the embedded Infinispan cache managed by Keycloak does not impose a limit on the number of stored sessions. This means that if sessions are configured to last for extended periods, such as 3 months or a year, they will remain in Keycloak until memory is exhausted. When memory becomes full, it can lead to issues.&lt;/p&gt;

&lt;p&gt;Consider whether you truly need the embedded Infinispan cache if you already have an external cache in place. While I wouldn’t recommend disabling the embedded cache entirely, setting hard limits can help manage memory usage and prevent problems.&lt;/p&gt;

&lt;p&gt;For example, if you're handling around 5,000 sessions at a time, setting a limit of 10,000 or even 50,000 sessions in memory should be reasonable. Once the embedded cache reaches its limit, it will evict sessions, but only from the embedded cache, not the external cache.&lt;/p&gt;

&lt;p&gt;When a session is needed and has been evicted from the embedded cache, Keycloak will reload it from the external cache. &lt;/p&gt;

&lt;p&gt;To ensure this setup works, you'll need to configure the embedded cache settings in Keycloak appropriately.&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;

&lt;span class="nt"&gt;&amp;lt;infinispan&lt;/span&gt;
    &lt;span class="na"&gt;xmlns:xsi=&lt;/span&gt;&lt;span class="s"&gt;"http://www.w3.org/2001/XMLSchema-instance"&lt;/span&gt;
    &lt;span class="na"&gt;xsi:schemaLocation=&lt;/span&gt;&lt;span class="s"&gt;"urn:infinispan:config:15.0 http://www.infinispan.org/schemas/infinispan-config-15.0.xsd"&lt;/span&gt;
    &lt;span class="na"&gt;xmlns=&lt;/span&gt;&lt;span class="s"&gt;"urn:infinispan:config:15.0"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;cache-container&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"keycloak"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        ...
        &lt;span class="nt"&gt;&amp;lt;distributed-cache&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"sessions"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;memory&lt;/span&gt; &lt;span class="na"&gt;max-count=&lt;/span&gt;&lt;span class="s"&gt;"50000"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/distributed-cache&amp;gt;&lt;/span&gt;
        ...
    &lt;span class="nt"&gt;&amp;lt;/cache-container&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/infinispan&amp;gt;&lt;/span&gt;


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

&lt;/div&gt;
&lt;h3&gt;
  
  
  Limit owners
&lt;/h3&gt;

&lt;p&gt;Since sessions are stored in the external cache, which typically uses two owners, having just one owner in the embedded cache is usually sufficient. As long as the traffic to the external cache remains manageable for your system, setting the embedded cache to one owner per session should work well.&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;

&lt;span class="nt"&gt;&amp;lt;infinispan&lt;/span&gt;
    &lt;span class="na"&gt;xmlns:xsi=&lt;/span&gt;&lt;span class="s"&gt;"http://www.w3.org/2001/XMLSchema-instance"&lt;/span&gt;
    &lt;span class="na"&gt;xsi:schemaLocation=&lt;/span&gt;&lt;span class="s"&gt;"urn:infinispan:config:15.0 http://www.infinispan.org/schemas/infinispan-config-15.0.xsd"&lt;/span&gt;
    &lt;span class="na"&gt;xmlns=&lt;/span&gt;&lt;span class="s"&gt;"urn:infinispan:config:15.0"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;cache-container&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"keycloak"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        ...
        &lt;span class="nt"&gt;&amp;lt;distributed-cache&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"sessions"&lt;/span&gt; &lt;span class="na"&gt;owners=&lt;/span&gt;&lt;span class="s"&gt;"1"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/distributed-cache&amp;gt;&lt;/span&gt;
        ...
    &lt;span class="nt"&gt;&amp;lt;/cache-container&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/infinispan&amp;gt;&lt;/span&gt;


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

&lt;/div&gt;
&lt;h3&gt;
  
  
  Keep eyes on Keycloak API performance
&lt;/h3&gt;

&lt;p&gt;While this topic is a bit advanced, it could be quite relevant depending on your needs.&lt;/p&gt;

&lt;p&gt;Consider a scenario where you have a custom user attribute, such as &lt;code&gt;globalUserId&lt;/code&gt;, which serves as a primary key identifier for users within your ecosystem (distinct from the Keycloak USER ID - UUID).&lt;/p&gt;

&lt;p&gt;In this case, if you need to perform operations on a Keycloak user, you would first need to retrieve the Keycloak User ID using your &lt;code&gt;globalUserId&lt;/code&gt;. This process can introduce performance bottlenecks.&lt;/p&gt;

&lt;p&gt;Keycloak uses JPA to manage its entities. When you use the official Keycloak API to &lt;a href="https://www.keycloak.org/docs-api/21.1.0/rest-api/index.html#_users_resource" rel="noopener noreferrer"&gt;query users by attributes&lt;/a&gt; with a request like:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;

POST /{realm}/users?q=globalUserId:value


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

&lt;/div&gt;

&lt;p&gt;the underlying database query executed is:&lt;/p&gt;

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

&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;ue1_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ue1_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CREATED_TIMESTAMP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ue1_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EMAIL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ue1_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EMAIL_CONSTRAINT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ue1_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EMAIL_VERIFIED&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ue1_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ENABLED&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ue1_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FEDERATION_LINK&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ue1_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FIRST_NAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ue1_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LAST_NAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ue1_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NOT_BEFORE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ue1_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;REALM_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ue1_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SERVICE_ACCOUNT_CLIENT_LINK&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ue1_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;USERNAME&lt;/span&gt; 
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;USER_ENTITY&lt;/span&gt; &lt;span class="n"&gt;ue1_0&lt;/span&gt; 
&lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;USER_ATTRIBUTE&lt;/span&gt; &lt;span class="n"&gt;a1_0&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;ue1_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;a1_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;USER_ID&lt;/span&gt; 
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;a1_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NAME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; 
&lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;LOWER&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a1_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;VALUE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; 
&lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;ue1_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;REALM_ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt; 
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;ue1_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;USERNAME&lt;/span&gt; 
&lt;span class="k"&gt;OFFSET&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="k"&gt;ROWS&lt;/span&gt; 
&lt;span class="k"&gt;FETCH&lt;/span&gt; &lt;span class="k"&gt;FIRST&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="k"&gt;ROWS&lt;/span&gt; &lt;span class="k"&gt;ONLY&lt;/span&gt;


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

&lt;/div&gt;

&lt;p&gt;As shown, this query joins the &lt;code&gt;USER_ENTITY&lt;/code&gt; table with the &lt;code&gt;USER_ATTRIBUTE&lt;/code&gt; table to fetch the entire user entity. For a unique custom attribute with a small user base (fewer than a thousand users), this query can result in a latency of up to one second to retrieve user information, including the Keycloak UUID.&lt;/p&gt;

&lt;p&gt;Unfortunately, Keycloak’s Admin API does not offer a way to query directly for the UUID from the &lt;code&gt;USER_ATTRIBUTE&lt;/code&gt; table.&lt;/p&gt;

&lt;p&gt;To address this, you might consider creating a custom Service Provider Interface (SPI) to build an endpoint that queries only the user attributes and returns the Keycloak user IDs associated with the queried attribute.&lt;/p&gt;

&lt;p&gt;There are many resources available online for creating a custom API in Keycloak using SPI, specifically looking into &lt;code&gt;RealmResourceProviderFactory&lt;/code&gt;. If you’d like a detailed guide on this process, feel free to ask for a blog post!&lt;/p&gt;

&lt;p&gt;I hope you find it useful! &lt;/p&gt;

</description>
      <category>keycloak</category>
      <category>infinispan</category>
      <category>security</category>
      <category>idp</category>
    </item>
    <item>
      <title>Understanding the 0.6-Second Detection Time for Full Outages</title>
      <dc:creator>Mohammed Ammer</dc:creator>
      <pubDate>Sat, 14 Sep 2024 14:32:54 +0000</pubDate>
      <link>https://dev.to/mohammedalics/understanding-the-06-second-detection-time-for-full-outages-4iji</link>
      <guid>https://dev.to/mohammedalics/understanding-the-06-second-detection-time-for-full-outages-4iji</guid>
      <description>&lt;p&gt;If you’ve explored the widely-read workbook on Site Reliability Engineering (SRE), you might have encountered the section on the five methods for alerting based on Service Level Objectives (SLOs) - &lt;a href="https://sre.google/workbook/alerting-on-slos/" rel="noopener noreferrer"&gt;Chapter 5&lt;/a&gt;. In the first method, which is the most basic and not generally recommended, an alert is triggered when the target error rate exceeds the SLO threshold. This is represented as &lt;strong&gt;"1: Target Error Rate ≥ SLO Threshold."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvcpfoyr3lqyo97j7vjg6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvcpfoyr3lqyo97j7vjg6.png" alt="1: Target Error Rate ≥ SLO Threshold - alerting on SLOs" width="800" height="717"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One aspect of this method is the claim that the detection time for a full outage is just &lt;strong&gt;0.6 seconds&lt;/strong&gt;. I found myself questioning where this 0.6-second figure comes from, as it seems calculated.&lt;/p&gt;

&lt;p&gt;The error rate for alerting is designed to trigger as soon as possible, especially in the case of a full outage where the error rate would be 100%. So, why is the detection time cited as 0.6 seconds?&lt;/p&gt;

&lt;p&gt;Despite extensive searching and effort to understand this, the explanation was not clear to me. I talked to &lt;a href="https://www.linkedin.com/in/raymondstanleycarpio/" rel="noopener noreferrer"&gt;@Ray&lt;/a&gt; to give some help and after all clear, I decided to write this blog post, hoping to clarify the concept in my own way.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In the book, it states:&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;if the SLO is 99.9% over 30 days, alert if the error rate over the previous 10 minutes is ≥ 0.1%:&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;- alert: HighErrorRate
  expr: job:slo_errors_per_request:ratio_rate10m{job="myjob"} &amp;gt;= 0.001
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Assumptions
&lt;/h3&gt;

&lt;p&gt;To grasp the 0.6-second detection time, let’s make two assumptions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Alerts are evaluated in real-time.&lt;/li&gt;
&lt;li&gt;We have a consistent rate of 100 events per second.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Question
&lt;/h3&gt;

&lt;p&gt;From the graph provided, it’s evident that with an &lt;strong&gt;error rate of 1%&lt;/strong&gt;, the detection time is around &lt;strong&gt;1 minute&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe2nlngd3pxkh2yuhysh0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe2nlngd3pxkh2yuhysh0.png" alt="Detection time at error rate 1%" width="800" height="397"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Given that there is no fractional time involved, let's address the following question:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why is the detection time 1 minute for an error rate of 1%?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The alert expression used is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sum(rate(slo_errors[10m])) by (job)
/
sum(rate(slo_requests|10m])) by (job) &amp;gt; 0.001
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This calculates a time window of 10 minutes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;So when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;1 second = 100 events&lt;/li&gt;
&lt;li&gt;10 minutes = 600 seconds = 60,000 events&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To achieve a &lt;strong&gt;0.1% error rate&lt;/strong&gt; (i.e. alert to fire), you need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;0.1% of the 60,000 events to fail in 10 minutes = 60 events&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;For an error rate of 1%:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;1 failed event every 1 second
To accumulate these 60 failed events and meet the 0.1% error rate threshold, it would take 60 seconds (1 minute).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;For a full outage (100% error rate):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;100 failed event every 1 second
&lt;strong&gt;To accumulate these 60 failed events and meet the 0.1% error rate threshold, it would take 0.6 second!&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let us put it in equation&lt;/p&gt;

&lt;h3&gt;
  
  
  Detection Time Equation
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Define Parameters:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Event Rate (R)&lt;/strong&gt;: Number of events per second.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Failure Rate Threshold (F)&lt;/strong&gt;: Error rate threshold in decimal form (e.g., 0.001 for 0.1%).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Evaluation Window (W)&lt;/strong&gt;: Time window in seconds for the alerting calculation (e.g., 600 seconds for 10 minutes).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Calculate Total Events Needed for Threshold:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Total events in the evaluation window = R * W&lt;/li&gt;
&lt;li&gt;Required number of failed events to hit the threshold = (R * W) * F&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Determine Detection Time:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Detection time (T) is the time required to accumulate the number of failed events necessary to meet the threshold.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The formula to calculate detection time is:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;T = Required Failed Events / Failure Rate&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Simplifying, the detection time can be given by:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;T = ((R * W) * F) / R&lt;/strong&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Example
&lt;/h4&gt;

&lt;p&gt;For instance, if the event rate is 100 events per second (R = 100), the error rate threshold is 0.1% (F = 0.001), and the evaluation window is 10 minutes (600 seconds) (W = 600):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Calculate Required Failed Events:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Total events in 10 minutes = R * W = 100 * 600 = 60,000&lt;/li&gt;
&lt;li&gt;Required failed events = 60,000 * 0.001 = 60&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Calculate Detection Time:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Detection time = 60 / 100 = 0.6 seconds&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Thus, for a full outage, where the failure rate is 100%, the detection time would be around 0.6 seconds.&lt;/p&gt;

&lt;p&gt;I hope this explanation helps clear up the 0.6-second detection time!&lt;/p&gt;

</description>
      <category>sre</category>
      <category>alerting</category>
      <category>monitoring</category>
      <category>metrics</category>
    </item>
    <item>
      <title>Simplifying Keycloak Configuration with Terraform and Terragrunt</title>
      <dc:creator>Mohammed Ammer</dc:creator>
      <pubDate>Sat, 04 May 2024 23:51:48 +0000</pubDate>
      <link>https://dev.to/mohammedalics/simplifying-keycloak-configuration-with-terraform-and-terragrunt-3ohm</link>
      <guid>https://dev.to/mohammedalics/simplifying-keycloak-configuration-with-terraform-and-terragrunt-3ohm</guid>
      <description>&lt;p&gt;&lt;a href="https://www.keycloak.org" rel="noopener noreferrer"&gt;Keycloak&lt;/a&gt;, an open-source identity and access management solution, provides robust authentication and authorization services for modern applications. However, configuring Keycloak instances manually can be tedious and error-prone. In this blog post, we'll explore how to simplify Keycloak configuration using Terraform and Terragrunt, enabling infrastructure as code (IaC) practices for managing Keycloak realms, clients, users, and more.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Terraform and Terragrunt?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.terraform.io" rel="noopener noreferrer"&gt;Terraform&lt;/a&gt; is an open-source tool that lets you manage your infrastructure as code. With Terraform, you can define your infrastructure in simple configuration files, and Terraform will automatically create, manage, and update your infrastructure according to those configurations. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://terragrunt.gruntwork.io" rel="noopener noreferrer"&gt;Terragrunt&lt;/a&gt; is a thin wrapper for Terraform that provides extra tools for keeping your Terraform configurations DRY, managing remote state, and working with multiple Terraform modules. It helps you maintain clean, DRY, and repeatable Terraform code by providing extra functionality and best practices.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;By leveraging both, we can have easy&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Infrastructure as Code (IaC)&lt;/strong&gt;: By defining Keycloak configuration as code, we can version control, review changes, and ensure consistency across environments.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Modularization&lt;/strong&gt;: Modularize our Keycloak configuration, making it easier to manage complex setups.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;State Management&lt;/strong&gt;: Manage the state of our infrastructure, preventing configuration drift and ensuring that our infrastructure remains in the desired state.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Let us go!
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Install Terraform and Terragrunt
&lt;/h3&gt;

&lt;p&gt;Make sure you have Terraform and Terragrunt installed on your machine. You can find installation instructions on the official Terraform and Terragrunt documentation&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Terraform: &lt;a href="https://developer.hashicorp.com/terraform/tutorials/aws-get-started/install-cli" rel="noopener noreferrer"&gt;https://developer.hashicorp.com/terraform/tutorials/aws-get-started/install-cli&lt;/a&gt; &lt;/li&gt;
&lt;li&gt;Terragrunt: &lt;a href="https://terragrunt.gruntwork.io/docs/getting-started/install/" rel="noopener noreferrer"&gt;https://terragrunt.gruntwork.io/docs/getting-started/install/&lt;/a&gt; &lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Project Structure
&lt;/h3&gt;

&lt;p&gt;Creating a one-size-fits-all structure for a Terraform project can be challenging because it largely depends on the specific requirements of each project. Below is the structure I've found most suitable for organizing Keycloak components, concepts, and setup.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.
├── README.md                 &amp;lt;- The project overview
├── .tool-versions            &amp;lt;- Used tools versions (managed by asdf. see https://asdf-vm.com) 
├── README.md                 &amp;lt;- The project overview
├── modules                   &amp;lt;- Terraform modules
|   └── common
│       ├── provider.tf
│       └── variables.tf
│       └── output.tf
│       └── main.tf
│       └── README.md
│       └── docs/
|   └── clients
│       ├── provider.tf
│       └── variables.tf
│       └── output.tf
│       └── main.tf
│       └── README.md
│       └── docs/
|   └── my-realm
│       ├── provider.tf
│       └── variables.tf
│       └── output.tf
│       └── main.tf
│       └── README.md
│       └── docs/
|   └── other
│       ├── provider.tf
│       └── variables.tf
│       └── output.tf
│       └── main.tf
│       └── README.md
│       └── docs/
└── how-to                    &amp;lt;- Documentation
└── stage                     &amp;lt;- Terraform for environment stage
    ├── .terraform.lock.hcl   &amp;lt;- Terraform lock file
    └── terragrunt.hcl        &amp;lt;- Terragrunt file
    └── env.yaml              &amp;lt;- environment related variables
    └── main.tf               &amp;lt;- environment modules
└── prod                      &amp;lt;- Terraform for environment prod
    ├── .terraform.lock.hcl   &amp;lt;- Terraform lock file
    └── terragrunt.hcl        &amp;lt;- Terragrunt file
    └── env.yaml              &amp;lt;- environment related variables
    └── main.tf               &amp;lt;- environment modules

└── local                     &amp;lt;- Terraform for environment local
    ├── .terraform.lock.hcl   &amp;lt;- Terraform lock file
    └── terragrunt.hcl        &amp;lt;- Terragrunt file
    └── env.yaml              &amp;lt;- environment related variables
    └── main.tf               &amp;lt;- environment modules
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;In this project structure, I've included a modules directory containing a set of modules shared across all environments. Each module includes a &lt;code&gt;main.tf&lt;/code&gt; file encapsulating the module's resources, along with &lt;code&gt;input.tf&lt;/code&gt;, &lt;code&gt;output.tf&lt;/code&gt;, and &lt;code&gt;variable.tf&lt;/code&gt; files for easy configuration across different environments.&lt;/p&gt;

&lt;h3&gt;
  
  
  Common Module
&lt;/h3&gt;

&lt;p&gt;Let's assume that in the &lt;code&gt;common&lt;/code&gt; module, we configure realm events by using the &lt;code&gt;jboss-logging&lt;/code&gt; event listener with some non-default configurations. Below is an example of how the &lt;code&gt;main.tf&lt;/code&gt; file may look:&lt;/p&gt;

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

&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"keycloak_realm_events"&lt;/span&gt; &lt;span class="s2"&gt;"realm_events"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;realm_id&lt;/span&gt;                     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;realm_id&lt;/span&gt;
  &lt;span class="nx"&gt;events_enabled&lt;/span&gt;               &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="nx"&gt;events_expiration&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1800&lt;/span&gt;
  &lt;span class="nx"&gt;admin_events_enabled&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="nx"&gt;admin_events_details_enabled&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="err"&gt;]&lt;/span&gt;

  &lt;span class="nx"&gt;events_listeners&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s2"&gt;"jboss-logging"&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;To include the &lt;code&gt;realm_id&lt;/code&gt; variable, it must be defined in the &lt;code&gt;variables.tf&lt;/code&gt; file as shown below:&lt;/p&gt;

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

&lt;span class="k"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"realm_id"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Realm ID"&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&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;And we must configure the used providers in our module. In this case, the &lt;code&gt;provider.tf&lt;/code&gt; will look like as:&lt;/p&gt;

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

&lt;span class="k"&gt;terraform&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;required_providers&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="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"mrparkers/keycloak"&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;h3&gt;
  
  
  Master Realm Module
&lt;/h3&gt;

&lt;p&gt;In the &lt;code&gt;realm-master&lt;/code&gt; module, the &lt;code&gt;main.tf&lt;/code&gt; file should reference the common module. Here is an example of how the &lt;code&gt;main.tf&lt;/code&gt; file may look:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;

&lt;span class="k"&gt;data&lt;/span&gt; &lt;span class="s2"&gt;"keycloak_realm"&lt;/span&gt; &lt;span class="s2"&gt;"master"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;realm&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"master"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="s2"&gt;"realm-master"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"../../modules/common"&lt;/span&gt;
  &lt;span class="nx"&gt;realm_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;keycloak_realm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;master&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;


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

&lt;/div&gt;

&lt;p&gt;Similarly, the &lt;code&gt;provider.tf&lt;/code&gt; file should be configured as follows:&lt;/p&gt;

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

&lt;span class="k"&gt;terraform&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;required_providers&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="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"mrparkers/keycloak"&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;h3&gt;
  
  
  Local environment
&lt;/h3&gt;

&lt;p&gt;In the &lt;code&gt;main.tf&lt;/code&gt; file, we need to define the master realm in order to reference the &lt;code&gt;realm-master&lt;/code&gt; module in our project.&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;

&lt;span class="c1"&gt;# Define master realm&lt;/span&gt;
&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="s2"&gt;"realm-master"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"../modules/realm-master"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;


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

&lt;/div&gt;

&lt;p&gt;Within each environment (e.g., &lt;code&gt;prod&lt;/code&gt;, &lt;code&gt;stage&lt;/code&gt; and &lt;code&gt;local&lt;/code&gt;), there's an &lt;code&gt;env.yaml&lt;/code&gt; file containing all the environment-specific variables.&lt;/p&gt;

&lt;p&gt;For example, the &lt;code&gt;env.yaml&lt;/code&gt; file for the local environment may look like this:&lt;/p&gt;

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

&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;local&lt;/span&gt;
&lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://localhost:8080/keycloak&lt;/span&gt;



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

&lt;/div&gt;

&lt;p&gt;And of course, don't forget to include the &lt;code&gt;terragrunt-local.hcl&lt;/code&gt; file, which should be defined in the parent module.&lt;/p&gt;

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

&lt;span class="nx"&gt;include&lt;/span&gt; &lt;span class="s2"&gt;"root"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;find_in_parent_folders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"terragrunt-local.hcl"&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;h3&gt;
  
  
  Terragrunt configuration
&lt;/h3&gt;

&lt;p&gt;As mentioned earlier, Terragrunt is highly beneficial for keeping your configuration Don't Repeat Yourself (DRY). For example, the &lt;code&gt;terragrunt.hcl&lt;/code&gt; file may look like this:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;

&lt;span class="c1"&gt;# Generates the backend for all modules.&lt;/span&gt;
&lt;span class="nx"&gt;remote_state&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;backend&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"s3"&lt;/span&gt;
  &lt;span class="nx"&gt;config&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;encrypt&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="nx"&gt;key&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"keycloak/${path_relative_to_include()}/terraform.tfstate"&lt;/span&gt;
    &lt;span class="nx"&gt;region&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;AWS REGION&amp;gt;"&lt;/span&gt;
    &lt;span class="nx"&gt;bucket&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"terraform-states"&lt;/span&gt;
    &lt;span class="nx"&gt;dynamodb_table&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"terraform-lock"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Read the local "env.yaml" in every environment.&lt;/span&gt;
&lt;span class="nx"&gt;locals&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;vars&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;yamldecode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"${path_relative_to_include()}/env.yaml"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="nx"&gt;environment&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vars&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;environment&lt;/span&gt;
  &lt;span class="nx"&gt;url&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vars&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Generate the "provider.tf" file for every module.&lt;/span&gt;
&lt;span class="nx"&gt;generate&lt;/span&gt; &lt;span class="s2"&gt;"provider"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;path&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"provider.tf"&lt;/span&gt;
  &lt;span class="nx"&gt;if_exists&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"overwrite"&lt;/span&gt;
  &lt;span class="nx"&gt;contents&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
terraform {
  required_providers {
    keycloak = {
      source  = "mrparkers/keycloak"
      version = "4.4.0"
    }

    http = {
      source  = "hashicorp/http"
      version = "3.2.1"
    }
  }
}

data "http" "config" {
  url = "&amp;lt;INTERNAL CONFIGURATION URL&amp;gt;"
}

provider "keycloak" {
  client_id     = jsondecode(data.http.config.response_body).terraform-client-id
  client_secret = jsondecode(data.http.config.response_body).terraform-client-secret
  url           = "${local.url}"
}
&lt;/span&gt;&lt;span class="no"&gt;
EOF
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Generate the "backend.tf" file for every module.&lt;/span&gt;
&lt;span class="nx"&gt;generate&lt;/span&gt; &lt;span class="s2"&gt;"backend"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;path&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"backend.tf"&lt;/span&gt;
  &lt;span class="nx"&gt;if_exists&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"overwrite"&lt;/span&gt;
  &lt;span class="nx"&gt;contents&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
terraform {
  backend "s3" {}
}
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;


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

&lt;/div&gt;

&lt;p&gt;If you're familiar with Terragrunt, you'll find the &lt;code&gt;terragrunt.hcl&lt;/code&gt; file above quite familiar, except for the http &lt;code&gt;config&lt;/code&gt; part, which I'll describe later in section.&lt;/p&gt;

&lt;p&gt;In &lt;code&gt;remote_state&lt;/code&gt;, we use AWS S3 to store the state of our environment configurations. Additionally, we generate the &lt;code&gt;backend&lt;/code&gt; and &lt;code&gt;provider&lt;/code&gt; for every environment.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;locals&lt;/code&gt; block is mainly used to read the &lt;code&gt;env.yaml&lt;/code&gt; file and assign the variables to local variables.&lt;/p&gt;

&lt;p&gt;Lastly, the http &lt;code&gt;config&lt;/code&gt; is responsible for making an HTTP call to wherever you securely store your environment configurations (at least the Terraform client ID and secret) to pass them to the &lt;code&gt;keycloak&lt;/code&gt; provider.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;terragrunt-local.hcl&lt;/code&gt; file is similar to the &lt;code&gt;terragrunt.hcl&lt;/code&gt; file, except for the remote state configuration, which isn't necessary in this case. The &lt;code&gt;local&lt;/code&gt; file is primarily for testing your configuration on a local Keycloak cluster setup.  &lt;/p&gt;




&lt;h3&gt;
  
  
  The Docker Compose
&lt;/h3&gt;

&lt;p&gt;To run our Terraform configurations, we require a Keycloak cluster setup. In below, we use Docker Compose to start a Keycloak cluster, enabling us to run our local Terraform configurations seamlessly against it.&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;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3'&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;postgres_data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;driver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;local&lt;/span&gt;

&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;postgres&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres&lt;/span&gt;
      &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;postgres_data:/var/lib/postgresql/data&lt;/span&gt;
      &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;keycloak&lt;/span&gt;
        &lt;span class="na"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;keycloak&lt;/span&gt;
        &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;password&lt;/span&gt;
      &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;5432:5432"&lt;/span&gt;
  &lt;span class="na"&gt;keycloak&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;quay.io/keycloak/keycloak:23.0.6&lt;/span&gt;
      &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;KC_DB_USERNAME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;keycloak&lt;/span&gt;
        &lt;span class="na"&gt;KC_DB_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;password&lt;/span&gt;
        &lt;span class="na"&gt;KC_DB_URL_HOST&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres&lt;/span&gt;
        &lt;span class="na"&gt;KC_DB&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres&lt;/span&gt;
        &lt;span class="na"&gt;KC_DB_SCHEMA&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;public&lt;/span&gt;
        &lt;span class="na"&gt;KC_HTTP_RELATIVE_PATH&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/keycloak&lt;/span&gt;
        &lt;span class="na"&gt;KC_HOSTNAME_ADMIN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;127.0.0.1&lt;/span&gt;
        &lt;span class="na"&gt;KC_HOSTNAME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;localhost&lt;/span&gt;
        &lt;span class="na"&gt;KEYCLOAK_ADMIN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;admin&lt;/span&gt;
        &lt;span class="na"&gt;KEYCLOAK_ADMIN_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;admin&lt;/span&gt;
      &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;start-dev&lt;/span&gt;
      &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8080:8080"&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8787:8787"&lt;/span&gt;
      &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;postgres&lt;/span&gt;
  &lt;span class="na"&gt;config_keycloak&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./keycloak-docker-config.sh:/opt/keycloak-docker-config.sh&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./opt/keycloak-docker-config.sh&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;keycloak&lt;/span&gt;


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

&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;keycloak-docker-config.sh&lt;/code&gt; script is primarily used to configure a Terraform client with &lt;code&gt;admin&lt;/code&gt; privileges, which Terraform will use during its operations.&lt;/p&gt;

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

&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;

apt update &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="nb"&gt;install &lt;/span&gt;jq curl

&lt;span class="k"&gt;until&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;--output&lt;/span&gt; /dev/null &lt;span class="nt"&gt;--silent&lt;/span&gt; &lt;span class="nt"&gt;--head&lt;/span&gt; &lt;span class="nt"&gt;--fail&lt;/span&gt; http://keycloak:8080/keycloak&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
    &lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'.'&lt;/span&gt;
    &lt;span class="nb"&gt;sleep &lt;/span&gt;5
&lt;span class="k"&gt;done&lt;/span&gt;

&lt;span class="c"&gt;# Get access token&lt;/span&gt;
&lt;span class="nv"&gt;TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/x-www-form-urlencoded"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"client_id=admin-cli"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"username=admin"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"password=admin"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"grant_type=password"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="s2"&gt;"http://keycloak:8080/keycloak/realms/master/protocol/openid-connect/token"&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.access_token'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# Create Terraform client (terraform/terraform)&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TOKEN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"clientId": "terraform", "name": "terraform", "enabled": true, "publicClient": false, "secret": "terraform", "serviceAccountsEnabled": true}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"http://keycloak:8080/keycloak/admin/realms/master/clients"&lt;/span&gt;

&lt;span class="c"&gt;# Get the Terraform service account user ID&lt;/span&gt;
&lt;span class="nv"&gt;USER_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  curl &lt;span class="nt"&gt;-X&lt;/span&gt; GET &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TOKEN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="s2"&gt;"http://keycloak:8080/keycloak/admin/realms/master/users?username=service-account-terraform"&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.[0].id'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# Get the admin role ID&lt;/span&gt;
&lt;span class="nv"&gt;ROLE_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  curl &lt;span class="nt"&gt;-X&lt;/span&gt; GET &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TOKEN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="s2"&gt;"http://keycloak:8080/keycloak/admin/realms/master/roles"&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.[] | select(.name == "admin") | .id'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# Add the admin role to the Terraform service account user&lt;/span&gt;
curl &lt;span class="nt"&gt;-kv&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TOKEN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'[{"id":"'&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;ROLE_ID&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s1"&gt;'", "name":"admin"}]'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"http://keycloak:8080/keycloak/admin/realms/master/users/&lt;/span&gt;&lt;span class="nv"&gt;$USER_ID&lt;/span&gt;&lt;span class="s2"&gt;/role-mappings/realm"&lt;/span&gt;


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

&lt;/div&gt;

&lt;p&gt;To run it, open the &lt;code&gt;terminal&lt;/code&gt; and run the below docker compose command&lt;/p&gt;

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

docker-compose up &lt;span class="nt"&gt;--build&lt;/span&gt;


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

&lt;/div&gt;

&lt;p&gt;After the docker-compose containers are up and running, navigate to &lt;code&gt;http://localhost:8080/keycloak&lt;/code&gt; and log in using &lt;code&gt;admin/admin&lt;/code&gt; as credentials. Ensure that the Terraform client is configured within the master realm.&lt;/p&gt;

&lt;p&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%2Fm18il661sbzjk61hmufm.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%2Fm18il661sbzjk61hmufm.png" alt="Terraform client"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now it's time to run your Terraform local configurations. Open your terminal and execute the following commands:&lt;/p&gt;

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

&lt;span class="c"&gt;# Navigate to the local environment&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cd local&lt;/span&gt; 
&lt;span class="c"&gt;# Ensure that Terraform-related files, including the auto-generated backend.tf and provider.tf, are removed&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; backend.tf provider.tf terraform.tfstate terraform.tfstate.backup .terraform.lock.hcl .terraform 
&lt;span class="c"&gt;# Initialize Terraform to create all necessary files&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;terragrunt init &lt;span class="nt"&gt;--terragrunt-config&lt;/span&gt; terragrunt-local.hcl 
&lt;span class="c"&gt;# Apply the Terraform configurations&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;terragrunt apply &lt;span class="nt"&gt;--terragrunt-config&lt;/span&gt; terragrunt-local.hcl 


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

&lt;/div&gt;

&lt;p&gt;Now, open your web browser and go to &lt;code&gt;http://localhost:8080/keycloak&lt;/code&gt;. Log in using your admin credentials and make sure your configurations are properly set up there!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I created a demo project on GitHub &lt;a href="https://github.com/mohammedalics/keycloak-terraform-demo" rel="noopener noreferrer"&gt;keycloak-terraform-demo&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

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

&lt;p&gt;Organizing and managing Keycloak configurations with Terraform can greatly streamline your development process. By following the structure and steps outlined in this guide, you can efficiently set up and maintain your Keycloak environments, ensuring consistency and scalability across your projects. If you have any questions or suggestions, feel free to leave them in the comments below. &lt;/p&gt;

&lt;p&gt;I hope you find it useful!&lt;/p&gt;

</description>
      <category>terraform</category>
      <category>keycloak</category>
      <category>terragrunt</category>
      <category>security</category>
    </item>
    <item>
      <title>Mastering Spring Cloud Gateway Testing: Filters (part 2)</title>
      <dc:creator>Mohammed Ammer</dc:creator>
      <pubDate>Wed, 24 Apr 2024 08:50:29 +0000</pubDate>
      <link>https://dev.to/mohammedalics/mastering-spring-cloud-gateway-testing-filters-part-2-3c5f</link>
      <guid>https://dev.to/mohammedalics/mastering-spring-cloud-gateway-testing-filters-part-2-3c5f</guid>
      <description>&lt;p&gt;In &lt;a href="https://dev.to/mohammedalics/mastering-spring-cloud-gateway-testing-predicates-part-1-1j24"&gt;Part 1&lt;/a&gt;, we dived into testing predicates efficiently. Now, let's continue in the same vein but shift our focus to filters. Just like predicates, filters play a crucial role in &lt;a href="https://spring.io/projects/spring-cloud-gateway"&gt;Spring Cloud Gateway&lt;/a&gt; routing by making changes to the request or the target backend.&lt;/p&gt;

&lt;p&gt;In this post, we'll delve into writing effective integration tests for filters. These tests ensure that our custom filters perform precisely as intended, validating that they are accomplishing the specific tasks they were designed for.&lt;/p&gt;

&lt;p&gt;Before going forward, I strongly recommend you go through &lt;a href="https://dev.to/mohammedalics/mastering-spring-cloud-gateway-testing-predicates-part-1-1j24"&gt;Part 1&lt;/a&gt; to leverage main concepts about Spring Cloud Gateway routing if you doubt about it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let us go!
&lt;/h2&gt;

&lt;p&gt;For our demonstration, we've created &lt;code&gt;AddLogisticProvidersHeaderGatewayFilterFactory&lt;/code&gt;. This function adds a new header containing shipping company information based on the client's IP location. That's all there is to it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Component&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AddLogisticProvidersHeaderGatewayFilterFactory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;countryService&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;CountryService&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;logisticService&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;LogisticService&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nc"&gt;AbstractGatewayFilterFactory&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;AddLogisticProvidersHeaderGatewayFilterFactory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
        &lt;span class="nc"&gt;Config&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;java&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;GatewayFilter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;GatewayFilter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;exchange&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chain&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
            &lt;span class="c1"&gt;// Get the client IP address from the request&lt;/span&gt;
            &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;clientIP&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;exchange&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getFirst&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"X-Forwarded-For"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;country&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;clientIP&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;let&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;countryService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getCountry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;shippingCompanies&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;country&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;let&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;logisticService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getProviders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="n"&gt;shippingCompanies&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;let&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;request&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;exchange&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mutate&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headerName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;joinToString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;","&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="nd"&gt;@GatewayFilter&lt;/span&gt; &lt;span class="n"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exchange&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mutate&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="n"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exchange&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="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;headerName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&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;h2&gt;
  
  
  Testing strategy
&lt;/h2&gt;

&lt;p&gt;Filters serve as individual units of logic that can modify both the request and/or response. Using our custom filter as an example, we anticipate modifying the request by adding a new header with calculated values.&lt;/p&gt;

&lt;p&gt;By establishing an integration test specific to this filter, wherein we send various types of requests and validate whether the filter produces the expected output by adding or omitting the new header, we can ensure that the filter is functioning as intended.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configuration
&lt;/h2&gt;

&lt;p&gt;Based on the description above, here is how the Spring Cloud Gateway configuration for our integration test may look:&lt;br&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;cloud&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;gateway&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;routes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;route_all_to_oauth&lt;/span&gt;
          &lt;span class="na"&gt;uri&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://localhost:${wiremock.server.port}&lt;/span&gt;
          &lt;span class="na"&gt;predicates&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Path=/**&lt;/span&gt;
          &lt;span class="na"&gt;filters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AddLogisticProvidersHeader&lt;/span&gt;
              &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                &lt;span class="na"&gt;headerName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;X-Shipping-Providers&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this configuration snippet, we use predicates to match all requests (as the specifics of the request aren't relevant for this test). However, in the filters section, we configure a single filter with the necessary settings. For the &lt;code&gt;AddLogisticProvidersHeader&lt;/code&gt; filter, we specify the header name as &lt;code&gt;X-Shipping-Providers&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Integration test
&lt;/h2&gt;

&lt;p&gt;Similar to the predicates integration test, I utilized the Spock framework to handle my integration test. Below is the Integration Specification for the AddLogisticProvidersHeader filter.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@ActiveProfiles&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"AddLogisticProvidersHeaderGatewayFilterFactoryIntegrationSpec"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AddLogisticProvidersHeaderGatewayFilterFactoryIntegrationSpec&lt;/span&gt; &lt;span class="n"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;AbstractIntegrationSpec&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;


    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;static&lt;/span&gt; &lt;span class="k"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="nc"&gt;SHIPPING_COMPANIES_RESULT&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"shippingCompaniesResult"&lt;/span&gt;

    &lt;span class="nd"&gt;@Unroll&lt;/span&gt;
    &lt;span class="n"&gt;def&lt;/span&gt; &lt;span class="s"&gt;"given request from #country by ip #ip, expected shipping providers header existence #expectedHeaderExistence and expected header value #expectedHeaderValue"&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;given&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;

        &lt;span class="n"&gt;wireMockServer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stubFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;anyUrl&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;atPriority&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;withHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"X-Shipping-Providers"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;matching&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&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;willReturn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;aResponse&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;withStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;with&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="n"&gt;expectedHeaderExistence&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                                &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;withHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;SHIPPING_COMPANIES_RESULT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expectedHeaderValue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                            &lt;span class="p"&gt;}&lt;/span&gt;
                            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;it&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="n"&gt;wireMockServer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stubFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;anyUrl&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;atPriority&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;willReturn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;aResponse&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;withStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;when&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;def&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;webTestClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;uri&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;build&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="n"&gt;ip&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"X-Forwarded-For"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;def&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exchange&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="n"&gt;then&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="n"&gt;expectedHeaderExistence&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;expectHeader&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;valueEquals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;SHIPPING_COMPANIES_RESULT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expectedHeaderValue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;expectHeader&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;doesNotExist&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;SHIPPING_COMPANIES_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;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;country&lt;/span&gt;   &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;ip&lt;/span&gt;              &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;expectedHeaderExistence&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;expectedHeaderValue&lt;/span&gt;
        &lt;span class="s"&gt;"France"&lt;/span&gt;  &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="s"&gt;"103.232.172.0"&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;                    &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="s"&gt;"HERMES"&lt;/span&gt;
        &lt;span class="s"&gt;"Germany"&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="s"&gt;"77.21.147.170"&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;                    &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="s"&gt;"DHL,HERMES"&lt;/span&gt;
        &lt;span class="s"&gt;"USA"&lt;/span&gt;     &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="s"&gt;"30.0.0.0"&lt;/span&gt;      &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;                   &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="k"&gt;null&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;In this test, we mock any request that has the header X-Shipping-Providers to return a header containing the same shipping companies as those added to the request header.&lt;/p&gt;

&lt;p&gt;Subsequently, we validate whether the header was added to the request by checking for the existence of the header and confirming that it contains the expected value.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;To try out the example by yourself, be sure to check out the accompanying GitHub repository &lt;a href="https://github.com/mohammedalics/spring-cloud-gateway-test"&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I hope you found it useful!&lt;/p&gt;

</description>
      <category>spring</category>
      <category>testing</category>
      <category>spock</category>
      <category>gateway</category>
    </item>
    <item>
      <title>Mastering Spring Cloud Gateway Testing: Predicates (part 1)</title>
      <dc:creator>Mohammed Ammer</dc:creator>
      <pubDate>Tue, 23 Apr 2024 23:52:59 +0000</pubDate>
      <link>https://dev.to/mohammedalics/mastering-spring-cloud-gateway-testing-predicates-part-1-1j24</link>
      <guid>https://dev.to/mohammedalics/mastering-spring-cloud-gateway-testing-predicates-part-1-1j24</guid>
      <description>&lt;p&gt;&lt;a href="https://spring.io/projects/spring-cloud-gateway" rel="noopener noreferrer"&gt;Spring Cloud Gateway&lt;/a&gt; stands as a reactive HTTP gateway designed to streamline microservices communication through its routing, filtering, and integration features.&lt;/p&gt;

&lt;p&gt;Within Spring Cloud Gateway, two pivotal components play key roles: &lt;strong&gt;Predicates&lt;/strong&gt; and &lt;strong&gt;Filters&lt;/strong&gt;. Before delving into these components and the challenge of testing them, it's essential to understand the core concept of &lt;strong&gt;routing&lt;/strong&gt;, which serves as the primary function of any 'gateway'."&lt;/p&gt;

&lt;p&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%2Fk7n55r2javlea7x1wvoi.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%2Fk7n55r2javlea7x1wvoi.png" alt="Spring Cloud Gateway"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Routing
&lt;/h2&gt;

&lt;p&gt;Routing is the process of directing traffic from one source to a specific destination. In the context of software development, routing refers to the mechanism by which requests are directed to the appropriate endpoints or resources within an application or system based on predefined rules or criteria.&lt;/p&gt;

&lt;h2&gt;
  
  
  Predicates
&lt;/h2&gt;

&lt;p&gt;Predicates are conditions that define when and how a request should be routed. They adds flexibility to customize routing rules based on various factors such as headers, paths, parameters and even body. By leveraging predicates, we can efficiently control traffic flow and implement dynamic routing strategies.&lt;/p&gt;

&lt;h2&gt;
  
  
  Filters
&lt;/h2&gt;

&lt;p&gt;Filters are interceptors that can modify incoming and outgoing HTTP requests and responses. Filters are applied to routes and can be customized to suit specific needs, giving the flexibility to modify the request, as well as the response,  to enhance the functionality and behavior of the applications without tightly coupling code to individual services.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;To summarize, when request goes through a Spring Cloud Gateway, the request goes into list of routes. Each route consists of list of predicates and filters. The request much match at most one route and the evaluation is based on getting acceptance of all defined predicates attached to the route. Once the route matched, the list of route's filters are executed in the same sequence as they were defined. &lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Let us go!
&lt;/h2&gt;

&lt;p&gt;Route logic can become quite complex. This complexity is further even more when implementing custom predicates and filters. It depends on your business.&lt;/p&gt;

&lt;p&gt;For our demo, we have two backend endpoints:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;/v1/**&lt;/code&gt; endpoint:

&lt;ul&gt;
&lt;li&gt;Designed for requests coming from specific Geo locations, such as countries.&lt;/li&gt;
&lt;li&gt;This endpoint requires the allowed shipping providers (logistics) to be included in the request header.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/v2/**&lt;/code&gt; endpoint:

&lt;ul&gt;
&lt;li&gt;Designed to accept requests from any country.&lt;/li&gt;
&lt;li&gt;No special headers are required for requests to this endpoint.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Based on our above description, here is the Spring Cloud Gateway configuration may look like:&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;cloud&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;gateway&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;routes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;route1&lt;/span&gt;
          &lt;span class="na"&gt;uri&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://localhost:8080/&lt;/span&gt;
          &lt;span class="na"&gt;predicates&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Path=/**&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Geo&lt;/span&gt;
              &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                &lt;span class="na"&gt;countries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DE"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;FR"&lt;/span&gt; &lt;span class="pi"&gt;]&lt;/span&gt;
          &lt;span class="na"&gt;filters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AddLogisticProvidersHeader&lt;/span&gt;
              &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                &lt;span class="na"&gt;headerName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;X-Shipping-Providers&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;RewritePath=/(?&amp;lt;segment&amp;gt;.*), /v1/$\{segment}&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;route2&lt;/span&gt;
          &lt;span class="na"&gt;uri&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://localhost:8080/&lt;/span&gt;
          &lt;span class="na"&gt;predicates&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Path=/**&lt;/span&gt;
          &lt;span class="na"&gt;filters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;RewritePath=/(?&amp;lt;segment&amp;gt;.*), /v2/$\{segment}&lt;/span&gt;


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

&lt;/div&gt;

&lt;p&gt;In the configurations above, when a request originates from &lt;code&gt;Germany&lt;/code&gt; or &lt;code&gt;France&lt;/code&gt;, our gateway:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Retrieves the logistics companies and adds them to the request header with the key &lt;code&gt;X-Shipping-Providers&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Rewrites the request path to include the root path &lt;code&gt;/v1/&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Forwards the modified request to the target URI.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For requests originating from other geographical locations, the gateway:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Appends &lt;code&gt;/v2/&lt;/code&gt; to the request path.&lt;/li&gt;
&lt;li&gt;Forwards the modified request to the target URI.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Testing Predicates
&lt;/h2&gt;

&lt;p&gt;Let's demonstrate what the &lt;code&gt;GeoRoutePredicateFactory&lt;/code&gt; predicate might look like.&lt;/p&gt;

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

&lt;span class="nd"&gt;@Component&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;GeoRoutePredicateFactory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;countryService&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;CountryService&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nc"&gt;AbstractRoutePredicateFactory&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;GeoRoutePredicateFactory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="nc"&gt;Config&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;java&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;Predicate&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ServerWebExchange&lt;/span&gt;&lt;span class="p"&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="nc"&gt;GatewayPredicate&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;exchange&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
            &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;countries&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;countries&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;country&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;country&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uppercase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Locale&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getDefault&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="n"&gt;countries&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ALL"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="nd"&gt;@GatewayPredicate&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;

            &lt;span class="c1"&gt;// Get the client IP address from the request&lt;/span&gt;
            &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;clientIP&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;exchange&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getFirst&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"X-Forwarded-For"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;?:&lt;/span&gt; &lt;span class="s"&gt;"127.0.0.1"&lt;/span&gt;
            &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;country&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;clientIP&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;let&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;countryService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getCountry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;clientIP&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="c1"&gt;// Check if at least one delivery option is available for the country&lt;/span&gt;
            &lt;span class="n"&gt;countries&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;country&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;uppercase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Locale&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getDefault&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="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;countries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;


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

&lt;/div&gt;

&lt;p&gt;Put simply, if all countries are allowed, the predicate returns true. If not, the predicate checks the client's request IP from the &lt;code&gt;X-Forwarded-For&lt;/code&gt; header and returns true only if the request originates from one of the allowed countries.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing strategy
&lt;/h2&gt;

&lt;p&gt;Predicates act as conditions for selecting a route for a request. If we can establish that a particular route is acceptable, it means that the predicates for that route have been satisfied. If there is only one predicate and the route is selected, it means the predicate evaluated to true; otherwise, it did not. Voilà!&lt;/p&gt;

&lt;h2&gt;
  
  
  Base mocks and Spring boot setup
&lt;/h2&gt;

&lt;p&gt;To ensure the correctness of our predicates, relying solely on unit tests is insufficient. Predicates are part of a larger set of predicates and may rely on the Spring Cloud Integration framework to function properly, such as the exchange method.&lt;/p&gt;

&lt;p&gt;Because of this, I advocate for using integration tests for the predicates (and filters, as we'll discuss later).&lt;/p&gt;

&lt;p&gt;I love using the &lt;a href="https://spockframework.org" rel="noopener noreferrer"&gt;Spock framework&lt;/a&gt; for its simplicity, readability, and maintainability. That's why we use Spock to drive our integration tests.&lt;/p&gt;

&lt;p&gt;To facilitate this, we create an abstract integration test Specification. Here, we initialize the &lt;code&gt;webTestClient&lt;/code&gt; to make our REST calls and use WireMock to mock our target backends. This setup ensures that our integration tests are comprehensive and reliable.&lt;/p&gt;

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

&lt;span class="nd"&gt;@SpringBootTest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;classes&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;GatwaytestApplication&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="nd"&gt;@AutoConfigureWireMock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nd"&gt;@AutoConfigureMockMvc&lt;/span&gt;
&lt;span class="nd"&gt;@ActiveProfiles&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"test"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;abstract&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AbstractIntegrationSpec&lt;/span&gt; &lt;span class="n"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Specification&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="nd"&gt;@Autowired&lt;/span&gt;
    &lt;span class="nc"&gt;ObjectMapper&lt;/span&gt; &lt;span class="n"&gt;objectMapper&lt;/span&gt;

    &lt;span class="nd"&gt;@Autowired&lt;/span&gt;
    &lt;span class="nc"&gt;WebTestClient&lt;/span&gt; &lt;span class="n"&gt;webTestClient&lt;/span&gt;

    &lt;span class="nd"&gt;@Autowired&lt;/span&gt;
    &lt;span class="nc"&gt;WireMockServer&lt;/span&gt; &lt;span class="n"&gt;wireMockServer&lt;/span&gt;

    &lt;span class="n"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;setup&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;WireMock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reset&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;h2&gt;
  
  
  Base test predicates setup
&lt;/h2&gt;

&lt;p&gt;To verify that the request is routed through the chosen route, we can revisit two important attributes of Spring Cloud Gateway:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;GATEWAY_HANDLER_MAPPER_ATTR&lt;/code&gt;: This attribute should have the value RoutePredicateHandlerMapping when the request is being routed through the predicates.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GATEWAY_ROUTE_ATTR&lt;/code&gt;: This attribute should contain the name of the route that matches the request.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If we can capture the values of the two attributes and use them to validate whether the predicate behaves as expected, then we can utilize them effectively. To do this, we require the abstract predicate integration specification provided below as a foundation for our predicate integration specifications.&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;

&lt;span class="nd"&gt;@Import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;abstract&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AbstractPredicateIntegrationSpec&lt;/span&gt; &lt;span class="n"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;AbstractIntegrationSpec&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="n"&gt;static&lt;/span&gt; &lt;span class="k"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="nc"&gt;HANDLER_MAPPER_HEADER&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"X-Gateway-Handler-Mapper-Class"&lt;/span&gt;
    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="n"&gt;static&lt;/span&gt; &lt;span class="k"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="nc"&gt;ROUTE_ID_HEADER&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"X-Gateway-RouteDefinition-Id"&lt;/span&gt;

    &lt;span class="nd"&gt;@TestConfiguration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;proxyBeanMethods&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Config&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

        &lt;span class="nd"&gt;@Bean&lt;/span&gt;
        &lt;span class="nd"&gt;@Order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nc"&gt;GlobalFilter&lt;/span&gt; &lt;span class="nf"&gt;modifyResponseFilter&lt;/span&gt;&lt;span class="p"&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="n"&gt;exchange&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;exchange&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getAttributeOrDefault&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;GATEWAY_HANDLER_MAPPER_ATTR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"N/A"&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="n"&gt;exchange&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getResponse&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;isCommitted&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="n"&gt;exchange&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getResponse&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;getHeaders&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;HANDLER_MAPPER_HEADER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="nc"&gt;Route&lt;/span&gt; &lt;span class="n"&gt;route&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;exchange&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getAttributeOrDefault&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;GATEWAY_ROUTE_ATTR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;null&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="n"&gt;route&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="k"&gt;null&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="n"&gt;exchange&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getResponse&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;isCommitted&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                        &lt;span class="n"&gt;exchange&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getResponse&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;getHeaders&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ROUTE_ID_HEADER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;route&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getId&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;return&lt;/span&gt; &lt;span class="n"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exchange&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;In the filter described above, we intercept the response and extract the values of two attributes: &lt;code&gt;GATEWAY_HANDLER_MAPPER_ATTR&lt;/code&gt; and &lt;code&gt;GATEWAY_ROUTE_ATTR&lt;/code&gt;. These values are then added to the response header using the names &lt;code&gt;HANDLER_MAPPER_HEADER&lt;/code&gt; and &lt;code&gt;ROUTE_ID_HEADER&lt;/code&gt; respectively.&lt;/p&gt;

&lt;p&gt;Having this information allows us to assert the route ID, ensuring that the result of the predicate under test is as expected&lt;/p&gt;

&lt;h2&gt;
  
  
  Test GeoRoutePredicateFactory configuration
&lt;/h2&gt;

&lt;p&gt;For our integration test to work, we need to define the spring cloud gateway routes. For that, we define a testing profile by the name of the predicate &lt;code&gt;application-GeoRoutePredicateFactoryIntegrationSpec.yml&lt;/code&gt; and have two routes. one route by id &lt;code&gt;route_based_target_predicate&lt;/code&gt; when the predicate actually passed, and &lt;code&gt;route_others&lt;/code&gt; if the predicate doesn't passed and the request goes to a different route. &lt;br&gt;
To enable our integration test, we must define the Spring Cloud Gateway routes. To achieve this, we create a testing profile named &lt;code&gt;application-GeoRoutePredicateFactoryIntegrationSpec.yml&lt;/code&gt;. Within this profile, we define two routes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;route_based_target_predicate&lt;/code&gt;: This route is activated when the predicate successfully passes.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;route_others&lt;/code&gt;: This route is activated if the predicate fails, and the request is directed to a different route.&lt;/li&gt;
&lt;/ol&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;cloud&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;gateway&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;routes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;route_based_target_predicate&lt;/span&gt;
          &lt;span class="na"&gt;uri&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://localhost:${wiremock.server.port}&lt;/span&gt;
          &lt;span class="na"&gt;predicates&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Path=/**&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Geo&lt;/span&gt;
              &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                &lt;span class="na"&gt;countries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DE"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;FR"&lt;/span&gt; &lt;span class="pi"&gt;]&lt;/span&gt;
          &lt;span class="na"&gt;filters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;PrefixPath=/prefix&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;route_others&lt;/span&gt;
          &lt;span class="na"&gt;uri&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://localhost:${wiremock.server.port}&lt;/span&gt;
          &lt;span class="na"&gt;predicates&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Path=/**&lt;/span&gt;
          &lt;span class="na"&gt;filters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;PrefixPath=/prefix&lt;/span&gt;


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

&lt;/div&gt;
&lt;h2&gt;
  
  
  GeoRoutePredicateFactory specification
&lt;/h2&gt;

&lt;p&gt;We complete the integration testing setup by defining the integration test class for our predicate specification.&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;

&lt;span class="nd"&gt;@ActiveProfiles&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"GeoRoutePredicateFactoryIntegrationSpec"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;GeoRoutePredicateFactoryIntegrationSpec&lt;/span&gt; &lt;span class="n"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;AbstractPredicateIntegrationSpec&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;


    &lt;span class="nd"&gt;@Unroll&lt;/span&gt;
    &lt;span class="n"&gt;def&lt;/span&gt; &lt;span class="s"&gt;"given request from #country by ip #ip, expected route #expectedRoute"&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

        &lt;span class="n"&gt;given&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;wireMockServer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stubFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ANY&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;willReturn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;aResponse&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;withStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;when&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;def&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;webTestClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;get&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="n"&gt;ip&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"X-Forwarded-For"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;


        &lt;span class="n"&gt;def&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exchange&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="n"&gt;then&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;expectStatus&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;isEqualTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;expectHeader&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;valueEquals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;HANDLER_MAPPER_HEADER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;RoutePredicateHandlerMapping&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getSimpleName&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;expectHeader&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;valueEquals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ROUTE_ID_HEADER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expectedRoute&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;country&lt;/span&gt;   &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;ip&lt;/span&gt;              &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;expectedRoute&lt;/span&gt;
        &lt;span class="s"&gt;"Germany"&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="s"&gt;"77.21.147.170"&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="s"&gt;"route_based_target_predicate"&lt;/span&gt;
        &lt;span class="s"&gt;"France"&lt;/span&gt;  &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="s"&gt;"103.232.172.0"&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="s"&gt;"route_based_target_predicate"&lt;/span&gt;
        &lt;span class="s"&gt;"USA"&lt;/span&gt;     &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="s"&gt;"30.10.0.10"&lt;/span&gt;    &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="s"&gt;"route_others"&lt;/span&gt;
        &lt;span class="k"&gt;null&lt;/span&gt;      &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;            &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="s"&gt;"route_others"&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;In our test setup, we define test cases in a "where" table containing the test data. The IP address serves as the input for our predicate logic. Based on our configuration, requests originating from &lt;code&gt;Germany&lt;/code&gt; and &lt;code&gt;France&lt;/code&gt; should be routed through &lt;code&gt;route_based_target_predicate&lt;/code&gt;, and the decision is made based on the IP address of the incoming request.&lt;/p&gt;

&lt;p&gt;For example, for IP addresses "77.21.147.170" and "103.232.172.0", we expect the route to be &lt;code&gt;route_based_target_predicate&lt;/code&gt; as we anticipate the predicate to pass. Otherwise, the route should be &lt;code&gt;route_others&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;To verify this behavior, we assert that the value of the &lt;code&gt;ROUTE_ID_HEADER&lt;/code&gt; matches the &lt;code&gt;expectedRoute&lt;/code&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;To try out the example by yourself, be sure to check out the accompanying GitHub repository &lt;a href="https://github.com/mohammedalics/spring-cloud-gateway-test" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In &lt;a href="https://dev.to/mohammedalics/mastering-spring-cloud-gateway-testing-filters-part-2-3c5f"&gt;part 2&lt;/a&gt;, I'll describe in details how you can master testing for filters.&lt;/p&gt;

&lt;p&gt;I hope you found it useful!&lt;/p&gt;

</description>
      <category>spring</category>
      <category>gateway</category>
      <category>testing</category>
      <category>spock</category>
    </item>
    <item>
      <title>Optimized Keycloak SPI Deployment using Maven Shade Plugin</title>
      <dc:creator>Mohammed Ammer</dc:creator>
      <pubDate>Sun, 31 Mar 2024 22:27:38 +0000</pubDate>
      <link>https://dev.to/mohammedalics/optimized-keycloak-spi-deployment-using-maven-shade-plugin-1ee6</link>
      <guid>https://dev.to/mohammedalics/optimized-keycloak-spi-deployment-using-maven-shade-plugin-1ee6</guid>
      <description>&lt;p&gt;When you develop Keycloak SPI, you probably add external dependencies (e.g. libraries). These dependencies are required to be in the classpath of Keycloak so your SPI can work as you expect. &lt;/p&gt;

&lt;p&gt;To include the external libraries in Keycloak path, as mentioned in Keycloak &lt;a href="https://www.keycloak.org/server/configuration-provider"&gt;Configuring providers&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;h3&gt;
  
  
  Using third-party dependencies
&lt;/h3&gt;

&lt;p&gt;When implementing a provider you might need to use some third-party dependency that is not available from the server distribution.&lt;br&gt;
In this case, you should copy any additional dependency to the providers directory and run the build command. Once you do that, the server is going to make these additional dependencies available at runtime for any provider that depends on them.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;To achieve that, you will probably need to download the JARs and copy it during building the Keycloak Docker Image. &lt;/p&gt;

&lt;p&gt;This is a bit messy, especially when you have a new release and every time want to make sure to have the same dependencies versions. &lt;/p&gt;

&lt;p&gt;To avoid all the hassle, I used &lt;a href="https://maven.apache.org/plugins/maven-shade-plugin/"&gt;Maven Shade Plugin&lt;/a&gt;. &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This plugin provides the capability to package the artifact in an uber-jar, including its dependencies and to shade - i.e. rename - the packages of some of the dependencies.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It is also very useful when you've multiple SPIs but use different versions of same library (through the renaming feature).  &lt;/p&gt;

&lt;p&gt;A sample build configuration can be like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;...
&lt;span class="nt"&gt;&amp;lt;build&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;plugins&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;plugin&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;maven-shade-plugin&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;executions&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;execution&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;goals&amp;gt;&lt;/span&gt;
                        &lt;span class="nt"&gt;&amp;lt;goal&amp;gt;&lt;/span&gt;shade&lt;span class="nt"&gt;&amp;lt;/goal&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;/goals&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;phase&amp;gt;&lt;/span&gt;package&lt;span class="nt"&gt;&amp;lt;/phase&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;/execution&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;/executions&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;org.apache.maven.plugins&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;version&amp;gt;&lt;/span&gt;${maven-shade.version}&lt;span class="nt"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/plugin&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/plugins&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/build&amp;gt;&lt;/span&gt;
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Follow the plugin documentation to configure the plugin by including/excluding/rename the dependencies. &lt;/p&gt;

&lt;p&gt;That is all. I hope you find it useful.&lt;/p&gt;

</description>
      <category>keycloak</category>
      <category>spi</category>
    </item>
    <item>
      <title>Seamless Migration to Keycloak: Token Exchange</title>
      <dc:creator>Mohammed Ammer</dc:creator>
      <pubDate>Sun, 31 Mar 2024 22:27:13 +0000</pubDate>
      <link>https://dev.to/mohammedalics/seamless-migration-to-keycloak-token-exchange-4b7p</link>
      <guid>https://dev.to/mohammedalics/seamless-migration-to-keycloak-token-exchange-4b7p</guid>
      <description>&lt;p&gt;In &lt;a href="https://dev.to/mohammedalics/seamless-migration-to-keycloak-refresh-token-4m4f"&gt;previous post&lt;/a&gt;, we spoke about the migration of Refresh Token, where Token Exchange is playing a big role to have a seamless migration. &lt;/p&gt;

&lt;p&gt;Keycloak is providing token exchange just by configuration, however, in our use case, we use &lt;a href="https://dev.to/mohammedalics/seamless-migration-to-keycloak-non-oidc-oauth-20-broker-58a6"&gt;custom identity provider&lt;/a&gt; due to our lack support to OpenID Connect. &lt;/p&gt;




&lt;p&gt;In this article, I'll discuss an extension to our custom identity provider (OAuth 2.0) to support &lt;code&gt;token exchange&lt;/code&gt;. &lt;/p&gt;

&lt;h2&gt;
  
  
  Enable external exchange
&lt;/h2&gt;

&lt;p&gt;In our &lt;code&gt;OauthServiceIdentityProvider&lt;/code&gt;, we override the &lt;code&gt;supportsExternalExchange&lt;/code&gt; to return &lt;code&gt;true&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OauthServiceIdentityProvider&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;AbstractOAuth2IdentityProvider&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;OauthServiceIdentityProviderConfig&lt;/span&gt;&lt;span class="o"&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="nd"&gt;@Override&lt;/span&gt;
   &lt;span class="kd"&gt;protected&lt;/span&gt; &lt;span class="kt"&gt;boolean&lt;/span&gt; &lt;span class="nf"&gt;supportsExternalExchange&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
   &lt;span class="o"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&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="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Exchange implementation
&lt;/h2&gt;

&lt;p&gt;To implement the exchange logic, override the method &lt;code&gt;exchangeExternalImpl&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;In the below example, I added some validations then called  the &lt;code&gt;doGetFederatedIdentity(..)&lt;/code&gt; to get the federation. Different than the doGetFederatedIdentity in &lt;a href="https://dev.to/mohammedalics/seamless-migration-to-keycloak-non-oidc-oauth-20-broker-58a6"&gt;Brokering non-OIDC OAuth 2.0 identities&lt;/a&gt;, I use &lt;code&gt;useSessionContent&lt;/code&gt; to decide from where we get the &lt;code&gt;clientId&lt;/code&gt;. In case of token exchange, we get the clientId from the token claim &lt;code&gt;token.getOtherClaims().get(TOKEN_CLAIM_CLIENT_ID)&lt;/code&gt;&lt;br&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;@Override&lt;/span&gt;
&lt;span class="kd"&gt;protected&lt;/span&gt; &lt;span class="nc"&gt;BrokeredIdentityContext&lt;/span&gt; &lt;span class="nf"&gt;exchangeExternalImpl&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;EventBuilder&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;MultivaluedMap&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;params&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;subjectToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getFirst&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OAuth2Constants&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;SUBJECT_TOKEN&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;subjectToken&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;detail&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Details&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;REASON&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;OAuth2Constants&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;SUBJECT_TOKEN&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;" param unset"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Errors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;INVALID_TOKEN&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;ErrorResponseException&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OAuthErrorException&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;INVALID_TOKEN&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"token not set"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Status&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;BAD_REQUEST&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;subjectTokenType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getFirst&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OAuth2Constants&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;SUBJECT_TOKEN_TYPE&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;subjectTokenType&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;subjectTokenType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OAuth2Constants&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;REFRESH_TOKEN_TYPE&lt;/span&gt;&lt;span class="o"&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="nc"&gt;OAuth2Constants&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;REFRESH_TOKEN_TYPE&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;equals&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;subjectTokenType&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;detail&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Details&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;REASON&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;OAuth2Constants&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;SUBJECT_TOKEN_TYPE&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;" invalid"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Errors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;INVALID_TOKEN_TYPE&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;ErrorResponseException&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OAuthErrorException&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;INVALID_TOKEN&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"invalid token type"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Status&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;BAD_REQUEST&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;requestedType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getFirst&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OAuth2Constants&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;REQUESTED_TOKEN_TYPE&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;requestedType&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;requestedType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;equals&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OAuth2Constants&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;REFRESH_TOKEN_TYPE&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;detail&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Details&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;REASON&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"requested_token_type unsupported"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Errors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;INVALID_REQUEST&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;ErrorResponseException&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OAuthErrorException&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;INVALID_REQUEST&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"invalid requested token type"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Status&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;BAD_REQUEST&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;doGetFederatedIdentity&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;subjectToken&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;BrokeredIdentityContext&lt;/span&gt; &lt;span class="nf"&gt;doGetFederatedIdentity&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;accessToken&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;boolean&lt;/span&gt; &lt;span class="n"&gt;useSessionContent&lt;/span&gt;&lt;span class="o"&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;accessToken&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&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;IdentityBrokerException&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"No token from server."&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nc"&gt;JsonWebToken&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;try&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;JWSInput&lt;/span&gt; &lt;span class="n"&gt;jws&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;JWSInput&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;accessToken&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="n"&gt;jws&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;readJsonContent&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;JsonWebToken&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;username&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;getSubject&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;clientId&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;useSessionContent&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;clientId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getContext&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getClient&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getName&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;clientId&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;.&lt;/span&gt;&lt;span class="na"&gt;valueOf&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;getOtherClaims&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="no"&gt;TOKEN_CLAIM_CLIENT_ID&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;

        &lt;span class="o"&gt;}&lt;/span&gt;

        &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;userId&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;.&lt;/span&gt;&lt;span class="na"&gt;valueOf&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;getOtherClaims&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="no"&gt;TOKEN_CLAIM_USER_ID&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
        &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;userType&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;.&lt;/span&gt;&lt;span class="na"&gt;valueOf&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;getOtherClaims&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="no"&gt;TOKEN_CLAIM_USER_TYPE&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;

        &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;BrokeredIdentityContext&lt;/span&gt; &lt;span class="n"&gt;user&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;BrokeredIdentityContext&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setUsername&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setIdpConfig&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;getConfig&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
        &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setIdp&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setUserAttribute&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;TOKEN_CLAIM_USER_ID&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getContextData&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;put&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;TOKEN_CLAIM_USER_TYPE&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;userType&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;user&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;JWSInputException&lt;/span&gt; &lt;span class="n"&gt;e&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;IdentityBrokerException&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Invalid token"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&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;h2&gt;
  
  
  Add user attributes and mappings claims
&lt;/h2&gt;

&lt;p&gt;Same as the &lt;code&gt;authenticationFinished&lt;/code&gt; method, for the token exchange, we need to override the &lt;code&gt;exchangeExternalComplete&lt;/code&gt; to enrich our user session with whatever needed add the required user attributes and session notes. Session notes can then be used later to map Keycloak token claims.&lt;br&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;@Override&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;exchangeExternalComplete&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;UserSessionModel&lt;/span&gt; &lt;span class="n"&gt;userSession&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;BrokeredIdentityContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;MultivaluedMap&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;params&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;value&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="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;getContextData&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="no"&gt;TOKEN_CLAIM_USER_TYPE&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="nc"&gt;StringUtil&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isNotBlank&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;userSession&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setNote&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&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;userId&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;getUserAttribute&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;TOKEN_CLAIM_USER_ID&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;userId&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isBlank&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;userSession&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getUser&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;setSingleAttribute&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;TOKEN_CLAIM_USER_ID&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;warn&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"userId is null or blank"&lt;/span&gt;&lt;span class="o"&gt;);&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;exchangeExternalComplete&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userSession&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="n"&gt;params&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;By this article, I am done with our custom identity provider implementation to fully support OAuth 2.0 including token exchange. &lt;/p&gt;

&lt;p&gt;Are you using Terraform to configure your Keycloak cluster? well, it is not easy to make it with Terraform when speaking about custom identity provider. In &lt;a href="https://dev.to/mohammedalics/custom-identity-providers-in-keycloak-with-terraform-a-step-by-step-guide-81h"&gt;Custom Identity Providers in Keycloak with Terraform&lt;/a&gt;, I explained it in details to keep your platform Terraformed ;-)&lt;/p&gt;

&lt;p&gt;I hope you find it useful. &lt;/p&gt;

</description>
      <category>keycloak</category>
      <category>tokenexchange</category>
    </item>
    <item>
      <title>Seamless Migration to Keycloak: Refresh token</title>
      <dc:creator>Mohammed Ammer</dc:creator>
      <pubDate>Sun, 31 Mar 2024 22:27:08 +0000</pubDate>
      <link>https://dev.to/mohammedalics/seamless-migration-to-keycloak-refresh-token-4m4f</link>
      <guid>https://dev.to/mohammedalics/seamless-migration-to-keycloak-refresh-token-4m4f</guid>
      <description>&lt;p&gt;In the &lt;a href="https://dev.to/mohammedalics/seamless-migration-to-keycloak-authorization-code-flow-5ehm"&gt;previous post&lt;/a&gt;, we discussed the seamless migration of &lt;strong&gt;Authorization Code flow&lt;/strong&gt; from Spring Authorization Server to Keycloak, also the &lt;strong&gt;JWKs&lt;/strong&gt; merging to allow the resource servers validating the Keycloak tokens. &lt;/p&gt;

&lt;p&gt;Now we go in further in the migration of the &lt;strong&gt;refresh token&lt;/strong&gt; grant type, which "in most cases" has the most share in any identity server traffic. &lt;/p&gt;




&lt;h2&gt;
  
  
  Refresh Token
&lt;/h2&gt;

&lt;p&gt;I gave an example of having a &lt;a href="https://dev.to/mohammedalics/seamless-migration-to-keycloak-authorization-code-flow-5ehm"&gt;feature flag per client&lt;/a&gt;, where you control the client requests routing. &lt;/p&gt;

&lt;p&gt;The same with refresh token requests. If the client is eligible for Keycloak, the next refresh token request must return an &lt;em&gt;access token&lt;/em&gt; and &lt;em&gt;refresh token&lt;/em&gt; from Keycloak (not the old identity server anymore). &lt;/p&gt;

&lt;p&gt;Again, the user shouldn't be forced to re-login to be identified by Keycloak. This is exactly what is this article for.&lt;/p&gt;

&lt;p&gt;Here is a sample refresh token request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="s1"&gt;'https://example.com/oauth/token'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/x-www-form-urlencoded'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s1"&gt;'Authorization: Basic xxxxxxxxxxxxxxxxxx'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--data-urlencode&lt;/span&gt; &lt;span class="s1"&gt;'grant_type=refresh_token'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--data-urlencode&lt;/span&gt; &lt;span class="s1"&gt;'refresh_token=&amp;lt;REFRESH TOKEN&amp;gt;'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The routing flow
&lt;/h3&gt;

&lt;p&gt;First, we define the pre-conditions for the refresh token routing flow to Keycloak. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The request is a &lt;code&gt;token&lt;/code&gt; request (e.g. /oauth/token)&lt;/li&gt;
&lt;li&gt;Keycloak is &lt;code&gt;enabled&lt;/code&gt; for the client&lt;/li&gt;
&lt;li&gt;The refresh token issued by the old identity provider&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once all pre-conditions met, the request should be routed to Keycloak. &lt;/p&gt;

&lt;p&gt;Below diagram is to get an abstract overview about the flow:&lt;br&gt;
&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fufoseddmyknhwo5bsg62.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fufoseddmyknhwo5bsg62.png" alt="abstract diagram" width="800" height="550"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Below sequence diagram is to describe in more details the flow:&lt;br&gt;
&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fycstcjg4wpx8tu20wk68.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fycstcjg4wpx8tu20wk68.png" alt="sequence diagram" width="800" height="735"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let us move on to discuss the configuration required in the gateway. &lt;/p&gt;
&lt;h4&gt;
  
  
  Gateway Configuration
&lt;/h4&gt;
&lt;h2&gt;
  
  
  The gateway configuration should look like below:  
&lt;/h2&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;cloud&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;gateway&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;routes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;refresh_token_target_kc&lt;/span&gt;
          &lt;span class="na"&gt;uri&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://example.com&lt;/span&gt;
          &lt;span class="na"&gt;predicates&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Path=/oauth/token**&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;KeycloakEnabled&lt;/span&gt;
              &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;GrantType&lt;/span&gt;
              &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                &lt;span class="na"&gt;hints&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                  &lt;span class="na"&gt;grant_type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;refresh_token"&lt;/span&gt; &lt;span class="pi"&gt;]&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;JwtClaim&lt;/span&gt;
              &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                &lt;span class="na"&gt;hints&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                  &lt;span class="na"&gt;claims&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                    &lt;span class="na"&gt;iss&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;old-identity-provider"&lt;/span&gt;
          &lt;span class="na"&gt;filters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;oldIdpIntrospect&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;KcTokenExchangeModifyRequestBody&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;KcTokenChangeRequestUri&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h5&gt;
  
  
  Path predicate
&lt;/h5&gt;

&lt;p&gt;The path predicate is very straight forward. It is a built-in predicate to decide about the request path. We use it to decide if the request is a token request. If request path matches &lt;code&gt;/oauth/token**&lt;/code&gt;, we move to the following predicate.&lt;/p&gt;
&lt;h5&gt;
  
  
  KeycloakEnabled predicate
&lt;/h5&gt;

&lt;p&gt;Keycloak Enabled predicate is to check if target client is Keycloak enabled. Here we use our feature flags to check if Keycloak, realm and target client are enabled.&lt;/p&gt;
&lt;h5&gt;
  
  
  GrantType predicate
&lt;/h5&gt;

&lt;p&gt;Grant Type predicate is to check if the token request grant type is &lt;code&gt;refresh token&lt;/code&gt;. &lt;/p&gt;
&lt;h5&gt;
  
  
  JwtClaim predicate
&lt;/h5&gt;

&lt;p&gt;Jwt Claim predicate is to check the JWT claims for the token sent by the client. The predicate here is to validate on the issuer. The &lt;code&gt;iss&lt;/code&gt; should have the name of the old identity provider.&lt;/p&gt;



&lt;p&gt;If all conditions are met, then the response token and refresh token must be issued by Keycloak. For that, the three filters &lt;code&gt;oldIdpIntrospect&lt;/code&gt;, &lt;code&gt;KcTokenExchangeModifyRequestBody&lt;/code&gt; and &lt;code&gt;KcTokenChangeRequestUri&lt;/code&gt; are there.&lt;/p&gt;
&lt;h4&gt;
  
  
  Gateway Filters
&lt;/h4&gt;
&lt;h5&gt;
  
  
  oldIdpIntrospect Filter
&lt;/h5&gt;

&lt;p&gt;The refresh token sent by the client must be valid and accepted by the old identity provider for the Keycloak to exchange it. To make that happen, I'll use &lt;strong&gt;&lt;a href="https://datatracker.ietf.org/doc/html/rfc7662"&gt;OAuth 2.0 Token Introspection&lt;/a&gt;&lt;/strong&gt; from the old identity provider to validate the refresh token, if the refresh token is &lt;code&gt;active&lt;/code&gt;, then I call Keycloak to exchange the token. &lt;/p&gt;

&lt;p&gt;You may need to implement the &lt;code&gt;introspect&lt;/code&gt; endpoint in the old identity provider (for instance Spring Security OAuth) if not exist. Here is an example of introspect request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;--location&lt;/span&gt; &lt;span class="nt"&gt;--globoff&lt;/span&gt; &lt;span class="s1"&gt;'https://example.com/oauth/introspect'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/x-www-form-urlencoded'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s1"&gt;'Authorization: Basic &amp;lt;ENCODED_CREDENTIALS&amp;gt;'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--data-urlencode&lt;/span&gt; &lt;span class="s1"&gt;'token=&amp;lt;TOKEN&amp;gt;'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h5&gt;
  
  
  Token Exchange Filters
&lt;/h5&gt;

&lt;p&gt;Although the client is known by Keycloak, the refresh token is unknown. Hence, if we route the traffic to Keycloak, the request will be rejected by default. &lt;/p&gt;

&lt;p&gt;If you are into OAuth 2.0, the first thing may come to your mind is the &lt;strong&gt;&lt;a href="https://datatracker.ietf.org/doc/html/rfc8693"&gt;OAuth 2.0 Token Exchange&lt;/a&gt;&lt;/strong&gt;, where Keycloak exchanges the old identity provider token by an access token and refresh token issued by Keycloak. &lt;/p&gt;

&lt;p&gt;Here is an example of a token exchange request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;--location&lt;/span&gt; &lt;span class="nt"&gt;--globoff&lt;/span&gt; &lt;span class="s1"&gt;'https://example.com/realms/myrealm/protocol/openid-connect/token'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/x-www-form-urlencoded'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--data-urlencode&lt;/span&gt; &lt;span class="s1"&gt;'client_id=&amp;lt;CLIENT_ID&amp;gt;'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--data-urlencode&lt;/span&gt; &lt;span class="s1"&gt;'client_secret=&amp;lt;CLIENT_SECRET&amp;gt;'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--data-urlencode&lt;/span&gt; &lt;span class="s1"&gt;'subject_token=&amp;lt;REFRESH_TOKEN&amp;gt;'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--data-urlencode&lt;/span&gt; &lt;span class="s1"&gt;'subject_issuer=&amp;lt;OLD_IDP_ISSUER&amp;gt;'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--data-urlencode&lt;/span&gt; &lt;span class="s1"&gt;'grant_type=urn:ietf:params:oauth:grant-type:token-exchange'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--data-urlencode&lt;/span&gt; &lt;span class="s1"&gt;'subject_token_type=urn:ietf:params:oauth:token-type:refresh_token'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--data-urlencode&lt;/span&gt; &lt;span class="s1"&gt;'requested_token_type=urn:ietf:params:oauth:token-type:refresh_token'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--data-urlencode&lt;/span&gt; &lt;span class="s1"&gt;'scope=&amp;lt;SCOPE&amp;gt;'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We created two filters for the Token Exchange to happen&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;KcTokenExchangeModifyRequestBody&lt;/code&gt;: To rewrite the request body to meet the token exchange request body. &lt;/li&gt;
&lt;li&gt;
&lt;code&gt;KcTokenChangeRequestUri&lt;/code&gt;: To rewrite the URI to meet Keycloak token exchange URI. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Notes:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Keycloak has to be configured to have token exchange enabled for your client.&lt;/li&gt;
&lt;li&gt;If you have custom identity provider SPI implemented as in &lt;a href="https://dev.to/mohammedalics/seamless-migration-to-keycloak-non-oidc-oauth-20-broker-58a6"&gt;Keycloak: Brokering non-OIDC OAuth 2.0 identities&lt;/a&gt;, you must override the related token exchange methods for the token exchange to work probably, especially if you will need to map claims from the old tokens to the new one. I have a full detailed article to guide you on how can you make it. See &lt;a href="https://dev.to/mohammedalics/seamless-migration-to-keycloak-token-exchange-4b7p"&gt;Seamless Migration to Keycloak: Token Exchange&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I hope you find it useful.&lt;/p&gt;

</description>
      <category>keycloak</category>
      <category>springsecurity</category>
      <category>iam</category>
      <category>migration</category>
    </item>
    <item>
      <title>Seamless Migration to Keycloak: Authorization Code Flow</title>
      <dc:creator>Mohammed Ammer</dc:creator>
      <pubDate>Sun, 31 Mar 2024 22:27:04 +0000</pubDate>
      <link>https://dev.to/mohammedalics/seamless-migration-to-keycloak-authorization-code-flow-5ehm</link>
      <guid>https://dev.to/mohammedalics/seamless-migration-to-keycloak-authorization-code-flow-5ehm</guid>
      <description>&lt;p&gt;ِAs mentioned earlier in my &lt;a href="https://dev.to/mohammedalics/seamless-migration-to-keycloak-insights-into-transitioning-from-spring-authorization-server-47l3"&gt;Insights into Transitioning from Spring Authorization Server&lt;/a&gt;, migrating from one identity provider to another is not an easy task, especially when you have wide range of clients. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hard to set due dates for the clients' migration&lt;/strong&gt;: For the client to comply to Keycloak schema, you might wait for so long (really long) until that happen.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Support old clients&lt;/strong&gt;: The old versions of the apps will keep using the old identity provider hence you will need to keep maintaining the old identity provider as long as the older versions of the apps are supported. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rollback&lt;/strong&gt;: Rollback to use the old identity provider during the migration is not easy if the clients are to manage that. Especially in the early release, I prefer to have control to route the clients to either the old identity provider or Keycloak. In case of any failures (and until the issue get fixed), we have the flexibility to switch the user back to the old identity provider without any degradation to the user experience. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Avoid risk SLOs degradation for a non-functional change&lt;/strong&gt;: Although migrating identity providers is a strategic decision and important to the organization when decided, I wouldn't accept customer experience degradation such move. Easy to switch back and forth helps to avoid that.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All above are enough reasons to aim for a seamless migration and for that to happen we need a &lt;strong&gt;&lt;code&gt;Gateway&lt;/code&gt;&lt;/strong&gt;. &lt;/p&gt;




&lt;h2&gt;
  
  
  Identity Provider Gateway
&lt;/h2&gt;

&lt;p&gt;The idea here is to have a gateway service to intercept all traffic to the old identity provider, and based on configuration, the gateway routes the request to the proper identity provider. Remember, we talk about seamless migration where Keycloak brokering fits here. We talked about a custom identity provider to allow Keycloak to &lt;a href="https://dev.to/mohammedalics/seamless-migration-to-keycloak-non-oidc-oauth-20-broker-58a6"&gt;broker OAuth2.0 without OpenID Connect support&lt;/a&gt;. Check it out for more details. &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%2Fs380gbqd30krxa8eettu.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%2Fs380gbqd30krxa8eettu.png" alt="identity provider gateway"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In case of old identity provider, the gateway will just bypass the request, however if Keycloak is enabled for the target client and grant type, the gateway will either &lt;strong&gt;rewrite the URL&lt;/strong&gt;, &lt;strong&gt;payload&lt;/strong&gt; or &lt;strong&gt;reply with redirection&lt;/strong&gt; to comply with Keycloak schemas.&lt;/p&gt;




&lt;p&gt;Before we go in details, what about the resource servers? &lt;/p&gt;

&lt;h2&gt;
  
  
  Resource servers
&lt;/h2&gt;

&lt;p&gt;Well, It depends on how resource servers deal with the bearer tokens and the identity provider. Resource servers may already validating the token early in level of gateway or the validation left for the resource server (e.g. backend service). &lt;br&gt;
You may use the identity provider to validate every JWT or you use stateless JWTs where resource servers are validating on their own, independent on the identity provider.&lt;/p&gt;

&lt;h3&gt;
  
  
  Validate Keycloak JWTs
&lt;/h3&gt;

&lt;p&gt;I'll assume here a &lt;strong&gt;stateless JWTs&lt;/strong&gt;. In this case, the resource servers validate the token signature (using JSON Web Key Set (JWKS)) and expiration date. &lt;/p&gt;

&lt;p&gt;For the resource servers to get the JWKS for Keycloak, the gateway intercept the requests to JWKS and combine the JWKS from Keycloak with the old identity provider one(s). Here, the resource servers have the JWKS for both identity providers and can validate the token signature regardless of the issuer. &lt;/p&gt;

&lt;h2&gt;
  
  
  Authorization Code flow
&lt;/h2&gt;

&lt;p&gt;Although Authorization Code flow is complex compared to other OAuth2.0 flows but not really when it comes to migration. &lt;/p&gt;

&lt;p&gt;The authorization code flow starts with the authorization request, where the client sends a request similar to:&lt;/p&gt;

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

https://authorization-server.com/oauth/authorize
?client_id=a17c21ed
&amp;amp;response_type=code
&amp;amp;state=5ca75bd30
&amp;amp;redirect_uri=https%3A%2F%2Fexample-app.com%2Fauth
&amp;amp;scope=photos


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

&lt;/div&gt;

&lt;p&gt;The request will go through group of &lt;code&gt;predicates&lt;/code&gt; and &lt;code&gt;filter(s)&lt;/code&gt; in our gateway service to decide about the request. &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%2F4u6wfjp8vrek6y91cac2.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%2F4u6wfjp8vrek6y91cac2.png" alt="Authorization Code flow"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Prepare your Keycloak client
&lt;/h3&gt;

&lt;p&gt;To make Keycloak ready to serve the clients, make sure that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Keycloak has the same clients as the existing authorization server and secrets. &lt;/li&gt;
&lt;li&gt;Give the clients same grant types as the existing authorization server. &lt;/li&gt;
&lt;li&gt;Create same scopes &lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/mohammedalics/seamless-migration-to-keycloak-non-oidc-oauth-20-broker-58a6"&gt;Map remote token claims to Keycloak token&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Gateway Service
&lt;/h3&gt;

&lt;p&gt;I use &lt;a href="https://spring.io/projects/spring-cloud-gateway" rel="noopener noreferrer"&gt;Spring Cloud Gateway&lt;/a&gt; to act as my identity gateway. To dynamically control the enabled realms or/and clients for Keycloak, below is the configuration you might need:&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;keycloak&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;authorizeUrl&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://auth.company.com/keycloak/realms/%s/protocol/openid-connect/auth"&lt;/span&gt;
  &lt;span class="na"&gt;tokenUrl&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://auth.company.com/keycloak/realms/%s/protocol/openid-connect/token"&lt;/span&gt;
  &lt;span class="na"&gt;realms&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;myrealm&lt;/span&gt;
      &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="na"&gt;clients&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;app1&lt;/span&gt;
          &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;app2&lt;/span&gt;
          &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;


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

&lt;/div&gt;

&lt;p&gt;where &lt;code&gt;keycloak.enabled&lt;/code&gt; is a master flag to completely control the traffic to Keycloak. &lt;code&gt;realms&lt;/code&gt; goes into the same direction.&lt;/p&gt;

&lt;p&gt;We still need a &lt;code&gt;configuration properties&lt;/code&gt; class to access it, here is it:&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;@ConfigurationProperties&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"keycloak"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nf"&gt;KeycloakProperties&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;val&lt;/span&gt; &lt;span class="nl"&gt;enabled:&lt;/span&gt; &lt;span class="nc"&gt;Boolean&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;val&lt;/span&gt; &lt;span class="nl"&gt;realms:&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;Realm&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;emptyList&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt;
    &lt;span class="n"&gt;val&lt;/span&gt; &lt;span class="nl"&gt;tokenUrl:&lt;/span&gt; &lt;span class="nc"&gt;String&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;val&lt;/span&gt; &lt;span class="nl"&gt;authorizeUrl:&lt;/span&gt; &lt;span class="nc"&gt;String&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;data&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nf"&gt;Realm&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;val&lt;/span&gt; &lt;span class="nl"&gt;name:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;val&lt;/span&gt; &lt;span class="nl"&gt;enabled:&lt;/span&gt; &lt;span class="nc"&gt;Boolean&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;val&lt;/span&gt; &lt;span class="nl"&gt;clients:&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;Client&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;emptyList&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
&lt;span class="o"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nf"&gt;Client&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;val&lt;/span&gt; &lt;span class="nl"&gt;name:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;val&lt;/span&gt; &lt;span class="nl"&gt;enabled:&lt;/span&gt; &lt;span class="nc"&gt;Boolean&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="o"&gt;)&lt;/span&gt;


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

&lt;/div&gt;
&lt;h4&gt;
  
  
  Gateway Routes
&lt;/h4&gt;

&lt;p&gt;Here is the gateway routes configuration:&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;cloud&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;gateway&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;routes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kc_authorize&lt;/span&gt;
          &lt;span class="na"&gt;uri&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://auth.company.com&lt;/span&gt;
          &lt;span class="na"&gt;predicates&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Path=/gatewayservice/oauth/authorize**&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;KeycloakEnabled&lt;/span&gt;
              &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;CookieNegate=OAUTHSESSION,.*&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;QueryNegate=scope,kc_idp&lt;/span&gt;
          &lt;span class="na"&gt;filters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;AuthorizeRedirectToKc&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;old_idp&lt;/span&gt;
          &lt;span class="na"&gt;uri&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://auth.company.com&lt;/span&gt;
          &lt;span class="na"&gt;predicates&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Path=/gatewayservice/oauth/**&lt;/span&gt;
          &lt;span class="na"&gt;filters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;RewritePath=/(?&amp;lt;segment&amp;gt;.*), /oldidp/$\{segment}&lt;/span&gt;


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

&lt;/div&gt;

&lt;p&gt;Let us speak about every predicate and filter. &lt;/p&gt;

&lt;h5&gt;
  
  
  Path Predicate
&lt;/h5&gt;

&lt;p&gt;The &lt;code&gt;path&lt;/code&gt; predicate is very straight forward. It is a built-in predicate to decide about the request path. We use it to decide if the request is an authorization request. If request path matches &lt;code&gt;/gatewayservice/oauth/authorize**&lt;/code&gt;, we move to the following predicate. &lt;/p&gt;

&lt;h4&gt;
  
  
  KeycloakEnabled Predicate
&lt;/h4&gt;

&lt;p&gt;This predicate is to check if target client is Keycloak enabled. Here we use our feature flags to check if Keycloak, realm and target client are enabled. &lt;/p&gt;

&lt;h4&gt;
  
  
  CookieNegate Predicate
&lt;/h4&gt;

&lt;p&gt;Due to the nature of the authorization code flow where authorization request shows twice. The 1st time when the client first trigger the flow and the 2nd time when the client gets the &lt;code&gt;code&lt;/code&gt; from the identity server. &lt;br&gt;
The cookie negate predicate is to make sure that the authorize request is the 1st one. &lt;/p&gt;

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

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CookieNegateRoutePredicateFactory&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;CookieRoutePredicateFactory&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;override&lt;/span&gt; &lt;span class="n"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;apply&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;config:&lt;/span&gt; &lt;span class="nc"&gt;Config&lt;/span&gt;&lt;span class="o"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;Predicate&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ServerWebExchange&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;return&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;apply&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;negate&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;h4&gt;
  
  
  QueryNegate Predicate
&lt;/h4&gt;

&lt;p&gt;Same as CookieNegate predicate but for query parameters. &lt;code&gt;kc_idp&lt;/code&gt; is our defined scope for the Keycloak client in the brokered authorization server. If the request parameters has &lt;code&gt;kc_idp&lt;/code&gt; scope, the request is brokered already and should be skipped. &lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;QueryNegateRoutePredicateFactory&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;QueryRoutePredicateFactory&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="n"&gt;override&lt;/span&gt; &lt;span class="n"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;apply&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;config:&lt;/span&gt; &lt;span class="nc"&gt;Config&lt;/span&gt;&lt;span class="o"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;Predicate&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ServerWebExchange&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;return&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;apply&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;negate&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;h4&gt;
  
  
  AuthorizeRedirectToKc filter
&lt;/h4&gt;

&lt;p&gt;Now, all predicates passed and the authorization request should be targeting Keycloak. The &lt;code&gt;AuthorizeRedirectToKc&lt;/code&gt; is to map the request to the equivalent Keycloak URL in response &lt;code&gt;Location&lt;/code&gt; header with HTTP status code &lt;code&gt;301&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%2Fpx1u9irive8lvtk8cck3.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%2Fpx1u9irive8lvtk8cck3.png" alt="Authorize request mapping to Keycloak"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nf"&gt;AuthorizeRedirectToKcGatewayFilterFactory&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;val&lt;/span&gt; &lt;span class="nl"&gt;keycloakService:&lt;/span&gt; &lt;span class="nc"&gt;KeycloakService&lt;/span&gt;
&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;RedirectToGatewayFilterFactory&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="n"&gt;override&lt;/span&gt; &lt;span class="n"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;apply&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;config:&lt;/span&gt; &lt;span class="nc"&gt;Config&lt;/span&gt;&lt;span class="o"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;GatewayFilter&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Set the http status by FOUND and uri empty as it will be calculated and not possible to make it fixed through configuration&lt;/span&gt;
        &lt;span class="n"&gt;config&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;config&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;HttpStatus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;FOUND&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;toString&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;
            &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isIncludeRequestParams&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="kd"&gt;super&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;config&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;override&lt;/span&gt; &lt;span class="n"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;apply&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;httpStatus:&lt;/span&gt; &lt;span class="nc"&gt;HttpStatusHolder&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;uri:&lt;/span&gt; &lt;span class="no"&gt;URI&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;isIncludedRequestParams:&lt;/span&gt; &lt;span class="nc"&gt;Boolean&lt;/span&gt;&lt;span class="o"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;GatewayFilter&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;GatewayFilter&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="n"&gt;exchange&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chain&lt;/span&gt; &lt;span class="o"&gt;-&amp;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;exchange&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isCommitted&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;val&lt;/span&gt; &lt;span class="n"&gt;kcAuthorizeUri&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;getKcAuthorizeUri&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exchange&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;kcAuthorizeUri&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;error&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="s"&gt;"Can't construct the keycloak authorize request from: &amp;lt;${exchange.request.uri}"&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
                    &lt;span class="nc"&gt;ServerWebExchangeUtils&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setResponseStatus&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exchange&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;HttpStatusHolder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;HttpStatus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;BAD_REQUEST&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
                    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="nd"&gt;@GatewayFilter&lt;/span&gt; &lt;span class="n"&gt;exchange&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setComplete&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
                &lt;span class="o"&gt;}&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="nd"&gt;@GatewayFilter&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;apply&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;httpStatus&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kcAuthorizeUri&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;isIncludedRequestParams&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;filter&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exchange&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chain&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;}&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="nd"&gt;@GatewayFilter&lt;/span&gt; &lt;span class="nc"&gt;Mono&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;empty&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Void&lt;/span&gt;&lt;span class="o"&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="n"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;getKcAuthorizeUri&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;exchange:&lt;/span&gt; &lt;span class="nc"&gt;ServerWebExchange&lt;/span&gt;&lt;span class="o"&gt;):&lt;/span&gt; &lt;span class="no"&gt;URI&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;val&lt;/span&gt; &lt;span class="n"&gt;clientId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;exchange&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;queryParams&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="no"&gt;CLIENT_ID&lt;/span&gt;&lt;span class="o"&gt;]?.&lt;/span&gt;&lt;span class="na"&gt;first&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;?:&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;keycloakService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;authorizeUrl&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;clientId&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;companion&lt;/span&gt; &lt;span class="n"&gt;object&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;val&lt;/span&gt; &lt;span class="no"&gt;CLIENT_ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"client_id"&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;In this post, we described in details how is the authorization code flow can be routed to Keycloak without any need from the client to change their implementation. In &lt;a href="https://dev.to/mohammedalics/seamless-migration-to-keycloak-refresh-token-4m4f"&gt;Seamless Migration to Keycloak: Refresh token&lt;/a&gt;, I describe how could we make it for the refresh token requests. &lt;/p&gt;

&lt;p&gt;I hope you find it useful.&lt;/p&gt;

</description>
      <category>keycloak</category>
      <category>iam</category>
      <category>migration</category>
      <category>springsecurity</category>
    </item>
    <item>
      <title>Securing Keycloak: Configuring Admin Access within Your Private Network</title>
      <dc:creator>Mohammed Ammer</dc:creator>
      <pubDate>Sun, 31 Mar 2024 22:26:55 +0000</pubDate>
      <link>https://dev.to/mohammedalics/securing-keycloak-configuring-admin-access-within-your-private-network-4f1g</link>
      <guid>https://dev.to/mohammedalics/securing-keycloak-configuring-admin-access-within-your-private-network-4f1g</guid>
      <description>&lt;p&gt;When it comes to administrative capabilities, Keycloak boasts a wealth of features that empower users to efficiently manage their system. Alongside a user-friendly web admin tool, Keycloak offers a robust REST API, enabling seamless programmatic control.&lt;/p&gt;

&lt;p&gt;In this article, I'll discuss on how to prevent the public access to Keycloak admin. &lt;/p&gt;

&lt;p&gt;For this, you need to decide about the public and private host for Keycloak. For instance, ingress will look like:&lt;br&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;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;networking.k8s.io/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Ingress&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;keycloak-ingress&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;your-namespace&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;internal.example.com&lt;/span&gt;
    &lt;span class="na"&gt;http&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/keycloak&lt;/span&gt;
        &lt;span class="na"&gt;pathType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Prefix&lt;/span&gt;
        &lt;span class="na"&gt;backend&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;keycloak&lt;/span&gt;
            &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;number&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8080&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;external.example.com&lt;/span&gt;
    &lt;span class="na"&gt;http&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/keycloak&lt;/span&gt;
        &lt;span class="na"&gt;pathType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Prefix&lt;/span&gt;
        &lt;span class="na"&gt;backend&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;keycloak&lt;/span&gt;
            &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;number&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8080&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, in the &lt;code&gt;deployment.yaml&lt;/code&gt; file, add environment variables as below:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;      &lt;span class="s"&gt;...&lt;/span&gt;
      &lt;span class="s"&gt;containers&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;keycloak&lt;/span&gt;
          &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;quay.io/keycloak/keycloak:24.0.2&lt;/span&gt; &lt;span class="c1"&gt;# keycloak official docker image or your customised one&lt;/span&gt;
          &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;KC_HOSTNAME&lt;/span&gt;
              &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;external.example.com&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;KC_HOSTNAME_ADMIN&lt;/span&gt;
              &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;internal.example.com&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, after you deploy Keycloak. Navigating  &lt;a href="https://external.example.com/keycloak/admin/"&gt;https://external.example.com/keycloak/admin/&lt;/a&gt; will redirect you automatically to &lt;a href="https://internal.example.com/keycloak/admin/"&gt;https://internal.example.com/keycloak/admin/&lt;/a&gt; &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You can still use &lt;code&gt;web-proxy&lt;/code&gt; to control access to Keycloak if you've such requirements. I prefer to have to have a context path for Keycloak to facilitate that work. To configure it, you need to add below environment variables in &lt;code&gt;deployment.yaml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="na"&gt;KC_HOSTNAME_PATH&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;keycloak&lt;/span&gt;
  &lt;span class="na"&gt;KC_HTTP_RELATIVE_PATH&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/keycloak&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is all! I hope you find it useful.&lt;/p&gt;

</description>
      <category>keycloak</category>
      <category>iam</category>
      <category>security</category>
    </item>
    <item>
      <title>Optimized Keycloak build</title>
      <dc:creator>Mohammed Ammer</dc:creator>
      <pubDate>Sun, 31 Mar 2024 22:26:50 +0000</pubDate>
      <link>https://dev.to/mohammedalics/optimized-keycloak-build-4gep</link>
      <guid>https://dev.to/mohammedalics/optimized-keycloak-build-4gep</guid>
      <description>&lt;p&gt;Keycloak provides a documentation on how is to &lt;a href="https://www.keycloak.org/server/configuration"&gt;configure Keycloak&lt;/a&gt; the proper way in Kubernetes. I wouldn't repeat the documentation so please make sure you go through it. &lt;/p&gt;

&lt;p&gt;The most important part when said:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;We recommend optimizing Keycloak to provide faster startup and better memory consumption before deploying Keycloak in a production environment. This section describes how to apply Keycloak optimizations for the best performance and runtime behavior - &lt;a href="https://www.keycloak.org/server/configuration#_optimize_the_keycloak_startup"&gt;Optimize the startup&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For that to happen, let us write our Dockerfile&lt;/p&gt;

&lt;h2&gt;
  
  
  The Dockerfile
&lt;/h2&gt;

&lt;p&gt;Let us have a look to the entire Dockerfile before we go into each Dockefile layer.&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;ARG&lt;/span&gt;&lt;span class="s"&gt; KC_VERSION&lt;/span&gt;

&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;quay.io/keycloak/keycloak:${KC_VERSION}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;builder&lt;/span&gt;

&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; KC_HEALTH_ENABLED=true&lt;/span&gt;
&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; KC_METRICS_ENABLED=true&lt;/span&gt;
&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; KC_DB=postgres&lt;/span&gt;
&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; KC_FEATURES&lt;/span&gt;
&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; KC_METRICS_SPI_VERSION&lt;/span&gt;
&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; KC_HOSTNAME_PATH=keycloak&lt;/span&gt;
&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; KC_HTTP_RELATIVE_PATH=/keycloak&lt;/span&gt;

&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; KC_VERSION=${KC_VERSION}&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; KC_HEALTH_ENABLED=${KC_HEALTH_ENABLED}&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; KC_DB=${KC_DB}&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; KC_HOSTNAME_PATH=${KC_HOSTNAME_PATH}&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; KC_HTTP_RELATIVE_PATH=${KC_HTTP_RELATIVE_PATH}&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; KC_FEATURES=${KC_FEATURES}&lt;/span&gt;

&lt;span class="k"&gt;ADD&lt;/span&gt;&lt;span class="s"&gt; --chown=keycloak:keycloak https://github.com/aerogear/keycloak-metrics-spi/releases/download/${KC_METRICS_SPI_VERSION}/keycloak-metrics-spi-${KC_METRICS_SPI_VERSION}.jar /opt/keycloak/providers/keycloak-metrics-spi-${KC_METRICS_SPI_VERSION}.jar&lt;/span&gt;

&lt;span class="k"&gt;RUN &lt;/span&gt;/opt/keycloak/bin/kc.sh build &lt;span class="nt"&gt;--cache-stack&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;kubernetes

&lt;span class="c"&gt;# Installing additional RPM packages https://www.keycloak.org/server/containers&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;registry.access.redhat.com/ubi9:9.3&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;ubi-micro-build&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /mnt/rootfs
&lt;span class="k"&gt;RUN &lt;/span&gt;dnf &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; curl-7.76.1 &lt;span class="nt"&gt;--installroot&lt;/span&gt; /mnt/rootfs  &lt;span class="nt"&gt;--releasever&lt;/span&gt; 9 &lt;span class="nt"&gt;--setopt&lt;/span&gt; &lt;span class="nv"&gt;install_weak_deps&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;false&lt;/span&gt; &lt;span class="nt"&gt;--nodocs&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    dnf &lt;span class="nt"&gt;--installroot&lt;/span&gt; /mnt/rootfs clean all &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    rpm &lt;span class="nt"&gt;--root&lt;/span&gt; /mnt/rootfs &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nt"&gt;--nodeps&lt;/span&gt; setup

&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; quay.io/keycloak/keycloak:${KC_VERSION}&lt;/span&gt;

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder /opt/keycloak/ /opt/keycloak/&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=ubi-micro-build /mnt/rootfs /&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; ./entrypoint.sh /deployments/entrypoint.sh&lt;/span&gt;

&lt;span class="k"&gt;ENTRYPOINT&lt;/span&gt;&lt;span class="s"&gt; ["/deployments/entrypoint.sh"]&lt;/span&gt;

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

&lt;/div&gt;



&lt;h3&gt;
  
  
  Build Keycloak
&lt;/h3&gt;

&lt;p&gt;For maintainability, parameterize the Keycloak version so it is easier from the build tool (CI/CD) to upgrade Keycloak without touching the Dockerfile. Below is the first stage in our multi-stage Dockerfile defined as &lt;code&gt;builder&lt;/code&gt;&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;ARG&lt;/span&gt;&lt;span class="s"&gt; KC_VERSION&lt;/span&gt;

&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;quay.io/keycloak/keycloak:${KC_VERSION}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;builder&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Arguments
&lt;/h4&gt;

&lt;p&gt;Here is the Dockerfile arguments.&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;ARG&lt;/span&gt;&lt;span class="s"&gt; KC_HEALTH_ENABLED=true&lt;/span&gt;
&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; KC_METRICS_ENABLED=true&lt;/span&gt;
&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; KC_DB=postgres&lt;/span&gt;
&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; KC_FEATURES&lt;/span&gt;
&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; KC_METRICS_SPI_VERSION&lt;/span&gt;
&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; KC_HOSTNAME_PATH=keycloak&lt;/span&gt;
&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; KC_HTTP_RELATIVE_PATH=/keycloak&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;KC_HEALTH_ENABLED&lt;/code&gt; and &lt;code&gt;KC_METRICS_ENABLED&lt;/code&gt;: Both are important for monitoring Keycloak. I enabled them by default.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;KC_DB&lt;/code&gt;: For the target database provider. I consider &lt;code&gt;postgres&lt;/code&gt; database. &lt;/li&gt;
&lt;li&gt;
&lt;code&gt;KC_FEATURES&lt;/code&gt;: To control the enabling features in Keycloak from CI/CD. &lt;/li&gt;
&lt;li&gt;
&lt;code&gt;KC_METRICS_SPI_VERSION&lt;/code&gt;: If &lt;code&gt;KC_METRICS_ENABLED&lt;/code&gt; is true, the target version of the &lt;a href="https://github.com/aerogear/keycloak-metrics-spi"&gt;Keycloak Metrics SPI&lt;/a&gt;. Keycloak Metrics SPI is not an official metrics from Keycloak but a well known SPI. It worth to check and decide about it. &lt;/li&gt;
&lt;li&gt;
&lt;code&gt;KC_HOSTNAME_PATH&lt;/code&gt; and &lt;code&gt;KC_HTTP_RELATIVE_PATH&lt;/code&gt; are required to be added by the same name if you choose different context path for Keycloak than the default &lt;code&gt;/&lt;/code&gt;. In my case, I choose &lt;code&gt;/keycloak&lt;/code&gt; as context path.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Prepare the environment variables
&lt;/h4&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; KC_VERSION=${KC_VERSION}&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; KC_HEALTH_ENABLED=${KC_HEALTH_ENABLED}&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; KC_DB=${KC_DB}&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; KC_HOSTNAME_PATH=${KC_HOSTNAME_PATH}&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; KC_HTTP_RELATIVE_PATH=${KC_HTTP_RELATIVE_PATH}&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; KC_FEATURES=${KC_FEATURES}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keycloak uses the environment variables during the build to read from and act based on it. In the above snippet, the required environment variables for our build added. &lt;/p&gt;

&lt;h4&gt;
  
  
  Prepare the SPIs
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;ADD&lt;/span&gt;&lt;span class="s"&gt; --chown=keycloak:keycloak https://github.com/aerogear/keycloak-metrics-spi/releases/download/${KC_METRICS_SPI_VERSION}/keycloak-metrics-spi-${KC_METRICS_SPI_VERSION}.jar /opt/keycloak/providers/keycloak-metrics-spi-${KC_METRICS_SPI_VERSION}.jar&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A example build step that downloads a JAR file from a URL and adds it to the providers directory&lt;/p&gt;

&lt;h4&gt;
  
  
  Build Keycloak
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;RUN &lt;/span&gt;/opt/keycloak/bin/kc.sh build &lt;span class="nt"&gt;--cache-stack&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;kubernetes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the most important step where we build Keycloak based on the environment variables we provided in the build step and cache stack as &lt;code&gt;kubernetes&lt;/code&gt;. &lt;/p&gt;

&lt;h4&gt;
  
  
  Install additional RPM packages
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# Installing additional RPM packages https://www.keycloak.org/server/containers&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;registry.access.redhat.com/ubi9:9.3&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;ubi-micro-build&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /mnt/rootfs
&lt;span class="k"&gt;RUN &lt;/span&gt;dnf &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; curl-7.76.1 &lt;span class="nt"&gt;--installroot&lt;/span&gt; /mnt/rootfs  &lt;span class="nt"&gt;--releasever&lt;/span&gt; 9 &lt;span class="nt"&gt;--setopt&lt;/span&gt; &lt;span class="nv"&gt;install_weak_deps&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;false&lt;/span&gt; &lt;span class="nt"&gt;--nodocs&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    dnf &lt;span class="nt"&gt;--installroot&lt;/span&gt; /mnt/rootfs clean all &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    rpm &lt;span class="nt"&gt;--root&lt;/span&gt; /mnt/rootfs &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nt"&gt;--nodeps&lt;/span&gt; setup
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This step is not necessary, although if you need to have additional RPM packages in the container, you can install it as shown in the above example for installing &lt;code&gt;curl&lt;/code&gt;. &lt;/p&gt;

&lt;h4&gt;
  
  
  Final image.
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; quay.io/keycloak/keycloak:${KC_VERSION}&lt;/span&gt;

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder /opt/keycloak/ /opt/keycloak/&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=ubi-micro-build /mnt/rootfs /&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; ./entrypoint.sh /deployments/entrypoint.sh&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The final stage is our image where Keycloak build files are pre-installed and ready to start up. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;From the &lt;code&gt;builder&lt;/code&gt; stage, we copy the Keycloak build&lt;/li&gt;
&lt;li&gt;From the &lt;code&gt;ubi-micro-build&lt;/code&gt; stage, we copy the installed packages&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;entrypoint.sh&lt;/code&gt; is our custom entrypoint file to start Keycloak. In its basic form can be as simple as below:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;exec&lt;/span&gt; /opt/keycloak/bin/kc.sh start &lt;span class="nt"&gt;--optimized&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I prefer to have the entrypoint in separate file so later I have the flexibility to customize it without modifying the Dockerfile. &lt;em&gt;Indeed I use additional scripts in the entrypoint. For instance, integrate Datadog with Keycloak. I can write a separate article to describe it ;)&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Keycloak is an identity provider which provides the front-end for the users same as admins. Someone would think to narrow the exposed part of Keycloak to only what necessary public and keep everything else to be accessed only through VPN / Internal network. &lt;br&gt;
In the next blog post, I describe how you can &lt;a href="https://dev.to/mohammedalics/securing-keycloak-configuring-admin-access-within-your-private-network-4f1g"&gt;separate Keycloak public and private APIs/URLs&lt;/a&gt; for an enhanced security. &lt;/p&gt;

&lt;p&gt;I hope you find it useful.&lt;/p&gt;

</description>
      <category>keycloak</category>
      <category>kubernetes</category>
      <category>dockerfile</category>
      <category>iam</category>
    </item>
    <item>
      <title>Seamless Migration to Keycloak: Insights into Transitioning from Spring Authorization Server</title>
      <dc:creator>Mohammed Ammer</dc:creator>
      <pubDate>Sun, 31 Mar 2024 22:26:44 +0000</pubDate>
      <link>https://dev.to/mohammedalics/seamless-migration-to-keycloak-insights-into-transitioning-from-spring-authorization-server-47l3</link>
      <guid>https://dev.to/mohammedalics/seamless-migration-to-keycloak-insights-into-transitioning-from-spring-authorization-server-47l3</guid>
      <description>&lt;p&gt;Transitioning between authorization solutions is often a strategic move. Here we dive into the journey from Spring Authorization Server to &lt;a href="https://www.keycloak.org"&gt;Keycloak&lt;/a&gt; – two prominent players in the realm of identity and access management (IAM)&lt;/p&gt;




&lt;p&gt;In 2020, Spring decided to deprecate "&lt;a href="https://spring.io/projects/spring-security-oauth"&gt;Spring Security OAuth&lt;/a&gt;" and announced an &lt;a href="https://spring.io/projects/spring-security-oauth#support"&gt;End-of-Life as end of May 2022&lt;/a&gt;. Back then, we started to think about an alternative to Spring. Although Spring announced back the return of Spring Security OAuth in &lt;a href="https://spring.io/projects/spring-authorization-server"&gt;Spring Authorization Server&lt;/a&gt;, the earlier is poor in features compared to Keycloak and its strong community. There could be many reasons driven by evolving security needs, scalability requirements, or for enhanced user experience to device for Keycloak. &lt;/p&gt;

&lt;p&gt;Starting on the journey of transitioning from Spring Authorization Server to Keycloak is quite the undertaking, isn't it? 👀 &lt;br&gt;
But fear not, because I'm here to guide you through it! Recognizing the complexity of this move, I've decided to break it down into a series of articles, tackling each challenge along the way to ensure a smooth transition 🤝.&lt;/p&gt;

&lt;p&gt;You might be wondering, &lt;strong&gt;why go through all this trouble if both are based on &lt;a href="https://datatracker.ietf.org/doc/html/rfc6749"&gt;Auth2.0 standard&lt;/a&gt;?&lt;/strong&gt; Well, despite the popularity of both Spring Authorization Server and Keycloak, detailed migration guides are surprisingly scarce. Each has different URLs, slightly difference in payload and the most important is the missing OpenID Connect (OIDC) in Spring Security OAuth which makes it harder to have the easy move you expect (e.g. using the default OIDC broker in Keycloak). That's where I come in – to provide you with a roadmap for success 👨‍🎓&lt;/p&gt;

&lt;p&gt;Before we dive into the nitty-gritty details, let's make sure we're all on the same page. Understanding the basics of Identity and Access Management (IAM) is key. If terms like OAuth 2.0, authorization code flow, refresh token flow, and OpenID Connect (OIDC) sound like a foreign language to you, Just Google it. I am sure you'll find many resources for that. Building a solid understanding of these fundamentals will set the stage for a smoother migration journey. So, are you ready to get started? Let's do this! 🚀&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Getting started&lt;/strong&gt;&lt;br&gt;
When it comes to tackling big tasks like this, I'm a big advocate for starting small and taking things one step at a time. This approach allows for learning from mistakes and making necessary adjustments along the way – it's all about that iterative process!&lt;/p&gt;




&lt;p&gt;So, &lt;em&gt;where do we begin?&lt;/em&gt; &lt;br&gt;
Well, let us assume the current authorization server as simple as below:&lt;br&gt;
&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkzdp1d4whqgm3qfovct5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkzdp1d4whqgm3qfovct5.png" alt="spring authorization server diagram" width="800" height="416"&gt;&lt;/a&gt;&lt;br&gt;
The authorization server depends on two services, one for the user profile and another that stores the user credentials. &lt;/p&gt;

&lt;p&gt;It's crucial to maintain a seamless user experience during migration. Users shouldn't need to re-login for new tokens from Keycloak or transfer data between data sources. This applies to resource servers too.&lt;/p&gt;

&lt;p&gt;To ensure this, we need a traffic-controlling gateway and careful management of client migration. Instead of big moves, migrating client by client minimizes the risk of full outages in case of failures.&lt;/p&gt;

&lt;p&gt;Based on that, Here is -&lt;em&gt;in abstract&lt;/em&gt;- the new design:&lt;br&gt;
&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9n3p8um56vratigolo2l.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9n3p8um56vratigolo2l.png" alt="keycloak and spring authorization server" width="800" height="535"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In below articles, I'll cover some topics to achieve a smooth migration: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://dev.to/mohammedalics/seamless-migration-to-keycloak-non-oidc-oauth-20-broker-58a6"&gt;Brokering non-OIDC OAuth 2.0 identities&lt;/a&gt; to solve absence of OIDC in Spring Security OAuth.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/mohammedalics/seamless-migration-to-keycloak-authorization-code-flow-5ehm"&gt;Authorization Code flow migration&lt;/a&gt; and retain OAuth Clients and Resource Servers Untouched &lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/mohammedalics/seamless-migration-to-keycloak-refresh-token-4m4f"&gt;Refresh token migration&lt;/a&gt; and the same, OAuth Clients untouched.&lt;/li&gt;
&lt;li&gt;The migration of refresh token grant type can't be completed without utilizing the &lt;a href="https://dev.to/mohammedalics/seamless-migration-to-keycloak-token-exchange-4b7p"&gt;Token Exchange in our OAuth 2.0 custom identity provider&lt;/a&gt; described in the above article. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Beside that, there are some selected topics where I found it very useful to share. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://dev.to/mohammedalics/optimized-keycloak-build-4gep"&gt;Optimized Keycloak build&lt;/a&gt;: Creating a suitable Docker file for Keycloak based on your setup&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/mohammedalics/securing-keycloak-configuring-admin-access-within-your-private-network-4f1g"&gt;Ensure Keycloak stays within the internal network while exposing only its APIs publicly&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/mohammedalics/optimized-keycloak-spi-deployment-using-maven-shade-plugin-1ee6"&gt;Use maven shade plugin to include SPIs third-party libraries in the same SPI JAR&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Use &lt;a href="https://dev.to/mohammedalics/custom-identity-providers-in-keycloak-with-terraform-a-step-by-step-guide-81h"&gt;Terraform to configure a custom identity provider&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;For Datadog users, A guide to &lt;a href="https://dev.to/mohammedalics/integrating-keycloak-with-datadog-enabling-keycloak-traces-in-kubernetes-using-datadog-apm-1563"&gt;integrate Keycloak with Datadog&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I hope it will help you!&lt;/p&gt;

</description>
      <category>keycloak</category>
      <category>iam</category>
      <category>spring</category>
      <category>migration</category>
    </item>
  </channel>
</rss>
