Preserve user sessions over Keycloak restart

Hi,

I have been trying to figure out how to persist the Infinispan cache as I want to preserve the user sessions over Keycloak restart. This topic has been discussed in several other threads but I am still struggling to get it working.

Threads I have been reading/following

Preserve login session over KeyCloak restart with Infinispan local file-based cache - Configuring the server - Keycloak

jGroups/Infinispan Bedevilling: No DB Table Created (Unable to persist Infinispan internal caches as no global state enabled) - Miscellanaeous - Keycloak

and more (I am a new user and can only put 2 links in a post)


My setup

  • 1 Keycloak instance running on a docker container
  • Uses a PostgreSQL db running on another docker container
  • Keycloak version 11.0.3 (Its old… I know)

My goal

Using the built-in infinispan that comes with Keycloak with local file-based caching or DB caching without an external Inifinispan cluster

1 Like

What I have tried

Attempt 1 - Using Keycloak 11.0.3 and configure the built-in Infinispan cache to use file-based persistence (Ideas taken from - Preserve login session over KeyCloak restart with Infinispan local file-based cache - Configuring the server - Keycloak)

Result - Keycloak container starts without any errors but failed to persist user sessions after restarting the container. (The web front-end redirected to the login page after refreshing the page)

  • Created a Dockerfile with the following contents

FROM Quay

RUN mkdir /opt/jboss/keycloak/cache
RUN chmod 777 /opt/jboss/keycloak/cache

  • docker-compose.yml

keycloak:
container_name: ${APP_NAME}_keycloak
image: keycloak

    build:
        context: ./keycloak
        dockerfile: Dockerfile

    restart: unless-stopped

    volumes:
        - ./scripts/keycloak-import:/opt/jboss/keycloak/imports
        - my_volume:/opt/jboss/keycloak/cache
        - ./keycloak/standalone/configuration/standalone.xml:/opt/jboss/keycloak/standalone/configuration/standalone.xml

    command: -b 0.0.0.0 -Dkeycloak.profile.feature.upload_scripts=enabled -Dkeycloak.import=${KEYCLOAK_DIR}/imports/keycloak-${APP_ENV}.json -Dkeycloak.migration.strategy=IGNORE_EXISTING

    ports:
        - 8080:8080

    depends_on:
        - db    

volumes:
my_volume:
external: true
certificates:

  • ./keycloak/standalone/configuration/standalone.xml (I took a copy of the original one and edited). Here’s the XML for the Infinispan

     > <subsystem xmlns="urn:jboss:domain:infinispan:10.0">
    
        <cache-container name="keycloak" module="org.keycloak.keycloak-model-infinispan">
            <local-cache name="realms">
                <object-memory size="10000"/>
            </local-cache>
            <local-cache name="users">
                <object-memory size="10000"/>
            </local-cache>
            <local-cache name="sessions">
                 <expiration lifespan="-1"/>
                 <file-store path="/opt/jboss/keycloak/cache" shared="false" preload="true" purge="false" fetch-state="true"/>
            </local-cache>
            <local-cache name="authenticationSessions">
                 <expiration lifespan="-1"/>
                 <file-store path="/opt/jboss/keycloak/cache" shared="false" preload="true" purge="false" fetch-state="true"/>
            </local-cache>
            <local-cache name="offlineSessions"/>
            <local-cache name="clientSessions"/>
            <local-cache name="offlineClientSessions"/>
            <local-cache name="loginFailures"/>
            <local-cache name="work"/>
            <local-cache name="authorization">
                <object-memory size="10000"/>
            </local-cache>
            <local-cache name="keys">
                <object-memory size="1000"/>
                <expiration max-idle="3600000"/>
            </local-cache>
            <local-cache name="actionTokens">
                <object-memory size="-1"/>
                <expiration interval="300000" max-idle="-1"/>
            </local-cache>
        </cache-container>
        <cache-container name="server" default-cache="default" module="org.wildfly.clustering.server">
            <local-cache name="default">
                <transaction mode="BATCH"/>
            </local-cache>
        </cache-container>
        <cache-container name="web" default-cache="passivation" module="org.wildfly.clustering.web.infinispan">
            <local-cache name="passivation">
                <locking isolation="REPEATABLE_READ"/>
                <transaction mode="BATCH"/>
                <file-store passivation="true" purge="false"/>
            </local-cache>
            <local-cache name="sso">
                <locking isolation="REPEATABLE_READ"/>
                <transaction mode="BATCH"/>
            </local-cache>
            <local-cache name="routing"/>
        </cache-container>
        <cache-container name="ejb" aliases="sfsb" default-cache="passivation" module="org.wildfly.clustering.ejb.infinispan">
            <local-cache name="passivation">
                <locking isolation="REPEATABLE_READ"/>
                <transaction mode="BATCH"/>
                <file-store passivation="true" purge="false"/>
            </local-cache>
        </cache-container>
        <cache-container name="hibernate" module="org.infinispan.hibernate-cache">
            <local-cache name="entity">
                <object-memory size="10000"/>
                <expiration max-idle="100000"/>
            </local-cache>
            <local-cache name="local-query">
                <object-memory size="10000"/>
                <expiration max-idle="100000"/>
            </local-cache>
            <local-cache name="timestamps"/>
        </cache-container>
    </subsystem>

