Keycloak Migration Made Easy: Tips and Best Practices

INCONE60 Green - Digital and green transition of small ports
Andrzej Chybicki: projekty związane z wykorzystaniem sztucznej inteligencji to znacząca część naszych projektów
Keycloak Migration Made Easy: Tips and Best Practices

Here we’ll explore the most significant changes introduced in recent Keycloak releases and how they impact migration efforts. We’ll walk through practical examples to resolve common challenges, ensuring a smooth transition to newer versions. Whether it’s adapting to updated configurations or managing deprecated features, this post should provide additional tips to streamline your Keycloak migration process.

One of the most challenging migrations for many users has been the upgrade to Keycloak 17, where the underlying architecture shifted from WildFly to the Quarkus framework. This transition marked a significant departure from the traditional application server model, requiring users to adopt a more modern, lightweight approach tailored to Quarkus. The way Keycloak is configured has fundamentally changed – rather than deploying it on an external application server, it now operates as a standalone application, which simplifies deployment.

For example, custom providers, which were previously packaged dynamically as modules for WildFly, now need to be rebuilt and adapted (as runtime is immutable), involving changes to dependencies, classloading, and packaging methods. This design simplifies deployments in environments like Kubernetes but demands a shift in workflows for teams accustomed to the WildFly solutions. While the move to Quarkus offers performance gains and a more modern development experience, it still requires careful planning and testing to ensure a smooth migration.

Key changes introduced in new releases

Migration introduces important changes that impact configuration, endpoints, and custom provider implementations:

  • HTTPS requirement for production mode – starting Keycloak with the command [keycloak_quarkus_root]/26.1.0/bin/kc.bat start now requires an HTTPS certificate, as the start option is intended for production use. For local development, the start-dev option should be used instead.
  • removal of /auth from base path – the /auth segment has been removed from the default base path. Applications relying on Keycloak endpoints must update their configurations to reflect this change. 
  • realm ID changes – in previous versions, the realm ID was identical to the realm name. Starting from Keycloak 21, the realm ID is now a unique, system-generated value. Applications relying on realm IDs should account for this change during migration.
  • deprecation of userLocalStorage – custom providers using the userLocalStorage method of the KeycloakSession interface must switch to the users method, as userLocalStorage was deprecated starting from Keycloak 19.
  • transport jdbc-ping as new default – in the latest version of Keycloak (26.1.0), the default method for discovering other nodes within a cluster has shifted to using its database, rather than relying on just multicast. This change eliminates the need for additional network configurations, particularly in cloud environments.

Issues with major migrations

When migrating Keycloak, it’s highly recommended to upgrade version by version, rather than jumping several releases at once. This incremental approach allows you to identify and resolve issues as they arise, minimizing the risk of unexpected complications. Upgrading across multiple versions—especially if spanning a dozen or more releases—can significantly complicate the process due to accumulated changes, deprecated features, and architectural shifts like the move to Quarkus. By addressing compatibility and configuration adjustments one step at a time, you ensure better control over the migration and reduce downtime or disruptions in production environments.

However, in many cases, a large one-time migration—such as moving from Keycloak 12 directly to Keycloak 26—is unavoidable and becomes a challenge that teams must address effectively. This process often involves significant changes to both the Keycloak server and dependent applications, particularly frontend clients that rely on its APIs. 

In this guide, we’ll outline a practical step-by-step approach to such a major migration. 

Dockerfile configuration

In our example project, all custom themes and SPI (Service Provider Interface) extensions were directly copied into the base Keycloak image without a dedicated build process. So it was done in a standard Wildfly way.

FROM quay.io/keycloak/keycloak:12.0.2

COPY themes/custom-theme /opt/jboss/keycloak/themes/custom-theme
COPY api-extensions/target/spi-resource-0.0.1-SNAPSHOT.jar

ENTRYPOINT /opt/jboss/tools/docker-entrypoint.sh -b 0.0.0.0

The new approach could use a multi-stage process with separate containers like:

FROM eclipse-temurin:17-jdk as spi_builder
[…]
FROM quay.io/keycloak/keycloak:$BASE_IMAGE_TAG as keycloak_builder
[…]
FROM quay.io/keycloak/keycloak:$BASE_IMAGE_TAG

The SPI is built during the container build process using Maven. This approach ensures that the dependencies are fetched and the resulting JAR is optimized for deployment.

