Keycloak Push MFA Extension
A Keycloak extension that adds push-based multi-factor authentication with passkey-inspired key continuity and pseudonymous credential handles.
Quick Start
Using Maven (recommended)
Add the dependency to your project:
<dependency>
<groupId>de.arbeitsagentur.opdt</groupId>
<artifactId>keycloak-push-mfa-extension</artifactId>
<!-- Check the badge above or Maven Central for the latest version -->
<version>1.11.0</version>
</dependency>Copy the JAR to Keycloak's providers/ directory and restart Keycloak.
Building from Source
# Build the provider
mvn -DskipTests package
# Run Keycloak with the demo realm
docker compose up- Keycloak Admin Console: http://localhost:8080 (login:
admin/admin) - Demo Realm:
demowith test usertest/test - Demo Configuration: See
config/demo-realm.jsonfor a working example
Introduction
This project extends Keycloak with a push-style second factor that borrows some passkey-like ideas without implementing WebAuthn or passkeys themselves. After initial enrollment, the mobile app never receives the real user identifier from Keycloak; instead, it works with a credential id that only the app can map back to the real user, and later approvals remain bound to the enrolled device key. Everything is implemented with standard Keycloak SPIs plus a small JAX-RS resource exposed under /realms/<realm>/push-mfa.
High Level Flow
sequenceDiagram
autonumber
participant Browser as User / Browser
participant Keycloak as Keycloak Server
participant Provider as Push Provider (FCM/APNs)
participant Mobile as Mobile App
Note over Browser, Mobile: **Phase 1: Enrollment (Register Push MFA Device)**
Browser->>Keycloak: Login & Trigger Enrollment
Keycloak-->>Browser: Render QR Code & Start SSE Listener
par Parallel Actions
Browser->>Keycloak: SSE Request (Read Current Status)
Browser->>Mobile: Scan QR Code
end
Note over Mobile: Verify Token & Generate User Key Pair
Mobile->>Keycloak: POST /enroll/complete
Note right of Mobile: Payload: Device JWT + Public JWK<br/>Signed with new Device Private Key
Keycloak->>Keycloak: Verify Signature & Store Device Credential
Keycloak-->>Browser: SSE Event: { status: "APPROVED" }
Browser->>Keycloak: Auto-Submit Form (Enrollment Complete)
Note over Browser, Mobile: **Phase 2: Login (Push MFA Confirmation)**
Browser->>Keycloak: Login (Username/Password)
Keycloak->>Keycloak: Generate Challenge & ConfirmToken
par Parallel Actions
Keycloak-->>Browser: Render "Waiting for approval..." Page
Browser->>Keycloak: SSE Request (Read Current Challenge Status)
Keycloak->>Provider: Send Push Notification
Note right of Keycloak: Payload: ConfirmToken<br/>(Credential ID, ChallengeID)
end
Provider->>Mobile: Deliver Push Notification
Mobile->>Mobile: Decrypt Token & Resolve User ID
Mobile-->>Browser: (User Prompt: Approve?)
Browser-->>Mobile: User Taps "Approve"
Mobile->>Keycloak: POST /login/challenges/{cid}/respond
Note right of Mobile: Payload: LoginToken (Action: Approve)<br/>Auth: DPoP Header + Access Token<br/>Signed with Device Private Key
Keycloak->>Keycloak: Verify DPoP, Signature & Challenge ID
Keycloak-->>Browser: SSE Event: { status: "APPROVED" }
Browser->>Keycloak: Auto-Submit Form (Login Success)
Mermaid diagram — view rendered on GitHub
The SSE endpoints keep one browser stream per watch page and read challenge state from shared storage. Pending streams get periodic heartbeat comments and are rotated after a configurable maximum lifetime so browsers reconnect cleanly through proxies and firewalls. Cross-node delivery works because every node reads the same challenge state from shared storage; if a node dies, the browser's normal EventSource reconnect can land on another node and that node becomes responsible for the stream.
Documentation
| Document | Description |
|---|---|
| Setup Guide | Step-by-step configuration instructions and Keycloak concepts |
| Flow Details | Technical details of enrollment, login, SSE, and DPoP authentication |
| API Reference | REST endpoints for mobile apps |
| Configuration | All configuration options reference |
| App Implementation | Guide for mobile app developers |
| SPI Reference | Push notification, event, and rate limiting SPIs |
| UI Customization | Theme and template customization |
| Security | Security model and mobile app obligations |
| Firebase Cloud Messaging Provider | Send real push notifications to your mobile app |
| Push Mfa Simulator | Testing without a real mobile app |
| Troubleshooting | Common issues and solutions |
| Load Testing | Two-node browser+SSE loadtest setup and reproduction commands |
Local Development
Prerequisites
- JDK: See
pom.xmlfor the required Java version. - Maven: Used for building the project.
- Docker: Required for local setup (see
docker-compose.yml) and integration tests.
Checks (formatting and tests)
Run the following commands locally to ensure code quality:
- Formatting:
mvn spotless:apply(Ensures consistent code style). - Verification:
mvn verify(Runs the full test suite and builds the project). - Build without tests:
mvn -DskipTests package