Skip to content

docs: Java 17 / Jakarta EE 11 / Tomcat 11, pac4j SAML, upgrade notes (PR 8 of jakarta migration #861) - #942

Merged
ismisepaul merged 39 commits into
feat/jakarta-07-jsp-taglibsfrom
feat/jakarta-08-docs
Oct 23, 2026
Merged

ismisepaul merged 39 commits into
feat/jakarta-07-jsp-taglibsfrom
feat/jakarta-08-docs

Conversation

@ismisepaul

Copy link
Copy Markdown
Member

PR 8 of the Jakarta migration stack (#861)

Stacked on PR 7. Last PR in the stack.

Changes

  • AGENTS.md
    • Said the project targets Java 8; it has targeted Java 17 since build: upgrade from JDK 8 to JDK 17 (#802) #847.
    • Documents the Jakarta EE 11 / Tomcat 11 platform, which javax packages are still fine, the jakarta.tags.* taglibs, Spring being test-only, and the pac4j SAML SSO and its config files.
    • Adds troubleshooting for the two local Docker issues hit while testing this stack (below).
  • README.md: new Upgrading an existing Docker environment section. The Tomcat container mounts a named conf volume over /usr/local/tomcat/conf, and Docker only seeds a volume from the image when it is created, so existing environments would start Tomcat 11 with their old Tomcat 9/10.1 server.xml/web.xml. Remove <project>_conf when upgrading; the data/mongodata volumes are untouched.
  • docs/testing.md: JDK 8 → JDK 17 prerequisite.

Related issue filed

Refs #861

🤖 Generated with Claude Code

ismisepaul and others added 3 commits October 22, 2026 20:49
… 8 of jakarta migration #861)

- AGENTS.md said the project targets Java 8; it has targeted Java 17
  since the JDK 17 upgrade (#847). Documents the Jakarta EE 11 /
  Tomcat 11 platform, which javax packages remain fine, the
  jakarta.tags.* taglibs, Spring being test-only, and the pac4j-based
  SAML SSO and its configuration files.
- docs/testing.md: JDK 8 -> JDK 17 prerequisite.

Refs #861

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- README / AGENTS: an existing Docker environment keeps its old Tomcat
  9/10.1 conf/ in the named conf volume, so Tomcat 11 would start with
  stale server.xml / web.xml. Remove the volume when upgrading; data
  volumes are untouched.
- AGENTS: the it-test profile tags its Mongo build as
  mongo:${MONGODB_VERSION}, overwriting the official image locally so
  the next IT run fails on chown. Documents the re-pull workaround.

Refs #861

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The upgrade instructions need backup and restoration guidance before deleting a volume containing TLS keys and custom connection settings.

2 open issues
What changed in this PR

This documentation-only PR completes the Jakarta migration stack by updating platform guidance and documenting local Docker upgrade and troubleshooting steps.

Changes:

  • Updates Java 17, Jakarta EE 11, Tomcat 11, and pac4j SAML guidance.
  • Adds Docker configuration-volume upgrade instructions and the Mongo image-tag workaround tracked in #938.
  • Updates the testing prerequisite to JDK 17.
File Description
README.md Adds upgrade instructions for existing Docker environments.
docs/​testing.md Updates the required JDK to Java 17.
AGENTS.md Documents the migrated platform, SAML implementation, and Docker troubleshooting.

Review effort: Balanced

Comment thread AGENTS.md Outdated
Comment thread README.md
Copilot AI balanced review requested due to automatic review settings October 22, 2026 21:15
Addresses review on #942: the conf volume also holds the TLS keystore and
database.properties / mongo.properties, not just Tomcat's XML. The
upgrade steps now back the volume up first and explain restoring only a
customised keystore or connection settings, never the old server.xml /
web.xml. Backup and selective restore commands were checked against a
scratch volume.

Refs #861

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The upgrade instructions need backup and selective restoration steps to avoid losing persisted TLS identity and connection settings.

1 open issue
2 resolved since last review

Review effort: Balanced

Comment thread README.md
Copilot AI balanced review requested due to automatic review settings October 22, 2026 21:18
Addresses review on #942: covers operators who hand-edited server.xml /
web.xml, alongside the existing backup and selective keystore /
properties restore.

Refs #861

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The documented backup commands need restrictive archive permissions to protect TLS keystores and database credentials from other local accounts.

2 open issues
1 resolved since last review

Review effort: Balanced

Comment thread AGENTS.md Outdated
Comment thread README.md Outdated
Copilot AI balanced review requested due to automatic review settings October 22, 2026 21:23
Addresses review on #942: the archive holds the TLS private key and the
database password, but under the default 022 umask it was created 0644.
The backup now runs with umask 077 and chmods the archive to 600 before
writing (which also restricts an existing archive being overwritten).
The docs say to delete it after the upgrade, and that it is root-owned
on Linux. Checked against a scratch volume, overwriting an existing
0644 file.

Refs #861

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The dependency guidance incorrectly describes Spring as test-only despite production SAML code requiring Spring modules.

No open issues 🎉

1 previously missed issue

In code that hasn't changed since last review

Medium severity Avoid removing Spring runtime dependencies required by production SSO

AGENTS.md:37

Spring is not test-only after the SAML migration: production code in src/main/java/utils/SamlSso.java:458,499 instantiates Spring's ByteArrayResource, and pom.xml:24 notes that pac4j-saml pulls in Spring at compile scope. Distinguish the test-only servlet mocks from the Spring modules required by production SSO so this guidance does not encourage removing runtime dependencies from the WAR.

2 resolved since last review

Review effort: Balanced

Copilot AI balanced review requested due to automatic review settings October 22, 2026 21:26

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The documentation-only changes have no blocking issues; the remaining Spring dependency clarification is a minor correction.

No open issues 🎉

1 previously missed issue

In code that hasn't changed since last review

Low severity Clarify Spring runtime dependency used by SAML implementation

AGENTS.md:37

Please distinguish test-only spring-test from Spring's runtime dependencies. Production code in SamlSso.java:458 and 499 uses Spring's ByteArrayResource, and pom.xml:24 notes that pac4j-saml brings Spring in at compile scope. The blanket “only in tests” statement is therefore inaccurate; clarify that Spring is also required by the SAML implementation.

Review effort: Balanced

Copilot AI balanced review requested due to automatic review settings October 22, 2026 21:29

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The documentation matches the inspected configuration, preserves the previously addressed safeguards, and has no unresolved blocking findings.

No open issues 🎉

Review effort: Balanced

Copilot AI balanced review requested due to automatic review settings October 23, 2026 16:41

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The documentation matches the reviewed implementation, addresses prior feedback, and has no unresolved blocking findings.

No open issues 🎉

Review effort: Balanced

Copilot AI balanced review requested due to automatic review settings October 23, 2026 16:53

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The documentation aligns with the inspected implementation, and no unresolved blocking issues remain.

No open issues 🎉

Review effort: Balanced

Copilot AI balanced review requested due to automatic review settings October 23, 2026 17:20
Running only mvn test let compile, Docker and integration-test failures
reach CI. Document every test.yml step and the things CI does that a local
run has to repeat: the challenge DB user host rewrite, restoring the
official Mongo image, clearing leftover Mongo containers, and running in a
throwaway worktree.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The documentation aligns with the reviewed implementation and configuration, and no unresolved blocking issues remain.

No open issues 🎉

Review effort: Balanced

Copilot AI balanced review requested due to automatic review settings October 23, 2026 17:22

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The documentation aligns with the current platform, addresses prior feedback, and has no unresolved blocking findings.

No open issues 🎉

Review effort: Balanced

@ismisepaul ismisepaul moved this from Backlog to Next Release Backlog in Security Shepherd Oct 23, 2026
@ismisepaul ismisepaul moved this from Next Release Backlog to In Progress in Security Shepherd Oct 23, 2026
Copilot AI balanced review requested due to automatic review settings October 23, 2026 17:47

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The documentation aligns with the reviewed implementation and configuration, and no unresolved blocking findings remain.

No open issues 🎉

Review effort: Balanced

@ismisepaul
ismisepaul merged commit dd4cedb into dev Oct 23, 2026
10 of 16 checks passed
ismisepaul added a commit that referenced this pull request Oct 23, 2026
) (#939)

* feat: reimplement SAML SSO on pac4j-saml (PR 5 of jakarta migration #861)

java-saml has no usable Jakarta release (its Auth class only accepts
javax.servlet types), which left ACS, Logout, SLS and SSOLogin unable to
compile. SSO now runs on pac4j-saml 6.6 (OpenSAML 5, Jakarta-native,
itself built on Spring 7).

- utils.SamlSso builds the SAML2Client from the existing
  onelogin.saml.properties keys, so deployments keep their config: the
  SP's PEM cert/key go into an in-memory keystore (PKCS#8 or PKCS#1),
  and the IdP settings are turned into an IdP metadata document.
  Nothing is written to disk.
- utils.SsoUserMapper holds the attribute -> user/role mapping that
  used to be inline in ACS, now unit-testable.
- The five servlets delegate to SamlSso; the Getter.authUserSSO hand-off
  and session setup are unchanged.

Bugs fixed along the way (each covered by a test):
- SLS only defined handleRequest(), so /saml/sls answered 405 to every
  IdP logout message. It now handles GET and POST.
- Logout read the NameID/session index from the freshly created session
  after invalidating the old one, so SLO requests carried no NameID.
  ACS now stores the SAML profile in the post-login session and Logout
  reads it before invalidating.
- ACS hit a NullPointerException when a configured attribute was
  missing, and left a blank page when the assertion had no attributes;
  both now return to the login page with a message.

Build: spring-core is no longer declared test-scoped (that would strip
pac4j's compile-scope copy from the WAR); a spring-framework-bom import
keeps every Spring module on ${spring.version}. commons-io 2.18 -> 2.22
to match what pac4j is built against. log4j-slf4j2-impl routes pac4j /
OpenSAML logging into log4j2.

Refs #861

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: authenticate SAML logout messages and end only the named sessions

Addresses review on #939.

Logout message trust:
- pac4j validates a logout message's XML signature only when one is
  present, and never checks HTTP-Redirect query-string signatures (the
  configured SLO binding). Unsigned or forged LogoutRequests and
  LogoutResponses were therefore accepted. When want_messages_signed is
  set, SamlSso now requires a valid IdP signature: XML (HTTP-POST) or
  query-string (HTTP-Redirect, verified per SAML Bindings 3.4.4.1 with
  the configured IdP certificate; SHA-1 algorithms are not accepted).

Session targeting:
- SLS invalidated whichever session the delivering browser had. An IdP
  LogoutRequest now ends exactly the sessions created by the login it
  names (NameID + SessionIndex), via a new SamlSessionRegistry that ACS
  registers the post-login session in. This works whichever browser
  delivers the message and with no session cookie. An unsigned request
  carrying only a NameID ends nothing.
- A LogoutResponse ends nothing: Logout already ended the local session
  before sending the user to the IdP.

Also:
- Send NameQualifier / SPNameQualifier back in LogoutRequests
  (useNameQualifier), so IdPs issuing qualified NameIDs can match them.
- Default requested_authncontextcomparison to "exact" (pac4j drops
  RequestedAuthnContext entirely without one), and still read the key as
  misspelled in the old sample file.
- @OverRide on SLS doGet/doPost.

Tests: SamlLogoutTest covers signed/unsigned/unknown-key/tampered
LogoutResponses and LogoutRequests in both bindings, session targeting
across browsers and without a cookie, and SessionIndex mismatch; plus
NameID qualifier and comparison-default tests. Each fix was checked to
fail its tests when switched off.

Refs #861

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: bind SAML redirect signatures to the message pac4j reads; end every SessionIndex

Addresses review on #939.

- The HTTP-Redirect signature check matched query parameters by their
  raw names, while the servlet container (and pac4j) URL-decodes names.
  A captured signed query with an unsigned message prepended as
  %53AMLRequest=... passed the check on the signed copy while pac4j
  processed the unsigned one. The check now accepts only a GET, rejects
  percent-encoded aliases and repeated signed parameters, and requires
  each signed value to equal the single value the container returns.
- An IdP LogoutRequest naming several SessionIndexes ended only the
  first; it now ends each one. A NameID-wide logout (no SessionIndex)
  still requires a signed request.

Regression tests (the slsGet fixture now URL-decodes parameter names
like a container): encoded alias, repeated parameter, redirect
signature on a POST, and two SessionIndexes for one NameID. Three of
the four fail against the previous SamlSso; the repeated-parameter
case already failed safe and is kept as a guard.

Refs #861

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* build: correct the spring.version comment

Spring is no longer test-only: pac4j-saml needs it at runtime and SamlSso
uses spring-core's ByteArrayResource (review on #942).

Refs #861

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: check SAML LogoutRequest destination, freshness and replay; ECDSA format

Addresses review on #939. pac4j 6.6's LogoutRequest validation checks
signature (if present) and issuer only, so before a LogoutRequest may
end sessions SamlSso now checks:
- Destination: a signed request must be addressed to this SP's logout
  endpoint (SAML Bindings 3.4.5.2 / 3.5.5.2), and any Destination given
  must match. A request signed for another SP that shares NameIDs can no
  longer end Shepherd sessions.
- Freshness: IssueInstant within 5 minutes (plus 5 minutes clock skew),
  and NotOnOrAfter not passed.
- Replay: request IDs (per issuer) are recorded atomically in a bounded
  cache that outlives the acceptance window, so a captured request can't
  be replayed to end a later login.

Also: XML-DSig ecdsa-sha* redirect signatures are raw r || s (IEEE
P1363), not DER; verify them with the SHA*withECDSAinP1363Format JCA
algorithms so EC-signing IdPs work.

Regression tests: wrong / missing Destination, stale, future-dated and
expired requests, replay after a fresh login, and an EC IdP's
ecdsa-sha256 redirect signature. All seven fail against the previous
SamlSso.

Refs #861

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: match complete SAML NameIDs, advertise verifiable SLO bindings, optional SP key

Addresses review on #939.

- Sessions are registered and ended by the complete NameID: value,
  Format, NameQualifier and SPNameQualifier, with omitted fields taking
  their SAML defaults (unspecified format; IdP / SP entity IDs as
  qualifiers, SAML Core 8.3.7). A NameID-only logout can no longer end
  sessions of a different principal that happens to share the value.
- pac4j's SP metadata advertised HTTP-POST-SimpleSign and SOAP logout
  endpoints, which SamlSso can't verify signatures for; an IdP choosing
  them would have its signed logouts rejected. The metadata now lists
  only HTTP-POST and HTTP-Redirect (filtered before metadata signing).
- Like java-saml, the SP certificate/key may be omitted when the SP
  signs nothing (authnrequest_signed, logoutrequest_signed and
  sign_metadata all false); a temporary in-memory key pair satisfies
  pac4j. Setting only one of the two, or omitting them while a signing
  option is on, fails with a message naming the settings.

Regression tests for each; each fails with its fix switched off.

Refs #861

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: treat requested SAML authentication contexts as alternatives

Addresses review on #939. pac4j 6.6 rejects a response unless every
requested AuthnContextClassRef appears in it, but SAML treats the
requested classes as alternatives (SAML Core 3.3.2.2.1). With two
classes configured, a valid signed response carrying one of them was
rejected and ACS reported "Not authenticated".

SamlSso now uses a SAML2Client whose response validator requires, for
"exact" comparison (the default), at least one returned class to be one
of those requested. For minimum / better / maximum the IdP judges
relative strength, which an SP can't check in general (a stronger class
may be one that wasn't listed), so the returned class is accepted, as
java-saml did.

Tests: one-of-several accepted, unrequested class rejected under exact,
stronger unlisted class accepted under minimum, any class accepted when
none is requested. The two acceptance cases fail with pac4j's validator.

Refs #861

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: advertise no SAML logout endpoint when no SLS URL is configured

Addresses review on #939. With onelogin.saml2.sp.single_logout_service.url
empty, pac4j advertised the ACS URL as the single logout endpoint, but
ACS doesn't process logout messages, so an IdP following the metadata
would send LogoutRequests that left sessions active. The SP metadata now
lists no SingleLogoutService in that case, and SamlSso refuses IdP
LogoutRequests rather than handling them against the ACS URL. The
sample config documents leaving the setting empty to disable
IdP-initiated logout.

Tests: metadata without the SLS setting has no logout endpoint, and an
IdP LogoutRequest is refused without ending sessions. Both fail under
the previous behaviour.

Refs #861

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* build: restrict and PGP-verify the Shibboleth repository (OpenSAML)

pac4j-saml depends on OpenSAML 5, which Shibboleth publishes only to its
own repository (build.shibboleth.net), not Maven Central. That repository
was trusted implicitly via pac4j-saml's POM, for any artifact, with
checksum mismatches only warned about, and nothing checked signatures.

- pom.xml declares shibboleth-releases with checksumPolicy=fail.
- Maven remote repository filtering (.mvn/maven.config,
  .mvn/rrf/groupId-shibboleth-releases.txt) lets that repository serve
  only org.opensaml and net.shibboleth; everything else comes from
  Central only.
- pgpverify-maven-plugin checks every dependency's PGP signature on
  every build (disableChecksum=true, so a jar tampered with in the local
  repository after a successful build is still caught). .mvn/pgp-keys.map
  pins org.opensaml and net.shibboleth to the Shibboleth release key
  (B5B5DD332142AD657E8D87AC7D27E610B8A3DC52, as published at
  shibboleth.net/downloads/PGP_KEYS); other groupIds are listed by name
  and need a valid signature. There is deliberately no wildcard rule:
  any matching rule lets an artifact pass, so a catch-all would undo the
  pin (caught by the test below). The one unsigned legacy artifact,
  hamcrest-core 1.1 via json-simple (#944), is allowed by exact version.
- src/test/scripts/pgp-verification-test.sh, run in the CI build job:
  genuine artifacts pass; OpenSAML pinned to a different key fails
  ("Not allowed artifact org.opensaml..."); a tampered opensaml-core-api
  jar fails ("PGP Signature INVALID").
- CI's change filter now includes .mvn/**.

Refs #861

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* build: require Maven 3.9.2+ so the Shibboleth repository filter applies

Addresses review on #881. Maven before 3.9.2 does not expand
${maven.multiModuleProjectDirectory} in .mvn/maven.config, so the
remote-repository filter's allow-list is never found and the filter
fails open. Checked against real releases: 3.9.0 and 3.9.1 fetched
OpenSAML from Shibboleth with org.opensaml removed from the allow-list;
3.9.2, 3.9.4, 3.9.8, 3.9.16 and 3.10.0 refused it. A relative path is
resolved against the local repository and fails open on every version.

- maven-enforcer-plugin (first plugin in the build) requires
  [3.9.2,), so older Maven stops with an explicit message.
- pgp-verification-test.sh gains step 0: with org.opensaml removed from
  the allow-list, resolution must fail and nothing may be downloaded
  from Shibboleth for org.opensaml. Run in CI, this proves the filter is
  enforced on the runner's own Maven.

Refs #861

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: keep no-op logouts out of the replay cache; omit temporary SP keys

Addresses review on #939.

- With want_messages_signed=false, an unsigned LogoutRequest that ends
  nothing (NameID only, or a SessionIndex we don't track) was still
  recorded in the bounded replay cache, so a flood of them could fill it
  and make genuine signed logouts be refused. Replay recording now
  happens only for requests that will act: signed requests, or unsigned
  ones naming a tracked login (SamlSessionRegistry.isRegistered). The
  destination/freshness checks still apply to every request.
- With no SP certificate configured, the temporary in-memory key was
  still published in SP metadata; an IdP encrypting to it would break
  after a restart. Metadata now has no KeyDescriptor in that case;
  configured certificates are still published.

Tests: a flood of unsigned no-op logouts against a small cache no longer
blocks a signed logout; an unsigned logout naming a tracked login still
ends it; keyless metadata publishes no key, configured metadata does.
The flood and keyless cases fail with their fixes switched off.

Refs #861

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: SLS must not log users out by redirecting to login.jsp

Addresses review on #939. login.jsp invalidates the current session
when no login/registration message is pending, so SLS redirecting there
after an empty or rejected logout message let any page log a user out
by sending their browser to /saml/sls. A LogoutResponse delivered
through a different, still-logged-in browser did the same.

- Invalid or missing logout message: 400 Bad Request, no redirect.
- LogoutResponse: redirect to index.jsp if the browser's session is
  still logged in, else login.jsp.

Tests (SLSTest): GET/POST without a message return 400 with no redirect;
an invalid message keeps the session and does not redirect; a
LogoutResponse keeps a logged-in session (index.jsp) and sends a
logged-out browser to login.jsp. Four of the five fail against the
previous SLS.

Refs #861

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: bind SAML logins to the browser, gate SSO on sso.enabled, reject blank IDs

Addresses review on #939.

Login CSRF (P1): pac4j's default message store keeps nothing and it
accepts responses without InResponseTo, so nothing tied a login response
to the browser that started the login. A valid response for an
attacker's account could be posted through a victim's browser and sign
it into that account.
- SamlRequestStore records each sent AuthnRequest bound to a random
  token in a dedicated cookie (HttpOnly, Secure, SameSite=None so it
  survives the IdP's cross-site POST, scoped to the ACS path; the session
  cookie is untouched). pac4j only accepts a Response whose InResponseTo
  names a pending request bound to the delivering browser; each request
  is used once and expires after 10 minutes.
- Unsolicited (IdP-initiated) responses, which carry no InResponseTo and
  can't be bound to a browser, are rejected.

Also:
- ACS, SSOLogin, SLS and SSOMetadata answer 404 unless SSO is enabled
  (LoginMethod.isSaml, as Login already checks), so a retained SAML
  configuration can't authenticate anyone while sso.enabled is off.
- SsoUserMapper treats blank (empty / whitespace-only) identifiers and
  display names as missing, so distinct principals can't share an
  account through a blank ssoName; non-blank values are kept as sent.

Tests: the fixture now runs a real SP-initiated login (startLogin) and
answers it, so the existing authentication tests exercise the full round
trip; no assertion was weakened. New: unsolicited response rejected;
response delivered through another browser (no cookie, or its own)
rejected; response answering another browser's login rejected; all four
SSO endpoints 404 with SSO disabled, and ACS doesn't authenticate; blank
ssoName/userName rejected, non-blank kept as sent. Each fix fails its
tests when switched off. 251 unit and 312 integration tests pass.

Refs #861

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs: SAML sample notes SP-initiated-only login and the binding cookie

IdP-initiated (unsolicited) responses are now refused and logins are
bound to the browser by the shepherd_saml_request cookie; say so where
operators configure SSO.

Refs #861

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: scope the SAML binding cookie to the SAML directory; isolate fixture state

Addresses review on #939.

- The binding cookie had Path=<ACS path> (/saml/acs), so /saml/dologin
  never received it: every new login issued a fresh token that replaced
  the previous one, and with two tabs logging in at once the first
  callback was rejected. The cookie is now scoped to the ACS URL's
  directory (e.g. /saml/, keeping any context prefix), so a second login
  reuses the browser's token.
- Test fixture: the simulated browser state (pending AuthnRequest ID,
  binding cookie) is now cleared whenever a client is installed, so it
  can't leak between tests; and the browser sends its cookie only where
  the cookie's Path matches, instead of tests copying it by hand.
  authenticate_ResponseToAnotherBrowsersLogin_IsRejected now switches to
  a separate browser explicitly (newBrowser()) before the victim's
  login; its assertion is unchanged.

Tests: two overlapping logins in one browser both succeed; the cookie is
Path=/saml/, HttpOnly, Secure, SameSite=None. Both fail with the old
ACS-only path. SamlSsoTest passes in three random method orders.

Refs #861

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: refresh the SAML binding cookie's Max-Age on every login start

A reused binding token was not re-sent, so the cookie kept the expiry of
the first login. A second login started near that expiry had its callback
arrive without the cookie and was rejected. The cookie is now re-sent on
every login with a full lifetime. The test fixture models the browser
dropping cookies by Max-Age to cover this.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: require the bearer confirmation to answer the correlated request

pac4j binds a SAML Response to the browser's pending AuthnRequest only
through the Response's InResponseTo, which is unsigned unless the whole
message is signed (want_messages_signed defaults to false), and never
checks the InResponseTo of the bearer SubjectConfirmationData. A valid
assertion for one account could therefore be placed in an envelope
naming another browser's pending request and log that browser in.

The bearer confirmation, inside the signed assertion, must now name the
same request as the Response (SAML Profiles 4.1.4.3). A confirmation with
no InResponseTo is refused too, since only SP-initiated login is
supported. Decrypted assertions go through the same check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs: note the bearer confirmation must name the AuthnRequest

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: give each SAML login its own binding cookie

All logins shared one binding cookie. Two tabs that opened the login page
before either had the cookie each got a different token, and the second
Set-Cookie replaced the first, so the first tab's login was rejected.
Each AuthnRequest now gets its own cookie, named after a hash of its ID,
which lives as long as the request and is deleted once the login
completes. That also replaces the Max-Age refresh needed by the shared
cookie.

The test fixture now keeps a cookie jar and can start logins in parallel
tabs. The cross-browser rejection test uses the per-request cookie name,
so it still presents a cookie of the right name with the wrong token.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants