Skip to content

Release 2.2.0: correct documentation that overstated what the bundle does - #10

Merged
ondrej-kuhnel merged 1 commit into
mainfrom
SLS-docs-accuracy-and-2-2-0
Aug 20, 2026
Merged

ondrej-kuhnel merged 1 commit into
mainfrom
SLS-docs-accuracy-and-2-2-0

Conversation

@ondrej-kuhnel

Copy link
Copy Markdown
Member

Cuts the 2.2.0 release and, with it, fixes a defect class that turned out to run through the docs: claims of protection the code does not implement.

Why this was worth a sweep

Three of these were found and fixed while doing other work (AutoRegistrationPolicy "requires a verified email", "Enforces revocation", rate limiting "slows credential stuffing"). Three hits in the five files touched so far is a bad base rate, so the remaining 21 were checked the same way.

The bundle is contract-first — 76 interfaces, ~149 abstract methods — so the recurring shape is a feature summary written in bundle voice for something that is actually an abstract hook, or a service nothing calls. The detail further down each page was usually correct; the summary was not, and summaries are how feature docs get read.

Corrected

Doc Said Actually
two-factor-authentication.md, security-configuration.md enforced holds un-enrolled users at setup; wire the success handler to get it shouldEnforceFor*() has no caller; the handler only forwards a scheb token, which exists solely for users already enrolled
magic-link-login.md under "Hardening built into the flow": every path padded to a deadline padTo() has no caller — the neutral body is bundle behaviour, the neutral time is not
admin-customer-management.md, session-management-…md "block account", "sign out from all devices" revoking stamps revokedAt; the sign-out listener is the integrator's
passkey-login.md, oauth-social-login.md the delete/unlink flow refuses to remove the last sign-in method canRemoveCredential() / canUnlinkProvider() are abstract
passkey-login.md "the bundle performs the standard WebAuthn ceremony server-side" verifyAndPersist() and the assertion verifier are hooks
password-expiration.md settings readable in the global scope the checker picks scope from user type and never reads global
security-configuration.md every guarded controller bar passkey flashes the account-state key the two-factor recovery challenge throws instead
passkey-frontend.md verify endpoints take the credential JSON back raw they expect {"credential": "<string>"}

Not just corrections

The first pass at this read like an audit report — "nothing in the bundle calls it", implementation details cited as evidence, a note explaining why a scope that had just been deleted from a list was absent. That is the wrong genre for a manual, and it also left three new "write a listener" instructions with nowhere to go.

So the corrections were rewritten as instructions, and controllers-you-provide.md gained §7 (two-factor enforcement listener) and §8 (session revocation listener) in the house shape — what the bundle gives you, what to build, what breaks if you skip it. The feature guides now point there instead of re-explaining.

Code

Only two lines, neither a runtime bug:

  • symfony/clock is now declared. services.yaml binds @clock in three places; the package was only ever present because symfony/security-bundle requires it. If that changes upstream, container compilation breaks here for a reason nobody would guess.
  • PHPUnit reports deprecation detail rather than a bare count, so the one the suite currently triggers on prefer-lowest can be identified.

Things deliberately not changed: PasswordExpirationChecker still builds its own DateTimeImmutable rather than taking a clock (inconsistent, but the tests use relative dates and nothing is gained); TotpSecretGenerator::verifyCode() still has no replay guard (its only caller is the setup controller, where replay is meaningless).

Release housekeeping

[Unreleased] → [2.2.0] - 2026-08-20. UPGRADE.md had 2.1 → 2.2 filed above 2.0 → 2.1 in an oldest-first file — my error from #8, now in order. README claimed 326 tests at "level max"; it is 384 at level 8 plus strict rules.

Minor is right: StateCookieSigner and RateLimitGuard gained constructor arguments, both defaulted or supplied by the bundle's own wiring, and no interface changed.

🤖 Generated with Claude Code

…does

Three earlier fixes established a pattern -- documentation promising a
guarantee the code does not deliver -- so the remaining 21 doc files were
swept for the same defect. The bundle is contract-first, with 76
interfaces and ~149 abstract methods, so the recurring shape was a feature
summary written in bundle voice for something that is really an abstract
hook or a service nothing calls.

Corrected, each verified against the source:

- 2FA `enforced` mode was described as holding un-enrolled users at the
  setup step. TwoFactorEnforcementChecker returns a verdict and has no
  caller; nothing acts on it. security-configuration.md compounded this by
  naming the success handler as the way to get enforcement -- it only
  forwards a scheb token, which exists solely for users already enrolled.
- Magic-link timing padding sat under "Hardening built into the flow"
  although padTo() has no caller. The neutral response body is bundle
  behaviour; the neutral response time is not.
- The admin "block account" and "sign out from all devices" actions, and
  the session-management summary, presented revocation as a sign-out. It
  stamps revokedAt; the listener that acts on it is the integrator's.
- The passkey and OAuth last-method guards were described as bundle
  behaviour. canRemoveCredential() and canUnlinkProvider() are abstract --
  only the app knows what other sign-in methods exist.
- The WebAuthn ceremony was described as running server-side in the
  bundle. verifyAndPersist() and the assertion verifier are hooks.
- password_expiration was documented as readable in the `global` scope,
  which the checker never consults.
- The account-state message key was said to reach every guarded controller
  bar passkey; the two-factor recovery challenge throws instead.
- passkey-frontend.md said the verify endpoints take the credential JSON
  back raw. They expect {"credential": "<string>"}.

Two sections were added to controllers-you-provide.md for the listeners
these corrections now point at -- two-factor enforcement and session
revocation -- so the guides can say "this part is yours, here is what to
build" rather than only what the bundle omits.

Also fixed: UPGRADE.md had 2.1 -> 2.2 filed above 2.0 -> 2.1 in an
oldest-first file, and README claimed 326 tests at PHPStan level max
(384 tests, level 8 plus strict rules).

symfony/clock is now declared. services.yaml binds the clock service in
three places and the package was only ever present because
symfony/security-bundle requires it. PHPUnit reports deprecation detail
rather than a bare count, so the one the suite currently triggers can be
identified.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@ondrej-kuhnel
ondrej-kuhnel merged commit 242a984 into main Aug 20, 2026
2 checks passed
@ondrej-kuhnel
ondrej-kuhnel deleted the SLS-docs-accuracy-and-2-2-0 branch August 20, 2026 07:10
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