Attempt 2 - Using Keycloak 19.0.3 and configure the built-in Infinispan cache to use database persistence (Ideas taken from - jGroups/Infinispan Bedevilling: No DB Table Created (Unable to persist Infinispan internal caches as no global state enabled) - Miscellanaeous - Keycloak)

Result - Keycloak container starts without any errors, the JGROUPSPING table is created in the keycloak PostgreSQL database with 1 row (cluster_name - “ISPN”) but again the user session is lost after restarting the Keycloak container

  • docker-compose.yml

keycloak:
container_name: ${APP_NAME}_keycloak
image: Quay
restart: “no” # unless-stopped

    volumes:
        - ./scripts/keycloak-import:opt/keycloak/imports
        - ./keycloak/conf/cache-ispn.xml:opt/keycloak/conf/cache-ispn.xml

    command: >-
        start-dev
        --cache=ispn
        --cache-config-file=cache-ispn.xml
        --db=postgres
        --db-url-host=db
        --db-url-database=keycloak
        --db-username=$DB_USERNAME
        --db-password=$DB_PASSWORD
        --proxy=edge
        --http-relative-path=/auth/

    ports:
        - 8080:8080

    depends_on:
        - db   

volumes:
my_volume:
external: true
certificates:

  • ./keyclock/conf/cache-ispn.xml
<?xml version="1.0" encoding="UTF-8"?>

<jgroups>
    <stack name="jdbc-ping-tcp" extends="tcp">
        <JDBC_PING connection_driver="org.postgresql.Driver"
                   connection_url="jdbc:postgresql://127.0.0.1:5438/keycloak"
                   connection_username="{UserName}"
                   connection_password="{Password}"
                   initialize_sql="CREATE TABLE IF NOT EXISTS JGROUPSPING (own_addr varchar(200) NOT NULL, cluster_name varchar(200) NOT NULL, ping_data BYTEA, constraint PK_JGROUPSPING PRIMARY KEY (own_addr, cluster_name));"
                   info_writer_sleep_time="500"
                   remove_all_data_on_view_change="true"
                   stack.combine="REPLACE"
                   stack.position="MPING" />
    </stack>
</jgroups>

