Keycloak User Storage SPI Example with Remote Service

In this tutorial, I will share with you how to implement user look-up and authentication using Keycloak’s User Storage SPI. For starters, SPI stands for Service Provider Interface, and it is a way to write extensions to Keycloak to connect to external user databases and credential stores.

In this tutorial, we will make Keycloak load user details from a remote Spring Boot Microservice and validate the user’s password.

At the time of writing this tutorial, the latest version of Keycloak is 21, and the Spring Boot version is 3.

For video lessons on how to secure your Spring Boot application with OAuth 2.0. and Spring Security 5, please checkout my complete video course OAuth 2.0. in Spring Boot applications.

Create a New Maven-Based Java Project

To implement the remote user storage SPI example, I will need to create a new project. This can be a new Spring Boot application created with Spring Boot Initializr tool. Once you create a new maven-based Java project, add the following Keycloak maven dependencies:

Add Keycloak Maven Dependencies

<!-- https://mvnrepository.com/artifact/org.keycloak/keycloak-server-spi -->
<dependency>
    <groupId>org.keycloak</groupId>
    <artifactId>keycloak-server-spi</artifactId>
    <version>21.1.1</version> <!-- 11.0.3 -->
    <scope>provided</scope>
</dependency>

<!-- https://mvnrepository.com/artifact/org.keycloak/keycloak-core -->
<dependency>
    <groupId>org.keycloak</groupId>
    <artifactId>keycloak-core</artifactId>
    <version>21.1.1</version>
</dependency>

<!-- https://mvnrepository.com/artifact/org.keycloak/keycloak-services -->
<dependency>
    <groupId>org.keycloak</groupId>
    <artifactId>keycloak-services</artifactId>
    <version>21.1.1</version>
    <scope>provided</scope>
</dependency>

 <!-- https://mvnrepository.com/artifact/org.keycloak/keycloak-model-legacy -->
<dependency>
    <groupId>org.keycloak</groupId>
    <artifactId>keycloak-model-legacy</artifactId>
    <version>21.1.1</version>
</dependency>

Implementing Remote User Storage Provider

Now that we have a new Java project created, the next step will be to implement the Remote User Storage Provider class, which will implement a few very important interfaces.  A simple way to understand the responsibility of the Remote User Storage Provider class is to think of it as a translator between Keycloak and your external user store. It takes the user data from your external store and converts it into a Keycloak user representation and vice versa. It also handles the authentication logic for validating user credentials against your external store.

Let’s have a look at the interfaces that our provider class will need to implement:

public class RemoteUserStorageProvider implements UserLookupProvider, CredentialInputValidator, UserStorageProvider {
   ...
}
  • UserLookupProvider: This interface defines methods for looking up users by their username, email, or id. It also allows you to get the number of users in your external store. You need to implement this interface if you want Keycloak to be able to find and display users from your external store in the admin console or the account management console.
  • CredentialInputValidator: This interface defines methods for validating user credentials, such as passwords or OTPs, against your external store. You need to implement this interface if you want Keycloak to be able to authenticate users using the credentials stored in your external store.
  • UserStorageProvider: This interface defines methods for creating, updating, removing, and querying users in your external store. It also allows you to manage user roles and groups. You need to implement this interface if you want Keycloak to be able to perform these operations on users from your external store.

After adding unimplemented methods, our provider class will look similar to the one below:

public class RemoteUserStorageProvider implements UserLookupProvider, CredentialInputValidator, UserStorageProvider {

    @Override
    public boolean supportsCredentialType(String credentialType) {
        // TODO Auto-generated method stub
    }

    @Override
    public boolean isValid(RealmModel realm, UserModel user, CredentialInput credentialInput) {
        // TODO Auto-generated method stub	
    }

    @Override
    public UserModel getUserById(RealmModel realm, String id) {
        // TODO Auto-generated method stub
    }

    @Override
    public UserModel getUserByUsername(RealmModel realm, String username) {
        // TODO Auto-generated method stub	 
    }

    @Override
    public UserModel getUserByEmail(RealmModel realm, String email) {
        // TODO Auto-generated method stub
    }

    @Override
    public boolean isConfiguredFor(RealmModel realm, UserModel user, String credentialType) {
        // TODO Auto-generated method stub		 
    }

