Skip to content

feat(security): Tier 5b — SAML2 single sign-on (SP side, opt-in saml2) [26.6.36]#44

Merged
ancongui merged 5 commits into
mainfrom
feat/spring-security-tier5b-saml2
Jun 19, 2026
Merged

feat(security): Tier 5b — SAML2 single sign-on (SP side, opt-in saml2) [26.6.36]#44
ancongui merged 5 commits into
mainfrom
feat/spring-security-tier5b-saml2

Conversation

@ancongui

Copy link
Copy Markdown
Contributor

Tier 5b — SAML2 single sign-on (Service Provider side)

The SP half of the SAML 2.0 Web-Browser-SSO profile — Spring's saml2Login() — delegating XML-signature verification to samael and adding a Spring-faithful, hardened wrapper. Opt-in saml2 feature; the default build is unaffected (gated module, like ldap/webauthn).

What's new

  • RelyingPartyRegistration + builder + InMemoryRelyingPartyRegistrationRepository — one SP↔IdP relationship, configured from IdP metadata XML or explicit asserting-party details.
  • SP-initiated AuthnRequestauthn_request_redirect (HTTP-Redirect binding) + Saml2AuthenticationRequestRepository (TTL'd outgoing request-id store for InResponseTo matching).
  • authenticate — verifies a POST-binding SAML Response (signature + recipient / InResponseTo / status / time conditions via samael, plus audience + replay enforced here) and maps the NameID + configured attributes to an Authentication (Spring's OpenSaml4AuthenticationProvider).
  • metadata_xml (Spring's Saml2MetadataFilter), AssertionReplayCache + InMemoryAssertionReplayCache.

Security / hardening

  • Fail-closed on a missing IdP signing certificate — without one samael would skip signature verification entirely (an auth bypass), so building a registration refuses it.
  • Audience restriction enforced fail-closedsamael skips it when AudienceRestriction is absent, so authenticate requires this SP's entity id to be a listed audience (Spring parity).
  • Signature-algorithm allow-list pinned to SHA-256+ RSA/ECDSA by default (samael otherwise accepts all algorithms).
  • One-time-use replay protection the profile requires but samael does not track; an empty-NameID assertion is rejected; size-bounded decoding; native XML-Security calls serialized (not concurrency-safe); poisoned-mutex recovery.

Verification

  • 15 unit tests (registration + fail-closed cert, metadata, redirect, both repositories, replay cache, attribute→authorities mapping, audience fail-closed incl. the absent-restriction case, NameID-less → anonymous, duplicate-attribute claims merge, unsigned/malformed/oversized rejection). fmt + clippy -D warnings clean with --features saml2. Full-workspace cargo check clean at 26.6.36.
  • Adversarially reviewed (22-agent, 4 dimensions): 9 findings confirmed (1 medium audience-skip + 8 low). All actionable ones fixed here.
  • Signature-verification correctness rests on samael (its own crypto suite covers accept/reject of XML signatures; xmlsec1 --verify confirmed our test fixtures are cryptographically valid). samael's in-process signing segfaults against libxmlsec1 1.3.x — but production only verifies, never signs, so this does not affect the shipped path; the suite therefore tests this module's own logic directly rather than depending on fragile in-test signing.

Docs

Parity appendix (EN + ES) SAML2 section + status; CHANGELOG v26.6.36; MODULES.md; rebuilt book PDF + EPUB.

Follow-ups

Single-logout, signed AuthnRequests, and encrypted assertions.

Andres Contreras added 5 commits June 20, 2026 00:22
…on xmlsec 1.3) — PARKED

Opt-in `saml2` feature (samael 0.0.21 + libxml2/xmlsec1/OpenSSL) implementing
the Spring `saml2Login()` SP side. Parked, not released — see the blocker below.

Implemented and tested (7 passing tests):
- RelyingPartyRegistration + builder (Spring's RelyingPartyRegistration), with a
  fail-closed guard: a registration whose IdP carries no signing certificate is
  rejected, because samael would otherwise skip signature verification (an auth
  bypass).
- Signature verification pinned to a safe algorithm allow-list (RSA/ECDSA-SHA256+)
  — samael defaults to "all algorithms" (algorithm-substitution risk).
- AuthnRequest generation (HTTP-Redirect binding) + Saml2AuthenticationRequestRepository
  (TTL'd outgoing request-id store for InResponseTo matching).
- authenticate(): base64 decode (size-bounded) -> samael verify (signature +
  audience/recipient/InResponseTo/status/time conditions) -> one-time-use replay
  protection via AssertionReplayCache (which samael does NOT do) -> NameID +
  attributes -> Authentication.
- All native XML-Security calls serialized through a process-global guard
  (xmlsec/libxml2 are not concurrency-safe).
- SP metadata generation.

BLOCKER (why parked): samael 0.0.21's in-process *signing* path segfaults against
Homebrew's libxmlsec1 1.3.11 (xmlsec 1.3 was a breaking release; samael pins its
C libs via nix and #[ignore]s its own sign+verify roundtrip test, and its `idp`
signing tests SIGSEGV here). samael's *verification* path works against 1.3.x
(its crypto tests pass), so the production code is sound — but the 6 tests that
generate signed fixtures via samael's signing are #[ignore]d with that reason.

Decision (user): pivot to Tier 5c (ACL, pure-Rust) now; revisit SAML2 with a
pinned/stable xmlsec or a better Rust SAML library, at which point the ignored
tests can be re-enabled (or re-signed via the xmlsec1 CLI). This branch is not
merged.
…fragile in-test signing)

Production verifies IdP-signed responses via samael (whose own crypto suite
proves it accepts valid / rejects invalid signatures against the installed
xmlsec; xmlsec1 --verify confirms our fixtures are cryptographically valid).
samael's in-process *signing* segfaults on libxmlsec1 1.3.x, so rather than
depend on fragile out-of-process fixture signing, the suite tests this module's
own logic directly: attribute->authorities mapping on a real samael Assertion,
one-time-use replay, registration fail-closed, and end-to-end rejection of
unsigned / malformed / oversized responses. 12 tests, fmt + clippy clean.
Applies the confirmed findings from the multi-agent adversarial review of the
SAML2 SP (1 medium, the rest low):

- Enforce the AudienceRestriction fail-closed (medium): samael skips the audience
  check entirely when the assertion omits Conditions/AudienceRestriction, so
  authenticate() now requires this SP's entity id to be a listed audience (new
  audience_includes helper + stored sp_entity_id). Spring's
  OpenSaml4AuthenticationProvider fails closed here; we now match it.
- Reject a verified assertion with no usable NameID instead of returning Ok with
  an empty (anonymous-aliasing) principal.
- Merge duplicate-named <Attribute> blocks into claims (was last-wins overwrite)
  so claims stay consistent with the roles gathered from every block.
- Recover from a poisoned mutex in the request store / replay cache
  (unwrap_or_else(into_inner)) instead of panicking, matching the XMLSEC guard.
- Docs: note that allow_idp_initiated disables InResponseTo binding (making the
  replay cache the sole freshness control — use a shared cache in multi-instance
  deployments), that InMemoryAssertionReplayCache is per-process, and the
  authenticate() request-id/remove caller contract. Corrected the module/method
  docs to state audience is enforced by this module (not samael).

Adds 3 regression tests (audience fail-closed incl. the absent-restriction case,
NameID-less → anonymous, duplicate-attribute claims merge). 15 tests pass; fmt +
clippy clean with --features saml2.
Bumps the workspace 26.6.35 -> 26.6.36 and ships the Tier 5b docs:

- Parity appendix (EN + ES): SAML2 row marked supported (partial) + a dedicated
  "SAML2 single sign-on" section; roadmap updated (SAML2 SSO done; SLO / signed
  AuthnRequest / encrypted assertions remain follow-ups).
- CHANGELOG v26.6.36 entry.
- MODULES.md firefly-security SAML2 note.
- Rebuilt book PDF + EPUB (EN + ES).

The opt-in `saml2` module (samael + libxml2/xmlsec1/OpenSSL) delivers SP
registration, SP-initiated AuthnRequest, and signed-response verification with
one-time-use replay, fail-closed audience enforcement, an algorithm allow-list,
and a fail-closed missing-IdP-cert guard. The default build is unaffected.
@ancongui ancongui merged commit e18e12e into main Jun 19, 2026
4 checks passed
@ancongui ancongui deleted the feat/spring-security-tier5b-saml2 branch June 19, 2026 23:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant