keycloak Posts

User Lifecycle Management with Keycloak

Created a presentation for JVM Indonesia Meetup, talking about how Keycloak handling a user lifecycle management. From first time activating, to deactivating.

The presentation can be found on slideshare.

How to Solve Keycloak Error, Uncaught server error: org.keycloak.authentication.AuthenticationFlowException

Had this error today when creating a custom authentication SPI for Keycloak

16:17:13,588 ERROR [org.keycloak.services.error.KeycloakErrorHandler] (default task-1) Uncaught server error: 
org.keycloak.authentication.AuthenticationFlowException

        at org.keycloak.authentication.AuthenticationProcessor.authenticateOnly(AuthenticationProcessor.java:913)
        at org.keycloak.protocol.oidc.endpoints.TokenEndpoint.resourceOwnerPasswordCredentialsGrant(TokenEndpoint.java:554)
        at org.keycloak.protocol.oidc.endpoints.TokenEndpoint.processGrantRequest(TokenEndpoint.java:187)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:498)
        at org.jboss.resteasy.core.MethodInjectorImpl.invoke(MethodInjectorImpl.java:140)
        at org.jboss.resteasy.core.ResourceMethodInvoker.internalInvokeOnTarget(ResourceMethodInvoker.java:509)
        at org.jboss.resteasy.core.ResourceMethodInvoker.invokeOnTargetAfterFilter(ResourceMethodInvoker.java:399)
        at org.jboss.resteasy.core.ResourceMethodInvoker.lambda$invokeOnTarget$0(ResourceMethodInvoker.java:363)
        at org.jboss.resteasy.core.interception.PreMatchContainerRequestContext.filter(PreMatchContainerRequestContext.java:358)
        at org.jboss.resteasy.core.ResourceMethodInvoker.invokeOnTarget(ResourceMethodInvoker.java:365)
        at org.jboss.resteasy.core.ResourceMethodInvoker.invoke(ResourceMethodInvoker.java:337)
        at org.jboss.resteasy.core.ResourceLocatorInvoker.invokeOnTargetObject(ResourceLocatorInvoker.java:137)
        at org.jboss.resteasy.core.ResourceLocatorInvoker.invoke(ResourceLocatorInvoker.java:106)
        at org.jboss.resteasy.core.ResourceLocatorInvoker.invokeOnTargetObject(ResourceLocatorInvoker.java:132)
        at org.jboss.resteasy.core.ResourceLocatorInvoker.invoke(ResourceLocatorInvoker.java:100)
        at org.jboss.resteasy.core.SynchronousDispatcher.invoke(SynchronousDispatcher.java:443)
        at org.jboss.resteasy.core.SynchronousDispatcher.lambda$invoke$4(SynchronousDispatcher.java:233)
        at org.jboss.resteasy.core.SynchronousDispatcher.lambda$preprocess$0(SynchronousDispatcher.java:139)
        at org.jboss.resteasy.core.interception.PreMatchContainerRequestContext.filter(PreMatchContainerRequestContext.java:358)
        at org.jboss.resteasy.core.SynchronousDispatcher.preprocess(SynchronousDispatcher.java:142)
        at org.jboss.resteasy.core.SynchronousDispatcher.invoke(SynchronousDispatcher.java:219)

This error happens because im not putting any userModel into Keycloak’s flowContext and adding a userModel solve this problem. Code can be seen here,

https://github.com/edwin/keycloak-password-encryptor/blob/master/src/main/java/com/edw/keycloak/spi/CustomKeycloakPasswordEncryptor.java#L93

How to Encrypt and Decrypt Password on Keycloak or Red Hat SSO

Previously i found one good question, "can we encrypt password at the user end before transmitting it to the server". I can see the purpose of this question is to prevent a plain text password being transmitted thru the network. And despite network is already on SSL, there are possibility that some SSL offloading or re-encryption happens along the way.

Okay so the concept is pretty much like this,

Solution is quite simple, an encryption is needed especially for sensitive data such as passwords in order to prevent those fields to be shown as a plain text.

