What Is a Conditional Flow in Keycloak?
The complete, in-depth guide to the "if this, then that" branch that lets one login sequence behave differently for different people.
Picture a single toll bridge with one lane, but a small sign posted right at the entrance: “Vehicles over 3 tons, proceed to the weigh station before crossing.” Every car glides straight through, never even noticing the sign applies to them. Every truck, however, is quietly diverted through an extra checkpoint the moment it triggers that condition — same bridge, same overall destination, but a genuinely different path depending on what’s actually driving up to it. Earlier in this series, the general idea of an Authentication Flow introduced sub-flows and requirement types as the machinery behind Keycloak’s login logic. This guide zooms in specifically on the piece of that machinery responsible for the toll-bridge sign itself — the mechanism that decides, for one particular user at one particular moment, whether an entire branch of the flow even applies to them at all. In Keycloak, that mechanism is called a Conditional Flow.
This guide is a comprehensive, ground-up exploration of Conditional Flows specifically: what makes a sub-flow “conditional” rather than merely nested, every built-in condition type Keycloak ships with, how each one is evaluated internally, how multiple conditions combine, and the real-world patterns and mistakes that come up again and again when organizations start using them. No prior identity or security background is assumed — every term is explained in full the moment it first appears.
1Core Concepts
What Is a Conditional Flow?
A Conditional Flow (more precisely, a Conditional sub-flow) is a named group of executions nested inside a larger Authentication Flow, whose entire contents are evaluated only if one or more attached condition executions are satisfied first. If the condition isn’t met, Keycloak skips straight past the whole group, as though none of the executions inside it existed for this particular login attempt.
It’s worth being precise here: a “condition” isn’t a separate kind of configuration object living somewhere else — it’s simply a special category of execution, placed as the very first item inside the Conditional sub-flow, whose entire job is to check something about the current login attempt and report back a yes-or-no verdict. Everything else inside that same sub-flow is then either evaluated (if the condition says yes) or entirely skipped (if it says no).
Conditional vs. Ordinary Sub-Flow
An ordinary sub-flow, given a Required or Alternative type, is simply a grouping mechanism — a way to bundle several executions together and treat them as one unit for the purposes of their parent’s own combination logic, with no branching involved at all. A Conditional sub-flow adds exactly one new idea on top of that: an entry gate. Its contents are still evaluated using the same Required/Alternative logic internally, but only for users who first pass through that gate.
Return to the toll bridge from the opening. An ordinary sub-flow is like grouping “weigh the vehicle” and “check its cargo manifest” into one labeled inspection bay — useful bookkeeping, but every vehicle still passes through it. A Conditional sub-flow adds the entrance sign itself: only vehicles matching the stated condition (over 3 tons) are routed into that inspection bay at all; everyone else drives straight past its entrance without ever knowing it exists.
Why This Matters More Than It First Appears
Without Conditional Flows, an organization wanting different login requirements for different kinds of users would face an uncomfortable choice: apply the strictest requirement to absolutely everyone (creating unnecessary friction for low-risk users), or maintain several entirely separate flows and somehow route different users to different ones outside of Keycloak’s own configuration (adding external complexity and coordination overhead). Conditional Flows resolve this cleanly, inside one single flow, using nothing but configuration.
One Flow, Many Effective Experiences
It’s worth dwelling on just how significant this single capability is in practice. A single Realm might serve tens of thousands of users spanning ordinary employees, finance staff handling sensitive approvals, contractors on temporary access, and a handful of administrators with far-reaching system access. Without Conditional Flows, satisfying all four groups’ genuinely different risk profiles from within one Realm would be, at best, an awkward compromise. With Conditional Flows, each group can be routed through exactly the login experience appropriate to its own risk level, while every one of them still shares the exact same underlying Realm, the exact same set of Clients, and the exact same overall administrative structure.
The Relationship to Requirement Types, Restated Precisely
Earlier in this series, four requirement types were introduced: Required, Alternative, Disabled, and Conditional. It’s worth being exact about how Conditional relates to the other three. Required and Alternative both describe how a step’s outcome (success or failure) affects its siblings once the step actually runs. Disabled and Conditional both describe whether a step runs at all in the first place — Disabled always skips, unconditionally, for every user; Conditional skips or runs depending on a check performed fresh for each individual login attempt. Conditional is, in this sense, best understood as a dynamic, per-user version of the same underlying idea Disabled expresses statically for everyone.
2Architecture & Components
graph TD
Parent["Parent Flow
(e.g. Browser Flow)"]
Parent --> Req["Required: Username/Password Form"]
Parent --> Cond["Conditional Sub-Flow"]
Cond --> Check["Condition Execution
(e.g. User Role: admin)"]
Cond --> Inner1["Required: OTP Form"]
Cond --> Inner2["Alternative: WebAuthn Check"]
Check -->|"Condition met"| Inner1
Check -->|"Condition not met"| Skip["Entire sub-flow skipped"]
Conditional Sub-Flow
The named grouping object itself, nested inside a parent Authentication Flow, marked with the Conditional requirement type.
Condition Execution
The first item inside the sub-flow, evaluating a specific fact about the login attempt and determining whether the rest proceeds.
Inner Executions
The actual authentication steps (an OTP check, a security-key check, and so on) that only run if the condition passes.
Skip vs. Evaluate
The binary result of the condition check: either the inner executions are evaluated normally, or the entire group is bypassed as if it weren’t part of the flow.
3The Built-In Condition Types
| Condition Type | What It Checks |
|---|---|
| User Role | Whether the logging-in user holds a specified Realm Role or Client Role |
| User Attribute | Whether a specific attribute on the user’s account matches an expected value |
| User Configured | Whether the user has already enrolled in a given authenticator (for example, whether they’ve set up an OTP device at all) |
| Level of Authentication (ACR/LoA) | Whether the requested authentication strength for this specific login meets or exceeds a defined threshold |
| Client Scope | Whether the requesting Client has been granted a particular Client Scope |
A single Conditional sub-flow isn’t limited to exactly one condition execution. Several condition checks can be placed at the top of the same sub-flow, and how they combine follows the same Required/Alternative logic covered earlier in this series — a Required condition alongside another Required condition means both must pass; conditions marked Alternative to each other mean at least one must pass.
Why Keycloak Provides a Fixed Set Rather Than a Free-Form Rule Language
It’s reasonable to wonder why Keycloak ships a defined menu of condition types rather than an open-ended scripting language for expressing any imaginable rule. The deliberate trade-off favors safety and auditability: a fixed set of well-tested, well-understood condition types is far easier to review, reason about, and keep secure across upgrades than an arbitrary rule engine that could express nearly anything, including subtle logic errors difficult to catch in review. For the rare case where none of the built-in condition types can express a genuinely custom check, the Authenticator SPI covered elsewhere in this series remains available as an extension point, but it’s deliberately a heavier-weight path than simply picking a built-in condition from a dropdown.
Negating a Condition
Most condition types can also be configured to check for the opposite of their usual meaning — for instance, “user does NOT hold this Role” or “user has NOT yet configured this authenticator.” This negation capability is what makes the paired-branch pattern described in Chapter 7 possible: one Conditional sub-flow checking the positive case, and a second, separate one checking its negation, together covering the full population without any gap or overlap between them.
4Internal Working: How a Conditional Sub-Flow Is Actually Evaluated
Parent Flow Reaches the Sub-Flow
Processing the parent flow in order, Keycloak arrives at the position where the Conditional sub-flow sits among its siblings.
Condition Execution(s) Evaluated First
Before touching anything else inside the sub-flow, Keycloak runs the condition execution(s) at its top, checking the relevant fact — a Role, an attribute, an enrollment status, or whatever the specific condition type examines.
Verdict Reached
The condition (or combination of conditions) resolves to a single yes-or-no outcome for this specific login attempt.
Branch Taken
On a “yes,” Keycloak proceeds to evaluate every remaining execution inside the sub-flow, exactly as it would for any ordinary nested group. On a “no,” it skips every remaining execution inside the sub-flow entirely, treating the whole group as absent.
Parent Flow Continues
Regardless of which branch was taken, Keycloak resumes processing the parent flow’s remaining siblings exactly where it left off, folding the sub-flow’s outcome (success, failure, or skipped-as-satisfied) into the parent’s own combination logic.
This distinction trips up many newcomers: when a condition isn’t met and the sub-flow is skipped, this does not count as a failure that blocks the login. A skipped Conditional sub-flow is treated as though it were never part of the flow for this user at all — neither a pass nor a fail, simply absent.
5Condition Deep Dive: User Role
How It Works
The User Role condition checks whether the currently logging-in user carries a specified Role — either a Realm Role or a Client Role, both covered in depth elsewhere in this series. If the user holds that Role, the condition passes; otherwise it fails, and the sub-flow is skipped for them.
Production Example: Elevated Requirements for Administrators
An internal company platform commonly configures a Conditional sub-flow gated on the “platform-admin” Realm Role, enforcing a hardware security-key check specifically for administrator accounts, while every other employee logging into the very same application proceeds through a lighter password-and-OTP path. This lets a security team concentrate its strongest control exactly where the consequences of a compromised account are most severe.
A Practical Nuance: Which Roles Are Visible at This Point
Because the User Role condition checks the user’s Roles as already resolved by the time authentication logic runs, it reflects whatever Role assignments — direct, via Groups, or via composite Roles — were in effect at that moment. A Role change made mid-session doesn’t retroactively alter an already-completed login; it only affects the user’s next fresh login attempt, consistent with how Role changes behave more generally, as covered elsewhere in this series.
Choosing Between a Realm Role and a Client Role for This Purpose
Because the User Role condition can check either scope, it’s worth applying the same one-sentence scoping test covered elsewhere in this series: would this distinction still make sense if a specific application were removed from the picture? An organization-wide “elevated-privilege” designation, meaningful across many internal tools, is naturally expressed as a Realm Role. A distinction meaningful only within one specific application — say, “power-user status inside this one reporting tool” — is better expressed as a Client Role scoped to that Client, keeping the condition’s meaning tightly aligned with where it actually applies.
Group Membership as an Indirect Path to the Same Condition
Because Roles are frequently assigned through Group membership rather than directly to individual users, as covered elsewhere in this series, the User Role condition effectively also captures Group-based assignment for free — a user placed into a Group carrying the “finance-approver” Role satisfies the condition exactly as if that Role had been assigned to them directly. This is one of the more understated benefits of preferring Group-based Role assignment generally: Conditional Flows built on Roles automatically stay correct as Group membership changes, with no separate update needed to the flow itself.
6Condition Deep Dive: User Attribute
How It Works
The User Attribute condition checks whether a named attribute on the user’s account matches a configured expected value. Attributes are flexible, freeform fields on a user’s profile — things like a department code, a subscription tier, or a custom flag set by an administrative process — that don’t always map neatly onto the broader, more formal concept of a Role.
If a Role is like a badge’s printed job title, an attribute is more like a small sticky note added to the back of the badge for a specific, narrower purpose — “enrolled in the pilot program,” “opted into early access,” “belongs to cost center 4471.” It carries real information, but it’s a lighter-weight, more ad hoc kind of fact than a formally defined Role.
Production Example: Gradual Feature Rollout
A SaaS product piloting a new mandatory security-key requirement with a small group of volunteer customers before a full rollout commonly tags those specific accounts with a custom attribute (like “mfa-pilot: true”) and gates a Conditional sub-flow on that attribute. This lets the organization test the new requirement’s real-world impact on a controlled subset of users, without needing to define an entirely new Role just for a temporary pilot program.
When to Choose Attribute Over Role
As a general rule, reach for a Role when the distinction represents a stable, meaningful category of user that other parts of the system (authorization checks, reporting, audits) also care about. Reach for an attribute when the distinction is narrower, more temporary, or specific enough that formalizing it as a Role would add more organizational overhead than it’s worth.
Attribute Comparison Nuances
An attribute-based condition typically compares a stored value against an expected one, and it’s worth being precise about what “matches” means for the specific attribute in question. A simple flag like “true” or “false” compares cleanly and unambiguously. A free-text attribute (say, a department name typed inconsistently as “Finance,” “finance,” or “Finance Dept” by different administrators over time) can produce a condition that silently fails to match for some users purely due to inconsistent data entry, not any flaw in the flow’s logic itself. Establishing a clear, consistent convention for how a given attribute’s values are entered — ideally enforced through a controlled administrative process rather than free typing — meaningfully reduces this entire category of quiet, hard-to-diagnose failure.
Attributes Set by External Systems
In many real deployments, the attribute a Conditional Flow checks isn’t set through Keycloak’s own admin console at all, but synchronized in from an external system of record — an HR platform, a customer relationship management system, or a dedicated identity governance tool — through an automated integration. This is a legitimate and common pattern, but it does mean the condition’s real-world behavior depends on that external synchronization running correctly and promptly; a delay or failure in the upstream sync can leave a condition evaluating against stale data for some period of time, worth accounting for when investigating an unexpected result.
7Condition Deep Dive: User Configured
How It Works
The User Configured condition checks whether the user has already enrolled in a specific authenticator — for example, whether they’ve already registered an OTP device. This lets a flow behave differently depending on the user’s own enrollment history, rather than any externally assigned Role or attribute.
Production Example: Enrolling Users the First Time They Log In
A common, well-established pattern combines this condition with its logical opposite to build a two-branch structure: one Conditional sub-flow, gated on “User Configured: OTP,” presents the OTP entry screen for users who already have it set up; a second Conditional sub-flow, gated on the negation of that same check, instead walks brand-new users through OTP enrollment for the very first time. Together, these two branches smoothly handle both “already enrolled” and “not yet enrolled” users within one single flow, no separate onboarding process required outside Keycloak itself.
This exact combination — enrollment prompt for new users, verification for existing ones — is precisely how Keycloak’s own default flows encourage voluntary MFA adoption without a hard cutover: users are nudged toward enrollment over time, and once enrolled, are then verified on every subsequent login, all governed by nothing more than this one condition type checked in two complementary directions.
8Condition Deep Dive: Level of Authentication (ACR/LoA)
What Is a Level of Authentication?
An Authentication Context Class Reference (commonly abbreviated ACR) is a standardized way for an application to request, and for an identity provider to report back, how strong a given login actually was — for example, “level 1” might represent a plain password, while “level 2” might represent a password plus a verified second factor. A Level of Authentication (LoA) condition checks whether the login currently in progress needs to reach a specific one of these levels.
Why This Differs From the Other Conditions
Every condition covered so far in this guide checks a fact about the user themselves. The Level of Authentication condition instead checks a fact about the request — specifically, what strength of authentication the requesting application asked for on this particular login attempt. This lets a single application request a stronger login only for a specific sensitive action (like approving a large payment), rather than the strength being fixed purely by who the user is.
Where the User Role and User Attribute conditions are like checking a badge before deciding which inspection bay applies, an ACR/LoA condition is more like the delivery truck itself declaring, on this specific trip, “I’m carrying a higher-value cargo today, please apply the enhanced inspection” — the requirement is attached to the specific transaction being requested, not fixed permanently to the vehicle or driver.
Production Example: Step-Up for a Specific Sensitive Action
A banking application might request a routine login for viewing an account balance, but explicitly request a higher authentication level the moment a customer initiates a large wire transfer, forcing a fresh, stronger verification specifically for that one action — a genuinely dynamic, request-driven use of Conditional Flows rather than one fixed permanently to the user’s Role or attributes.
How This Interacts With an Already-Existing Session
A particularly useful property of the Level of Authentication condition is that it can force a genuinely fresh check even for a user who already holds a perfectly valid, still-active session at a lower authentication level. Rather than treating “already logged in” as sufficient forever, the application can explicitly demand the session be strengthened, in the moment, for one specific sensitive step — the identity equivalent of a bank asking a customer already standing at the counter to additionally show a second form of identification before authorizing an unusually large withdrawal, even though the teller already greeted them by name moments earlier.
Standardization Benefits Across Multiple Applications
Because ACR/LoA is a standardized concept rather than something Keycloak invented in isolation, multiple applications integrating with the same Realm can express step-up requirements in a consistent, portable way, and a security team can reason about “what does level 2 actually guarantee here” once, centrally, rather than separately for each individual application’s own bespoke step-up implementation.
9Combining Multiple Conditions and Nested Conditional Sub-Flows
Multiple Conditions in the Same Sub-Flow
When two or more condition executions sit at the top of the same Conditional sub-flow, they combine using the same Required/Alternative rules that govern any other sibling executions. Two conditions both marked Required means both facts must hold true simultaneously for the sub-flow to proceed; two conditions marked Alternative to each other mean either one being true is sufficient.
Nesting One Conditional Sub-Flow Inside Another
A Conditional sub-flow can itself contain another Conditional sub-flow nested inside it, allowing a genuinely layered decision: “if this user holds the sensitive Role, then further check whether they’ve already enrolled in a hardware key; if so, require it; if not, fall back to requiring OTP instead.” Each layer narrows the population still further before the next condition is even checked.
graph TD
Outer["Conditional: User Role = finance-approver"]
Outer --> Inner["Conditional: User Configured = Security Key"]
Inner -->|"Configured"| Req1["Required: Security Key Check"]
Inner -->|"Not configured"| Fallback["Required: OTP Form (fallback)"]
Outer -->|"Role not held"| SkipAll["Entire structure skipped"]
Each additional layer of nested conditions doubles the number of distinct paths a reviewer must mentally trace to fully understand a flow’s real behavior. Two or three thoughtfully nested layers remain manageable; four or five, especially without clear documentation, quickly become a structure that only the person who originally built it can confidently explain.
An Alternative to Deep Nesting: Multiple Sibling Conditional Sub-Flows
Rather than nesting one Conditional sub-flow deeply inside another, an equally valid and often more readable alternative is placing several Conditional sub-flows as plain siblings at the same level, each with its own distinct, self-contained condition covering one specific population. A flow with three sibling Conditional sub-flows — one for administrators, one for finance staff, one for everyone else — is frequently easier for a reviewer to scan and understand at a glance than the equivalent logic expressed as two or three layers of nesting, even when the two structures produce identical results for every actual user.
Combining Sibling Sub-Flows Correctly
When using the sibling approach, it’s worth double-checking that the populations covered by each sibling Conditional sub-flow are genuinely intended to be mutually exclusive, or, if overlap is possible, that the resulting combined behavior for a user matching more than one is actually the intended outcome. A user who happens to hold both the “administrator” and “finance-approver” Roles, for example, might trigger two separate sibling sub-flows in sequence unless the flow’s design deliberately accounts for that overlap.
Choosing Between Nesting and Sibling Structures in Practice
A useful heuristic for deciding between the two approaches: reach for nesting when the second condition is genuinely dependent on the first already having been true — as in the hardware-key-versus-fallback example earlier in this chapter, where checking enrollment status only makes sense once the Role check has already established that this user belongs to the sensitive population in the first place. Reach for siblings when the conditions are independent of one another and simply happen to apply to overlapping or entirely separate populations within the same overall flow. Defaulting to siblings whenever the relationship between two conditions isn’t clearly hierarchical tends to produce the more maintainable structure over time.
10Data Flow & Lifecycle
Design. An administrator identifies the specific population or scenario that needs different treatment, and chooses the appropriate condition type (Role, attribute, enrollment status, or authentication level) to express that distinction.
Construction. The Conditional sub-flow is added to a customized copy of the relevant parent flow, following the copy-then-modify pattern covered elsewhere in this series, with the condition execution placed first and the actual authentication steps nested beneath it.
Testing. Because the branch only activates for a specific subset of users, testing needs to deliberately cover both sides: at least one account that satisfies the condition, and at least one that doesn’t, confirming each experiences the intended, different path.
Binding and Rollout. The customized flow is bound to its purpose (such as the Realm’s active Browser flow), and the Conditional logic becomes live for every subsequent matching login.
Ongoing Review. As the underlying Role assignments, attributes, or enrollment patterns shift over time, the condition’s real-world effect should be periodically re-verified, since a condition that made sense when written can quietly stop matching its original intent as the broader population it’s evaluated against changes.
graph LR
A["Population Identified
(who needs different treatment)"] --> B["Condition Type Chosen
(Role, Attribute, Configured, ACR)"]
B --> C["Sub-Flow Constructed
(on a copied parent flow)"]
C --> D["Both Branches Tested
(condition met and not met)"]
D --> E["Bound to Production"]
E --> F{"Population or Requirements Shift?"}
F -->|"Yes"| G["Condition Reviewed and Adjusted"]
G --> D
Documenting Intent Alongside Configuration
Because a condition’s underlying purpose is rarely obvious just from reading its technical configuration months later, maintaining a short, plain-language note alongside each Conditional sub-flow — recorded wherever the organization already tracks its identity configuration decisions — pays for itself the first time someone other than the original author needs to understand or modify it. “This branch enforces a hardware key for the transaction-approver Role, added following the Q2 security review” communicates far more, far faster, than the bare configuration alone ever could.
Retiring a Condition That’s No Longer Needed
Just as a condition is designed and rolled out deliberately, it deserves an equally deliberate retirement when its purpose has run its course — for instance, once a gradual MFA rollout described earlier in this guide reaches full adoption, the “not yet enrolled” branch guiding new users through setup may no longer serve any real population and can be safely simplified away, reducing the flow’s ongoing complexity rather than leaving a permanently dormant branch behind indefinitely.
11Advantages, Disadvantages & Trade-offs
Advantages
- Lets one flow serve multiple populations with genuinely different requirements, without maintaining several entirely separate flows
- Enables gradual rollouts (like the User Configured pattern in Chapter 7) that ease users into a new requirement rather than forcing an abrupt cutover
- Concentrates the strongest security controls precisely where risk is highest, avoiding unnecessary friction for lower-risk users
- Expressed entirely through configuration, with no application code changes needed to react to the underlying facts a condition checks
Disadvantages & Trade-offs
- Nested or numerous conditions make a flow considerably harder to audit and mentally trace than a flat, unconditional sequence
- A condition based on a fact that changes underneath it (a Role reassignment, an attribute update) can silently shift the flow’s real-world effect without anyone editing the flow itself
- Testing must deliberately exercise every meaningfully distinct branch, which grows more effortful as more conditions and nesting layers are added
- A condition checking the wrong signal (Chapter 12 covers this in depth) can create a false sense of security around a control that isn’t actually firing as intended
- Conditions built on data synchronized in from an external system inherit that system’s own reliability and timeliness as a dependency the identity team doesn’t fully control
Taken together, these trade-offs point toward a consistent guiding principle: reach for a Conditional Flow when the underlying distinction between populations is genuine and durable enough to justify the added complexity, and resist the temptation to add a new condition for every minor, one-off exception that arises — each one adds a small, permanent tax on every future review of the flow. Treating Conditional Flows as a deliberate, occasionally-used tool rather than a default reflex for every new requirement keeps this trade-off firmly on the favorable side over the long run.
12Security
Problem
An organization gates a Conditional sub-flow enforcing a second factor on a user attribute, intending it to reflect risk level, but that same attribute happens to be editable by the user themselves through a self-service profile page.
Why It Fails
Any user can simply change the attribute’s value to whatever satisfies the condition’s “skip” branch, silently opting themselves out of a control the organization intended to be mandatory — turning a security feature into a self-service bypass.
Correct Approach
Gate genuinely security-relevant conditions on Roles or attributes that only an administrator or a separately audited backend process can set, never anything reachable through a user-facing profile or settings screen.
Verifying a Condition Actually Fires as Intended
Presence of a Conditional sub-flow in the configuration proves nothing about its real-world effect. A condition with subtly incorrect logic — checking the wrong Role name, comparing an attribute against the wrong expected value — can silently never trigger for anyone, leaving the intended control entirely dormant while the organization believes it’s active. Testing with real accounts on both sides of the condition, as described in Chapter 10, is the only reliable way to confirm this.
The Level of Authentication Condition Deserves Extra Scrutiny
Because the ACR/LoA condition trusts a value effectively requested by the application itself, an application with a bug (or a compromised or malicious application) could theoretically request a lower authentication level than a genuinely sensitive action actually warrants. Applications performing highly sensitive actions should be reviewed to confirm they request the appropriate level consistently, rather than assuming the identity layer alone fully compensates for an application-side mistake.
Fail-Safe Design: What Happens When a Condition Check Itself Errors Out
A well-designed Conditional Flow should also account for the less obvious case of the condition check itself failing to evaluate cleanly — for instance, an attribute lookup timing out due to an external synchronization issue described in Chapter 6. Organizations building genuinely security-critical Conditional Flows should confirm, through deliberate testing, which way such an error actually resolves: whether it defaults to skipping the sub-flow (potentially bypassing an intended control) or defaults to treating the condition as unmet in the safer, stricter direction. This is a detail easy to overlook during initial design but important to verify before relying on a condition for a genuinely mandatory control.
Reviewing Conditions Alongside Role and Attribute Governance
Because so many condition types ultimately hinge on Role or attribute assignment, a Conditional Flow’s real security guarantee is only as strong as the governance around who can grant those Roles or set those attributes in the first place. A security review of a Conditional Flow is, in an important sense, incomplete without also reviewing who can assign the “finance-approver” Role it checks, or who can set the attribute it compares — the flow’s logic can be flawless while the underlying assignment process is not.
13Monitoring, Logging & Metrics
Condition Trigger Rate
Tracking what percentage of logins actually trigger a given Conditional sub-flow confirms whether it’s firing for roughly the expected population, or unexpectedly never (or always) activating.
Enrollment Progress
For the gradual-rollout pattern from Chapter 7, tracking how many users have moved from the “not yet enrolled” branch to the “already enrolled” branch over time measures real rollout progress.
Condition Configuration Changes
Logging every edit to a condition’s configuration (which Role it checks, what value it compares) supports later review of exactly when and how a control’s real-world scope changed.
Cross-Referencing With Role Changes
Correlating a sudden drop in a condition’s trigger rate with a recent bulk Role reassignment can catch an unintended side effect before it goes unnoticed for long.
Building a Simple Dashboard Around a High-Stakes Condition
For a Conditional Flow gating a genuinely high-stakes control — the administrator step-up example from Chapter 5, say — it’s worth the modest extra effort of building a small, dedicated view showing that specific condition’s trigger rate and success rate over time, separate from general login metrics. Because this population is typically small, a dashboard scoped specifically to it makes an anomaly (a sudden drop to zero triggers, or a spike in failures) far easier to notice quickly than if the same signal were buried inside aggregate, Realm-wide login statistics.
14Design Patterns & Anti-patterns
Good Patterns
- Gate on stable, administrator-controlled facts: Roles and administrator-managed attributes, never anything a user can self-edit for a security-relevant condition
- Pair enrollment conditions in both directions: the “already configured” and “not yet configured” pattern from Chapter 7 for any gradual rollout
- Keep nesting shallow and documented: two or three deliberate layers, each with a clear, written rationale, rather than accumulated depth nobody can explain
- Test both branches explicitly: maintain at least one test account satisfying, and one account not satisfying, each meaningful condition
Anti-patterns to Avoid
Common Mistakes
- Gating a mandatory security control on a user-editable attribute, as covered in Chapter 12
- Assuming a Conditional sub-flow is working correctly based on its presence in the configuration alone, without ever testing both branches
- Stacking many conditions and nested sub-flows without documenting what each layer is actually for
- Letting a condition drift out of sync with organizational reality (checking a Role that was renamed or retired) without a periodic review catching it
- Adding a brand-new Conditional sub-flow for every minor, one-off exception rather than pausing to check whether an existing condition or grouping already covers it
- Leaving no written record of why a particular condition exists, forcing every future reviewer to reverse-engineer its intent from the configuration alone
15Real-World & Industry Examples
Financial Services: Role-Gated Step-Up
A bank’s internal operations platform commonly requires a hardware security key specifically for staff holding a “transaction-approver” Role, while other employees using the same platform for read-only reporting continue through a lighter path — the User Role condition applied directly.
Consumer Platforms: Gradual MFA Rollout
A consumer web service encouraging voluntary MFA adoption uses the User Configured condition in both directions described in Chapter 7 — prompting enrollment for new adopters, verifying it for existing ones — letting the organization track and grow adoption over months without a disruptive, all-at-once mandate.
Payments: Request-Driven Step-Up via ACR/LoA
A payment platform requests a routine authentication level for browsing transaction history, but explicitly requests a higher level the moment a customer initiates a transfer above a defined threshold, triggering a fresh, stronger verification precisely for that one sensitive action.
SaaS Platforms: Attribute-Gated Feature Pilots
A software platform piloting a new authentication requirement with a small group of opted-in customer accounts tags those accounts with a custom attribute and gates the new requirement behind a Conditional sub-flow checking it, exactly as described in Chapter 6, before expanding the requirement more broadly once the pilot proves successful.
Multi-Tenant Platforms: Sibling Conditions by Customer Policy
A business-to-business software platform serving many separate customer organizations from a single shared Realm commonly uses sibling Conditional sub-flows keyed to a “security-policy-tier” attribute set once per customer organization, letting each customer’s own chosen policy (light, standard, or strict authentication requirements) apply automatically to every user belonging to that organization, without needing a dedicated Realm per customer.
Healthcare: Layered Conditions for Sensitive Record Access
A healthcare system combining a Role check (“clinician”) with an enrollment-status check (“has configured a hardware security key”) through nested Conditional sub-flows can require the strongest available second factor specifically from clinical staff who have already completed enrollment, while gracefully falling back to a temporary alternative for clinicians still in the process of onboarding their hardware key, an example of the nested pattern introduced in Chapter 9 solving a genuinely practical rollout problem.
16Frequently Asked Questions
17Summary and Key Takeaways
Key Takeaways
- A Conditional Flow is a sub-flow whose contents are evaluated only if an attached condition execution is satisfied first — otherwise the whole group is skipped, neither passed nor failed.
- Keycloak’s built-in condition types cover User Role, User Attribute, User Configured (enrollment status), Level of Authentication (ACR/LoA), and Client Scope.
- The User Configured condition, applied in both directions, is the standard pattern for a gradual, non-disruptive rollout of a new authentication requirement.
- The ACR/LoA condition is unique in checking a fact about the request itself — what strength the application asked for — rather than a fact about the user.
- Multiple conditions can combine, and Conditional sub-flows can nest inside each other, but each additional layer meaningfully increases the effort needed to audit the flow’s true behavior.
- Never gate a genuinely mandatory security control on an attribute or fact the end user can edit themselves — doing so turns the control into a self-service bypass.
- A Conditional sub-flow’s presence in the configuration proves nothing about its real-world effect — always verify both the “condition met” and “condition not met” paths with actual test accounts.
- When more than one condition applies at once, prefer expressing them as clear, self-contained sibling sub-flows over deep nesting unless one condition is genuinely dependent on another already being true — the sibling structure is almost always easier for a future reviewer to trust at a glance.