    @Override
    public void close() {
        // TODO Auto-generated method stub
    }

}

Let me briefly describe each of these methods to you.

  • supportsCredentialType: This method checks if your external store supports a given credential type, such as password or OTP. You need to return true if your store supports the credential type, and false otherwise. Keycloak will use this method to determine which credential types are available for your users.
  • isValid: This method validates a given credential input, such as a password or an OTP, against your external store. You need to return true if the credential input matches the one stored in your external store for the given user, and false otherwise. Keycloak will use this method to authenticate users using the credentials from your external store.
  • getUserById: This method looks up a user by their id in your external store. You need to return a UserModel object that represents the user with the given id, or null if no such user exists. Keycloak will use this method to find users by their id in your external store.
  • getUserByUsername: This method looks up a user by their username in your external store. You need to return a UserModel object that represents the user with the given username, or null if no such user exists. Keycloak will use this method to find users by their username in your external store.
  • getUserByEmail: This method looks up a user by their email in your external store. You need to return a UserModel object that represents the user with the given email, or null if no such user exists. Keycloak will use this method to find users by their email in your external store.
  • isConfiguredFor: This method checks if a given user is configured for a given credential type in your external store. You need to return true if the user has a credential of the given type stored in your external store, and false otherwise. Keycloak will use this method to determine which credential types are configured for your users.
  • close: This method closes any resources that you opened during the initialization of your provider. You need to release any connections, statements, or other resources that you used in your provider. Keycloak will use this method to close your provider when it is no longer needed.

Next, I will provide you with a source code that implements most of these methods.

package com.appsdeveloperblog.keycloak;

import org.keycloak.component.ComponentModel;
import org.keycloak.credential.CredentialInput;
import org.keycloak.credential.CredentialInputValidator;
import org.keycloak.credential.LegacyUserCredentialManager;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.SubjectCredentialManager;
import org.keycloak.models.UserModel;
import org.keycloak.models.credential.PasswordCredentialModel;
import org.keycloak.storage.StorageId;
import org.keycloak.storage.UserStorageProvider;
import org.keycloak.storage.adapter.AbstractUserAdapter;
import org.keycloak.storage.user.UserLookupProvider;

public class RemoteUserStorageProvider implements UserLookupProvider, CredentialInputValidator, UserStorageProvider {
    private KeycloakSession session;
    private ComponentModel model;
    private UsersApiLegacyService usersService;

    public RemoteUserStorageProvider(KeycloakSession session, ComponentModel model,
            UsersApiLegacyService usersService) {
        this.session = session;
        this.model = model;
        this.usersService = usersService;
    }

    protected UserModel createAdapter(RealmModel realm, String username) {

        // Create a new user adapter based on the AbstractUserAdapter class
        return new AbstractUserAdapter(session, realm, model) {

            // Override the getUsername method to return the username from the remote
            // service
            @Override
            public String getUsername() {
                return username;
            }

            @Override
            public SubjectCredentialManager credentialManager() {

                // Create a new credential manager based on the LegacyUserCredentialManager
                // class
                return new LegacyUserCredentialManager(session, realm, this) {
                };
            }
        };
    }

    @Override
    public boolean supportsCredentialType(String credentialType) {
        return PasswordCredentialModel.TYPE.equals(credentialType);
    }

    @Override
    public boolean isValid(RealmModel realm, UserModel user, CredentialInput credentialInput) {
        VerifyPasswordResponse verifyPasswordResponse = usersService.verifyUserPassword(user.getUsername(),
                credentialInput.getChallengeResponse());

        if (verifyPasswordResponse == null)
            return false;

        return verifyPasswordResponse.getResult();
    }

    @Override
    public UserModel getUserById(RealmModel realm, String id) {
        StorageId storageId = new StorageId(id);
        String username = storageId.getExternalId();
        
        return getUserByUsername(realm, username);
    }

    @Override
    public UserModel getUserByUsername(RealmModel realm, String username) {
        UserModel returnValue = null;

        User user = usersService.getUserByUserName(username);

        if (user != null) {
            returnValue = createAdapter(realm, username);
        }

        return returnValue;
    }

    @Override
    public UserModel getUserByEmail(RealmModel realm, String email) {
        return getUserByUsername(realm, email);
    }