For this sample, im trying to simulate a simple login by using Keycloak rest api and see whether we can encrypt some fields there. Lets start with a pom file,

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.edw</groupId>
    <artifactId>KeycloakPasswordEncryptor</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <keycloak.version>4.8.3.Final</keycloak.version>

        <version.maven-bundle-plugin>2.3.7</version.maven-bundle-plugin>
        <version.maven-compiler-plugin>3.5.1</version.maven-compiler-plugin>
        <version.maven-resources-plugin>3.0.1</version.maven-resources-plugin>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.keycloak</groupId>
            <artifactId>keycloak-core</artifactId>
            <version>${keycloak.version}</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.keycloak</groupId>
            <artifactId>keycloak-server-spi</artifactId>
            <version>${keycloak.version}</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.keycloak</groupId>
            <artifactId>keycloak-server-spi-private</artifactId>
            <version>${keycloak.version}</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.keycloak</groupId>
            <artifactId>keycloak-services</artifactId>
            <version>${keycloak.version}</version>
            <scope>provided</scope>
        </dependency>

        <!--        unit testing -->
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-engine</artifactId>
            <version>5.1.0</version>
            <scope>test</scope>
        </dependency>
    </dependencies>


    <build>
        <defaultGoal>install</defaultGoal>
        <plugins>

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-assembly-plugin</artifactId>
                <version>3.1.1</version>

                <configuration>
                    <descriptorRefs>
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                </configuration>

                <executions>
                    <execution>
                        <id>make-assembly</id>
                        <phase>package</phase>
                        <goals>
                            <goal>single</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

</project>

Next is creating a java class to do all the authentication logic, we can put our password decryption method here

package com.edw.keycloak.spi;

import com.edw.keycloak.spi.helper.EncryptionHelper;
import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.authentication.AuthenticationFlowError;
import org.keycloak.authentication.Authenticator;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.credential.PasswordUserCredentialModel;

import javax.ws.rs.core.Response;
import java.util.List;

public class CustomKeycloakPasswordEncryptor implements Authenticator {

    public void authenticate(AuthenticationFlowContext authenticationFlowContext) {

        // not bringing username
        if(authenticationFlowContext.getHttpRequest().getFormParameters().get("username") == null
                || authenticationFlowContext.getHttpRequest().getFormParameters().get("username").isEmpty()) {

            Response challenge =  Response.status(400)
                    .entity("{\"error\":\"invalid_request\",\"error_description\":\"No Username\"}")
                    .header("Content-Type", "application/json")
                    .build();
            authenticationFlowContext.failureChallenge(AuthenticationFlowError.INVALID_CREDENTIALS, challenge);
            return;
        }

        // not bringing password
        if(authenticationFlowContext.getHttpRequest().getFormParameters().get("password") == null
                || authenticationFlowContext.getHttpRequest().getFormParameters().get("password").isEmpty()) {

            Response challenge =  Response.status(400)
                    .entity("{\"error\":\"invalid_request\",\"error_description\":\"No Password\"}")
                    .header("Content-Type", "application/json")
                    .build();
            authenticationFlowContext.failureChallenge(AuthenticationFlowError.INVALID_CREDENTIALS, challenge);
            return;
        }

        // capture username
        String username = authenticationFlowContext.getHttpRequest().getFormParameters().getFirst("username").trim();

        // search for corresponding user
        List<UserModel> userModels = authenticationFlowContext.getSession().users().searchForUser(username, authenticationFlowContext.getRealm());

        // user not exists
        if(userModels.isEmpty()) {
            Response challenge =  Response.status(400)
                    .entity("{\"error\":\"invalid_request\",\"error_description\":\"User Not Found\"}")
                    .header("Content-Type", "application/json")
                    .build();
            authenticationFlowContext.failureChallenge(AuthenticationFlowError.INVALID_CREDENTIALS, challenge);
            return;
        }

        // capture usermodel, means user is exist
        UserModel userModel = userModels.get(0);

        // capture password and dont forget to html-decode the content (im using a string replacement for this example)
        String password = authenticationFlowContext.getHttpRequest().getFormParameters().getFirst("password").trim();
        password = password.replace("%3D", "=");

        // decrypt the password
        password = EncryptionHelper.decrypt(password);

        // password is incorrect
        PasswordUserCredentialModel credentialInput = UserCredentialModel.password(password);
        boolean valid = authenticationFlowContext.getSession().userCredentialManager().isValid(authenticationFlowContext.getRealm(),
                                                                                                userModel,
                                                                                                new PasswordUserCredentialModel[]{credentialInput} );
        if( !valid ) {
            Response challenge =  Response.status(400)
                    .entity("{\"error\":\"invalid_request\",\"error_description\":\"User Not Found\"}")
                    .header("Content-Type", "application/json")
                    .build();
            authenticationFlowContext.failureChallenge(AuthenticationFlowError.INVALID_CREDENTIALS, challenge);
            return;
        }

        // set user
        authenticationFlowContext.setUser(userModel);

        // all validation success
        authenticationFlowContext.success();
    }

