Giter Club home page Giter Club logo

keycloak-jpa-cache's Introduction

๐Ÿ› This is alpha software Do not use this in production.

Keycloak JPA cache

Uses JPA instead of Infinispan for remote cache entities. Overrides the LegacyDatastoreProvider.

Requires Keycloak >= 24.

Heavily inspired by keycloak-cassandra-extension. Enormous thanks to these amazing engineers.

Why

From our post on Keycloak Discussions, Future of seamless upgrades? #24655:

... the one thing that causes peoples' jaws to hit the floor is when I tell them that Keycloak upgrades may not be seamless (e.g. without downtime), and there is a risk of losing sessions. We've found that many companies abandon plans to use Keycloak at this point, even if they were 99% sold on every other aspect. In many ways, it's the only feature that matters, as Keycloak provides feature parity (and in many cases superior functionality) to most other tools it is evaluated against.

To have a prayer of solving the above problem, at minimum you have to use external Infinispan. Infinispan is hard to operate, even with all of the awesome operators and tools that team has built. And, this configuration for Keycloak and Infinispan is poorly documented and highly complex to get right. Furthermore, upgrading Infinispan itself is a daunting and error-prone task.

The present day expectation of "cloud native" apps is that they are stateless, ephemeral images that come up and down quickly. This really isn't possible when using embedded or external Infinispan with Keycloak.

By replacing the cache with a JPA implementation, it massively simplifies operation; specifically restarts and upgrades. Because of the performance loss (maybe? benchmark TBD), this is intended for users of Keycloak with small to medium deployments. Such deployments don't have fully staffed, 24x7 NOCs, but their IAM system should still have great operational characteristics. This intends to solve that.

How to use

Setup

Applies to any deployment type:

  • Set KC_COMMUNITY_JPA_CACHE_ENABLED=true
  • Set KC_CACHE=local
  • Set KC_FEATURES_DISABLED=authorization

DIY

  • Build the jar with mvn clean install -DskipTests
  • Put the target/keycloak-jpa-cache-<version>.jar in your Keycloak providers/ directory

Docker

TBD. You can currently try after building with the included docker-compose.yml file. Just run docker compose up.

