Skip to content
Ashish.
All posts
Diagram showing the interaction between Keycloak, Spring Boot, and a user during a risk-based authentication flow.

Implementing Risk-Based Authentication with Keycloak and Spring Boot

A technical walkthrough on implementing risk-based authentication using Keycloak and Spring Boot, covering adaptive authentication and risk scoring strategies.

By Ashish Kumar

Risk-based authentication (RBA) transforms the traditional binary identity check into a state machine where access decisions depend on a calculated probability of compromise. Instead of a static validation against a database, Keycloak pauses the transaction at a specific node, delegates a query to an external Spring Boot service to evaluate contextual signals, and waits for a verdict before proceeding. This decoupling ensures the identity provider does not become a bottleneck for high-volume behavioral data ingestion, while the specialized risk engine dynamically alters the authentication flow based on real-time context.

The Mechanism of Adaptive Decision Making

In a standard setup, Keycloak validates credentials and issues a token. In an RBA setup, the system treats the authentication flow as a directed graph where each node is an execution step. The core mechanism relies on intercepting this flow to inject a dynamic risk assessment layer.

Consider a scenario where Alice logs in from her usual office network. Her session cookie is valid, her IP is whitelisted, and her device fingerprint matches a stored hash. The system calculates a low risk score, allowing immediate access. Conversely, if an attacker named Bob attempts to use stolen credentials from a proxy server in a different country, the mechanism must detect the anomaly in the "Request Context" before issuing a token.

The decision logic operates on a weighted scoring model. A login from a known device might contribute negative points (lowering risk), while a login from a Tor exit node adds significant positive points (increasing risk). The threshold for triggering a step-up challenge, such as Multi-Factor Authentication (MFA), is typically set between 60 and 80, depending on the application's sensitivity.

Interception via Keycloak SPI

Keycloak implements this logic through its Service Provider Interface (SPI), specifically the AuthenticationExecutionProvider. This interface allows you to inject custom Java code directly into the authentication flow. When the flow reaches your custom node, Keycloak invokes the processExecution method, which serves as the entry point to suspend the flow, fetch data, and decide whether to succeed, fail, or trigger a new challenge.

Your custom node acts as a gatekeeper within the flow graph. If you return a status indicating "success," the flow moves to the next node. If you return "failure," the flow terminates with an error. If you return a specific status like "CHALLENGE," Keycloak presents a new form to the user, forcing re-authentication or additional factors.

public class RiskAssessmentExecutionHandler extends AuthenticationExecutionHandler {
    
    // Injected dependency or remote HTTP client instance
    private final RiskScoringClient springBootRiskService;
 
    @Override
    public void processExecution(AuthenticationFlowContext context) {
        // 1. Extract context parameters (IP, User-Agent, Device ID)
        // Use getClientConnection() for standard Keycloak SPI access
        String ipAddress = context.getClientConnection().getRemoteAddr();
        String deviceFingerprint = context.getEndpointRequest().getHeader("X-Device-ID");
        
        // 2. Delegate scoring to external service
        RiskScore score = springBootRiskService.calculateScore(ipAddress, deviceFingerprint);
        
        // 3. Enforce policy based on score
        if (score.isHigh()) {
            // Triggers the UI form for MFA challenge
            // Note: ResponseType.SUCCESS here confirms the challenge was presented, 
            // not that the user passed it yet. The flow waits for user input.
            context.challenge(context.form()
                .createForm("step-up-mfa")
                .createResponse(AuthenticationChallenge.ResponseType.CHALLENGE));
        } else if (score.isMedium()) {
            context.success(); // Allow flow to proceed but log warning
        } else {
            context.success(); // Direct pass
        }
    }
}

This snippet demonstrates the core mechanism: the AuthenticationFlowContext holds the state of the current request. By calling context.challenge(), you force the user to interact with a specific form. The springBootRiskService is an injected dependency or a remote HTTP client instance, not a local variable instantiated directly within the handler without wiring.

Technical architecture diagram showing a user device connecting to Keycloak, which sends a request to a Spring Boot Risk Engine. The engine returns a risk score back to Keycloak, which then decides to either grant access or trigger MFA. Clean, flat design, blue and grey color …

The Spring Boot Risk Engine

The actual scoring logic resides in Spring Boot, which acts as the stateless risk engine. This service receives context data from Keycloak via HTTP POST and returns a numerical score. The algorithm must be deterministic and fast, as it sits on the critical path of authentication.

A precise scoring model assigns weights to specific signals. For example, a login from a known device might contribute -20 points (lowering risk), while a login from a Tor exit node might add +50 points. The threshold for "step-up" authentication is typically set between 60 and 80, depending on the sensitivity of the application.

@Service
public class RiskScoringService {
    private static final int STEP_UP_THRESHOLD = 70;
 