    public void action(AuthenticationFlowContext authenticationFlowContext) {
        authenticationFlowContext.success();
    }

    public boolean requiresUser() {
        return false;
    }

    public boolean configuredFor(KeycloakSession keycloakSession, RealmModel realmModel, UserModel userModel) {
        return false;
    }

    public void setRequiredActions(KeycloakSession keycloakSession, RealmModel realmModel, UserModel userModel) {

    }

    public void close() {

    }
}

Next is create a factory class for this,

package com.edw.keycloak.spi;

import org.keycloak.Config;
import org.keycloak.authentication.Authenticator;
import org.keycloak.authentication.AuthenticatorFactory;
import org.keycloak.authentication.ConfigurableAuthenticatorFactory;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.provider.ProviderConfigProperty;

import java.util.ArrayList;
import java.util.List;

public class CustomKeycloakPasswordEncryptorFactory implements AuthenticatorFactory, ConfigurableAuthenticatorFactory {

    public static final String PROVIDER_ID = "password-encryption";

    private static final CustomKeycloakPasswordEncryptor SINGLETON = new CustomKeycloakPasswordEncryptor();

    public String getDisplayType() {
        return "Simple Password Encryption";
    }

    public String getReferenceCategory() {
        return "Simple Password Encryption";
    }

    public boolean isConfigurable() {
        return false;
    }

    public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {
        return CustomKeycloakPasswordEncryptorFactory.REQUIREMENT_CHOICES;
    }

    public boolean isUserSetupAllowed() {
        return false;
    }

    public String getHelpText() {
        return "Simple Password Encryption";
    }

    private static AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
            AuthenticationExecutionModel.Requirement.REQUIRED,
            AuthenticationExecutionModel.Requirement.ALTERNATIVE,
            AuthenticationExecutionModel.Requirement.DISABLED
    };

    public List<ProviderConfigProperty> getConfigProperties() {
        return new ArrayList<ProviderConfigProperty>();
    }

    public Authenticator create(KeycloakSession keycloakSession) {
        return SINGLETON;
    }

    public void init(Config.Scope scope) {

    }

    public void postInit(KeycloakSessionFactory keycloakSessionFactory) {

    }

    public void close() {

    }

    public String getId() {
        return CustomKeycloakPasswordEncryptorFactory.PROVIDER_ID;
    }

    public int order() {
        return 0;
    }
}

Next is creating a text file with the name of "org.keycloak.authentication.AuthenticatorFactory", and put it under META-INF/services folder,

com.edw.keycloak.spi.CustomKeycloakPasswordEncryptorFactory

Run below command to build,

mvn clean package

And put the jar build result into Keycloak installation. In my case, i put it on keycloak-4.8.3.Final\standalone\deployments.

So basically, all our code part has been build and deployed and now lets focus on the Keycloak’s side. First we can start by creating a new Authentication

Once created, next is creating a new execution

Select the name of our Custom SPI, in my case it would be Simple Password Encryption.

And make it as Required,

Next is creating a client,

And the most important thing is, setting in the Authentication Flow Overrides.

Dont forget to create a user for this, in here im setting “1” as the password for this user.

Okay, once everything is already setup properly. Next we can do testing by doing a simple CURL to Keycloak’s API endpoint.

First we try with an un-encrypted password,

curl -L -X POST 'http://localhost:8080/auth/realms/whatever-realm/protocol/openid-connect/token' \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'client_id=clientid-02' \
--data-urlencode 'grant_type=password' \
--data-urlencode 'client_secret=4ddddab7-884f-4bb3-a403-660cf89ff5f2' \
--data-urlencode 'scope=openid' \
--data-urlencode 'username=edw' \
--data-urlencode 'password=1'

It will give below error response, due to combination of username and password is not found,

{
    "error": "invalid_request",
    "error_description": "User Not Found"
}

