Keycloak ClassCastException: AddressMapper & ClaimMapper Conflict

by Alex Johnson 66 views

When working with Keycloak, a popular open-source identity and access management solution, developers sometimes encounter the dreaded ClassCastException. This article dives deep into a specific scenario where this exception arises when the built-in AddressMapper is used in conjunction with a ClaimMapper. We'll explore the root cause of the problem, the impact it has on your applications, and how to effectively address it.

Understanding the ClassCastException in Keycloak

The ClassCastException is a common Java exception that occurs when an attempt is made to cast an object to a class of which it is not an instance. In the context of Keycloak, this typically arises when there is a mismatch between the expected data type and the actual data type being processed. Specifically, this article addresses the scenario where the AddressMapper, which is responsible for mapping address information, is used alongside a ClaimMapper, which adds custom claims to the user's identity token. When these two mappers interact in certain ways, a ClassCastException can occur, leading to application errors and a degraded user experience.

Root Cause Analysis: Why the Exception Occurs

The core of the issue lies in how Keycloak's internal AddressMapper and ClaimMapper handle data structures. The AddressMapper creates an object of type AddressClaimSet to represent the address information. This object is then added to the token. On the other hand, the ClaimMapper typically works with Map<String, Object> to manage claims. The conflict arises when the ClaimMapper attempts to add an additional attribute to the address structure. The code snippet below illustrates the problematic area:

public class OIDCAttributeMapperHelper {

    private static void mapClaim(List<String> split, Object attributeValue, Map<String, Object> jsonObject, boolean isMultivalued) {
        ....
        else {
            @SuppressWarnings("unchecked")
            Map<String, Object> nested = (Map<String, Object>) jsonObject.get(component);

            if (nested == null) {
                nested = new HashMap<>();
                jsonObject.put(component, nested);
            }

            jsonObject = nested;
        }
    }
...
}

In this code, the mapClaim method attempts to cast the value retrieved from the JSON object to a Map<String, Object>. However, when the AddressMapper has already created an AddressClaimSet, this cast fails, resulting in a ClassCastException. The following stack trace highlights the origin of the exception:

2025-11-25 07:54:17,716 ERROR [org.keycloak.services.error.KeycloakErrorHandler] (executor-thread-33) Uncaught server error: java.lang.ClassCastException: class org.keycloak.representations.AddressClaimSet cannot be cast to class java.util.Map (org.keycloak.representations.AddressClaimSet is in unnamed module of loader io.quarkus.bootstrap.runner.RunnerClassLoader @4bec1f0c; java.util.Map is in module java.base of loader 'bootstrap')
	at org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper.mapClaim(OIDCAttributeMapperHelper.java:345)
	at org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper.mapClaim(OIDCAttributeMapperHelper.java:317)
	at org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper.mapClaim(OIDCAttributeMapperHelper.java:278)
	at org.keycloak.protocol.oidc.mappers.UserAttributeMapper.setClaim(UserAttributeMapper.java:103)
	at org.keycloak.protocol.oidc.mappers.AbstractOIDCProtocolMapper.setClaim(AbstractOIDCProtocolMapper.java:159)
	at org.keycloak.protocol.oidc.mappers.AbstractOIDCProtocolMapper.transformUserInfoToken(AbstractOIDCProtocolMapper.java:80)
	at org.keycloak.protocol.oidc.TokenManager$3.applyMapper(TokenManager.java:821)
	at org.keycloak.protocol.oidc.TokenManager$3.applyMapper(TokenManager.java:818)

Impact on Applications and User Experience

The ClassCastException can have significant implications for applications relying on Keycloak for authentication and authorization. When this exception occurs, it typically results in a 500 Internal Server Error, preventing users from logging in or accessing protected resources. This can lead to a degraded user experience and potentially disrupt critical business operations. Imagine a scenario where users are unable to access their accounts due to this error; it can lead to frustration and loss of trust in the application.

Reproducing the Issue