Known issues

  • ClusterProvider implementation still relies on the replicated work cache using Infinispan. This is used to propagate evictions to the Realm or User local caches between instances. Todo replace this with something simpler, probably via the database.
  • Keycloak Authorization must be disabled (this is essential as otherwise Keycloak tries to use InfinispanStoreFactory in a lot of places).
  • Some tests are still skipped or failing. We need to understand if this is because the test fails to do everything in a single transaction (Keycloak doesn't do this internally) or if there is something we are missing.
  • Hasn't been benchmarked to look for issues under load.
  • Need to see if there are "L1 cache" ways to reduce load on the database, specifically in the context of a single transaction, or if JPA takes care of most of that.
  • You should probably enable sticky sessions on your load balancer, although we need to substantiate this with testing.

Portions of the code are taken from keycloak and the keycloak-cassandra-extension and the copyright is held by their respective owners.

All other documentation, source code and other files in this repository are Copyright 2024 Phase Two, Inc.

keycloak-jpa-cache's People

Contributors

rtufisi avatar xgp avatar

Stargazers

 avatar

Watchers

 avatar

Forkers

rtufisi

keycloak-jpa-cache's Issues

Create a new Openshift-v4 identity provider

  1. Select a real,
  2. Go to IdentityProviders tab
  3. Create a new OpenShift 4

Result:

b8f6da209040_keycloak-jpa-cache_keycloak_1 | 2024-05-06 17:54:31,595 ERROR [org.keycloak.services.error.KeycloakErrorHandler] (executor-thread-53) Uncaught server error: org.keycloak.broker.provider.IdentityBrokerException: Could not initialize oAuth metadata
b8f6da209040_keycloak-jpa-cache_keycloak_1 |    at org.keycloak.social.openshift.OpenshiftV4IdentityProvider.getAuthJson(OpenshiftV4IdentityProvider.java:52)
b8f6da209040_keycloak-jpa-cache_keycloak_1 |    at org.keycloak.social.openshift.OpenshiftV4IdentityProvider.<init>(OpenshiftV4IdentityProvider.java:39)
b8f6da209040_keycloak-jpa-cache_keycloak_1 |    at org.keycloak.social.openshift.OpenshiftV4IdentityProviderFactory.create(OpenshiftV4IdentityProviderFactory.java:28)
b8f6da209040_keycloak-jpa-cache_keycloak_1 |    at org.keycloak.social.openshift.OpenshiftV4IdentityProviderFactory.create(OpenshiftV4IdentityProviderFactory.java:16)
b8f6da209040_keycloak-jpa-cache_keycloak_1 |    at org.keycloak.services.resources.admin.IdentityProviderResource.createIdentityProviderInstance(IdentityProviderResource.java:283)
b8f6da209040_keycloak-jpa-cache_keycloak_1 |    at org.keycloak.services.resources.admin.IdentityProviderResource.getMapperTypes(IdentityProviderResource.java:301)
b8f6da209040_keycloak-jpa-cache_keycloak_1 |    at org.keycloak.services.resources.admin.IdentityProviderResource$quarkusrestinvoker$getMapperTypes_0be925c7ba3f074dcae485e618ff57eb387d5324.invoke(Unknown Source)
b8f6da209040_keycloak-jpa-cache_keycloak_1 |    at org.jboss.resteasy.reactive.server.handlers.InvocationHandler.handle(InvocationHandler.java:29)
b8f6da209040_keycloak-jpa-cache_keycloak_1 |    at io.quarkus.resteasy.reactive.server.runtime.QuarkusResteasyReactiveRequestContext.invokeHandler(QuarkusResteasyReactiveRequestContext.java:141)
b8f6da209040_keycloak-jpa-cache_keycloak_1 |    at org.jboss.resteasy.reactive.common.core.AbstractResteasyReactiveContext.run(AbstractResteasyReactiveContext.java:147)
b8f6da209040_keycloak-jpa-cache_keycloak_1 |    at io.quarkus.vertx.core.runtime.VertxCoreRecorder$14.runWith(VertxCoreRecorder.java:582)
b8f6da209040_keycloak-jpa-cache_keycloak_1 |    at org.jboss.threads.EnhancedQueueExecutor$Task.run(EnhancedQueueExecutor.java:2513)
b8f6da209040_keycloak-jpa-cache_keycloak_1 |    at org.jboss.threads.EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1538)
b8f6da209040_keycloak-jpa-cache_keycloak_1 |    at org.jboss.threads.DelegatingRunnable.run(DelegatingRunnable.java:29)
b8f6da209040_keycloak-jpa-cache_keycloak_1 |    at org.jboss.threads.ThreadLocalResettingRunnable.run(ThreadLocalResettingRunnable.java:29)
b8f6da209040_keycloak-jpa-cache_keycloak_1 |    at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
b8f6da209040_keycloak-jpa-cache_keycloak_1 |    at java.base/java.lang.Thread.run(Thread.java:840)
b8f6da209040_keycloak-jpa-cache_keycloak_1 | Caused by: org.apache.http.client.ClientProtocolException
b8f6da209040_keycloak-jpa-cache_keycloak_1 |    at org.apache.http.impl.client.InternalHttpClient.doExecute(InternalHttpClient.java:187)
b8f6da209040_keycloak-jpa-cache_keycloak_1 |    at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:83)
b8f6da209040_keycloak-jpa-cache_keycloak_1 |    at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:108)
b8f6da209040_keycloak-jpa-cache_keycloak_1 |    at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:56)
b8f6da209040_keycloak-jpa-cache_keycloak_1 |    at org.keycloak.social.openshift.OpenshiftV4IdentityProvider.getOauthMetadataInputStream(OpenshiftV4IdentityProvider.java:61)
b8f6da209040_keycloak-jpa-cache_keycloak_1 |    at org.keycloak.social.openshift.OpenshiftV4IdentityProvider.getAuthJson(OpenshiftV4IdentityProvider.java:48)
b8f6da209040_keycloak-jpa-cache_keycloak_1 |    ... 16 more
b8f6da209040_keycloak-jpa-cache_keycloak_1 | Caused by: org.apache.http.ProtocolException: Target host is not specified
b8f6da209040_keycloak-jpa-cache_keycloak_1 |    at org.apache.http.impl.conn.DefaultRoutePlanner.determineRoute(DefaultRoutePlanner.java:71)
b8f6da209040_keycloak-jpa-cache_keycloak_1 |    at org.apache.http.impl.client.InternalHttpClient.determineRoute(InternalHttpClient.java:125)
b8f6da209040_keycloak-jpa-cache_keycloak_1 |    at org.apache.http.impl.client.InternalHttpClient.doExecute(InternalHttpClient.java:184)
b8f6da209040_keycloak-jpa-cache_keycloak_1 |    ... 21 more
b8f6da209040_keycloak-jpa-cache_keycloak_1 | 