Next lets try with below CURL call, see the encrypted password field

curl -L -X POST 'http://localhost:8080/auth/realms/whatever-realm/protocol/openid-connect/token' \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'client_id=clientid-02' \
--data-urlencode 'grant_type=password' \
--data-urlencode 'client_secret=4ddddab7-884f-4bb3-a403-660cf89ff5f2' \
--data-urlencode 'scope=openid' \
--data-urlencode 'username=edw' \
--data-urlencode 'password=AA=='

It will gives a success result and show generated JWT token,

{
    "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCIg.......IWhs_qth4B3ITCAkubmq4me2ftj6Fa2uaQMwydXaEn0cA",
    "expires_in": 600,
    "refresh_expires_in": 1800,
    "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI......w1OiD1n1mObX7WCs",
    "token_type": "bearer",
    "id_token": "eyJhbGciOiJSUzI1NiI.........XqqH49DaBeI1pHCjSw",
    "not-before-policy": 1601650090,
    "session_state": "553cc783-xxxx-xxxx-xxxx-a6a6ab1b6e0e",
    "scope": "openid email profile"
}

It shows that we are able to connect to Keycloak by using an encrypted password, and Keycloak is able to decypt it.

Anyway, code for this can be found on below link.

https://github.com/edwin/keycloak-password-encryptor

Have fun using Keycloak

Run Multiple Containerized Keycloak Instances Behind an Apache HTTPD Proxy

On this session im trying to create a sample condition where im having a High-Availability cluster of multiple Keycloaks instances which are located behind an HTTPD Reverse Proxy. HTTPD will do a round robin request to two Keycloaks instances behind it, showing the capability of session sharing between different Keycloak instances.

Im using a containerized Keycloak image and run them by using Docker for simulating a condition of running more than one Keycloak instances with different IP.

The concept is pretty much like below image,

The first thing needed is setuping a database for this. In here im using MySQL, despite Keycloak is able to connect to different type of databases. And for this sample, database is installed on my host laptop and not using a containerized one.

CREATE USER 'keycloak'@'%' IDENTIFIED BY 'password';
CREATE DATABASE keycloak_db;

The next step is running two Keycloak instances by executing below command, in here im putting 192.168.56.1 as the ip for my host machine.

docker run -p 8081:8080 -e PROXY_ADDRESS_FORWARDING=true  \ 
	-e DB_VENDOR="mysql" -e DB_ADDR="192.168.56.1" -e DB_USER="keycloak" \ 
	-e DB_PASSWORD="password" -e DB_PORT="3306" -e DB_DATABASE="keycloak_db" \ 
	--add-host=HOST:192.168.56.1 jboss/keycloak


docker run -p 8082:8080 -e PROXY_ADDRESS_FORWARDING=true  \ 
	-e DB_VENDOR="mysql" -e DB_ADDR="192.168.56.1" -e DB_USER="keycloak" \ 
	-e DB_PASSWORD="password" -e DB_PORT="3306" -e DB_DATABASE="keycloak_db" \ 
	--add-host=HOST:192.168.56.1 jboss/keycloak

The good thing about this keycloak image is that by default it is running a standalone-ha.xml and automatically form a cluster when being run locally at the same time. This can be seen on Keycloak’s log

06:20:53,102 INFO  [org.infinispan.CLUSTER] (non-blocking-thread--p8-t4) 
	[Context=offlineClientSessions] ISPN100010: Finished rebalance with members [a629f48aafa9, 82298394e158], topology id 11
06:20:53,102 INFO  [org.infinispan.CLUSTER] (thread-35,ejb,a629f48aafa9) 
	[Context=sessions] ISPN100010: Finished rebalance with members [a629f48aafa9, 82298394e158], topology id 11
06:20:53,103 INFO  [org.infinispan.CLUSTER] (thread-30,ejb,a629f48aafa9) 
	[Context=work] ISPN100010: Finished rebalance with members [a629f48aafa9, 82298394e158], topology id 11
06:20:53,114 INFO  [org.infinispan.CLUSTER] (thread-30,ejb,a629f48aafa9) 
	[Context=loginFailures] ISPN100010: Finished rebalance with members [a629f48aafa9, 82298394e158], topology id 11
06:20:53,121 INFO  [org.infinispan.CLUSTER] (thread-31,ejb,a629f48aafa9) 
	[Context=actionTokens] ISPN100010: Finished rebalance with members [a629f48aafa9, 82298394e158], topology id 11

The last step would be creating an HTTPD setup for creating reverse proxy with a load-balancing capability. And we can achieve that by editing httpd.conf file, for this sample we are using a round-robin mechanism of lbmethod=byrequests.

<VirtualHost *:80>
	ServerName localhost
	ProxyRequests Off
	ProxyPreserveHost On
  
	<Proxy "balancer://mycluster">
		BalancerMember http://localhost:8081
		BalancerMember http://localhost:8082
	
		ProxySet lbmethod=byrequests failontimeout=on
	 
	</Proxy>
  
	ProxyPass / balancer://mycluster/
	ProxyPassReverse / balancer://mycluster/
</VirtualHost>

In order to activate loadbalancer and http proxying feature on Apache HTTPD, there are several variables that need to be unremark on httpd.conf file such as proxy_balancer_module and proxy_http_module.

Restart httpd and open browser directly, and we can see that Keycloak is working well.

We can also simulate a condition where one instance suddenly stopped to simulate a failover scenario by killing one Keycloak instance using docker kill command. Have fun with Keycloak 😀

Notes.
For a better performance, a sticky session approach is recommended compared to a round robin one.

https://www.keycloak.org/docs/latest/server_installation/#sticky-sessions

Integrating Spring Boot Login with Keycloak or Red Hat Single Sign On

When we are managing many applications, one of the most painful part is managing its user and access right. Because usually different applications have their own user management, and sometimes each user have different credentials between multiple applications.

We can solve this problem by having a one point user management where other application can use this tools for managing their user authentication and authorization. This is where Red Hat Single Sign On (or its opensource product, Keycloak) can comes in handy. It provides an end to end user management lifecyle, from activating a new user, managing them, assigning their access right until deactivating them. On this example, we’ll start with a simple login page by using Keycloak, and how other application (in this example is a Spring Boot app) is connecting to it.

First we need to create a java project with below pom file, im using keycloak-adapter bom and keycloak-spring-boot-starter library for this.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.edw</groupId>
    <artifactId>spring-boot-and-rhsso-otp</artifactId>
    <version>1.0-SNAPSHOT</version>

    <packaging>war</packaging>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.6.RELEASE</version>
        <relativePath/>
    </parent>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.keycloak.bom</groupId>
                <artifactId>keycloak-adapter-bom</artifactId>
                <version>9.0.2</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>org.keycloak</groupId>
            <artifactId>keycloak-spring-boot-starter</artifactId>
            <version>9.0.2</version>
        </dependency>

        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>jstl</artifactId>
        </dependency>

        <dependency>
            <groupId>org.apache.tomcat.embed</groupId>
            <artifactId>tomcat-embed-jasper</artifactId>
            <scope>provided</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

And perhaps the most important part in this project is, the application.properties. In here, we are putting our Keycloak’s url, realm name, client name, and secret. Also we are defining that only user with admin role can access a URL with /admin/ pattern.

### server port
server.port=8080
spring.application.name=Spring Boot with RHSSO Login

### rhsso configuration
keycloak.auth-server-url=https://rhsso/auth/
keycloak.realm=my-realm
keycloak.resource=my-client
keycloak.public-client=false
keycloak.bearer-only=false
keycloak.principal-attribute=preferred_username
keycloak.credentials.secret=11111111-1111-1111-1111-111111111111

### spring boot ui configuration
spring.mvc.view.prefix=/WEB-INF/jsp/
spring.mvc.view.suffix=.jsp

### authorization
keycloak.security-constraints[0].authRoles[0]=admin
keycloak.security-constraints[0].securityCollections[0].patterns[0]=/admin/*

Next is we need to create a user and role on Red Hat SSO,

After that, we need to create a client and its password,

And put those corresponding values inside application.properties.

We can test whether configuration works well or not by directly accessing to admin page (/admin/index). A successful configuration would prevent an unauthorized user from accessing admin page by showing a Keycloak login page. Admin page only accessible once a user has successfully login thru Keycloak or Red Hat Single Sign On.

Full code can be downloaded on my github page,

https://github.com/edwin/spring-boot-and-rhsso

Have fun with RHSSO and Keycloak (H)