Keycloak Connector
The Humifortis Keycloak Connector brings adaptive authentication directly into your Keycloak instance. Every login is evaluated against a live risk score, and Keycloak enforces the decision — without changing your application code.
What It Does
The connector is a lightweight Java plugin (JAR) that runs inside Keycloak. It provides four SPI components you wire into your authentication flow:
| Component | Purpose |
|---|---|
| HumifortisDeviceCollector | Collects 40+ device signals silently via FingerprintJS after credentials are submitted, before the risk decision is made |
| HumifortisRiskAuthenticator | Calls POST /evaluate with the full login context; enforces the returned decision |
| HumifortisHighCondition | Condition in the conditional MFA sub-flow — triggers MFA only when the engine returned CHALLENGE_MFA |
| HumifortisStepUpRouter | Routes the MFA challenge to the best available method (EMAIL_OTP, TOTP, or WEBAUTHN) based on the engine recommendation |
The connector also ships a HumifortisEventListener that captures background events (login success, logout, password change, etc.) and forwards them asynchronously — separate from the inline RBA decision.
Authentication flow at login
User submits credentials
│
▼
HumifortisDeviceCollector
├─ Generates nonce, serves device-collection page
├─ FingerprintJS collects 40+ signals (browser, screen, fonts, etc.)
├─ Computes binding: SHA-256(nonce:timestamp:visitorId)
├─ Stores signals in auth session notes
│
▼
HumifortisRiskAuthenticator
├─ Calls POST /evaluate (IP, UA, device_id, MFA profile, roles, geo, device signals…)
├─ Receives: { "action": "ALLOW" | "CHALLENGE_MFA" | "BLOCK" }
│
├─ ALLOW → context.success() — user proceeds
├─ CHALLENGE_MFA → sets NOTE_MFA_ENFORCED in session, continues
└─ BLOCK → context.failure(ACCESS_DENIED) — login rejected
│
▼ (only when CHALLENGE_MFA)
HumifortisHighCondition (in conditional sub-flow)
└─ Reads NOTE_MFA_ENFORCED; if set → triggers HumifortisStepUpRouter
│
▼
HumifortisStepUpRouter
├─ Sends EMAIL_OTP, TOTP, or WEBAUTHN challenge (based on engine recommendation)
├─ Validates response
└─ On success: fires auth_mfa_success event, grants access
Events Captured (Async Listener)
The HumifortisEventListener runs in the background and captures:
| Keycloak Event | Canonical Signal | Risk Delta |
|---|---|---|
| LOGIN | AUTH_LOGIN_SUCCESS | +2 |
| LOGIN_ERROR | AUTH_LOGIN_FAILED | +8 |
| LOGOUT | AUTH_LOGOUT | −5 |
| MFA challenge passed | AUTH_MFA_SUCCESS | −25 |
| MFA challenge failed | AUTH_MFA_FAILED | +12 |
| UPDATE_PASSWORD | AUTH_PASSWORD_CHANGE | +3 |
| UPDATE_EMAIL | AUTH_EMAIL_CHANGE | +5 |
| REMOVE_TOTP | AUTH_TOTP_REMOVED | +10 |
| UPDATE_TOTP | AUTH_TOTP_UPDATED | +5 |
| IMPERSONATE | AUTH_IMPERSONATE | +20 |
| REGISTER | AUTH_REGISTER | 0 |
All events are enriched server-side with IP geolocation, Tor/VPN/proxy detection, and velocity.
Quick Setup
1. Place the JAR
cp humifortis-keycloak-connector.jar /opt/keycloak/providers/
# Docker:
docker run ... \
-v /path/to/humifortis-keycloak-connector.jar:/opt/keycloak/providers/humifortis-keycloak-connector.jar:ro \
quay.io/keycloak/keycloak:26.1 start-dev
2. Configure environment variables in Keycloak
# Required
HUMIFORTIS_API_KEY=your-api-key-here
# Optional (these have sensible defaults)
HUMIFORTIS_API_URL=https://demo.humifortis.com # or your self-hosted URL
HUMIFORTIS_TIMEOUT_MS=5000 # default: 5000ms
HUMIFORTIS_FALLBACK_ALLOW=true # default: true (fail-open)
There is no HUMIFORTIS_MODE env var on the connector. The enforcement mode (enforce, dry_run, shadow) is configured in the Humifortis engine per-tenant, and returned as part of the POST /evaluate response. The connector always executes whatever mode the engine declares.
3. Configure the Keycloak authentication flow
- Keycloak Admin → your realm → Authentication
- Duplicate the default browser flow (never modify the built-in directly)
- In your copy, add these executions in order:
- HumifortisDeviceCollector — set to
REQUIRED, after the password form and before HumifortisRiskAuthenticator - HumifortisRiskAuthenticator — set to
REQUIRED, after credentials
- HumifortisDeviceCollector — set to
- Add a conditional sub-flow after HumifortisRiskAuthenticator:
- Condition: HumifortisHighCondition — set to
REQUIRED - Execution: HumifortisStepUpRouter — set to
REQUIRED
- Condition: HumifortisHighCondition — set to
- Bind this flow to the browser binding for your realm
4. Register the event listener
- Keycloak Admin → your realm → Events
- Under Event listeners, add
humifortis
5. Restart Keycloak
docker compose restart keycloak
# or
/opt/keycloak/bin/kc.sh build && /opt/keycloak/bin/kc.sh start
Docker Compose Example
keycloak:
image: quay.io/keycloak/keycloak:26.1
command: start-dev --import-realm
environment:
KC_HTTP_PORT: "8088"
KC_HOSTNAME_STRICT: "false"
HUMIFORTIS_API_URL: "http://humifortis:8080/api/v1"
HUMIFORTIS_API_KEY: "${HUMIFORTIS_API_KEY}"
HUMIFORTIS_TIMEOUT_MS: "5000"
HUMIFORTIS_FALLBACK_ALLOW: "true"
volumes:
- ./humifortis-keycloak-connector.jar:/opt/keycloak/providers/humifortis-keycloak-connector.jar:ro
- ./realm-config.json:/opt/keycloak/data/import/realm.json:ro
depends_on:
- humifortis
Entity ID Format
The connector identifies users with:
user:keycloak:{realm_id}:{user_id}
For example: user:keycloak:myrealm:e4b96c3a-1234-5678-abcd-ef0123456789
This is the entity ID you use when querying GET /risk/:entity_id from outside Keycloak.
Fail-Open vs Fail-Closed
| Setting | HUMIFORTIS_FALLBACK_ALLOW=true | HUMIFORTIS_FALLBACK_ALLOW=false |
|---|---|---|
| Engine unreachable | Login proceeds (fail-open) | Login blocked (fail-closed) |
| Timeout exceeded | Login proceeds | Login blocked |
| Recommended for | High-availability, public-facing | Regulated, high-security environments |
Device Trust Cookie
When a user passes MFA, the connector can set a first-party browser cookie (hf_device_trust) that marks this device as trusted. On a subsequent login from the same device, the engine can return ALLOW even at higher risk scores — with a configurable trust expiry window.
OTP Expiry Policy
Email OTP expiry is automatically scaled to risk level:
| Risk Level | OTP Validity |
|---|---|
| CRITICAL | 60 seconds |
| HIGH | 120 seconds |
| MEDIUM / LOW | 180 seconds (default) |
Troubleshooting
Login blocked unexpectedly: Check the Humifortis dashboard → Entities → find the user → review the risk timeline. The contributing signals explain the decision.
Connector can't reach engine: Verify HUMIFORTIS_API_URL is reachable from Keycloak's container/host. If HUMIFORTIS_FALLBACK_ALLOW=true (the default), logins should still proceed when the engine is unreachable.
No events appearing in the engine: Confirm the humifortis event listener is added to your realm under Events → Event listeners.
Users not getting MFA despite high risk: Verify the conditional sub-flow is set up with HumifortisHighCondition and HumifortisStepUpRouter. The sub-flow must follow HumifortisRiskAuthenticator in the flow.
See Also
- Quick Start — send your first event via the HTTP API
- API Reference — full request/response schema for
/evaluate - Self-Hosted — run the Humifortis engine on your own infrastructure