    @Override
    public boolean isConfiguredFor(RealmModel realm, UserModel user, String credentialType) {
        return user.credentialManager().isConfiguredFor(credentialType);
    }

    @Override
    public void close() {
        // TODO Auto-generated method stub
    }
}

Notice that in my case, methods getUserById() and getUserByEmail() both use getUserByUsername() to fetch user details by username. In your application, you can provide a different implementation of these methods.

Fetching User Details from a Remote Service

To fetch user details from a remote service, I have created a custom class called. UsersApiLegacyService.

public class UsersApiLegacyService {
    KeycloakSession session;

    public UsersApiLegacyService(KeycloakSession session) {
        this.session = session;
    }

    User getUserByUserName(String username) {
         ...
    }

    VerifyPasswordResponse verifyUserPassword(@PathParam("username") String username, String password) {
        ...
    }
}

This is a simple class with only two methods: getUserByUserName() and verifyUserPassword().

  • getUserByUserName() will need to send HTTP request to a remote Spring Boot Microservice and fetch user details,
  • verifyUserPassword() will need to send HTTP request to a remote Spring Boot Microservice to check if the provided user password is correct.

These are custom methods, and you can implement them based on the requirements of your application. Below is an example of one possible and simplified implementation.

package com.appsdeveloperblog.keycloak;

import java.io.IOException;

import javax.ws.rs.PathParam;

import org.jboss.logging.Logger;
import org.keycloak.broker.provider.util.SimpleHttp;
import org.keycloak.models.KeycloakSession;

import com.fasterxml.jackson.databind.ObjectMapper;

public class UsersApiLegacyService {
    KeycloakSession session;

    public UsersApiLegacyService(KeycloakSession session) {
        this.session = session;
    }

    private static final Logger LOG = Logger.getLogger(UsersApiLegacyService.class);

    User getUserByUserName(String username) {
        try {
            return SimpleHttp.doGet("http://localhost:8099/users/" + username, this.session).asJson(User.class);
        } catch (IOException e) {
            LOG.warn("Error fetching user " + username + " from external service: " + e.getMessage(), e);
        }
        return null;
    }

    VerifyPasswordResponse verifyUserPassword(@PathParam("username") String username, String password) {
        SimpleHttp simpleHttp = SimpleHttp.doPost("http://localhost:8099/users/" + username + "/verify-password",
                this.session);

        VerifyPasswordResponse verifyPasswordResponse = null;

        // Add the request parameters as a map
        simpleHttp.param("password", password);
        
        // Add any headers if needed
        simpleHttp.header("Content-Type", "application/x-www-form-urlencoded");

        try {
            String response = simpleHttp.asString();

            // Create an ObjectMapper instance
            ObjectMapper mapper = new ObjectMapper();

            // Convert the JSON string to a VerifyPasswordResponse object
            verifyPasswordResponse = mapper.readValue(response, VerifyPasswordResponse.class);

        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

        return verifyPasswordResponse;
    }
}
  • getUserByUserName(): This method takes a username as a parameter and returns a User object that contains the user details from the external user store. To do this, it uses the SimpleHttp class from Keycloak to send a GET request to the URL “http://localhost:8099/users/” + username, which is the endpoint of the remote Spring Boot Microservice that provides user information. It expects the response to be in JSON format and parses it into a User object using the asJson() method. If there is any error or exception during the HTTP request or the JSON parsing, it logs a warning message and returns null.
  • verifyUserPassword(): This method takes a username and a password as parameters and returns a VerifyPasswordResponse object that contains the result of verifying the password against the external user store. To do this, it uses the SimpleHttp class from Keycloak to send a POST request to the URL “http://localhost:8099/users/” + username + “/verify-password”, which is the endpoint of the remote Spring Boot Microservice that performs password verification. It adds the password as a request parameter using the param() method and sets the content type header to “application/x-www-form-urlencoded” using the header() method. It expects the response to be in JSON format and parses it into a VerifyPasswordResponse object using the ObjectMapper class from Jackson. If there is any error or exception during the HTTP request or the JSON parsing, it prints a stack trace and returns null.

Notice that the above class makes use of VerifyPasswordResponse class. I will provide the source code of this class next.

VerifyPasswordResponse class

You might have noticed that the UsersApiLegacyService class makes use of VerifyPasswordResponse class. Below is a source code and also an explanation of the role of this class.

package com.appsdeveloperblog.keycloak;

/**
 *
 * @author sergeykargopolov
 */
public class VerifyPasswordResponse {
    private boolean result;
    