outstanding tests

fails

UserSessionInitializerTest#testUserSessionInitializer @rtufisi

  • I don't think this is implemented yet
  • Expected: is <3L> but: was <0L>

UserSessionInitializerTest#testUserSessionInitializerWithDeletingClient @rtufisi

  • I don't think this is implemented yet
  • Expected: is <1L> but: was <0L>

won't do (for now)

These are both because they are expecting *Model classes to be used between transactions, which is never actually done inside Keycloak. Disabling these tests for now.

UserSessionProviderModelTest#testImportUserSessions

  • Caused by: java.lang.IllegalStateException: Session/EntityManager is closed

UserSessionProviderModelTest#testExpiredClientSessions

  • Caused by: java.lang.IllegalStateException: Session/EntityManager is closed

works

UserSessionProviderModelTest#testGetByClient f38f339 @rtufisi
UserSessionProviderModelTest#testRemoveSessions 1386e13 @rtufisi
UserSessionExpirationTest#testClientSessionIdleTimeoutOverride 98cc8db @xgp
UserSessionExpirationTest#testClientSessionIdleTimeoutOverrideTtl 98cc8db @xgp
UserSessionProviderModelTest#testActiveClientSessionStats f0a4779 @rtufisi
UserSessionProviderModelTest#testBrokerUserSessions affa1c5 @rtufisi
UserSessionProviderModelTest#testTransientUserSessionIsNotPersisted 0022527 @xgp
UserSessionProviderModelTest#testTransientUserSession 0022527 @xgp
UserSessionProviderModelTest#testClientSessionIsNotPersistedForTransientUserSession 0022527 @xgp
UserSessionProviderModelTest#testMultipleSessionsRemovalInOneTransaction
UserSessionProviderModelTest#testRemoveUserSession
UserSessionProviderModelTest#testRemoveUserSessionsByRealm
UserSessionConcurrencyTest#testConcurrentNotesChange
UserSessionProviderModelTest#testCreateSessions
UserSessionProviderModelTest#testUpdateSession
UserSessionProviderModelTest#testUpdateSessionInSameTransaction
UserSessionProviderModelTest#testRestartSession
UserSessionProviderModelTest#testCreateClientSession
UserSessionProviderModelTest#testUpdateClientSession
UserSessionProviderModelTest#testUpdateClientSessionWithGetByClientId
UserSessionProviderModelTest#testUpdateClientSessionInSameTransaction
UserSessionProviderModelTest#testGetUserSessions
UserSessionProviderModelTest#testRemoveUserSessionsByUser
UserSessionProviderModelTest#testOnClientRemoved
UserSessionProviderModelTest#testGetByClientPaginated
UserSessionProviderModelTest#testCreateAndGetInSameTransaction
UserSessionProviderModelTest#testAuthenticatedClientSessions
UserSessionProviderModelTest#testUserSessionNotes
UserSessionProviderModelTest#testClientSessionToUserSessionReference
UserSessionExpirationTest#testClientSessionIdleTimeout
UserSessionExpirationTest#testDeleteSession

Cleanup

  • Code review
  • DB indexes to optimize queries
  • Deprecation warnings
  • Overusing em.flush()
  • On realm/user/client removed events
  • Expirations
  • ?

Clean up `ClearExpiredUserSessions` error

keycloak-1  | 2024-04-17 17:52:02,979 ERROR [org.keycloak.services.scheduled.ScheduledTaskRunner] (Timer-0) Failed to run scheduled task ClearExpiredUserSessions: java.lang.NullPointerException: Cannot invoke "org.keycloak.cluster.ExecutionResult.isExecuted()" because "result" is null
keycloak-1  |   at org.keycloak.services.scheduled.ClusterAwareScheduledTaskRunner.runTask(ClusterAwareScheduledTaskRunner.java:62)
keycloak-1  |   at org.keycloak.services.scheduled.ScheduledTaskRunner.lambda$run$0(ScheduledTaskRunner.java:59)
keycloak-1  |   at org.keycloak.models.utils.KeycloakModelUtils.lambda$runJobInTransaction$1(KeycloakModelUtils.java:257)
keycloak-1  |   at org.keycloak.models.utils.KeycloakModelUtils.runJobInTransactionWithResult(KeycloakModelUtils.java:379)
keycloak-1  |   at org.keycloak.models.utils.KeycloakModelUtils.runJobInTransaction(KeycloakModelUtils.java:256)
keycloak-1  |   at org.keycloak.services.scheduled.ScheduledTaskRunner.run(ScheduledTaskRunner.java:53)
keycloak-1  |   at org.keycloak.timer.basic.BasicTimerProvider$1.run(BasicTimerProvider.java:53)
keycloak-1  |   at java.base/java.util.TimerThread.mainLoop(Timer.java:566)
keycloak-1  |   at java.base/java.util.TimerThread.run(Timer.java:516)

