1. Event System Overview
Keycloak provides a comprehensive Event System system to monitor all activities in the system. Every action from logging in, registering, to changing admin configuration is logged as events.
1.1 Two types of Events
| Type | Description | Example |
|---|---|---|
| Login Events (User Events) | Actions related to users | LOGIN, REGISTER, LOGOUT, TOKEN_EXCHANGE |
| Admin Events | Configuration changes via Admin Console/API | CREATE user, UPDATE realm, DELETE client |
1.2 Login Event Types
| Event Type | Description | When does it occur |
|---|---|---|
LOGIN | Successful login | User entered correct credentials |
LOGIN_ERROR | Login failed | Wrong username/password |
REGISTER | Register new account | User successfully created account |
REGISTER_ERROR | Registration failed | Duplicate email, validation failed |
LOGOUT | Logout | User logout or session expired |
CODE_TO_TOKEN | Exchange authorization code → token | OIDC Authorization Code flow |
CODE_TO_TOKEN_ERROR | Token exchange failed | Invalid code, expired code |
REFRESH_TOKEN | Refresh access token | Client uses refresh token |
REFRESH_TOKEN_ERROR | Refresh token failed | Token revoked or expired |
CLIENT_LOGIN | Client authentication | Service account login |
INTROSPECT_TOKEN | Token introspection | Resource server verify token |
UPDATE_PASSWORD | Change password | User change password |
RESET_PASSWORD | Reset password | User reset via email link |
VERIFY_EMAIL | Email verification | User click verification link |
SEND_RESET_PASSWORD | Send password reset email | Request forgotten password |
UPDATE_PROFILE | Update profile | User updates personal information |
REMOVE_TOTP | Delete TOTP device | User remove OTP authenticator |
UPDATE_TOTP | TOTP configuration | User set OTP |
GRANT_CONSENT | User grants consent | OAuth2 consent screen |
TOKEN_EXCHANGE | Token exchange | Exchange tokens between clients |
1.3 Admin Event Operations
| Operation | Description | Resource Types |
|---|---|---|
CREATE | Create new resource | USER, CLIENT, REALM, GROUP, ROLE... |
UPDATE | Update resource | USER, CLIENT, REALM_SETTINGS... |
DELETE | Delete resource | USER, CLIENT, SESSION... |
ACTION | Perform action | RESET_PASSWORD, SEND_VERIFY_EMAIL... |
2. Turn on Event Logging
2.1 Qua Admin Console
- Login Admin Console
- Select Realm → Realm Settings → tab Events
- Configuration User events settings:
- Save events: ON
- Expiration: 30 days (depending on compliance requirements)
- Saved types: Select event types to save (default: ALL)
- Configuration Admin events settings:
- Save events: ON
- Include representation: ON (save request/response body)
- Event listeners: Add necessary listeners
2.2 Qua REST API
# Bật event logging cho realm
curl -X PUT "http://localhost:8080/admin/realms/my-realm" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"eventsEnabled": true,
"eventsExpiration": 2592000,
"eventsListeners": ["jboss-logging"],
"enabledEventTypes": [
"LOGIN", "LOGIN_ERROR",
"REGISTER", "REGISTER_ERROR",
"LOGOUT",
"CODE_TO_TOKEN", "CODE_TO_TOKEN_ERROR",
"REFRESH_TOKEN", "REFRESH_TOKEN_ERROR",
"CLIENT_LOGIN", "CLIENT_LOGIN_ERROR",
"UPDATE_PASSWORD",
"RESET_PASSWORD",
"SEND_RESET_PASSWORD"
],
"adminEventsEnabled": true,
"adminEventsDetailsEnabled": true
}'
3. Event Listeners
Event Listeners handle events as they occur. Keycloak has the following listeners available:
3.1 jboss-logging Listener
Log events to Keycloak server log (enabled by default):
# Log output mẫu
2026-03-15 10:30:45,123 INFO [org.keycloak.events] (executor-thread-1)
type=LOGIN, realmId=my-realm, clientId=my-app, userId=abc-123,
ipAddress=192.168.1.100, auth_method=openid-connect,
auth_type=code, redirect_uri=https://myapp.com/callback,
[email protected]
3.2 Email Listener
Send email to user when there is an important event (for example: login from a new device):
# Bật email listener
Realm Settings → Events → Event listeners → Thêm "email"
# Events được gửi email por defecto:
- LOGIN_ERROR (quá nhiều lần → cảnh báo compromised account)
- UPDATE_PASSWORD
- REMOVE_TOTP
- UPDATE_TOTP
4. Event Details and Event Store
4.1 Login Event Structure
{
"time": 1710489045000,
"type": "LOGIN",
"realmId": "my-realm",
"clientId": "my-web-app",
"userId": "550e8400-e29b-41d4-a716-446655440000",
"sessionId": "abc-session-id",
"ipAddress": "192.168.1.100",
"details": {
"auth_method": "openid-connect",
"auth_type": "code",
"redirect_uri": "https://myapp.com/callback",
"consent": "no_consent_required",
"code_id": "xyz-code-id",
"username": "[email protected]",
"identity_provider": null
}
}
4.2 Admin Event Structure
{
"time": 1710489100000,
"realmId": "my-realm",
"authDetails": {
"realmId": "master",
"clientId": "security-admin-console",
"userId": "admin-user-id",
"ipAddress": "10.0.0.1"
},
"operationType": "CREATE",
"resourceType": "USER",
"resourcePath": "users/new-user-id",
"representation": "{\"username\":\"newuser\",\"email\":\"[email protected]\",\"enabled\":true}"
}
4.3 Event Store — Database
Events are stored in Keycloak's database:
| Table | Content |
|---|---|
EVENT_ENTITY | Login events |
ADMIN_EVENT_ENTITY | Admin events |
Note: Event store is saved in Keycloak DB by default. With a large number of events, you should use a custom Event Listener to ship events to the external system and set short expiration for the built-in store.
5. Event Filtering and Querying
5.1 Qua Admin Console
- Go to Events → tab User events or Admin events
- Filter events theo:
- Event type: LOGIN, LOGIN_ERROR, REGISTER...
- Client: Select specific client
- User: Search theo user ID
- Date range: From/To date
- IP Address: Filter theo IP
5.2 Qua REST API — Login Events
# Lấy tất cả login events
curl -s "http://localhost:8080/admin/realms/my-realm/events" \
-H "Authorization: Bearer $ACCESS_TOKEN" | jq .
# Filter theo event type
curl -s "http://localhost:8080/admin/realms/my-realm/events?type=LOGIN_ERROR" \
-H "Authorization: Bearer $ACCESS_TOKEN" | jq .
# Filter theo user
curl -s "http://localhost:8080/admin/realms/my-realm/events?user=user-uuid" \
-H "Authorization: Bearer $ACCESS_TOKEN" | jq .
# Filter theo client và date range
curl -s "http://localhost:8080/admin/realms/my-realm/events?\
client=my-app&\
dateFrom=2026-03-01&\
dateTo=2026-03-31&\
first=0&\
max=100" \
-H "Authorization: Bearer $ACCESS_TOKEN" | jq .
# Filter nhiều event types
curl -s "http://localhost:8080/admin/realms/my-realm/events?\
type=LOGIN&type=LOGIN_ERROR&type=REGISTER" \
-H "Authorization: Bearer $ACCESS_TOKEN" | jq .
5.3 Qua REST API — Admin Events
# Lấy admin events
curl -s "http://localhost:8080/admin/realms/my-realm/admin-events" \
-H "Authorization: Bearer $ACCESS_TOKEN" | jq .
# Filter theo operation type
curl -s "http://localhost:8080/admin/realms/my-realm/admin-events?\
operationTypes=CREATE&\
resourceTypes=USER" \
-H "Authorization: Bearer $ACCESS_TOKEN" | jq .
# Filter theo resource path
curl -s "http://localhost:8080/admin/realms/my-realm/admin-events?\
resourcePath=users" \
-H "Authorization: Bearer $ACCESS_TOKEN" | jq .
6. Custom Event Listener SPI
Keycloak allows creating custom Event Listeners through Service Provider Interface (SPI).
6.1 Create Maven Project
<!-- pom.xml -->
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>custom-event-listener</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<properties>
<keycloak.version>26.1.0</keycloak.version>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-server-spi</artifactId>
<version>${keycloak.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-server-spi-private</artifactId>
<version>${keycloak.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-services</artifactId>
<version>${keycloak.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>
6.2 Implement EventListenerProvider
// src/main/java/com/example/CustomEventListenerProvider.java
package com.example;
import org.keycloak.events.Event;
import org.keycloak.events.EventListenerProvider;
import org.keycloak.events.EventType;
import org.keycloak.events.admin.AdminEvent;
import org.keycloak.events.admin.OperationType;
import org.keycloak.models.KeycloakSession;
import org.jboss.logging.Logger;
import java.util.Map;
public class CustomEventListenerProvider implements EventListenerProvider {
private static final Logger log = Logger.getLogger(CustomEventListenerProvider.class);
private final KeycloakSession session;
public CustomEventListenerProvider(KeycloakSession session) {
this.session = session;
}
@Override
public void onEvent(Event event) {
// Xử lý Login Events
log.infof("Event: type=%s, realmId=%s, clientId=%s, userId=%s, ip=%s",
event.getType(),
event.getRealmId(),
event.getClientId(),
event.getUserId(),
event.getIpAddress());
// Xử lý theo event type
switch (event.getType()) {
case LOGIN:
handleLogin(event);
break;
case LOGIN_ERROR:
handleLoginError(event);
break;
case REGISTER:
handleRegister(event);
break;
default:
break;
}
}
@Override
public void onEvent(AdminEvent event, boolean includeRepresentation) {
// Xử lý Admin Events
log.infof("AdminEvent: operation=%s, resourceType=%s, resourcePath=%s, realmId=%s",
event.getOperationType(),
event.getResourceType(),
event.getResourcePath(),
event.getRealmId());
if (event.getOperationType() == OperationType.DELETE) {
handleAdminDelete(event);
}
}
private void handleLogin(Event event) {
// Ví dụ: Gửi event đến Kafka
String payload = buildEventPayload(event);
// kafkaProducer.send("keycloak-login-events", payload);
log.debugf("Login event sent to message broker: %s", payload);
}
private void handleLoginError(Event event) {
Map<String, String> details = event.getDetails();
String username = details != null ? details.get("username") : "unknown";
String error = event.getError();
log.warnf("Login failure: user=%s, error=%s, ip=%s",
username, error, event.getIpAddress());
// Ví dụ: Increment metric counter cho monitoring
// metricsService.incrementCounter("login_failures",
// "realm", event.getRealmId(),
// "error", error);
}
private void handleRegister(Event event) {
log.infof("New user registered: userId=%s, realm=%s",
event.getUserId(), event.getRealmId());
}
private void handleAdminDelete(AdminEvent event) {
log.warnf("Admin DELETE operation: resource=%s/%s by admin=%s",
event.getResourceType(),
event.getResourcePath(),
event.getAuthDetails().getUserId());
}
private String buildEventPayload(Event event) {
// Tạo JSON payload cho message broker
StringBuilder sb = new StringBuilder();
sb.append("{");
sb.append("\"type\":\"").append(event.getType()).append("\",");
sb.append("\"realmId\":\"").append(event.getRealmId()).append("\",");
sb.append("\"userId\":\"").append(event.getUserId()).append("\",");
sb.append("\"clientId\":\"").append(event.getClientId()).append("\",");
sb.append("\"ipAddress\":\"").append(event.getIpAddress()).append("\",");
sb.append("\"time\":").append(event.getTime());
sb.append("}");
return sb.toString();
}
@Override
public void close() {
// Cleanup resources
}
}
6.3 Implement EventListenerProviderFactory
// src/main/java/com/example/CustomEventListenerProviderFactory.java
package com.example;
import org.keycloak.Config;
import org.keycloak.events.EventListenerProvider;
import org.keycloak.events.EventListenerProviderFactory;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
public class CustomEventListenerProviderFactory implements EventListenerProviderFactory {
public static final String PROVIDER_ID = "custom-event-listener";
@Override
public EventListenerProvider create(KeycloakSession session) {
return new CustomEventListenerProvider(session);
}
@Override
public void init(Config.Scope config) {
// Đọc cấu hình từ keycloak config
// Ví dụ: String kafkaBrokers = config.get("kafka-brokers", "localhost:9092");
}
@Override
public void postInit(KeycloakSessionFactory factory) {
// Post-initialization
}
@Override
public void close() {
// Cleanup
}
@Override
public String getId() {
return PROVIDER_ID;
}
}
6.4 Register SPI
# src/main/resources/META-INF/services/org.keycloak.events.EventListenerProviderFactory
com.example.CustomEventListenerProviderFactory
6.5 Deploy and Activate
# Build
mvn clean package
# Deploy
cp target/custom-event-listener-1.0.0.jar $KEYCLOAK_HOME/providers/
$KEYCLOAK_HOME/bin/kc.sh build
# Kích hoạt: Admin Console → Realm Settings → Events → Event listeners
# Thêm "custom-event-listener"
7. Keycloak JSON Logging
To integrate with centralized logging, configure Keycloak to output JSON logs:
# Bật JSON logging
bin/kc.sh start \
--log=console \
--log-console-output=json
# Hoặc qua environment variables
KC_LOG=console
KC_LOG_CONSOLE_OUTPUT=json
7.1 JSON Log Output sample
{
"timestamp": "2026-03-15T10:30:45.123Z",
"level": "INFO",
"loggerClassName": "org.keycloak.events",
"loggerName": "org.keycloak.events",
"message": "type=LOGIN, realmId=my-realm, clientId=my-app, userId=abc-123, ipAddress=192.168.1.100",
"threadName": "executor-thread-1",
"threadId": 42,
"hostName": "keycloak-0",
"processName": "keycloak",
"processId": 1
}
7.2 Configure Log Levels
# Cấu hình log levels cho events
bin/kc.sh start \
--log=console \
--log-console-output=json \
--log-level=INFO \
--log-level=org.keycloak.events:DEBUG
# Environment variables
KC_LOG_LEVEL=INFO
KC_LOG_LEVEL=org.keycloak.events:DEBUG
8. Integrating ELK Stack
Ship Keycloak logs to ELK Stack (Elasticsearch, Logstash, Kibana) for centralized analysis.
8.1 General architecture
Keycloak (JSON logs)
↓
Filebeat (log shipper)
↓
Logstash (processing & enrichment)
↓
Elasticsearch (storage & indexing)
↓
Kibana (visualization & dashboards)
8.2 Filebeat Configuration
# filebeat.yml
filebeat.inputs:
- type: container
paths:
- /var/log/containers/keycloak-*.log
processors:
- decode_json_fields:
fields: ["message"]
target: "keycloak"
overwrite_keys: true
- add_fields:
target: ""
fields:
service.name: keycloak
environment: production
output.logstash:
hosts: ["logstash:5044"]
8.3 Logstash Pipeline
# logstash/pipeline/keycloak.conf
input {
beats {
port => 5044
}
}
filter {
if [service][name] == "keycloak" {
# Parse Keycloak event message
if [keycloak][message] =~ "^type=" {
kv {
source => "[keycloak][message]"
field_split => ", "
value_split => "="
target => "kc_event"
}
mutate {
add_field => {
"event_type" => "%{[kc_event][type]}"
"realm" => "%{[kc_event][realmId]}"
"client_id" => "%{[kc_event][clientId]}"
}
}
}
# GeoIP enrichment
if [kc_event][ipAddress] {
geoip {
source => "[kc_event][ipAddress]"
target => "geo"
}
}
# Detect suspicious patterns
if [kc_event][type] == "LOGIN_ERROR" {
mutate {
add_tag => ["login_failure"]
}
}
}
}
output {
if [service][name] == "keycloak" {
elasticsearch {
hosts => ["elasticsearch:9200"]
index => "keycloak-events-%{+YYYY.MM.dd}"
}
}
}
8.4 Kibana Dashboard
Create Kibana dashboards to monitor:
- Login Success/Failure Rate — Bar chart over time
- Top Login Errors — Pie chart theo error type
- Login by Geo Location — Map visualization
- Failed Logins by IP — Brute-force detection table
- User Registration Trend — Line chart by day
- Admin Operations Audit — Data table with full details
9. Grafana Loki Integration
Grafana Loki is a lighter log aggregation solution than ELK, suitable for Kubernetes environments.
9.1 Promtail Configuration
# promtail-config.yml
server:
http_listen_port: 9080
positions:
filename: /tmp/positions.yaml
clients:
- url: http://loki:3100/loki/api/v1/push
scrape_configs:
- job_name: keycloak
kubernetes_sd_configs:
- role: pod
relabel_configs:
- source_labels: [__meta_kubernetes_pod_label_app]
regex: keycloak
action: keep
- source_labels: [__meta_kubernetes_namespace]
target_label: namespace
- source_labels: [__meta_kubernetes_pod_name]
target_label: pod
pipeline_stages:
- json:
expressions:
level: level
logger: loggerName
message: message
timestamp: timestamp
- labels:
level:
logger:
- match:
selector: '{app="keycloak"} |~ "type=LOGIN|type=REGISTER|type=LOGOUT"'
stages:
- regex:
expression: 'type=(?P<event_type>\w+), realmId=(?P<realm>[\w-]+), clientId=(?P<client_id>[\w-]+), userId=(?P<user_id>[\w-]+)'
- labels:
event_type:
realm:
9.2 Grafana Dashboard Queries
# Login failures trong 1 giờ qua
{app="keycloak"} |~ "type=LOGIN_ERROR" | json | count_over_time({app="keycloak"} |~ "LOGIN_ERROR" [1h])
# Login events theo realm
sum by (realm) (count_over_time({app="keycloak"} |~ "type=LOGIN" [5m]))
# Top IPs với login failures
{app="keycloak"} |~ "type=LOGIN_ERROR" | regexp `ipAddress=(?P<ip>[\d.]+)` | count by (ip) | sort desc | limit 10
10. SIEM Integration
Integrate Keycloak events with Security Information and Event Management (SIEM) systems.
10.1 Splunk Integration
# Cấu hình Filebeat ship đến Splunk HEC
output.logstash:
enabled: false
output.http:
enabled: true
hosts: ["https://splunk-hec:8088"]
path: "/services/collector/event"
headers:
Authorization: "Splunk <HEC_TOKEN>"
format: json
10.2 SIEM Use Cases
| Use Case | Event Pattern | Action |
|---|---|---|
| Brute-force Detection | Multiple LOGIN_ERROR from same IP | Alert + Block IP |
| Account Takeover | Login from unusual GeoIP | Alert + Require MFA |
| Privilege Escalation | Admin assign role admin | Alert + Review |
| Data Exfiltration | Many unusual token requests | Alert + Revoke sessions |
| Suspicious Registration | Multiple REGISTER from same IP | Alert + CAPTCHA |
11. Audit Compliance
11.1 SOC2 Requirements
| SOC2 Control | Keycloak Implementation |
|---|---|
| CC6.1 — Logical access security | Event logging cho LOGIN, LOGIN_ERROR, PASSWORD changes |
| CC6.2 — User authentication | MFA events, registration events |
| CC6.3 — Access authorization | Admin Events cho role/permission changes |
| CC7.2 — Security monitoring | Real-time alerting on login failures |
| CC8.1 — Change management | Admin Events with representations |
11.2 HIPAA Requirements
| HIPAA Control | Keycloak Implementation |
|---|---|
| §164.312(b) — Audit controls | Enable all event types, admin events with representations |
| §164.312(d) — Person authentication | Event logging cho authentication attempts |
| §164.308(a)(5) — Security awareness | Email notifications cho suspicious activity |
11.3 Retention Policy
# Cấu hình event retention
# SOC2: minimum 1 năm
# HIPAA: minimum 6 năm
# Trong Keycloak (built-in store)
# Realm Settings → Events → Expiration: 365 days
# Trong Elasticsearch (centralized logging)
# ILM Policy:
# - Hot: 30 days (SSD)
# - Warm: 335 days (HDD)
# - Cold/Frozen: 5+ years (S3/GCS)
# - Delete: 7 years
12. Alert Automation
12.1 Prometheus Alerting
Keycloak expose metrics via /metrics endpoint (needs metrics-enabled):
# Bật metrics
bin/kc.sh start --metrics-enabled=true
# prometheus-alerts.yml
groups:
- name: keycloak-security
rules:
- alert: HighLoginFailureRate
expr: |
sum(rate(keycloak_login_error_total[5m])) by (realm)
/
sum(rate(keycloak_login_total[5m])) by (realm)
> 0.3
for: 5m
labels:
severity: warning
annotations:
summary: "High login failure rate in realm {{ $labels.realm }}"
description: "Login failure rate is {{ $value | humanizePercentage }} (threshold: 30%)"
- alert: BruteForceDetected
expr: |
sum(increase(keycloak_login_error_total[5m])) by (realm) > 50
for: 2m
labels:
severity: critical
annotations:
summary: "Possible brute-force attack on realm {{ $labels.realm }}"
description: "{{ $value }} login failures in 5 minutes"
- alert: UnusualRegistrationSpike
expr: |
sum(increase(keycloak_registrations_total[10m])) by (realm) > 100
for: 5m
labels:
severity: warning
annotations:
summary: "Unusual registration spike in realm {{ $labels.realm }}"
12.2 Alertmanager Routing
# alertmanager.yml
route:
receiver: default
routes:
- match:
severity: critical
receiver: pagerduty-security
continue: true
- match:
severity: critical
receiver: slack-security
- match:
severity: warning
receiver: slack-ops
receivers:
- name: default
email_configs:
- to: [email protected]
- name: slack-security
slack_configs:
- api_url: https://hooks.slack.com/services/xxx
channel: '#security-alerts'
title: '{{ .GroupLabels.alertname }}'
text: '{{ .CommonAnnotations.description }}'
- name: pagerduty-security
pagerduty_configs:
- service_key: <pagerduty-integration-key>
severity: critical
13. Best Practices
- Turn on both Login Events and Admin Events — Don't miss any activity in the system.
- Ship events to external system — Don't just rely on the built-in event store. Use ELK/Loki/SIEM for long-term storage.
- Enable admin event representations — Save request/response body for admin operations for full auditing.
- Set appropriate retention — Comply with compliance requirements (SOC2: 1 year, HIPAA: 6 years).
- Monitor login failure rates — Set alerts for brute-force detection and account takeover.
- Correlate events — Combine Keycloak events with application logs for a comprehensive picture.
- Protect event logs — Log data contains PII, needs encryption at rest and in transit, access restricted.