Skip to content

Enforce SAML OneTimeUse Condition to Prevent Assertion Replay #46325

@VISHNU0906

Description

@VISHNU0906

Description

Keycloak parses the <OneTimeUse> condition in SAML 2.0 assertions (Section 2.5.1.5) but does not enforce
single-use semantics.

OneTimeConditionValidator performs syntactic validation only — it verifies no more than one
<OneTimeUse> element exists, but does not track whether an assertion has been previously consumed.

This means a SAML assertion containing <OneTimeUse> can be replayed within its validity window. This is
particularly relevant in IdP-initiated SSO flows where InResponseTo binding is absent, leaving no other
mechanism to detect replayed assertions.

Current Behavior

  1. External IdP sends assertion with <Conditions><OneTimeUse/></Conditions>
  2. Keycloak accepts the assertion and creates a session
  3. Same assertion is replayed (intercepted POST, browser manipulation)
  4. Keycloak accepts it again — no deduplication

Affected Code

  • org.keycloak.saml.validators.OneTimeConditionValidator — syntax check only, no state tracking
  • org.keycloak.saml.validators.ConditionsValidator — orchestrates condition validation
  • IdP-initiated broker flow — no InResponseTo to provide implicit replay protection

Value Proposition

  • Closes a security gap in IdP-initiated SSO: InResponseTo provides replay protection in SP-initiated
    flows, but is absent in IdP-initiated flows. OneTimeUse enforcement fills this gap.
  • Spec compliance: SAML Core 2.0 Section 2.5.1.5 states relying parties "should maintain a cache of
    the assertions it has processed"
    when OneTimeUse is present. Keycloak currently ignores this.
  • Defense in depth: Even in SP-initiated flows, enforcing OneTimeUse adds a second layer of replay
    protection beyond InResponseTo.

Goals

  • Detect and reject replayed SAML assertions when <OneTimeUse> condition is present
  • Use Infinispan-backed cache (consistent with existing Keycloak caching patterns) to store consumed
    assertion IDs
  • Cache TTL derived from assertion's NotOnOrAfter value — entries auto-evict after validity window,
    preventing unbounded memory growth
  • Work correctly in clustered deployments via distributed Infinispan cache
  • Cover both IdP-initiated and SP-initiated (broker) flows

Non-Goals

  • Enforcing OneTimeUse when the element is not present in the assertion (opt-in by the asserting party)
  • Replacing or modifying InResponseTo validation — this is an additional, independent check
  • Caching all assertions — only assertions with <OneTimeUse> are tracked
  • Changing default SAML behavior for existing deployments — assertion replay rejection only applies when
    <OneTimeUse> is explicitly included by the IdP

Discussion

No response

Notes

Proposed Implementation

  1. Register new Infinispan cache samlOneTimeUseCache in cache configuration
  2. Enhance OneTimeConditionValidator:
    • On assertion with <OneTimeUse>: check cache for assertion ID
    • If found → reject (ValidationException: assertion replay detected)
    • If not found → insert with TTL = NotOnOrAfter - now()
    • Fallback TTL (configurable, default 2h) when NotOnOrAfter is absent
  3. Estimated scope: ~300 lines (implementation + unit/integration tests)

Why Infinispan?

  • Keycloak already uses Infinispan for session, auth session, and action token caches
  • Distributed cache provides cross-node replay protection in clustered deployments
  • Built-in TTL/eviction eliminates manual cleanup

I plan to contribute a pull request for this.

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions