Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
7 changes: 3 additions & 4 deletions docs/AUTHENTICATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <http://localhost:8025> 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 <http://localhost:8025> 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.
Expand Down
2 changes: 1 addition & 1 deletion docs/CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <http://localhost:8025>. 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 <http://localhost:8025>. 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
Expand Down
2 changes: 1 addition & 1 deletion docs/EXTENDING.md
Original file line number Diff line number Diff line change
@@ -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.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
Loading