From 20ef9e8449bd3d69e22df350f32fe7a383d4d0f8 Mon Sep 17 00:00:00 2001 From: Devon Hillard Date: Tue, 18 Aug 2026 16:28:01 -0600 Subject: [PATCH] chore: upgrade Spring User Framework to 5.3.1 5.3.1 fixes POST /user/resendRegistrationToken, which bound the full registration UserDto and so rejected the email-only payload resend-verification.js posts with HTTP 400. It now binds ResendVerificationDto and returns 200. Patch release otherwise: no API or configuration changes affecting this app. - build.gradle: 5.3.0 -> 5.3.1 - UserApiIntegrationTestFixed: cover the resend endpoint (email-only body succeeds, unknown address returns the identical generic body, malformed email returns 400). The first two fail against 5.3.0 and pass against 5.3.1 - docs/AUTHENTICATION.md, docs/CONFIGURATION.md: drop the known-limitation notes describing the 5.3.0 breakage - README.md, docs/EXTENDING.md: version references - CHANGELOG.md: 2026-08-18 entry --- CHANGELOG.md | 17 +++++++ README.md | 2 +- build.gradle | 2 +- docs/AUTHENTICATION.md | 7 ++- docs/CONFIGURATION.md | 2 +- docs/EXTENDING.md | 2 +- .../user/api/UserApiIntegrationTestFixed.java | 44 +++++++++++++++++++ 7 files changed, 68 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c1dcde..493fe2d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,23 @@ the library. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). +## 2026-08-18 + +### Changed +- Spring User Framework upgraded from 5.3.0 to 5.3.1 (`build.gradle`). Patch release: no API, + configuration, or behavior changes for this app beyond the resend fix below + +### Fixed +- The resend-verification page (`/user/request-new-verification-email.html`) works again. + `UserAPI.resendRegistrationToken` now binds `ResendVerificationDto` instead of the full registration + `UserDto`, so the email-only payload `resend-verification.js` posts passes validation and returns + HTTP 200 instead of 400. The fix is framework-side; this app needed only the version bump + +### Added +- `UserApiIntegrationTestFixed` covers `POST /user/resendRegistrationToken`: an email-only body succeeds, + an unknown address returns the identical generic body (anti-enumeration), and a malformed email is + rejected with HTTP 400. The first two fail against 5.3.0 and pass against 5.3.1 + ## 2026-08-17 ### Changed diff --git a/README.md b/README.md index e336042..7fb5484 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ Documentation for this demo is in [docs/](docs); the framework's own documentati | main | 4.1.x | 5.3.x | 21 | `main` | | 1.0.0-springboot3 | 3.5.x | 3.5.x | 17 | [`v1.0.0-springboot3`](https://github.com/devondragon/SpringUserFrameworkDemoApp/tree/v1.0.0-springboot3) | -`main` is on Spring Boot 4.1.0 and framework 5.3.0 ([build.gradle](build.gradle)). For the Spring Boot 3.5.6 +`main` is on Spring Boot 4.1.0 and framework 5.3.1 ([build.gradle](build.gradle)). For the Spring Boot 3.5.6 and framework 3.5.1 combination on Java 17, `git checkout v1.0.0-springboot3` after cloning. ## What this demo shows diff --git a/build.gradle b/build.gradle index 76ef97a..b161355 100644 --- a/build.gradle +++ b/build.gradle @@ -39,7 +39,7 @@ repositories { dependencies { // DigitalSanctuary Spring User Framework - implementation 'com.digitalsanctuary:ds-spring-user-framework:5.3.0' + implementation 'com.digitalsanctuary:ds-spring-user-framework:5.3.1' // WebAuthn support (Passkey authentication) implementation 'org.springframework.security:spring-security-webauthn' diff --git a/docs/AUTHENTICATION.md b/docs/AUTHENTICATION.md index cf5181a..dc765bb 100644 --- a/docs/AUTHENTICATION.md +++ b/docs/AUTHENTICATION.md @@ -25,10 +25,9 @@ failed logins and lockouts land in the audit log. `/user/request-new-verification-email.html` ([`resend-verification.js`](../src/main/resources/static/js/user/resend-verification.js) posts `POST /user/resendRegistrationToken`). Under `docker compose up` the message is captured by Mailpit - rather than delivered: open and click the link there. The resend page itself - is currently broken against `ds-spring-user-framework:5.3.0`: the endpoint binds the full - registration `UserDto`, whose `firstName`/`lastName`/`password`/`matchingPassword` are `@NotBlank`, - so the email-only payload the page posts is rejected with HTTP 400 and no mail goes out. + rather than delivered: open and click the link there. The endpoint always + returns the same generic success response whether the address is unknown, already verified, or + genuinely awaiting verification, so it leaks nothing about which accounts exist. - `false`: the account is created enabled, the framework logs the user straight in, and the browser lands on `/user/registration-complete.html`. `application-local.yml-example:131` sets it false, so the Gradle `local` run path needs no SMTP server at all. diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index abb0c27..a93bedb 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -75,7 +75,7 @@ pattern works for any other key, e.g. `USER_SECURITY_BCRYPTSTRENGTH` for `user.s - `spring.mail.username`, `spring.mail.password`, `spring.mail.host`, `spring.mail.port` (`application.yml:2-6`) configure the SMTP transport used for verification, password-reset, and notification email. The base file's `host` is a placeholder SES endpoint; set real credentials in your profile. - `user.registration.sendVerificationEmail` (`application.yml:113`) controls whether a new account must click a verification link before it can log in. `false` enables the account immediately at registration. -- Both Docker stacks (`compose.yaml` and `docker-compose-keycloak.yml`) run a `mailpit` service ([Mailpit](https://mailpit.axllent.org/)) that captures outbound mail instead of delivering it, and serves it as a web inbox on . The app container reaches it over plain SMTP with `SPRING_MAIL_HOST: mailpit` and `SPRING_MAIL_PORT: 1025`, auth and STARTTLS off. Neither stack overrides `user.registration.sendVerificationEmail`, so registration verification and password reset run their real email flows: read the message in the web inbox and click the link. Nothing leaves the machine, and Mailpit keeps no volume, so `docker compose down` discards the captured mail. The resend-verification page does not work against `ds-spring-user-framework:5.3.0`: `UserAPI.resendRegistrationToken` binds the full registration `UserDto`, whose `firstName`, `lastName`, `password`, and `matchingPassword` are `@NotBlank`, so the email-only payload that `resend-verification.js` posts is rejected with HTTP 400 and no mail is sent. That is a framework-side bug, not a stack setting. +- Both Docker stacks (`compose.yaml` and `docker-compose-keycloak.yml`) run a `mailpit` service ([Mailpit](https://mailpit.axllent.org/)) that captures outbound mail instead of delivering it, and serves it as a web inbox on . The app container reaches it over plain SMTP with `SPRING_MAIL_HOST: mailpit` and `SPRING_MAIL_PORT: 1025`, auth and STARTTLS off. Neither stack overrides `user.registration.sendVerificationEmail`, so registration verification and password reset run their real email flows: read the message in the web inbox and click the link. Nothing leaves the machine, and Mailpit keeps no volume, so `docker compose down` discards the captured mail. The resend-verification page works as of `ds-spring-user-framework:5.3.1`, which binds `ResendVerificationDto` in `UserAPI.resendRegistrationToken` so the email-only payload that `resend-verification.js` posts passes validation. Under 5.3.0 the endpoint bound the full registration `UserDto` and rejected every resend with HTTP 400. - `user.mail.fromAddress` sets the `From` address on outbound mail; it is set per profile (e.g. `application-local.yml-example:144`), not in the base file. ## Security settings this demo sets diff --git a/docs/EXTENDING.md b/docs/EXTENDING.md index 7e3f1b0..0f648f8 100644 --- a/docs/EXTENDING.md +++ b/docs/EXTENDING.md @@ -1,6 +1,6 @@ # Extending the Spring User Framework -This demo depends on `com.digitalsanctuary:ds-spring-user-framework:5.3.0` ([build.gradle](../build.gradle)). Every +This demo depends on `com.digitalsanctuary:ds-spring-user-framework:5.3.1` ([build.gradle](../build.gradle)). Every section below names one extension point the framework offers, the demo code that uses it, the configuration that wires it, and what you would write in your own application to do the same. diff --git a/src/test/java/com/digitalsanctuary/spring/user/api/UserApiIntegrationTestFixed.java b/src/test/java/com/digitalsanctuary/spring/user/api/UserApiIntegrationTestFixed.java index bd48b1e..7f9a219 100644 --- a/src/test/java/com/digitalsanctuary/spring/user/api/UserApiIntegrationTestFixed.java +++ b/src/test/java/com/digitalsanctuary/spring/user/api/UserApiIntegrationTestFixed.java @@ -70,6 +70,9 @@ class UserApiIntegrationTestFixed { private static final String TEST_EMAIL = "test@example.com"; + /** Generic, outcome-independent body /user/resendRegistrationToken returns (anti-enumeration). */ + private static final String RESEND_GENERIC_MESSAGE = "If your account requires verification, a new verification email has been sent."; + private UserDto testUserDto; @BeforeEach @@ -180,6 +183,47 @@ void shouldHandlePasswordReset() throws Exception { .andExpect(jsonPath("$.messages[0]").value("If account exists, password reset email has been sent!")); } + @Test + @DisplayName("Should resend verification email from an email-only request body") + void shouldResendVerificationEmailForEmailOnlyBody() throws Exception { + // Given - an unverified account + userService.registerNewUserAccount(testUserDto); + + // When - the resend page posts only an email (see resend-verification.js) + String requestBody = objectMapper.writeValueAsString(java.util.Map.of("email", TEST_EMAIL)); + + // Then - ds-spring-user-framework 5.3.1 binds ResendVerificationDto here. Under 5.3.0 the endpoint + // bound the full registration UserDto, so this payload failed @NotBlank validation with a 400. + mockMvc.perform(post(API_BASE_PATH + "/resendRegistrationToken").contentType(MediaType.APPLICATION_JSON) + .content(requestBody).with(csrf())).andExpect(status().isOk()).andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.messages[0]").value(RESEND_GENERIC_MESSAGE)); + } + + @Test + @DisplayName("Should not reveal account existence on resend (anti-enumeration)") + void shouldNotRevealAccountExistenceOnResend() throws Exception { + // Given - no account for this address + + // When - a resend is requested for an unknown email + String requestBody = objectMapper.writeValueAsString(java.util.Map.of("email", "nobody@example.com")); + + // Then - the same generic 200 body as the unverified-account case above + mockMvc.perform(post(API_BASE_PATH + "/resendRegistrationToken").contentType(MediaType.APPLICATION_JSON) + .content(requestBody).with(csrf())).andExpect(status().isOk()).andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.messages[0]").value(RESEND_GENERIC_MESSAGE)); + } + + @Test + @DisplayName("Should reject a malformed email on resend") + void shouldRejectMalformedEmailOnResend() throws Exception { + // When - the email fails @Email validation + String requestBody = objectMapper.writeValueAsString(java.util.Map.of("email", "not-an-email")); + + // Then + mockMvc.perform(post(API_BASE_PATH + "/resendRegistrationToken").contentType(MediaType.APPLICATION_JSON) + .content(requestBody).with(csrf())).andExpect(status().isBadRequest()); + } + @Test @DisplayName("Should require authentication for update user") void shouldRequireAuthForUpdateUser() throws Exception {