Skip to content

Add client attribute condition and client activity event workflow providers - #51629

Draft
antoniocasagrande-airia wants to merge 3 commits into
keycloak:mainfrom
antoniocasagrande-airia:workflow-client-attribute-condition-and-activity-event
Draft

Add client attribute condition and client activity event workflow providers#51629
antoniocasagrande-airia wants to merge 3 commits into
keycloak:mainfrom
antoniocasagrande-airia:workflow-client-attribute-condition-and-activity-event

Conversation

@antoniocasagrande-airia

@antoniocasagrande-airia antoniocasagrande-airia commented Aug 11, 2026

Copy link
Copy Markdown

Closes #51595

Adds the two client-scoped Workflows providers proposed in #51595, so client lifecycle workflows can select clients by attribute and track user-driven activity on public clients:

  • has-client-attribute condition — mirrors has-user-attribute for CLIENTS resources, taking key (presence-only) or key:value config. evaluate checks the client attribute map; toPredicate builds an EXISTS subquery over ClientAttributeEntity so scheduled activation picks up eligible clients directly in the JPA query. Since client attributes are single-valued, the expected value is compared literally (no multi-value set semantics). The value comparison reuses the per-database handling from JpaRealmProvider.searchClientsByAttributes (Oracle DBMS_LOB.COMPARE, PostgreSQL substr + full comparison).
  • client-activity event — complements client-authenticated (which only sees CLIENT_LOGIN, an event public clients can never produce) by treating LOGIN, CODE_TO_TOKEN and REFRESH_TOKEN user events as activity on the event's client. Combined with restart-in-progress, this lets a disable/delete workflow keep actively used clients out of the steps while dormant ones progress.

The motivating use case (detailed in #51595) is lifecycle management of dynamically registered OAuth clients (MCP clients registering via RFC 7591): flag them with an attribute at registration, disable after N days of inactivity, delete after M more.

Also includes a small consistency fix this work surfaced: JpaWorkflowStateProvider.scheduleStep stamped scheduledStepTimestamp with Instant.now() while getDueScheduledSteps compares it against Time.currentTimeMillis() in the same class. Production behavior is identical (Time delegates to the system clock), but under a test time offset a workflow restart schedules against the wrong clock, which made the activity-reset scenario untestable — ClientActivityWorkflowTest acts as the regression test. With the fix, steps scheduled during an offset run are scheduled relative to the simulated clock (cumulative), so BrokeredUserLifecycleWorkflowTest's tail offsets were adjusted from 5/10/10 to cumulative 5/15/25 days — arguably what the simulated timeline meant all along. ScheduleWorkflowTask/RunWorkflowTask have the same System.currentTimeMillis() pattern for the informational scheduledTime event payload; left untouched to keep the diff minimal. Full org.keycloak.tests.workflow package passes locally (103 tests).

Notes for reviewers

  • The condition test activates via the schedule rather than on: client-created, because the client creation event is currently published before attributes are persisted and can never match an attribute condition — that is Workflows: on: client-created conditions are evaluated before the client is populated, so they can never match #51594 (fix in flight at fix: defer ClientCreationEvent to after transaction commit [fj4WqyCCw3C5ShR1RfB7MoBPTpkRrBFYP1uT35g3MvT] #51598). Once that lands, event-based activation works with this condition unchanged.
  • ClientAttributeWorkflowConditionProvider reuses UserAttributeWorkflowConditionProvider.parseKeyValuePair so the two conditions keep one config grammar. Worth knowing that this inherits two Properties-syntax edge cases (affecting has-user-attribute equally today): a #-prefixed key parses as a comment and is silently dropped, and multi-line input produces multiple entries with stringPropertyNames().iterator().next() picking one non-deterministically. Kept as-is here for grammar consistency between the two conditions; happy to harden the shared parser (single-entry check, or a first-colon split) in this PR or a follow-up, whichever you prefer.
  • Failed attempts (LOGIN_ERROR, CODE_TO_TOKEN_ERROR, REFRESH_TOKEN_ERROR) also carry the client id but are deliberately excluded from the activity set — otherwise spamming bad credentials against a dormant client would keep resetting its idle clock. ClientActivityWorkflowTest#testFailedLoginDoesNotActivateWorkflow pins this.
  • Naming is open to bikeshedding: client-activity vs. extending client-authenticated to cover the user-driven events. A separate provider avoids changing the semantics of existing client-authenticated workflows.

This implementation is ported from providers we run out-of-tree in production against 26.6.4, adapted to the current SPI (getSupportedResourceType/validate/Root-based toPredicate).

AI disclosure

Per the contributing guidelines: AI agents were used to generate this port and its tests (adapted from our out-of-tree implementation). The changes have been reviewed and are understood and owned by the submitter.

… the same clock

JpaWorkflowStateProvider.scheduleStep stamped scheduledStepTimestamp with
Instant.now() while getDueScheduledSteps compares it against
Time.currentTimeMillis(). Production behavior is unchanged (Time delegates
to the system clock), but under a test time offset steps were scheduled
against the wrong clock, making restart scenarios untestable with
TimeOffSet. Steps scheduled during an offset run are now scheduled
relative to the simulated clock, so the cumulative offsets in
BrokeredUserLifecycleWorkflowTest are adjusted accordingly.

Signed-off-by: Antonio Casagrande <antoniocasagrande@airia.com>
…viders

Adds two client-scoped workflow providers for managing the lifecycle of
dynamically registered clients:

- has-client-attribute: mirrors has-user-attribute for CLIENTS resources,
  supporting presence-only (key) and exact-value (key:value) matching,
  with a JPA predicate so scheduled activation picks up eligible clients.
- client-activity: complements client-authenticated (which only sees
  CLIENT_LOGIN) by treating LOGIN, CODE_TO_TOKEN and REFRESH_TOKEN user
  events as activity on the event's client, so restart-in-progress keeps
  actively used public clients out of disable/delete steps.

Closes keycloak#51595

Signed-off-by: Antonio Casagrande <antoniocasagrande@airia.com>
LOGIN_ERROR, CODE_TO_TOKEN_ERROR and REFRESH_TOKEN_ERROR carry the
client id but must not count as activity: a bad actor spamming
credentials against a dormant client would otherwise keep resetting
its idle clock.

Signed-off-by: Antonio Casagrande <antoniocasagrande@airia.com>
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: client-scoped conditions and an activity event for user-driven (public) clients

1 participant