This is an automated email from the ASF dual-hosted git repository.

jamesbognar pushed a commit to branch docs
in repository https://gitbox.apache.org/repos/asf/juneau.git

commit a42d4de0a48d30987495edf8cdf1388b510c6483
Author: James Bognar <[email protected]>
AuthorDate: Sun Aug 16 13:40:59 2026 -0400

    READY-398: Document SAML ReplayCache multi-node deployment options
    
    Explain that InMemoryReplayCache is per-process and how to inject a shared
    ReplayCache for clustered SAML deployments.
---
 pages/topics/10.46.SamlAuthSupport.md | 17 +++++++++++++++++
 1 file changed, 17 insertions(+)

diff --git a/pages/topics/10.46.SamlAuthSupport.md 
b/pages/topics/10.46.SamlAuthSupport.md
index 645df9e9f0..884042ac16 100644
--- a/pages/topics/10.46.SamlAuthSupport.md
+++ b/pages/topics/10.46.SamlAuthSupport.md
@@ -77,6 +77,23 @@ Artifact + SOAP bindings are deferred &mdash; if your IdP 
only emits those, see
 | Clock skew | 60s default, 300s max | Mirrors the JWT validator's defaults; 
the builder refuses larger values. |
 | Encrypted assertions | opt-in via `decryptionCredential(...)` | A response 
carrying `<EncryptedAssertion>` is rejected with a `decryption_required` 
challenge when no key is configured; a wrong key yields `decryption_failed`. |
 
+## Replay protection in clustered deployments
+
+`SamlAssertionValidator` enforces one-time use of each assertion ID via a 
`ReplayCache`. The default is `InMemoryReplayCache`, which is **per-process 
only** &mdash; it's backed by an in-JVM map, so two nodes behind a load 
balancer don't see each other's recorded assertion IDs.
+
+In a multi-node deployment, inject a `ReplayCache` backed by a store shared 
across every node (e.g. a distributed cache or database) via 
`SamlAssertionValidator.Builder.replayCache(...)`, otherwise an attacker can 
replay the same assertion against a different node before it's been recorded 
there:
+
+```java
+SamlAssertionValidator validator = SamlAssertionValidator.create()
+    
.metadataResolver(SamlMetadataResolvers.url("https://idp.example.com/metadata";))
+    .spEntityId("https://sp.example.com";)
+    .expectedIssuer("https://idp.example.com";)
+    .replayCache(myClusteredReplayCache)   // shared store across every node
+    .build();
+```
+
+The check is always fail-closed: a cache that can't answer causes the 
assertion to be rejected. See [Concurrent Package § Replay 
Detection](/docs/topics/JuneauCommonsConcurrent#replay-detection) for the 
`ReplayCache` SPI contract (including `ReplayCache.FailMode`), and [OIDC 
Relying Party Login § Session 
stores](/docs/topics/OidcRelyingParty#session-stores) for the same shared-store 
principle applied to session storage &mdash; a different SPI, but the same 
clustering requirement.
+
 ## Marker claim, not subclass
 
 The returned 
[`ClaimsPrincipal`](/site/apidocs/org/apache/juneau/rest/server/auth/ClaimsPrincipal.html)
 is annotated with `issuerType=SAML` (rather than introducing a 
`SamlClaimsPrincipal` subclass) so downstream code can distinguish SAML-derived 
principals from JWT-derived principals without resorting to instanceof checks:

Reply via email to