Class SamlAssertionValidator

java.lang.Object
org.apache.juneau.rest.server.auth.saml.SamlAssertionValidator

public class SamlAssertionValidator extends Object
Validates a SAML 2.0 <samlp:Response> document and produces a ClaimsPrincipal marked with issuerType=SAML.

Wraps OpenSAML 5.x's parser, unmarshaller, signature validator, and (optionally) assertion decrypter. The validator enforces:

  • Mandatory assertion signature — every assertion must carry its own enveloped signature. A response that signs only the <Response> envelope (and not the assertion) is rejected as unsigned; response-envelope-level signatures are not supported.
  • Strict signature-algorithm allowlist — only rsa-sha256 and ecdsa-sha256 are accepted out of the box. SHA-1 is permanently rejected. The reference DigestMethod is separately allowlisted (SHA-256/384/512), so a SHA-1 digest is rejected even under a SHA-256 signature.
  • Response status — the <samlp:Status> must report the Success status code.
  • Clock skew — defaults to 60 seconds tolerance on NotBefore / NotOnOrAfter; capped at 300 seconds.
  • Audience restriction — <AudienceRestriction> must list the configured SP entity ID.
  • Bearer subject confirmation — enforced whenever a recipient (the SP ACS URL) is known, either because SamlAssertionValidator.Builder.recipient(String) was configured on this validator or because the caller invoked validate(String, String) with one. SamlAuthFilter always calls validate(String, String) with the ACS URL derived from the current request, so a validator reached only through the filter can never skip this check. When enforced, the assertion must carry a bearer <SubjectConfirmation> whose <SubjectConfirmationData> matches the expected Recipient, is within its NotBefore/NotOnOrAfter window, and satisfies the configured InResponseTo/Address expectations. A validator used standalone (not through SamlAuthFilter) skips this check unless SamlAssertionValidator.Builder.recipient(String) is configured — that mode is not safe for browser-SSO ACS endpoints, since nothing then binds the assertion to a specific recipient.
  • One-time use — each assertion ID is recorded in a ReplayCache and a second presentation of the same ID is rejected. The check is fail-closed: if the cache cannot answer, the assertion is rejected. An assertion whose validity window is unbounded — carrying neither a Conditions/NotOnOrAfter nor a bearer SubjectConfirmationData/NotOnOrAfter — is rejected outright, and the cache record is retained until the resolved NotOnOrAfter (plus clock skew) rather than a short default window that would later expire and re-open replay.
  • Encrypted assertions — if the response contains <EncryptedAssertion>, a decryptionCredential must be configured.
Marker claim

The returned ClaimsPrincipal is annotated with issuerType=SAML so downstream code can distinguish SAML-derived principals from JWT-derived principals without resorting to a subclass:

if ("SAML".equals(principal.getClaim("issuerType", String.class).orElse(null))) { // SAML-issued principal }

Builder usage

This recipe builds a standalone validator with no SamlAssertionValidator.Builder.recipient(String) configured, so bearer subject-confirmation is not enforced. It is safe for assertion-only / non-browser callers that validate an assertion outside of an ACS redirect. Deploying it behind SamlAuthFilter for browser-SSO is always safe regardless: the filter derives the ACS recipient from the request and calls validate(String, String), which enforces bearer confirmation for that call no matter how this validator was built.

var validator = SamlAssertionValidator.create() .metadataResolver(SamlMetadataResolvers.url("https://idp.example.com/metadata")) .spEntityId("https://sp.example.com") .expectedIssuer("https://idp.example.com") .build();

See Also:
Since:
10.0.0