When Keycloak's Event Store Runs on Defaults Without `enabled_event_types`
Keycloak 26.7.4 + keycloak provider v5.9.0, enabled_event_types omitted ("Omitting this field enables all event types"): the event store kept LOGIN, not REFRESH_TOKEN; the log listener got both. When It Runs โ Run Repo
Keycloak 26.7.4 + keycloak provider v5.9.0, enabled_event_types omitted ("Omitting this field enables all event types"): the event store kept LOGIN, not REFRESH_TOKEN; the log listener got both.
When It Runs โ Run Report #RR07 ยท Testing what infrastructure actually does.
Provider ยท Component: Keycloak ยท realm user-event storage (the event store) โ what gets saved when user events are turned on through the keycloak organization's IaC provider (
keycloak_realm_events) withenabled_event_typesleft out
Versions tested: measured โ Keycloak 26.7.4 with the keycloak provider v5.9.0 (keycloak/terraform-provider-keycloak; both the latest release as of 2026-09-26; Keycloak 26.7.5, 26.8.0 and 26.8.1, released on 2026-09-30, 2026-10-01 and 2026-10-13, and provider v5.10.0, released on 2026-10-05, were not measured; a single version pair), applied with OpenTofu 1.12.6. source-read โ the Keycloak 26.7.4 and provider v5.9.0 tags.
Config profile: the official imagequay.io/keycloak/keycloak:26.7.4(digest-pinned) instart-devmode on a dedicated test VM, with the event listener's log category atdebug; through OpenTofu, two realms withkeycloak_realm_events(events_enabled = true,events_listeners = ["jboss-logging"]) โenabled_event_typesleft out inrr07, set to four types in the comparison realmrr07-explicit; in each realm a public client with direct access grants and one user; a successful and a failed password grant, then a successful and a failed refresh-token grant.
Verified on: 2026-10-13 (desk sources re-checked; Keycloak 26.8.1 and provider v5.10.0 were the latest releases) ยท lab measured 2026-09-27 on Keycloak 26.7.4 with provider v5.9.0.
Affects: Keycloak 26.7.4 realms whose user-event saving is turned on through the keycloak provider v5.9.0keycloak_realm_eventsresource withenabled_event_typesleft out: the event store keeps only Keycloak's default saved types, soREFRESH_TOKENandREFRESH_TOKEN_ERRORare not saved (checked 5 and 120 seconds after the requests). Event listeners, such as thejboss-logginglog listener, still receive those events.
TL;DR: With Keycloak 26.7.4 and the keycloak provider v5.9.0, turning on realm events withoutenabled_event_types, which the provider documents as "Omitting this field enables all event types," savedLOGINandLOGIN_ERRORto the event store but notREFRESH_TOKENorREFRESH_TOKEN_ERROR, which reached only the log listener.
1. The Claim
The claim here is the keycloak organization's own IaC provider documentation, set against what Keycloak's event store actually keeps.
What the provider documents. The keycloak provider (keycloak/terraform-provider-keycloak, used from Terraform or OpenTofu) documents the enabled_event_types field of its keycloak_realm_events resource as follows (docs/resources/realm_events.md at the v5.9.0 tag, L45; checked 2026-09-26):
"
enabled_event_types- (Optional) The event types that will be saved to the database. Omitting this field enables all event types. Defaults to[]or all event types."
The example on the same page carries the comment "When omitted or left empty, keycloak will enable all event types" (L26), and describes events_enabled as: "When true, events from enabled_event_types are saved to the database, making them available through the admin console. Defaults to false." (L43).
How the default is commonly read. A third-party Keycloak auditing guide (skycloak.io; published 2026-06-15, updated 2026-09-07; checked 2026-09-26) describes the same setting in the admin console the same way: "The default is all types; for a lean DB footprint, select only the types you actively query." The same guide later suggests alerting on "Spikes in REFRESH_TOKEN_ERROR or CODE_TO_TOKEN_ERROR".
What Keycloak documents. Keycloak's server administration guide presents the saved event types as a list you add to โ "Click *Add saved types to see other events you can save."* (docs/documentation/server_admin/topics/events/login.adoc L20, 26.7.4) โ and the EventType Javadoc describes isSaveByDefault() as: "Determines whether this event is stored when the admin has not set a specific set of event types to save." So Keycloak does say that an empty setting means a default subset. The gap this report looks at is between that behavior and the provider's "Omitting this field enables all event types."
Why it would behave that way (source layer). In Keycloak 26.7.4, EventBuilder.sendNow (server-spi-private/src/main/java/org/keycloak/events/EventBuilder.java L261โ277) stores an event only if:
if (eventTypes.isEmpty() && event.getType().isSaveByDefault() || eventTypes.contains(event.getType().name())) {
โ that is, when the realm's list of saved types is empty, only types flagged "save by default" go to the event store. Then, outside that condition, every configured event listener receives the event (L270โ276). In EventType.java, LOGIN(0, true) (L29) and LOGIN_ERROR (L30) are flagged true, while REFRESH_TOKEN(5, false) (L42) and REFRESH_TOKEN_ERROR (L43) are flagged false. When the list is empty, GET โฆ/events/config answers with the default-saved types filled in rather than an empty list (RealmAdminResource.java L845โ850). On the provider side (v5.9.0), omitting enabled_event_types sends an empty list (provider/resource_keycloak_realm_events.go L60; the API field enabledEventTypes has no omitempty, keycloak/realm_events.go L11), and the field is read back into state only when it is set in the configuration (L94) โ which suggests tofu plan would report no difference, something we checked in the lab (ยง3).
What this report tests. When realm events are turned on through keycloak_realm_events with enabled_event_types left out โ which the provider documents as "Omitting this field enables all event types" โ does Keycloak save all event types, or only its default saved types, leaving REFRESH_TOKEN (success and error) and similar events out of the event store? The event store is what the admin console's Events tab, the admin REST events API, and the event database table show. Event listeners are a separate path: per the source above they receive every event regardless of the saved types, and we observed the jboss-logging listener separately (ยง3).
2. The Setup
-
Environment: a dedicated test VM with Docker; Keycloak from the official image
quay.io/keycloak/keycloak:26.7.4, pinned by digest (sha256:82a77884f3af238beab1e7afd63b5f530e1b5c0590bd7aa60b40a40463e29b2c; the linux/amd64 image in it,sha256:3d911baa186f352563854039b95f21a7e2c01c76b527fdc64f24a0885b927bdf, is what ran), started withstart-devand the bootstrap-admin variables from Keycloak's container guide. The log level isINFO,org.keycloak.events:debug:org.keycloak.eventsis the category of thejboss-loggingevent listener, which logs successful events atdebugby default, so this makes its success lines visible; it does not change what the event store keeps.curl8.5.0 andjq1.7 for the REST calls. -
Version: Keycloak 26.7.4 ยท the keycloak provider v5.9.0 (release asset
terraform-provider-keycloak_5.9.0_linux_amd64.zip, sha256 matching the release's SHA256SUMS) ยท OpenTofu 1.12.6 (release asset sha256 matching its SHA256SUMS). A single version pair. -
Configuration: one OpenTofu configuration (ยง4) creating two realms. In
rr07,keycloak_realm_eventssetsevents_enabled = trueandevents_listeners = ["jboss-logging"]and leavesenabled_event_typesout. Inrr07-explicit, it is the same exceptenabled_event_types = ["LOGIN", "LOGIN_ERROR", "REFRESH_TOKEN", "REFRESH_TOKEN_ERROR"].events_listenersis set explicitly in both, as in the provider's own example. Each realm has a public clientrr07-appwith direct access grants and a useralicewith a dummy password. In each realm, in the same order: a successful password grant (LOGIN), a password grant with a wrong password (LOGIN_ERROR), a successful refresh-token grant (REFRESH_TOKEN), and a refresh-token grant with an altered token (REFRESH_TOKEN_ERROR). The event store is queried 5 seconds and 120 seconds after the last request.
3. Expected vs. Observed โ with enabled_event_types left out, which events does the event store keep?
Expected (from the provider documentation): "Omitting this field enables all event types" โ so in rr07 all four event types would be saved, the same as in rr07-explicit.
Observed (Keycloak 26.7.4 with provider v5.9.0 via OpenTofu 1.12.6, measured 2026-09-27; all times UTC; raw in our lab record):
-
Apply, then plan:
tofu applycreated the eight resources โ "Apply complete! Resources: 8 added, 0 changed, 0 destroyed." โ andtofu plan -detailed-exitcoderight after it exited0with "No changes. Your infrastructure matches the configuration." Run again after the requests, it again exited0. -
The saved types the server reports (
GET โฆ/events/config): forrr07,eventsEnabled: true,eventsListeners: ["jboss-logging"], and anenabledEventTypeslist of 103 types that includesLOGINandLOGIN_ERRORbut notREFRESH_TOKENorREFRESH_TOKEN_ERRORโ the list the server returns when the realm's own list is empty (ยง1); forrr07-explicit, the four listed types. -
The requests (23:03:37โ23:03:39): in both realms, the password grant returned HTTP 200; the wrong password, 400
invalid_grant("Invalid user credentials"); the refresh, 200; the altered refresh token, 400invalid_grant("Invalid refresh token"). -
The event store (
GET /admin/realms/<realm>/events, at +5 s and +120 s): the same at both times โrr07held two events, oneLOGINand oneLOGIN_ERROR, and noREFRESH_TOKENorREFRESH_TOKEN_ERROR;rr07-explicitheld one of each of the four. -
The listener (the
jboss-logginglines in the container log): in both realms, one line for each of the four types โLOGINandREFRESH_TOKENatDEBUG,LOGIN_ERRORandREFRESH_TOKEN_ERRORatWARN(the latter witherror="invalid_token"andreason="Invalid refresh token").
Raw evidence: the OpenTofu output, the events/config responses, each request with its HTTP status and response, the event-store responses at both query times, and the container log, all kept in our lab record.
4. Reproduction
The minimal reproduction (raw output is kept in our lab record; the configuration and the commands below are in the reproduction repo linked below). All times UTC.
# 0. Start Keycloak 26.7.4 (official image, digest-pinned) in dev mode, with the event listener's
# log category at debug so its success lines are visible, and wait until it has started.
# docker run -d --name rr07-kc -p 8080:8080 -e KC_BOOTSTRAP_ADMIN_USERNAME=admin -e KC_BOOTSTRAP_ADMIN_PASSWORD=change_me quay.io/keycloak/keycloak:26.7.4@sha256:82a77884f3af238beab1e7afd63b5f530e1b5c0590bd7aa60b40a40463e29b2c start-dev --log-level="INFO,org.keycloak.events:debug"
# for i in $(seq 1 60); do docker logs rr07-kc 2>&1 | grep -E 'Keycloak 26\.7\.4 on JVM .* started' && break; sleep 3; done # the startup line, at most 3 minutes
# 1. Apply the OpenTofu configuration below (main.tf; OpenTofu 1.12.6, provider keycloak/keycloak 5.9.0),
# then check for drift right away (exit code 0 = no changes, 2 = changes).
# tofu init
# tofu apply -auto-approve
# tofu plan -detailed-exitcode
# 2. Read the saved types the server reports for each realm (admin token from the master realm).
# TOKEN=$(curl -s -d grant_type=password -d client_id=admin-cli -d username=admin -d password=change_me http://localhost:8080/realms/master/protocol/openid-connect/token | jq -r .access_token)
# curl -s -H "Authorization: Bearer $TOKEN" http://localhost:8080/admin/realms/rr07/events/config | jq .
# curl -s -H "Authorization: Bearer $TOKEN" http://localhost:8080/admin/realms/rr07-explicit/events/config | jq .
# 3. Generate the four events in each realm (run once with REALM=rr07, then with REALM=rr07-explicit).
# T=http://localhost:8080/realms/$REALM/protocol/openid-connect/token
# RT=$(curl -s -d grant_type=password -d client_id=rr07-app -d username=alice -d password=alice-pass-1 $T | jq -r .refresh_token) # LOGIN
# curl -s -d grant_type=password -d client_id=rr07-app -d username=alice -d password=wrong-password $T # LOGIN_ERROR
# curl -s -d grant_type=refresh_token -d client_id=rr07-app -d refresh_token="$RT" $T # REFRESH_TOKEN
# curl -s -d grant_type=refresh_token -d client_id=rr07-app -d refresh_token="${RT}x" $T # REFRESH_TOKEN_ERROR (altered token)
# 4. Query each realm's event store 5 seconds and 120 seconds after the last request (fresh admin token each time).
# TOKEN=$(curl -s -d grant_type=password -d client_id=admin-cli -d username=admin -d password=change_me http://localhost:8080/realms/master/protocol/openid-connect/token | jq -r .access_token)
# for REALM in rr07 rr07-explicit; do echo "== $REALM"; curl -s -H "Authorization: Bearer $TOKEN" "http://localhost:8080/admin/realms/$REALM/events?max=100" | jq -r '.[].type' | sort | uniq -c; done
# 5. Look at what the jboss-logging listener wrote for the same requests.
# docker logs --timestamps rr07-kc 2>&1 | grep 'org.keycloak.events'
# 6. Tear down (the test container only).
# docker rm -f rr07-kc
The OpenTofu configuration for step 1 (main.tf), exactly as we applied it:
terraform {
required_providers {
keycloak = {
source = "keycloak/keycloak"
version = "5.9.0"
}
}
}
provider "keycloak" {
client_id = "admin-cli"
username = "admin"
password = "change_me"
url = "http://localhost:8080"
}
# Realm rr07: user events on, enabled_event_types left out.
resource "keycloak_realm" "rr07" {
realm = "rr07"
enabled = true
}
resource "keycloak_realm_events" "rr07" {
realm_id = keycloak_realm.rr07.id
events_enabled = true
events_listeners = ["jboss-logging"]
}
# Realm rr07-explicit: the same, with the four types listed.
resource "keycloak_realm" "rr07_explicit" {
realm = "rr07-explicit"
enabled = true
}
resource "keycloak_realm_events" "rr07_explicit" {
realm_id = keycloak_realm.rr07_explicit.id
events_enabled = true
events_listeners = ["jboss-logging"]
enabled_event_types = ["LOGIN", "LOGIN_ERROR", "REFRESH_TOKEN", "REFRESH_TOKEN_ERROR"]
}
# In each realm: a public client with direct access grants, and one user.
locals {
realm_ids = {
"rr07" = keycloak_realm.rr07.id
"rr07-explicit" = keycloak_realm.rr07_explicit.id
}
}
resource "keycloak_openid_client" "app" {
for_each = local.realm_ids
realm_id = each.value
client_id = "rr07-app"
access_type = "PUBLIC"
standard_flow_enabled = false
direct_access_grants_enabled = true
}
resource "keycloak_user" "alice" {
for_each = local.realm_ids
realm_id = each.value
username = "alice"
email = "[email protected]"
email_verified = true
first_name = "Alice"
last_name = "Example"
initial_password {
value = "alice-pass-1"
temporary = false
}
}
Outputs for each step land in ยง3 (above) and our lab record.
A recorded run of this reproduction: https://youtu.be/yFowKYN7kcw
Reproduction repo: https://github.com/whenitruns/when-keycloaks-event-store-runs-on-defaults-without-enabled-event-types
5. Impact
An audit that relies on the event store โ the admin console's Events tab, the admin REST events API, or the event database table โ has no record of refresh-token activity, successful or failed, in a realm whose events were turned on through this provider with enabled_event_types left out โ in our run, neither event type was stored. The belief this quietly breaks is "the provider says omitting the list saves all event types, so refresh-token failures would be there." An investigator who finds no REFRESH_TOKEN_ERROR entries can read that as "no refresh failures happened." The log-listener path is a different matter: in our run, the jboss-logging listener wrote those events to the server log. This is about the completeness of an audit configuration, not a way around authentication: refresh tokens are validated as usual; the events simply are not stored.
6. What We Couldn't Conclude
-
Other listeners and log settings. We observed the
jboss-logginglistener with its log category atdebug. Other listeners, and other log levels, were not measured. -
The plan result beyond our run. The
tofu planresult in ยง3 is what we observed in this run; we make no broader claim about when OpenTofu or Terraform would or would not report a difference. - Realms that already have a saved-types list. In the source, the provider sends an explicit empty list when the field is left out, and Keycloak replaces a realm's saved types with any list it receives that is not null. We measured only newly created realms and did not measure what happens when this configuration is applied to a realm that already has a custom list.
- Other tools using this provider. We applied the provider with OpenTofu only.
- Other versions. Only Keycloak 26.7.4 with provider v5.9.0; we make no claim about other versions. Keycloak 26.7.5, 26.8.0 and 26.8.1 (released on 2026-09-30, 2026-10-01 and 2026-10-13) and provider v5.10.0 (released on 2026-10-05) were not measured.
-
Which other types are left out by default. We name only the two we measured. To see which types your realm saves, read
GET /admin/realms/<realm>/events/configor the saved-types list in the admin console. - Why the provider documents "all event types." We don't know, and we don't infer it.
- Admin events. Admin events are a separate setting and are not covered here.
7. Conclusion
With Keycloak 26.7.4 and the keycloak provider v5.9.0, in the setup recorded above, turning on realm events with enabled_event_types left out kept LOGIN and LOGIN_ERROR in the event store but not REFRESH_TOKEN or REFRESH_TOKEN_ERROR, while listing the four types in the comparison realm saved all four. The provider documents the omitted field as "Omitting this field enables all event types"; Keycloak applies its default saved types when the list is empty, and its admin guide presents the saved types as a list you add to. The point of this report is the gap between those two descriptions, recorded as of the Verified-on date and within this scope only.
Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes โ full credit and traffic to the original publisher.