    public RiskScore calculateScore(String ipAddress, String deviceFingerprint) {
        int score = 0;
        
        // Signal 1: IP Reputation
        if (ipReputationService.isTorNode(ipAddress)) {
            score += 50;
        }
        
        // Signal 2: Device Fingerprint Mismatch
        if (!deviceFingerprintService.isKnown(deviceFingerprint)) {
            score += 30;
        }
        
        // Signal 3: Velocity Check (Multiple logins in short time)
        if (velocityService.isAnomalousVelocity(ipAddress)) {
            score += 40;
        }
        
        return new RiskScore(score, score >= STEP_UP_THRESHOLD);
    }
}

This implementation assumes the existence of helper services for IP reputation and velocity checks. The key tradeoff here is latency. Every millisecond added to the authentication flow increases the chance of user abandonment. Therefore, the risk engine must cache results for known good actors and rely on synchronous lookups where possible to avoid blocking the critical authentication path. If asynchronous lookups are required for non-critical signals, they must be buffered or pre-computed so the processExecution method remains synchronous.

Orchestrating the Flow

The integration between Keycloak and Spring Boot requires careful configuration of the authentication flow in the Keycloak Admin Console. You create a new flow or modify an existing one by adding a custom execution node pointing to the Java class compiled into a Keycloak extension.

When Alice logs in, the flow proceeds as follows:

  1. Username/Password: Standard validation.
  2. Risk Assessment (Custom Node): Keycloak pauses, sends a request to Spring Boot, receives a score of 85.
  3. Decision: Keycloak triggers the step-up-mfa challenge.
  4. MFA Challenge: Alice enters a TOTP code.
  5. Completion: Token issued.

If Bob tries the same flow with a low score (e.g., score 10), the flow skips the MFA challenge entirely and proceeds to token issuance. This dynamic routing is the essence of adaptive authentication. It ensures that legitimate users are not hindered by friction, while attackers face immediate barriers.

Operational Considerations

Implementing this architecture introduces a dependency on the Spring Boot service. If the risk engine is unavailable, the authentication flow must fail gracefully. The standard approach is to configure a "bypass" mode in the risk engine, allowing the flow to proceed with a default "medium" risk score. This prevents a single point of failure from locking out all users.

Another critical aspect is data privacy. The risk engine receives sensitive data like IP addresses and device fingerprints. These must be handled in compliance with GDPR and CCPA. The data should be anonymized or hashed immediately after scoring to prevent long-term tracking.

Finally, the scoring algorithm itself is not static. It requires tuning based on false positive rates. If legitimate users are frequently challenged for MFA, the thresholds are too low. If attackers bypass the system, the thresholds are too high or the signal weights are incorrect. Continuous monitoring of the RiskScore distribution is essential for maintaining the balance between security and usability.

Conclusion

The mechanism described here transforms authentication from a binary "yes/no" check into a continuous risk evaluation process. By leveraging Keycloak's extensibility and Spring Boot's computational power, organizations can build a defense layer that adapts to the threat landscape in real-time. This approach ensures that security measures are applied proportionally to the perceived risk, optimizing both user experience and protection.

Common Pitfalls

Implementing risk-based authentication often leads to specific architectural and operational traps:

  1. Race Conditions in Session State: If the risk engine updates a user's risk profile asynchronously while the authentication request is pending, the decision logic may operate on stale data. Ensure that the risk calculation is performed within the same transactional scope or that the session state is locked during the evaluation.
  2. Service Timeouts Blocking Auth: If the Spring Boot risk service hangs or times out, the entire authentication flow stalls. Without a strict timeout (e.g., 200ms) and a fallback mechanism, a degraded risk service can lock out all users. Always implement a circuit breaker pattern.
  3. False Positive Tuning: A common mistake is setting thresholds too aggressively based on initial data. This leads to "alert fatigue" where legitimate users are constantly challenged. Start with a monitoring-only mode where challenges are logged but not enforced, then gradually lower the threshold as confidence in the model increases.

Practical Takeaways

To successfully deploy this architecture, adhere to these mental models:

  1. Latency is the Enemy: The risk engine is on the critical path. If it adds more than 100ms to the login process, user experience degrades significantly. Optimize for speed first, accuracy second.
  2. Fail Open, Not Closed: In the event of a system failure, it is better to allow access with a warning than to deny it entirely. Design your fallback logic to permit access with a default "medium" risk score.
  3. Data Minimization: Only send the specific signals required for the current decision. Do not transmit full device profiles or historical logs to the risk engine unless necessary for that specific calculation.

FAQ

Q: Can I use asynchronous calls for IP reputation checks within the processExecution method? A: No. The processExecution method is synchronous and blocks the authentication thread. If you need to call a slow external API, you must cache the reputation data locally in the Spring Boot service or use a pre-computed lookup table to ensure the response is immediate.

Q: How do I handle multiple concurrent logins from the same user? A: Implement a velocity check in your risk engine that tracks login timestamps per user ID or device fingerprint. If multiple successful authentications occur within a suspiciously short window (e.g., 1 minute) from different locations, flag the session for immediate review or step-up authentication.

Q: Is it possible to disable the risk check for specific users or groups? A: Yes. You can configure the Keycloak flow to skip the custom risk assessment node for specific realms, clients, or groups via the Admin Console. Alternatively, your risk engine can accept a user identifier and return a "bypass" score for whitelisted accounts.

Related posts