To reproduce this issue, you need to configure Keycloak with both the AddressMapper and a ClaimMapper that attempts to add an attribute to the address structure. For instance, if you want to add a type claim to the address claim, you'll likely encounter this exception. The desired JSON structure would look like this:

"address": {
  ...,
  "type": "STRUCTURED"
}

By setting up this configuration, you can simulate the scenario and observe the ClassCastException in action. This is crucial for understanding the problem and developing effective solutions.

Solutions and Workarounds for the ClassCastException

Fortunately, there are several approaches to address the ClassCastException that arises when using the AddressMapper and ClaimMapper in Keycloak. Let's explore some of these solutions:

1. Custom Address Claim Mapper

One effective solution is to create a custom address claim mapper. Instead of relying on the default AddressMapper and attempting to modify its output with a ClaimMapper, you can build a custom mapper that handles the entire address claim generation process. This gives you complete control over the structure and content of the address claim, allowing you to include any additional attributes you need without encountering type conflicts.

To implement a custom mapper, you would typically extend Keycloak's AbstractOIDCProtocolMapper and override the necessary methods to fetch user attributes and construct the desired address claim. This approach provides the most flexibility and ensures that the address claim is generated in the exact format required by your application.

When developing a custom mapper, consider these key aspects:

  • Fetching User Attributes: You'll need to retrieve the necessary user attributes from Keycloak's user model. This may involve accessing attributes directly or using more complex queries to fetch related data.
  • Constructing the Address Claim: Build the address claim as a Map<String, Object>, ensuring that all attributes are correctly formatted and included. This allows you to add custom fields, such as the type claim mentioned earlier, without encountering type conflicts.
  • Handling Multi-Valued Attributes: If your address claim includes multi-valued attributes (e.g., an array of phone numbers), ensure that your custom mapper correctly handles these values.
  • Error Handling: Implement robust error handling to gracefully manage situations where user attributes are missing or invalid.

By implementing a custom mapper, you eliminate the need to combine the default AddressMapper with a ClaimMapper, thus avoiding the ClassCastException.

2. Data Transformation within the ClaimMapper

Another approach is to perform data transformation within the ClaimMapper itself. This involves intercepting the output of the AddressMapper and transforming it into the desired structure before it's added to the token. While this approach can be more complex than a custom mapper, it can be useful in scenarios where you need to modify the output of other mappers as well.

To implement this, you would typically use a ClaimMapper that can access and modify the existing claims in the token. You can then check for the presence of the AddressClaimSet and transform it into a Map<String, Object> before adding the additional attributes. This ensures that the data structure is compatible with the ClaimMapper's expectations.

The transformation process might involve the following steps:

  • Check for AddressClaimSet: Verify if the address claim is an instance of AddressClaimSet.
  • Convert to Map: If it is, convert the AddressClaimSet to a Map<String, Object>. This can be done by extracting the individual fields from the AddressClaimSet and adding them to a new map.
  • Add Custom Attributes: Once the address claim is in a Map format, you can add your custom attributes, such as the type claim.
  • Replace the Claim: Replace the original AddressClaimSet with the transformed Map in the token.

This approach requires careful handling of data types and potential null values. It's crucial to ensure that the transformation process is robust and doesn't introduce new issues.

3. Version Compatibility Considerations

It's essential to consider the Keycloak version you are using. The behavior of mappers and the underlying data structures can change between versions. If you are encountering this issue, it's worth checking if it has been addressed in a newer version of Keycloak. Upgrading to the latest version might resolve the problem, as bug fixes and improvements are often included in new releases.

However, before upgrading, it's crucial to thoroughly test your application with the new version in a non-production environment. This ensures that the upgrade doesn't introduce any new compatibility issues or break existing functionality.

If upgrading is not an immediate option, you can explore workarounds that are specific to your Keycloak version. Consulting the Keycloak documentation and community forums can provide valuable insights into version-specific solutions.

Code Example: Custom Address Claim Mapper (Conceptual)

