RHSSO

Keycloak 26 Doesnt Show Custom User Attributes Tab

Had this problem and quite giving me a headache for a while, somehow the latest Keycloak version is not showing Attributes tab in Users menu which preventing me from creating a new custom user attribute.

Actually it is quite simple for enabling the custom attributes tab, we can go to Realm Settings menu and select the Unmanaged Attributes as Enabled.

The result would be like this,

Debugging HTTP Request and Responses in Red Hat Single Sign On

Red Hat Single Sign On (RHSSO) or its opensource project, which is Keycloak, is an open-source software product to allow single sign-on with identity and access management which can be deployed as a cloud service or containerized application. For this sample, we are trying to debug and print all http requests and responses that comes to RHSSO 7.4.6 which is being deployed on Openshift, for debugging purpose. But we also need to be very careful since it will print all http content which might contains sensitive values.

Okay, so lets start with creating a file “sso.cli” which have below content,

/subsystem=undertow/configuration=filter/expression-filter=requestDumperExpression:add(expression="dump-request")
/subsystem=undertow/server=default-server/host=default-host/filter-ref=requestDumperExpression:add

And deploy it as a ConfigMap,

$ oc create configmap jboss-cli --from-file=sso-extensions.cli=sso.cli

Next is mount it as a volume to RHSSO DeploymentConfig

$ oc set volume dc/sso --add --name=jboss-cli \
		-m /opt/eap/extensions -t configmap --configmap-name=jboss-cli \ 
		--default-mode='0755' --overwrite

Rollout the corresponding DeploymentConfig and we can observe that http request-response logs now is showing, we can use this curl command to test

$ curl --location --request POST 'https://sso.url/auth/realms/realm/protocol/openid-connect/userinfo' \
--header 'Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiw......YXNzPlSVE2Oj0ImIQd6zQkw2UEMiEyJz8FrsVaS7x2M8mQjy-xQrSTGZVXKWR7KLHa-MCRx4S33Ja5nQuD3K_VVihKTyn4cOHnQ'

with below logs as the result

21:46:28,071 INFO  [io.undertow.request.dump] (default task-1) 
----------------------------REQUEST---------------------------
               URI=/auth/realms/realm/protocol/openid-connect/userinfo
 characterEncoding=null
     contentLength=0
       contentType=null
            header=accept=*/*
            header=accept-encoding=gzip, deflate, br
            header=forwarded=for=10.161.5.3;host=sso.url;proto=https
            header=authorization=Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiw......YXNzPlSVE2Oj0ImIQd6zQkw2UEMiEyJz8FrsVaS7x2M8mQjy-xQrSTGZVXKWR7KLHa-MCRx4S33Ja5nQuD3K_VVihKTyn4cOHnQ
            header=x-forwarded-proto=https
            header=x-forwarded-port=443
            header=x-forwarded-for=10.161.5.3
            header=content-length=0
            header=host=sso.url
            header=x-forwarded-host=sso.url
            locale=[]
            method=POST
          protocol=HTTP/1.1
       queryString=
        remoteAddr=/10.161.5.3:0
        remoteHost=10.161.5.3
            scheme=https
              host=sso.url
        serverPort=8443
          isSecure=true
--------------------------RESPONSE--------------------------
     contentLength=73
       contentType=application/json
            header=X-XSS-Protection=1; mode=block
            header=X-Frame-Options=SAMEORIGIN
            header=Referrer-Policy=no-referrer
            header=Date=Wed, 06 Nov 2024 14:46:28 GMT
            header=Connection=keep-alive
            header=WWW-Authenticate=Bearer realm="realm", error="invalid_token", error_description="Token verification failed"
            header=Strict-Transport-Security=max-age=31536000; includeSubDomains
            header=X-Content-Type-Options=nosniff
            header=Content-Type=application/json
            header=Content-Length=73
            status=401

==============================================================

Error PKIX path building failed When Connecting to Keycloak with a Self Signed Certificate

For this scenario im using Keycloak version 17, which are being installed by using below command.

docker pull quay.io/keycloak/keycloak:17.0.0

And being run by using below command,

docker run -p 8443:8443 -e KC_HOSTNAME=localhost:8443 \ 
	-e KC_HOSTNAME_URL=https://localhost:8443 -e KC_DB=mysql \ 
	-e KC_DB_USERNAME=keycloak -e KC_DB_PASSWORD=password \ 
	-e KC_DB_URL=jdbc:mysql://192.168.56.1:3306/keycloak_db \
	quay.io/keycloak/keycloak:17.0.0 start

As for the Spring Boot sourcecode, we are utilizing the same code that are being used in below article,

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

So lets start by setting up our application.properties to pointing to Keycloak’s HTTPS port

keycloak.auth-server-url=https://localhost:8443/
keycloak.realm=external
keycloak.resource=client
keycloak.public-client=false
keycloak.bearer-only=false
keycloak.principal-attribute=preferred_username
keycloak.credentials.secret=xxxxxx

But when HTTPS is created by using a self signed certificate, it will display below error from the Java application console.

o.k.adapters.KeycloakDeployment - Failed to load URLs from https://localhost:8443/realms/external/.well-known/openid-configuration
javax.net.ssl.SSLHandshakeException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
	at java.base/sun.security.ssl.Alert.createSSLException(Alert.java:131)
	at java.base/sun.security.ssl.TransportContext.fatal(TransportContext.java:349)
	at java.base/sun.security.ssl.TransportContext.fatal(TransportContext.java:292)
	at java.base/sun.security.ssl.TransportContext.fatal(TransportContext.java:287)
Caused by: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
	at java.base/sun.security.validator.PKIXValidator.doBuild(PKIXValidator.java:439)
	at java.base/sun.security.validator.PKIXValidator.engineValidate(PKIXValidator.java:306)
	at java.base/sun.security.validator.Validator.validate(Validator.java:264)
	at java.base/sun.security.ssl.X509TrustManagerImpl.validate(X509TrustManagerImpl.java:313)
	at java.base/sun.security.ssl.X509TrustManagerImpl.checkTrusted(X509TrustManagerImpl.java:222)
	at java.base/sun.security.ssl.X509TrustManagerImpl.checkServerTrusted(X509TrustManagerImpl.java:129)
	at java.base/sun.security.ssl.CertificateMessage$T13CertificateConsumer.checkServerCerts(CertificateMessage.java:1340)
	... 86 common frames omitted
Caused by: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
	at java.base/sun.security.provider.certpath.SunCertPathBuilder.build(SunCertPathBuilder.java:141)
	at java.base/sun.security.provider.certpath.SunCertPathBuilder.engineBuild(SunCertPathBuilder.java:126)
	at java.base/java.security.cert.CertPathBuilder.build(CertPathBuilder.java:297)
	at java.base/sun.security.validator.PKIXValidator.doBuild(PKIXValidator.java:434)
	... 92 common frames omitted

Thats why we need to include Keycloak’s custom SSL certificate into our Spring Boot application so that Spring Boot able to recognize a self sign certificate.

We can start by using OpenSSL to capture Keycloak’s SSL Certificate

echo "" | openssl s_client -connect localhost:8443  -showcerts 2>/dev/null | openssl x509 -out certfile.cert

It will generate a certificate which is belongs to Keycloak, next step is to create a truststore to contain the corresponding certificate. Below command will create a keystore with the name of “customcacerts” and its password which is “changeit”

keytool -import -alias ca -file certfile.cert \
         -keystore customcacerts  -storepass changeit

And we can run our Spring Boot application with below command, using the created truststore and its password as parameter.

java -Djavax.net.ssl.trustStore=customcacerts \
         -Djavax.net.ssl.trustStorePassword=changeit -jar spring-boot.jar

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