Account console can't get UserSession

To reproduce:

  1. mvn clean install -DskipTests && docker compose up
  2. log into the admin console
  3. create a realm
  4. add a user to the realm and give a password
  5. load the account console and log in as the created user
  6. click on the "Device activity" and "Applications" links

in the DeviceActivityManager:

2024-04-17 17:07:46,512 INFO  [io.phasetwo.keycloak.jpacache.JpaCacheDatastoreProviderFactory] (executor-thread-2) Creating JpaCacheDatastoreProvider...
2024-04-17 17:07:46,514 TRACE [io.phasetwo.keycloak.jpacache.userSession.JpaCacheUserSessionProvider] (executor-thread-2) getUserSession(967aa904-97e4-4bfa-bbad-1e0166a45f24@856136f7, 7be511ac-7122-43a5-8027-c8f0c0ffd961)
2024-04-17 17:07:46,519 TRACE [io.phasetwo.keycloak.jpacache.singleUseObject.JpaCacheSingleUseObjectProvider] (executor-thread-2) contains(231c1eae-864a-42b1-ac8d-545f8a139a72.revoked)
2024-04-17 17:07:46,520 TRACE [io.phasetwo.keycloak.jpacache.userSession.JpaCacheUserSessionProvider] (executor-thread-2) getUserSession(967aa904-97e4-4bfa-bbad-1e0166a45f24@856136f7, 7be511ac-7122-43a5-8027-c8f0c0ffd961)
2024-04-17 17:07:46,522 TRACE [io.phasetwo.keycloak.jpacache.userSession.JpaCacheUserSessionProvider] (executor-thread-2) getUserSessionsStream(967aa904-97e4-4bfa-bbad-1e0166a45f24@856136f7, org.keycloak.models.cache.infinispan.UserAdapter@9d013ae3)
2024-04-17 17:07:46,530 ERROR [org.keycloak.services.error.KeycloakErrorHandler] (executor-thread-2) Uncaught server error: java.lang.NullPointerException: Cannot invoke "org.keycloak.models.UserSessionModel.getNote(String)" because "userSession" is null
  at org.keycloak.device.DeviceActivityManager.getCurrentDevice(DeviceActivityManager.java:43)
  at org.keycloak.services.resources.account.SessionResource.getAttachedDevice(SessionResource.java:184)
  at org.keycloak.services.resources.account.SessionResource.lambda$devices$1(SessionResource.java:89)
  at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.accept(ForEachOps.java:183)
  at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:197)
  at java.base/java.util.Iterator.forEachRemaining(Iterator.java:133)
  at java.base/java.util.Spliterators$IteratorSpliterator.forEachRemaining(Spliterators.java:1845)
  at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:509)
  at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499)
  at java.base/java.util.stream.ForEachOps$ForEachOp.evaluateSequential(ForEachOps.java:150)
  at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(ForEachOps.java:173)
  at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
  at java.base/java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:596)
  at org.keycloak.services.resources.account.SessionResource.devices(SessionResource.java:88)
  at org.keycloak.services.resources.account.SessionResource$quarkusrestinvoker$devices_7f87e26d20b2e8eb6d8b3ced042b7ff2046c31dc.invoke(Unknown Source)
  at org.jboss.resteasy.reactive.server.handlers.InvocationHandler.handle(InvocationHandler.java:29)
  at io.quarkus.resteasy.reactive.server.runtime.QuarkusResteasyReactiveRequestContext.invokeHandler(QuarkusResteasyReactiveRequestContext.java:141)
  at org.jboss.resteasy.reactive.common.core.AbstractResteasyReactiveContext.run(AbstractResteasyReactiveContext.java:147)
  at io.quarkus.vertx.core.runtime.VertxCoreRecorder$14.runWith(VertxCoreRecorder.java:582)
  at org.jboss.threads.EnhancedQueueExecutor$Task.run(EnhancedQueueExecutor.java:2513)
  at org.jboss.threads.EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1538)
  at org.jboss.threads.DelegatingRunnable.run(DelegatingRunnable.java:29)
  at org.jboss.threads.ThreadLocalResettingRunnable.run(ThreadLocalResettingRunnable.java:29)
  at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
  at java.base/java.lang.Thread.run(Thread.java:840)