    public VerifyPasswordResponse() {}
 
    public VerifyPasswordResponse(boolean result) {
        this.result = result;
    }

    public boolean getResult() {
        return result;
    }
 
    public void setResult(boolean result) {
        this.result = result;
    }
    
}

The role of this class is to represent the response from the remote Spring Boot Microservice that performs password verification. It has a single boolean field called result that indicates whether the password is valid or not. It also has a default constructor and a constructor that takes a boolean parameter, as well as getter and setter methods for the result field. The UsersApiLegacyService class uses this class to parse the JSON response from the verifyUserPassword() method and return it to the caller.

Implementing Remote User Storage Provider Factory

The next step will be to create a RemoteUserStorageProviderFactory class. This class will be used to create a new instance of the RemoteUserStorageProvider class, which we have created above. This new class will need to implement the UserStorageProviderFactory interface.

public class RemoteUserStorageProviderFactory implements UserStorageProviderFactory<RemoteUserStorageProvider> {
 
    @Override
    public RemoteUserStorageProvider create(KeycloakSession session, ComponentModel model) {

        ...
    }

    @Override
    public String getId() {
        ...
    }

}

The purpose of this interface is to create and register instances of your custom UserStorageProvider class. It has two methods that you need to implement:

  • create: This method takes a KeycloakSession and a ComponentModel as parameters and returns an instance of your RemoteUserStorageProvider class. The KeycloakSession gives you access to various Keycloak services and resources, such as realms, users, and clients. The ComponentModel contains the configuration parameters for your provider, such as the URL of the remote user store. You can use these parameters to initialize your provider instance and connect it to the external user store.
  • getId: This method returns a unique identifier for your provider factory. This identifier is used by Keycloak to find and load your provider factory when it scans the classpath. You can choose any string that is not already used by another provider factory, such as “remote-user-storage-provider”.

Once you provide an implementation for these methods, your RemoteUserStorageProviderFactory class will look similar to the one below:

package com.appsdeveloperblog.keycloak;

import org.keycloak.component.ComponentModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.storage.UserStorageProviderFactory;

public class RemoteUserStorageProviderFactory implements UserStorageProviderFactory<RemoteUserStorageProvider> {

    public static final String PROVIDER_NAME = "my-remote-user-storage-provider";

    @Override
    public RemoteUserStorageProvider create(KeycloakSession session, ComponentModel model) {

        return new RemoteUserStorageProvider(session, model, new UsersApiLegacyService(session));
    }

    @Override
    public String getId() {
        return PROVIDER_NAME;
    }

}
  • create: This method creates and returns an instance of your RemoteUserStorageProvider class. It passes the session and the model parameters to the constructor of your provider class. It also creates an instance of your UsersApiLegacyService class and passes it to the constructor of your provider class. The UsersApiLegacyService class is the one that communicates with the remote user store using HTTP requests. Bypassing it to your provider class, you enable your provider class to use its methods to look up and validate users from the external store.
  • getId: This method returns a string that identifies your provider factory. In this case, you have chosen “my-remote-user-storage-provider” as your identifier. This string must be unique among all the provider factories that Keycloak can find in its classpath. Keycloak will use this string to load your provider factory when it starts up or when you configure a new user storage provider in the admin console.

Packaging User Storage SPI

Now that we have prepared Java code, we can package our remote user storage provider application so that it can be deployed to Keycloak.

We will need to follow these steps:

  • Create a META-INF/services folder in our project if it does not exist already.
  • Create a new file named org.keycloak.storage.UserStorageProviderFactory in the META-INF/services folder. This file name is mandatory and must match the interface name of the provider factory.
  • Write the fully qualified class name of our provider factory class in the file. In our case, it is com.appsdeveloperblog.keycloak.RemoteUserStorageProviderFactory. This tells Keycloak which class to use as the provider factory for our custom user storage provider.
  • Open the pom.xml file and make some changes in the build section. Optionally, we can specify the final name of the jar file using the <finalName> element. We can use the provider name as the jar name for consistency. In our case, it is my-remote-user-storage-provider. Also, we need to comment out or remove the maven-assembly-plugin, as it causes some issues with the deployment of the jar file to Keycloak. We will use the default maven-jar-plugin instead.

After these steps, we can build our project and generate the jar file that contains our custom user storage provider implementation. We can then deploy it to Keycloak and test it.

Creating a new file called: org.keycloak.storage.UserStorageProviderFactory

In your Java project, create a new file named org.keycloak.storage.UserStorageProviderFactory in the META-INF/services folder. Once a new file is created, add to it the following line.

com.appsdeveloperblog.keycloak.RemoteUserStorageProviderFactory

The line above, which you will add to your file, is a fully qualified class name of our provider factory class. In my case, it is com.appsdeveloperblog.keycloak.RemoteUserStorageProviderFactory. This tells Keycloak which class to use as the provider factory for our custom user storage provider.

Remove the maven-assembly-plugin

I also needed to comment out the maven-assembly-plugin, as it causes some issues with the deployment of the jar file to Keycloak. This will make my application use the default maven-jar-plugin instead.

    <build>
    <finalName>my-remote-user-storage-provider</finalName>
<!-- 		<plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins> -->
    </build>

Add <fileName> Element

Notice in the code snippet above that I also added the <fileName> element in the <build> section of my pom.xml file.

<finalName>my-remote-user-storage-provider</finalName>

The <finalName> element in the pom.xml file allows you to specify the name of the jar file that will be generated when you build your project. By default, Maven uses the artifactId and the version of your project to name the jar file. For example, if your artifactId is custom-ldap-spi and your version is 1.0.0, then Maven will create a jar file named custom-ldap-spi-1.0.0.jar. However, you can override this default name by using the <finalName> element and providing your own name. In my case, I have chosen to name my jar file as my-remote-user-storage-provider.jar. This is optional and does not affect the functionality of your custom user storage provider, but it can help you to identify and organize your jar files better.

Deploying User Storage SPI to Keycloak

In this section, I will show you how to deploy our User Storage SPI application to Keycloak version 21. I will follow these steps:

  • Copy the jar file that contains our custom user storage provider implementation to a “providers” folder in the Keycloak server. The jar file should be located in the <Keycloak Home>/providers folder of the Keycloak installation directory. For example, if I have installed Keycloak in /opt/keycloak-21, then I should copy the jar file to /opt/keycloak-21/providers. You can use any method to copy the jar file,
  • Restart Keycloak. This is necessary for Keycloak to load the new provider factory and make it available for configuration. You can restart Keycloak by using the command line or the service manager of your operating system.
  • Add Remote User Storage Provider to a list of providers in Keycloak server User Federation section. This is where you can configure and enable the custom user storage provider for a specific realm. You can do this by logging into the admin console, selecting a realm, clicking on User Federation in the left menu, and clicking on Add Provider. You should see my-remote-user-storage-provider as one of the options in the dropdown list. I can then fill in the required parameters and save the provider.

Add User Storage Provider SPI to Keycloak

Add Remote User Storage Provider SPI to Keycloak. Step 2.

Conclusion

In this tutorial, you have learned how to implement a custom user storage provider for Keycloak using the User Storage SPI. You have created a Remote User Storage Provider that communicates with a remote Spring Boot Microservice to fetch user details and perform user authentication by validating user’s password. You have also learned how to package, deploy, and configure your custom user storage provider in Keycloak.

By following this tutorial, you have gained some insights into the architecture and extensibility of Keycloak. You have also seen how to use the SimpleHttp and ObjectMapper classes to make HTTP requests and parse JSON responses. You have also used the UserLookupProvider, CredentialInputValidator, and UserStorageProvider interfaces to implement the required methods for your custom user storage provider.

There are many ways to improve or extend your custom user storage provider. For example, you can implement more methods to support user creation, update, deletion, and role or group management. You can also add more configuration parameters or validation logic to your provider. You can also explore other SPIs that Keycloak provides for different purposes, such as authentication, authorization, events, etc.

I hope you have enjoyed this tutorial and learned something useful. If you have any questions or feedback, please feel free to leave a comment below.