
Implementing Conditional Access Policies with Keycloak and ForgeRock
A technical examination of implementing conditional access policies using Keycloak and ForgeRock for context-aware access control.
The Architecture of Context-Aware Access
Traditional authentication answers a single question: "Is this user who they say they are?" Conditional access answers a different one: "Is this specific user accessing this resource, at this time, from this device, under these conditions?" The mechanism that enables this shift is not a magic switch in the login screen; it is a Policy Decision Point (PDP) that intercepts the authentication flow, gathers contextual attributes, and evaluates a rule set before issuing an access token. In both Keycloak and ForgeRock, this requires moving beyond simple role-based access control (RBAC) to a model where the policy engine actively participates in the token issuance chain.
The core mechanism relies on attribute extraction. When a client initiates a login, the system must capture metadata that exists outside the username and password. This includes the source IP address, the User-Agent string (which can be parsed for device type), the time of request, and potentially external signals like device health status from a Mobile Device Management (MDM) system. These attributes form the "context." The policy engine compares this context against a set of rules. If the rule evaluates to true, the flow proceeds; if false, the flow is terminated or challenged. This logic must be defensible at the mechanism level: the system cannot simply trust the client's claims about its location or device; it must validate the data source.
The Mechanism of Context Evaluation
The mechanism of context evaluation functions as a gatekeeper that sits between the initial credential verification and the final token issuance. It intercepts the authentication flow, extracts context attributes from the request or user profile, and evaluates them against a rule set before the session is established. This process transforms the identity provider from a passive verifier into an active enforcer of business logic.
When a client initiates a login, the system must capture metadata that exists outside the username and password. This includes the source IP address, the User-Agent string, the time of request, and external signals like device health status. These attributes form the "context." The policy engine compares this context against a set of rules. If the rule evaluates to true, the flow proceeds; if false, the flow is terminated or challenged. This logic must be defensible at the mechanism level: the system cannot simply trust the client's claims about its location or device; it must validate the data source.
Keycloak Implementation via Custom SPI
Keycloak does not natively support complex, multi-attribute conditional logic out of the box for all scenarios without significant configuration overhead. While Keycloak offers a basic static IP whitelist feature, it lacks the dynamic, multi-attribute evaluation required for true conditional access, such as combining device health, time-of-day, and geo-location. Consequently, the robust mechanism for implementing these advanced policies is the Service Provider Interface (SPI). By writing a custom AuthenticationFlowExecution or extending the AuthorizationProvider, you can inject logic directly into the authentication chain.
Consider a scenario where a user attempts to log in from an unrecognized network. The standard flow would proceed to issue a token. To implement conditional access, we intercept the flow at the CLIENT_AUTH or FORM execution point. We write a Java class that implements AuthenticationProcessor. Inside this class, we extract the client IP and check it against a local cache or an external IP reputation service.
public class ConditionalAccessAuthenticator implements Authenticator {
@Override
public void authenticate(AuthenticationFlowContext context) {
ClientSessionContext clientContext = context.getClientSessionContext();
// Mechanism: Securely extract IP from headers or remote address
String ipAddress = context.getHttpRequest().getRemoteAddr();
if (ipAddress == null) {
ipAddress = context.getHttpRequest().getHeaders().getFirst("X-Forwarded-For");
}
// Mechanism: Check IP against a deny-list or allow-list
if (isBlockedIp(ipAddress)) {
context.challenge(new ChallengeResponse(403, "Access Denied from this location"));
return;
}
// Mechanism: If IP is valid, proceed to next step
context.success();
}
private boolean isBlockedIp(String ip) {
// Logic to check against external service or local list
return false;
}
}This code demonstrates the mechanism: the authenticator does not just verify credentials; it evaluates a condition. If the condition fails, it returns a challenge or a failure, halting the token issuance. This approach is superior to relying on Keycloak's built-in "IP Whitelist" because it allows for dynamic, programmatic evaluation. For example, you could integrate with a threat intelligence feed to dynamically update the block list without restarting the server.
However, this introduces a dependency. If the external service used to check the IP is slow, the authentication flow stalls. The mechanism must handle timeouts gracefully. A common pattern is to implement a "soft block" where a slow response defaults to allowing the user but flags the session for review, rather than blocking them outright and causing a denial-of-service effect.
ForgeRock Access Management Policy Enforcement
ForgeRock Access Management (AM) handles conditional access differently, leveraging a more centralized Policy Decision Point (PDP) model. In ForgeRock, policies are often defined using the Policy Language (PL) or integrated with Open Policy Agent (OPA) for high-performance, attribute-based access control (ABAC). The mechanism here separates the policy definition from the policy enforcement.
In a typical ForgeRock setup, the Access Manager acts as the Policy Enforcement Point (PEP). When a user requests access, the PEP forwards the request attributes to the PDP. The PDP evaluates the request against the policies stored in the policy repository.
Imagine a scenario where a user attempts to access a sensitive HR application. The policy engine receives the following context:
- Subject:
employee_jane_doe - Resource:
hr_salary_data - Action:
read - Context:
device_id = "mobile_123",location = "office_network",time = "14:00"
The policy definition in ForgeRock might look like this (simplified representation):
<Policy>
<Rule>
<Condition>
<Attribute name="device_health_status" operator="equals" value="compliant"/>
<Attribute name="location" operator="in" value="office_network, trusted_remote"/>
</Condition>
<Effect>Permit</Effect>
</Rule>
<Rule>
<Condition>
<Attribute name="device_health_status" operator="equals" value="non_compliant"/>
</Condition>
<Effect>Deny</Effect>
</Rule>
</Policy>The mechanism here is critical: the PEP does not decide; it delegates. The PDP evaluates the attributes. If the device_health_status is not compliant, the PDP returns a Deny decision. The PEP then terminates the session. This separation allows administrators to change policies without redeploying the application or the access manager code.
A key difference from Keycloak is the handling of external attributes. ForgeRock integrates with a Directory Service (like LDAP or OpenLDAP) or an external Identity Governance system to fetch the device_health_status. The mechanism involves a "Policy Rule" that queries this directory during the evaluation phase. However, the PDP evaluates existing attributes; the device_health_status attribute must be pre-populated in the user's session or retrieved via a custom attribute mapper before the PDP evaluates the policy. The PDP does not fetch external data directly within the XML rule. If the directory is unreachable, the default behavior (usually defined in the policy configuration) determines the outcome. In security-critical environments, the default should be "Deny" (fail-secure), though this can impact availability.
Data Flow and Dependency Analysis
The success of conditional access depends entirely on the data flow between the client, the identity provider, and the policy engine. In both Keycloak and ForgeRock, the flow is not linear; it involves branching based on the evaluation result.
When a user logs in:
- Request: The client sends credentials and context (headers, cookies).
- Extraction: The Identity Provider (Keycloak/ForgeRock) extracts the context attributes.
- Evaluation: The policy engine (SPI in Keycloak, PDP in ForgeRock) evaluates the attributes against rules.
- Decision: A decision is made (Permit, Deny, Challenge).
- Action: The session is created, challenged, or rejected.
If the policy engine relies on an external service (e.g., a geo-IP database or an MDM API), the latency of that service becomes part of the authentication time. If the external service is down, the system must have a fallback mechanism. In Keycloak, this means your custom SPI must catch exceptions and decide whether to fail open or closed. In ForgeRock, this is configured in the policy rule's "fallback" settings.
A critical dependency is the consistency of the attribute data. If the user's device health status is updated in the MDM system but the Identity Provider has not yet synchronized this data, the conditional access policy might grant access to a non-compliant device. This is a data consistency tradeoff. To mitigate this, systems often use short-lived tokens or require re-authentication after a certain period to refresh the context.
Operational Tradeoffs
Implementing conditional access introduces complexity. In Keycloak, the custom SPI approach gives you maximum flexibility but requires maintaining Java code and recompiling the server. This increases the operational burden. A custom SPI is opinionated: it is best for organizations with strong development teams who need fine-grained control over the authentication flow.
In ForgeRock, the PDP model is more declarative. It is easier to manage policies visually and through APIs, but it can be less performant if the policy rules are complex and the PDP is not scaled correctly. The tradeoff here is between flexibility and manageability.
Furthermore, the "chaining" of policies is a mechanism to consider. You might have a primary policy that checks the IP, and a secondary policy that checks the device. If the first policy passes but the second fails, the user is denied. This layered approach increases security but also increases the number of potential points of failure. If the secondary policy service is slow, the user experience degrades even if the primary check was fast.
Ultimately, the choice between these mechanisms depends on the organization's risk tolerance and technical maturity. If the cost of a false positive (blocking a legitimate user) is high, the mechanism must be designed to allow manual override or rapid recovery. If the cost of a false negative (allowing an attacker) is high, the mechanism should default to deny and require manual review.
Conclusion
Conditional access transforms identity management from a static gatekeeper to a dynamic guardian. Whether implemented via Keycloak's custom SPI or ForgeRock's PDP, the mechanism relies on the same fundamental principle: context matters. By extracting attributes, evaluating them against rules, and enforcing decisions before token issuance, organizations can significantly reduce the attack surface. However, this power comes with the responsibility of managing dependencies, latency, and failure modes. The most effective implementations treat the policy engine not as a black box, but as a critical component of the application architecture, ensuring that every decision is traceable, defensible, and resilient.
Summary of Architectural Tradeoffs
The decision between Keycloak's imperative SPI model and ForgeRock's declarative PDP model ultimately hinges on your organization's balance between development agility and operational governance. Keycloak offers granular control at the cost of higher maintenance overhead and deployment complexity, making it suitable for teams capable of managing custom code. ForgeRock provides a centralized, policy-driven approach that simplifies management but introduces reliance on external directory services and potential performance bottlenecks if not architected correctly. Both models require rigorous attention to failure modes, ensuring that the system defaults to a secure state without crippling availability during external service outages.
Next Steps
To begin securing your environment, audit your current authentication rotation policies and identify which attributes are currently being passed to the token issuer. Try the linked Keycloak SPI snippet locally to simulate an IP-based conditional check before integrating it into your production flow.
Common Pitfalls
- Latency Blindness: Failing to account for the network latency of external calls (e.g., MDM or Geo-IP services) within the critical authentication path, leading to user timeouts.
- Stale Attribute Data: Relying on cached user attributes without a defined refresh strategy, resulting in policies granting access based on outdated device health or location data.
- Fail-Open Risks: Configuring fallback mechanisms that default to "Allow" when external dependencies are unreachable, effectively bypassing security controls during outages.
Practical Takeaways
- Trust, but Verify: Never assume client-provided context (like IP or User-Agent) is accurate; always validate against server-side headers or trusted external sources.
- Fail Secure by Default: In the event of a dependency failure, the policy engine should default to denying access unless availability is explicitly prioritized over security.
- Attribute Lifecycle: Treat attributes as transient data; ensure your architecture synchronizes device and user state frequently enough to remain relevant to the policy window.
FAQ
Q: Can I use Keycloak's built-in IP Whitelist for dynamic conditional access? A: No. Keycloak's native whitelist is static and cannot evaluate multi-attribute conditions (e.g., "IP is in office range AND device is compliant"). You must use a custom SPI for dynamic logic.
Q: Does the ForgeRock PDP fetch external data directly from the XML policy? A: No. The PDP evaluates attributes already present in the request or user session. External data (like device health) must be pre-fetched via mappers or attribute resolvers before the PDP evaluation occurs.
Q: How do I handle timeouts in a custom Keycloak SPI? A: Implement a timeout wrapper around your external service calls. If the call exceeds a threshold, you can choose to fail open (allow access but flag the session) or fail closed (block access), depending on your risk tolerance.
Related posts
Implementing Entitlement Management for Fine-Grained Authorization
An examination of entitlement management and fine-grained authorization using XACML and policy engines for secure access control.
Implementing Identity Governance with Custom Policy Engines
An examination of implementing identity governance using custom policy engines like OPA, Cedar, and XACML for policy-as-code.
RFC 8693: Token Exchange, Delegation, and Impersonation
RFC 8693 defines token exchange, delegation, and impersonation mechanisms for OAuth 2.0, enabling secure identity propagation across service boundaries.