Skip to content

fix: defer ClientCreationEvent to after transaction commit [fj4WqyCCw3C5ShR1RfB7MoBPTpkRrBFYP1uT35g3MvT] - #51598

Open
waterWang wants to merge 1 commit into
keycloak:mainfrom
waterWang:fix/client-creation-event-timing
Open

fix: defer ClientCreationEvent to after transaction commit [fj4WqyCCw3C5ShR1RfB7MoBPTpkRrBFYP1uT35g3MvT]#51598
waterWang wants to merge 1 commit into
keycloak:mainfrom
waterWang:fix/client-creation-event-timing

Conversation

@waterWang

Copy link
Copy Markdown

Fixes #51594

Problem

The ClientCreationEvent was published synchronously inside JpaRealmProvider.addClient
before the client representation (attributes, redirect URIs, protocol mappers, etc.)
was fully populated by the caller. Any on: client-created workflow resource condition
that inspects client state (e.g., a custom WorkflowConditionProvider reading a client
attribute) always saw a half-initialized client and silently returned false, preventing
the workflow from ever activating.

Root cause

JpaRealmProvider.addClient persists the bare ClientEntity (only id, clientId,
enabled, standardFlowEnabled, realmId) and immediately publishes the
ClientCreationEvent. The caller — typically RepresentationToModel.createClient
applies attributes, redirect URIs, protocol mappers, etc. after addClient returns.

Fix

Defer the event publication to after the outer transaction commits using
enlistAfterCompletion. By commit time the caller has finished applying the full
representation, so any workflow conditions evaluate against the complete client state.

Verification

  • The enlistAfterCompletion callback runs after the main transaction commits
    (in DefaultKeycloakTransactionManager.commit()), at which point the JPA
    persistence context is still open and the ClientModel entity is still managed.
  • If the main transaction rolls back, the after-completion callback is skipped
    (rolled back), so no spurious event is published for a failed client creation.
  • The existing pattern (WorkflowExecutor.runTaskenlistAfterCompletion) is
    already used elsewhere in the workflow subsystem.

Related

…cloak#51594)

The ClientCreationEvent was published synchronously inside JpaRealmProvider.addClient
before the client representation (attributes, redirect URIs, protocol mappers, etc.)
was fully populated by the caller. Any on:client-created workflow resource condition
that inspects client state always saw a half-initialized client and silently returned
false, preventing the workflow from ever activating.

Defer the event publication to after the transaction commits using
enlistAfterCompletion. By commit time the caller has finished applying the full
representation, so conditions evaluate against the complete client state.

Fixes keycloak#51594
@waterWang
waterWang requested a review from a team as a code owner August 10, 2026 23:33
Copilot AI balanced review requested due to automatic review settings August 10, 2026 23:33

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Defers client-creation events until transaction completion so listeners can inspect fully populated clients.

Changes:

  • Publishes ClientCreationEvent through an after-completion transaction callback.
  • Suppresses publication when client creation rolls back.
Suppressed comments (1)

model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java:952

  • Publishing here runs while DefaultKeycloakTransactionManager is already marked completed. A matching workflow reaches WorkflowExecutor.runTask(), whose enlistAfterCompletion() call then throws Transaction already completed; processEvent() catches that exception, so the workflow still never activates. Publish from a fresh transactional session with the client reloaded, or defer only workflow processing so work is enqueued in an active transaction.
            public void commit() {
                session.getKeycloakSessionFactory().publish(new ClientModel.ClientCreationEvent() {

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

// client representation (attributes, redirect URIs, protocol mappers, etc.)
// is fully populated before any on:client-created workflow resource conditions
// are evaluated. See #51594.
session.getTransactionManager().enlistAfterCompletion(new KeycloakTransaction() {
@antoniocasagrande-airia

Copy link
Copy Markdown

Thanks for picking this up — issue reporter here. Deferring the publish past client population is the right instinct, but I think enlistAfterCompletion overshoots: it moves the event to after the main JPA transaction has committed, and the primary consumer this issue is about writes to the database when it receives the event.

The path: WorkflowEventListener.onEvent(ProviderEvent)DefaultWorkflowProvider.submit(...) → activation → JpaWorkflowStateProvider, which does em.persist(...) for new WORKFLOW_STATE rows and criteria executeUpdate() deletes for restart/cancel handling — all through the session's EntityManager, relying on the surrounding transaction to flush. In DefaultKeycloakTransactionManager.commit() the order is preparetransactions (which includes the JPA transaction) → afterCompletion, so by the time this callback publishes the event the JPA transaction is already committed: the em.persist lands in a persistence context that will never flush again (activation silently lost), and the executeUpdate() paths throw TransactionRequiredException. The condition would now evaluate against a fully populated client, but the resulting state write is lost — the workflow still never activates, just one step further down the chain.

enlistPrepare seems like the right hook instead: prepare transactions commit first, while the JPA transaction is still active. At that point the caller (e.g. RepresentationToModel.createClient) has long since applied attributes/redirect URIs/mappers — population happens during the request, and only the publish needs to move to commit time — so conditions see the complete client and listener writes still flush with the main commit. It also keeps the current semantics that the event is part of the creating transaction (a listener failure can still roll back the creation, and nothing observes a committed client that later disappears), which afterCompletion silently changes for every ClientCreationEvent subscriber, not just workflows.

Two smaller notes:

  • The rollback story with enlistPrepare stays trivial: if the request rolls back before commit, the publish never runs; no need for the no-op KeycloakTransaction scaffolding.
  • Whichever hook wins, a regression test that creates a client matching an on: client-created workflow condition and asserts the WORKFLOW_STATE row exists after the request completes would catch exactly this class of bug — condition evaluation and state persistence have to be verified together.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Workflows: on: client-created conditions are evaluated before the client is populated, so they can never match

4 participants