2024-04-17 17:07:46,531 WARN  [io.agroal.pool] (executor-thread-2) Datasource '<default>': JDBC resources leaked: 1 ResultSet(s) and 1 Statement(s)

in the AccountRestService:

2024-04-17 17:08:16,724 INFO  [io.phasetwo.keycloak.jpacache.JpaCacheDatastoreProviderFactory] (executor-thread-2) Creating JpaCacheDatastoreProvider...
2024-04-17 17:08:16,727 TRACE [io.phasetwo.keycloak.jpacache.userSession.JpaCacheUserSessionProvider] (executor-thread-2) getUserSession(967aa904-97e4-4bfa-bbad-1e0166a45f24@856136f7, 7be511ac-7122-43a5-8027-c8f0c0ffd961)
2024-04-17 17:08:16,734 TRACE [io.phasetwo.keycloak.jpacache.singleUseObject.JpaCacheSingleUseObjectProvider] (executor-thread-2) contains(231c1eae-864a-42b1-ac8d-545f8a139a72.revoked)
2024-04-17 17:08:16,736 TRACE [io.phasetwo.keycloak.jpacache.userSession.JpaCacheUserSessionProvider] (executor-thread-2) getUserSession(967aa904-97e4-4bfa-bbad-1e0166a45f24@856136f7, 7be511ac-7122-43a5-8027-c8f0c0ffd961)
2024-04-17 17:08:16,739 TRACE [io.phasetwo.keycloak.jpacache.userSession.JpaCacheUserSessionProvider] (executor-thread-2) getUserSessionsStream(967aa904-97e4-4bfa-bbad-1e0166a45f24@856136f7, org.keycloak.models.cache.infinispan.UserAdapter@9d013ae3)
2024-04-17 17:08:16,747 ERROR [org.keycloak.services.error.KeycloakErrorHandler] (executor-thread-2) Uncaught server error: java.lang.NullPointerException: Cannot invoke "org.keycloak.models.UserSessionModel.getAuthenticatedClientSessions()" because "s" is null
  at org.keycloak.services.resources.account.AccountRestService.lambda$applications$3(AccountRestService.java:450)
  at java.base/java.util.stream.ReferencePipeline$7$1.accept(ReferencePipeline.java:273)
  at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:197)
  at java.base/java.util.Iterator.forEachRemaining(Iterator.java:133)
  at java.base/java.util.Spliterators$IteratorSpliterator.forEachRemaining(Spliterators.java:1845)
  at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:509)
  at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499)
  at java.base/java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:921)
  at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
  at java.base/java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:682)
  at org.keycloak.services.resources.account.AccountRestService.applications(AccountRestService.java:453)
  at org.keycloak.services.resources.account.AccountRestService$quarkusrestinvoker$applications_7e4d5772b3781c2331e05f25a7f914308c08b5bd.invoke(Unknown Source)
  at org.jboss.resteasy.reactive.server.handlers.InvocationHandler.handle(InvocationHandler.java:29)
  at io.quarkus.resteasy.reactive.server.runtime.QuarkusResteasyReactiveRequestContext.invokeHandler(QuarkusResteasyReactiveRequestContext.java:141)
  at org.jboss.resteasy.reactive.common.core.AbstractResteasyReactiveContext.run(AbstractResteasyReactiveContext.java:147)
  at io.quarkus.vertx.core.runtime.VertxCoreRecorder$14.runWith(VertxCoreRecorder.java:582)
  at org.jboss.threads.EnhancedQueueExecutor$Task.run(EnhancedQueueExecutor.java:2513)
  at org.jboss.threads.EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1538)
  at org.jboss.threads.DelegatingRunnable.run(DelegatingRunnable.java:29)
  at org.jboss.threads.ThreadLocalResettingRunnable.run(ThreadLocalResettingRunnable.java:29)
  at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
  at java.base/java.lang.Thread.run(Thread.java:840)

2024-04-17 17:08:16,748 WARN  [io.agroal.pool] (executor-thread-2) Datasource '<default>': JDBC resources leaked: 1 ResultSet(s) and 1 Statement(s)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.