While a full implementation of a custom address claim mapper is beyond the scope of this article, here's a conceptual example to illustrate the key steps involved:

public class CustomAddressClaimMapper extends AbstractOIDCProtocolMapper implements OIDCAccessTokenMapper, OIDCUserInfoMapper {

    private static final List<ProviderConfigProperty> configProperties = new ArrayList<>();

    // Configure the mapper properties
    static {
        // Add configuration properties here
    }

    @Override
    public String getDisplayCategory() {
        return "Claim Mapper";
    }

    @Override
    public String getDisplayType() {
        return "Custom Address Claim";
    }

    @Override
    public String getHelpText() {
        return "Maps user attributes to a custom address claim.";
    }

    @Override
    public List<ProviderConfigProperty> getConfigProperties() {
        return configProperties;
    }

    @Override
    public AccessToken transformAccessToken(AccessToken token, ProtocolMapperModel mapperModel, KeycloakSession session, ClientSessionContext clientSessionCtx) {
        // Implement access token transformation
        return token;
    }

    @Override
    public AccessToken transformUserInfoToken(AccessToken token, ProtocolMapperModel mapperModel, KeycloakSession session, ClientSessionContext clientSessionCtx, UserSessionModel userSession, ClientModel client) {
        // Implement user info token transformation
        return token;
    }

    // Add a new method to fetch the full address object from user attributes
    private Map<String, Object> getAddressClaim(UserModel user) {
        Map<String, Object> address = new HashMap<>();

        // This is the new attribute added
        address.put("type", "STRUCTURED");

        address.put("street", user.getFirstAttribute("street"));
        address.put("city", user.getFirstAttribute("city"));
        address.put("region", user.getFirstAttribute("region"));
        address.put("country", user.getFirstAttribute("country"));
        address.put("postcode", user.getFirstAttribute("postcode"));

        return address;
    }


    public static ProtocolMapperRepresentation toRepresentation(ProtocolMapperModel entity) {
        return AbstractOIDCProtocolMapper.toRepresentation(entity);
    }

    @Override
    public String getId() {
        return "custom-address-claim-mapper";
    }
}

This example demonstrates the basic structure of a custom mapper. You would need to fill in the implementation details for fetching user attributes and constructing the address claim based on your specific requirements. Notice how a new method getAddressClaim was added to fetch the address object directly from user attributes.

Best Practices for Avoiding ClassCastException

To minimize the risk of encountering the ClassCastException when working with Keycloak mappers, consider these best practices:

  • Understand Data Structures: Familiarize yourself with the data structures used by Keycloak mappers, particularly the AddressClaimSet and Map<String, Object>. This will help you avoid type mismatches.
  • Plan Your Claim Mapping Strategy: Before configuring mappers, carefully plan how you want to map user attributes to claims. Consider the complexity of your requirements and choose the most appropriate approach (e.g., custom mapper vs. data transformation).
  • Test Thoroughly: Always test your mapper configurations in a non-production environment before deploying them to production. This will help you identify and resolve any issues early on.
  • Use Custom Mappers When Necessary: Don't hesitate to use custom mappers when the default mappers don't meet your needs. Custom mappers provide the most flexibility and control over claim generation.
  • Keep Keycloak Updated: Stay up-to-date with the latest Keycloak releases to benefit from bug fixes and improvements.

Conclusion

The ClassCastException that occurs when mixing the AddressMapper with a ClaimMapper in Keycloak can be a challenging issue to resolve. However, by understanding the root cause of the problem and implementing the appropriate solutions, you can effectively address this exception and ensure the smooth operation of your applications. Whether you choose to create a custom address claim mapper or perform data transformation within the ClaimMapper, the key is to carefully plan your approach and thoroughly test your configurations.

By following the best practices outlined in this article, you can minimize the risk of encountering this exception and build robust and secure applications with Keycloak.

For further information on Keycloak and its features, consider visiting the official Keycloak website or exploring community resources. You might find helpful information and tutorials on sites like Keycloak.org.