<cache-container name="keycloak">
    <transport lock-timeout="60000" stack="jdbc-ping-tcp"/>
    <local-cache name="realms">
        <encoding>
            <key media-type="application/x-java-object"/>
            <value media-type="application/x-java-object"/>
        </encoding>
        <memory max-count="10000"/>
    </local-cache>
    <local-cache name="users">
        <encoding>
            <key media-type="application/x-java-object"/>
            <value media-type="application/x-java-object"/>
        </encoding>
        <memory max-count="10000"/>
    </local-cache>
    <distributed-cache name="sessions" owners="2">
        <expiration lifespan="-1"/>
        <persistence>
            <file-store preload="true" fetch-state="true"/>
        </persistence>
    </distributed-cache>
    <distributed-cache name="authenticationSessions" owners="2">
        <expiration lifespan="-1"/>
        <persistence>
            <file-store preload="true" fetch-state="true"/>
        </persistence>
    </distributed-cache>
    <distributed-cache name="offlineSessions" owners="2">
        <expiration lifespan="-1"/>
        <persistence>
            <file-store preload="true" fetch-state="true"/>
        </persistence>
    </distributed-cache>
    <distributed-cache name="clientSessions" owners="2">
        <expiration lifespan="-1"/>
        <persistence>
            <file-store preload="true" fetch-state="true"/>
        </persistence>
    </distributed-cache>
    <distributed-cache name="offlineClientSessions" owners="2">
        <expiration lifespan="-1"/>
        <persistence>
            <file-store preload="true" fetch-state="true"/>
        </persistence>
    </distributed-cache>
    <distributed-cache name="loginFailures" owners="2">
        <expiration lifespan="-1"/>
        <persistence>
            <file-store preload="true" fetch-state="true"/>
        </persistence>
    </distributed-cache>
    <local-cache name="authorization">
        <encoding>
            <key media-type="application/x-java-object"/>
            <value media-type="application/x-java-object"/>
        </encoding>
        <memory max-count="10000"/>
    </local-cache>
    <replicated-cache name="work">
        <expiration lifespan="-1"/>
        <persistence>
            <file-store preload="true" fetch-state="true"/>
        </persistence>
    </replicated-cache>
    <local-cache name="keys">
        <encoding>
            <key media-type="application/x-java-object"/>
            <value media-type="application/x-java-object"/>
        </encoding>
        <expiration max-idle="3600000"/>
        <memory max-count="1000"/>
    </local-cache>
    <distributed-cache name="actionTokens" owners="2">
        <encoding>
            <key media-type="application/x-java-object"/>
            <value media-type="application/x-java-object"/>
        </encoding>
        <expiration max-idle="-1" lifespan="-1" interval="300000"/>
        <memory max-count="-1"/>
        <persistence>
            <file-store preload="true" fetch-state="true"/>
        </persistence>
    </distributed-cache>
</cache-container>

I would truly appreciate it someone could provide me with some guidance

With a lot of trial and error and I think I have it working using the Keycloak 19.0.3 and file-based persistence. (Keycloak Standalone mode and the built-in Infinispan cache)

Thank you for sharing your journey on this topic. I know this is something that a lot of people here have struggled with. Would you mind sharing your final configuration?

@xgp Yes - Happy to share the configuration I am using so far (Please note that I am still using start-dev in the commands below which should be updated to use start for production use - TODO later)

  • Dockerfile

FROM Quay

RUN mkdir /opt/keycloak/cache
RUN chmod 777 /opt/keycloak/cache

  • docker-compose.yml (keycloak service)

keycloak:
container_name: ${APP_NAME}_keycloak
image: keycloak

    build:
        context: ./keycloak
        dockerfile: Dockerfile

    restart: unless-stopped

    volumes:
  	# Scripts
        - ./scripts/keycloak-import:opt/keycloak/imports  

  	# Cache
  	- ./keycloak/cache:opt/keycloak/cache

  	# Infinispan configuration
        - ./keycloak/conf/cache-ispn.xml:opt/keycloak/conf/cache-ispn.xml   

    command: >-
  	start-dev
  	--cache=ispn
  	--cache-config-file=cache-ispn.xml
  	--db=postgres
  	--db-url-host=db
  	--db-url-database=keycloak
  	--db-username=$DB_USERNAME
  	--db-password=$DB_PASSWORD
  	--proxy=edge
  	--http-relative-path=/auth/

    environment:
        KEYCLOAK_ADMIN: $DB_USERNAME
        KEYCLOAK_ADMIN_PASSWORD: $DB_PASSWORD

    ports:
        - 8080:8080

    depends_on:
        - db  
  • ./keycloak/conf/cache-ispan-file.xml
<?xml version="1.0" encoding="UTF-8"?>

<jgroups>
    <stack name="jdbc-ping-tcp" extends="tcp">
        <JDBC_PING connection_driver="org.postgresql.Driver"
                   connection_url="jdbc:postgresql://{IpAddress or host name}:5438/keycloak"   <!-- Enter the IP address / host name here  -->
                   connection_username="{Username}"  <!-- Enter your db user name here -->
                   connection_password="{Password}"   <!-- Enter your db password here --
                   initialize_sql="CREATE TABLE IF NOT EXISTS JGROUPSPING (own_addr varchar(200) NOT NULL, cluster_name varchar(200) NOT NULL, ping_data BYTEA, constraint PK_JGROUPSPING PRIMARY KEY (own_addr, cluster_name));"
                   info_writer_sleep_time="500"
                   remove_all_data_on_view_change="true"
                   stack.combine="REPLACE"
                   stack.position="MPING" />
    </stack>