### Runtime dependencies build container
FROM registry.access.redhat.com/ubi9 AS runtime_dependencies_builder
RUN mkdir -p /mnt/rootfs

RUN dnf install --installroot /mnt/rootfs curl --releasever 9 --setopt install_weak_deps=false --nodocs -y \
&& dnf --installroot /mnt/rootfs clean all

### SPI build container
FROM eclipse-temurin:17-jdk as spi_builder
ARG BASE_IMAGE_TAG
WORKDIR /workspace/app

COPY api-extensions/mvnw .
COPY api-extensions/.mvn .mvn
COPY api-extensions/pom.xml .

# dos2unix:
RUN sed -i -e 's/\r//g' mvnw
RUN ./mvnw dependency:go-offline
COPY api-extensions/src src
RUN ./mvnw -o package -DskipTests
 

The new process copies multiple custom themes into the Quarkus-based Keycloak during the build stage, ensuring they are included in the final optimized runtime. So this approach improves startup performance and aligns with immutable container philosophy.

### Build container
FROM quay.io/keycloak/keycloak:$BASE_IMAGE_TAG as keycloak_builder
COPY --from=spi_builder /workspace/app/target/spi-resource-0.0.1-SNAPSHOT-jar-with-dependencies.jar /opt/keycloak/providers/

#Copy custom themes
COPY themes/custom-theme /opt/keycloak/themes/custom-theme

#Build an optimized server runtime
RUN /opt/keycloak/bin/kc.sh build

### Runtime container
FROM quay.io/keycloak/keycloak:$BASE_IMAGE_TAG
COPY --from=keycloak_builder /opt/keycloak/ /opt/keycloak/
WORKDIR /opt/keycloak
ENTRYPOINT ["/opt/keycloak/bin/kc.sh"]

Basically, by leveraging multi-stage builds and lightweight images, the new process aligns with best practices for containerized deployments. You can find more details about these steps here: https://www.keycloak.org/server/containers.

Default /auth context path changed

With the transition to the Quarkus-based Keycloak distribution, the default context path has been modified—/auth is no longer part of the URL by default. This change aligns with Quarkus’s goal of providing a more streamlined and minimalistic approach to web applications, reducing unnecessary path prefixes.

For users or applications that still require the /auth context path, it can be reintroduced using the http-relative-path build option. For instance, running Keycloak with the following command restores the /auth context:

bin/kc.[sh|bat] start-dev –http-relative-path /auth

Or using a docker-compose way:

KC_HTTP_RELATIVE_PATH: /auth

This allows for compatibility with existing clients or configurations that rely on the /auth prefix. With the relative path specified, Keycloak will still automatically redirect requests from the root (e.g., localhost:8080/) to the /auth path (e.g., localhost:8080/auth). This ensures that applications or users accustomed to the previous URL structure continue to function as expected without requiring major changes.

Changes within provider management

With the shift to the Quarkus-based Keycloak distribution, there are significant changes in how custom providers (SPIs) are deployed and managed. In the WildFly-based distribution, custom providers were deployed by copying them into the standalone/deployments directory, and dependencies were also placed in specific locations within the WildFly server structure. However, in the new Quarkus distribution, this deployment model has been streamlined. Custom providers should now be copied into the /providers directory.

Additionally, Quarkus does not support the EAR packaging format or the jboss-deployment-structure.xml files, which were commonly used in the WildFly distribution to configure deployments and manage dependencies. As a result, the packaging process is simplified, but custom configurations previously made through these files must now be handled differently.

Furthermore, if custom providers utilized JavaEE APIs, such as session or stateless beans, these will no longer be supported in the Quarkus distribution.

Migrating custom themes

Old Keycloak relied on the FreeMarker template engine for rendering dynamic content in themes. This approach is still used in new releases (especially in v1 and v2 themes), but there have been updates to the template syntax and theme structure to align with the new Quarkus architecture. Custom templates may need to be revised to ensure compatibility with newer versions of FreeMarker, as the theme structure may have evolved.

Additionally, certain legacy template functions and macros that were present in, for example Keycloak 12, might have been deprecated or replaced with new, more efficient alternatives.

Custom themes that were previously customized are likely not optimized for dark mode. To avoid complications without having to rewrite them from scratch, the simplest solution is to add the appropriate option darkMode=false in the theme.properties file. Additionally, starting from version 26.1.0, the “Themes” tab now includes switches that allow for enabling dark mode on a per-realm basis.

Making new Keycloak server work with legacy frontend client

