polish(security): SUF-02/05 follow-ups from #333/#334 review#336
Conversation
The @RequestBody binding on POST /user/setPassword consumes the request body before isStepUpSatisfied() runs, so an implementation reading getInputStream()/getReader() would get an empty stream. Document that the step-up proof must be read from a header, request parameter, or the session (or a content-caching filter installed) rather than the body.
…ed-by-default (SUF-02) - Log a startup WARN (mirroring the SUF-01 fail-fast pattern) when POST /user/setPassword is disabled by default -- no StepUpService bean and user.security.allowInitialPasswordSetWithoutStepUp=false -- so the disabled state surfaces at boot instead of only when a user hits the 403. - Return a distinct response code for the disabled branch (403 -> code 7) vs the step-up-denied branch (401 -> code 6) so a client can disambiguate 'disabled on this server' from 'step-up verification failed'. - Unit tests: the warning fires only when disabled-by-default (not when a StepUpService is present nor when the opt-in flag is true); the two failure branches assert their distinct codes.
…dSetWithoutStepUp metadata Add both security properties to additional-spring-configuration-metadata.json so they surface in IDE property auto-completion (SUF-01 / SUF-02).
…t-page headers (SUF-02/05) - Boot the real ObjectProvider<StepUpService> (empty in the default context): a passwordless setPassword returns 403 (code 7), proving the wiring stays fail-closed rather than relying on a mocked provider. - Verify the WebInterceptorConfig @Value-wired reset-page headers (Referrer-Policy: no-referrer, Cache-Control: no-store) survive the Spring Security filter chain end-to-end, not just a hand-registered interceptor in a standalone MockMvc setup.
There was a problem hiding this comment.
Pull request overview
Polish follow-up to the SUF-02/SUF-05 security work from #333/#334, focused on clarifying integrator guidance, improving operational visibility, and tightening client-facing response semantics without changing the underlying fail-closed behavior.
Changes:
- Clarifies
StepUpServiceSPI guidance to avoid relying on re-reading the request body after@RequestBodybinding. - Adds a startup WARN when
/user/setPasswordis disabled-by-default, and introduces distinct JSON response codes for “disabled” vs “step-up denied”. - Adds configuration metadata entries and full-context tests covering real
ObjectProvider<StepUpService>wiring and reset-page security headers through the Spring Security filter chain.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| src/test/java/com/digitalsanctuary/spring/user/api/UserAPIUnitTest.java | Updates SUF-02 unit assertions for distinct codes and adds unit coverage for the new startup warning. |
| src/test/java/com/digitalsanctuary/spring/user/api/UserApiTest.java | Adds full-context tests for disabled-by-default /setPassword behavior and reset-page headers surviving the security chain. |
| src/main/resources/META-INF/additional-spring-configuration-metadata.json | Registers SUF-01/SUF-02 properties for IDE metadata/autocomplete. |
| src/main/java/com/digitalsanctuary/spring/user/security/StepUpService.java | Updates SPI Javadoc to reflect request-body consumption realities for @RequestBody endpoints. |
| src/main/java/com/digitalsanctuary/spring/user/api/UserAPI.java | Adds startup WARN for disabled-by-default /setPassword and changes disabled branch response code to 7. |
| CHANGELOG.md | Documents the new distinct response codes and startup warning behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Review SummaryThis is a tightly-scoped polish PR addressing review feedback from the merged SUF-02/05 security work (#333/#334). I verified the diff against the current code (checked out at the PR's merge ref) and traced through the relevant call sites — everything is consistent and correct. What it does
Code quality
Minor / non-blocking observations
SecurityNo security concerns — the fail-closed default behavior is unchanged (as stated in the PR description), and this is purely observability/docs/test polish on top of already-merged, already-unreleased ( Test coverageGood. The new tests close a real gap (mocked-provider-only coverage → real-provider coverage) and directly assert the new VerdictSolid, low-risk, well-tested documentation/observability polish. No correctness or security issues found. The magic-number nit is the only suggestion I'd consider before merge, and even that's optional given the existing codebase convention isn't fully consistent either. |
Consistency with the neighboring request.getInputStream() (Copilot review nit).
|
Review: security/suf-polish (#336) Reviewed the diff end-to-end (UserAPI.java, StepUpService.java, config metadata, and both test files) against CLAUDE.md conventions. This is a tight, well-scoped "polish" PR: small, each change traces to a specific triage item, and the new tests exercise real wiring rather than re-mocking what was already covered. No blocking issues found. Code quality / correctness
Test coverage
Config metadata
Security
Minor / non-blocking
Solid, well-tested follow-up PR. No blocking issues from this review (note: I do not have CI/build execution in this environment to independently confirm ./gradlew check, but static review of the diff found no compile-time or logical issues). |
Post-merge polish addressing valid review feedback from the merged SUF security PRs (#333, #334). All items verified against the code before implementing; none reopen the closed issues or are security-critical (the fail-closed core is unchanged). Nothing here is a released-API break — SUF-02/05 shipped in #334 but are still unreleased (
5.0.2-SNAPSHOT).Items (from the #333/#334 review triage)
StepUpServiceSPI Javadoc was misleading (real defect). It told integrators they could read the step-up proof from the request "body," butPOST /user/setPasswordbinds@Valid @RequestBody SetPasswordDto, which drains the input stream beforeisStepUpSatisfied()runs — a consumer readinggetInputStream()/getReader()gets an empty stream. Javadoc now says: read the proof from a header, request parameter, or the session (or install a content-caching filter).setPasswordis disabled-by-default. When noStepUpServicebean is present andallowInitialPasswordSetWithoutStepUp=false, the endpoint silently returns403. A@PostConstructnow logs a startup warning (mirroring the SUF-01 fail-fast pattern) so operators learn about it at boot, not from a production403.codefor the two failure branches. Disabled-by-default was reusingcode 6(also the step-up-denied code). Disabled is nowcode 7(HTTP 403); step-up-denied stayscode 6(HTTP 401), so clients can disambiguate.user.security.requireCanonicalAppUrl(SUF-01) anduser.security.allowInitialPasswordSetWithoutStepUp(SUF-02) inadditional-spring-configuration-metadata.jsonfor IDE auto-completion.ObjectProvider<StepUpService>(empty → passwordlesssetPasswordreturns403/code 7) and verify theWebInterceptorConfig@Value-wired reset-page headers survive the Spring Security filter chain — previously only exercised via standalone/mocked MockMvc.Deliberately excluded (per the triage + prior decisions): extending SUF-05 headers to
registrationConfirm(same "reset token already in the email URL" reasoning that killed the SUF-05 follow-up; verification token has an even smaller window), and cosmetic nits (hardcoded action literal, direct interceptor instantiation, rawassert, import order).Testing
setPassword→403through the real provider; reset-page headers through the real interceptor wiring../gradlew checkgreen (all tests + ArchUnit); the new@PostConstructdoes not disturb any full-context boot.🤖 Generated with Claude Code