</jgroups>

<cache-container name="keycloak">
    <global-state>
        <persistent-location path="/opt/keycloak/cache/"/>   <!-- Make sure to use the same path used in the Dockerfile &  docker-compose.xml file  -->
    </global-state>
    <transport lock-timeout="60000"/>
     <!-- stack="jdbc-ping-tcp"/> -->

    <local-cache name="realms">
        <encoding>
            <key media-type="application/x-java-object"/>
            <value media-type="application/x-java-object"/>
        </encoding>
        <memory max-count="10000"/>
    </local-cache>
    <local-cache name="users">
        <encoding>
            <key media-type="application/x-java-object"/>
            <value media-type="application/x-java-object"/>
        </encoding>
        <memory max-count="10000"/>
    </local-cache>
    <distributed-cache name="sessions" owners="2">
        <expiration lifespan="-1"/>
        <persistence passivation="true">
            <file-store shared="false" preload="true" purge="false" fetch-state="true"/>
        </persistence>
    </distributed-cache>
    <distributed-cache name="authenticationSessions" owners="2">
        <expiration lifespan="-1"/>
        <persistence passivation="true">
            <file-store shared="false" preload="true" purge="false" fetch-state="true"/>
        </persistence>
    </distributed-cache>
    <distributed-cache name="offlineSessions" owners="2">
        <expiration lifespan="-1"/>
        <persistence passivation="true">
            <file-store shared="false" preload="true" purge="false" fetch-state="true"/>
        </persistence>
    </distributed-cache>
    <distributed-cache name="clientSessions" owners="2">
        <expiration lifespan="-1"/>
        <persistence passivation="true">
            <file-store shared="false" preload="true" purge="false" fetch-state="true"/>
        </persistence>
    </distributed-cache>
    <distributed-cache name="offlineClientSessions" owners="2">
        <expiration lifespan="-1"/>
        <persistence passivation="true">
            <file-store shared="false" preload="true" purge="false" fetch-state="true"/>
        </persistence>
    </distributed-cache>
    <distributed-cache name="loginFailures" owners="2">
        <expiration lifespan="-1"/>
        <persistence passivation="true">
            <file-store shared="false" preload="true" purge="false" fetch-state="true"/>
        </persistence>
    </distributed-cache>
    <local-cache name="authorization">
        <encoding>
            <key media-type="application/x-java-object"/>
            <value media-type="application/x-java-object"/>
        </encoding>
        <memory max-count="10000"/>
    </local-cache>
    <replicated-cache name="work">
        <expiration lifespan="-1"/>
        <persistence passivation="true">
            <file-store shared="false" preload="true" purge="false" fetch-state="true"/>
        </persistence>
    </replicated-cache>
    <local-cache name="keys">
        <encoding>
            <key media-type="application/x-java-object"/>
            <value media-type="application/x-java-object"/>
        </encoding>
        <expiration max-idle="3600000"/>
        <memory max-count="1000"/>
    </local-cache>
    <distributed-cache name="actionTokens" owners="2">
        <encoding>
            <key media-type="application/x-java-object"/>
            <value media-type="application/x-java-object"/>
        </encoding>
        <expiration max-idle="-1" lifespan="-1" interval="300000"/>
        <memory max-count="-1"/>
        <persistence passivation="true">
            <file-store shared="false" preload="true" purge="false" fetch-state="true"/>
        </persistence>
    </distributed-cache>
</cache-container>
1 Like

Hi @robertcck

There is this example provided by Thomas Darimont here : keycloak-project-example/deployments/local/clusterx/haproxy-database-ispn at main · thomasdarimont/keycloak-project-example · GitHub

With database persistence, but it requires a patch on keycloak-model-infinispan since Keycloak 15.0.2

This thread might also be interesting : Keycloak.X persisting infinispan to jdbc store

We also tried the setup : external Infinispan cluster (so remote cluster) with database persistence configured in this cluster, there was a lifespan issue while persisting user sessions but it has been fixed recently and will work now : https://github.com/keycloak/keycloak/issues/10755