checkLoginIframe related issues

In one of our projects, we had to upgrade Keycloak by 14 versions. However, for various reasons, we couldn’t migrate the frontend, which was running on a rather old version of Angular and the keycloak-angular library. Therefore, we will now go through the tweaks we had to apply in order to restore the login process functionality.

Keycloak-angular is a wrapper library for keycloak-js that makes using it easier in Angular applications. It extends the original features with additional functionality and adds new methods to make it easier to use within an Angular app. It also provides a basic implementation of AuthGuard, allowing you to customize your logic by using the authentication logic. It’s also possible to use the HttpClient Interceptor, which adds an authorization header to selected HTTP requests.

"keycloak-angular": "^8.1.0",
"keycloak-js": "^12.0.4",

After integrating the new version of Keycloak (26.1.0) with the old frontend client, we encountered an issue after logging in, which manifested as follows:

This option specifies whether Keycloak should check the login status using an iframe. It should be used with caution, as improper configuration may lead to issues such as continuous page reloads. Newer versions of Keycloak may have improved or changed session management, particularly around cross-site cookies, authentication flows, or iframe handling (setupCheckLoginIframe, check3pCookiesSupported). These changes could affect how the frontend handles login states, especially if it is using deprecated methods for checking login states or processing callbacks.

Given the significant version gap between the frontend and the server, one useful approach might be to disable the setupCheckLoginIframe option, which could also help in situations where infinite redirect loops occur after the upgrade.

Here’s an example of how to disable it in your initialization:

function initializeKeycloak(keycloak: KeycloakService, permissionsService: PermissionsService) 
{
  return () =>
keycloak.init({

   config: {
     url: environment.keycloakUrl,
     realm: 'test-realm',
     clientId: 'test-realm-web',
   },
   initOptions: {
        checkLoginIframe: false
   }
}).then(() => permissionsService.init());
}

Missing nonce claim

In newer versions of Keycloak, in accordance with the OpenID Connect Core 1.0 specification, the nonce claim is now only added to the ID token if the parameter was included in the authorization request. According to the specification, the nonce claim is mandatory in the ID token but should not be included in tokens after a refresh request. Previously, nonce was added to all tokens (Access, Refresh, and ID) in all responses, including refresh responses.

As a result, using an older version of the keycloak-js adapter may cause login issues, such as “Invalid nonce, clearing token” errors or an infinite redirection loop after login attempts. To resolve this, users can add the predefined “Nonce backwards compatible” mapper via the “By Configuration” button in the dedicated client scope. More information can be found in the official Keycloak documentation (https://www.keycloak.org/docs/latest/upgrading/index.html#nonce-claim-is-only-added-to-the-id-token).

Post-logout redirect URI issues

According to the release notes for version 18, Keycloak no longer supports the redirect_uri parameter for logging out. Instead, you need to use post_logout_redirect_uri along with either the client_id or id_token_hint parameter. In practice, this means when calling the logout function, you must replace redirect_uri with post_logout_redirect_uri. In our case (with legacy keycloak-js), the logout process can be implemented like this:

window.location.replace(this.keycloak['_instance']['endpoints'].logout() +
  '?post_logout_redirect_uri=' + encodeURIComponent(window.location.origin) +
  '&client_id=test-realm-web');

This change should resolve the most common issues with redirects after logging out of the application.

Summary

Over the past few years, Keycloak has undergone significant changes, particularly with the breaking changes introduced during the migration from WildFly to Quarkus. These changes were necessary for performance improvements, more efficient resource usage, and better scalability.

While the migration process can appear challenging, it is generally achievable, even when working with older clients. However, the ease of migration largely depends on the specific use case, especially the level of customization involved. For instance, if themes were highly customized in the previous version of Keycloak, adapting them to newer distributions may require more time and effort, as the structure and templating engines have evolved.

Similarly, integrations with legacy systems might need careful planning to ensure compatibility with newer versions of Keycloak. On the other hand, for standard setups with minimal customization, the transition is often smoother and quicker. The process of migration can also be supported by detailed documentation and a strong community, which has grown significantly in recent years.

Overall, while each migration project has its own challenges, with proper planning and testing, transitioning to a newer version of Keycloak is generally not so complicated, and the long-term benefits of the upgrade, such as improved performance and security features, make it worth the effort.

Are you planning to implement Keycloak in your organization?

If you're looking for a partner to provide comprehensive support in this process, be sure to contact us.