Releases: devondragon/SpringUserFramework
5.0.1
Maven Central: com.digitalsanctuary:ds-spring-user-framework:5.0.1
Features
- WebAuthn credential-management re-authentication now returns distinct HTTP status codes
- Affects: DELETE /user/webauthn/password, DELETE /user/webauthn/credentials/{id}, PUT /user/webauthn/credentials/{id}/label
- Status mapping:
- Missing/blank currentPassword → 400 Bad Request
- Incorrect currentPassword → 401 Unauthorized
- Account locked (too many failed attempts) → 423 Locked
- Implementation details:
- Added WebAuthnReauthenticationException (401) and WebAuthnAccountLockedException (423), both extend WebAuthnException (400 default).
- WebAuthnManagementAPI.requireCurrentPasswordIfSet now:
- Checks lock first; if locked, throws WebAuthnAccountLockedException (423) before any password verification.
- Throws WebAuthnException (400) when currentPassword is missing/blank; this does not count toward lockout.
- On wrong password, increments LoginAttemptService, then throws WebAuthnReauthenticationException (401).
- On success, clears failed-attempt counter via LoginAttemptService.loginSucceeded to mirror login semantics.
- WebAuthnManagementAPIAdvice maps subtypes to precise statuses; base WebAuthnException remains 400.
- Client impact:
- These endpoints were introduced in 5.0.0 and previously returned 400 for all failures; clients can now branch on 400/401/423 to give better UX.
- If a client already treated any 4xx as “re-auth failed,” no change is required.
Fixes
- Security: trustedHosts matching is now case-insensitive (RFC 4343)
- AppUrlResolver normalizes configured user.security.trustedHosts to lower case and lower-cases X-Forwarded-Host during comparison.
- Prevents legitimate mixed-case hosts (e.g., App.Example.Com) from being ignored on the CWE-640 path, which previously forced a fallback to the container’s server name.
- Continues to honor only the first entry in a multi-valued X-Forwarded-Host (proxy chains).
- Model: revert Role.privileges to FetchType.EAGER (User.roles remains LAZY)
- Addresses LazyInitializationException footgun when calling role.getPrivileges() outside an open transaction/session, for negligible performance gain.
- Rationale: privileges are small, static reference data; there’s no bulk-load path across many Roles.
- Performance is preserved: the authentication path still loads User → roles → privileges in one round trip via UserRepository.findWithRolesByEmail using @EntityGraph.
- Repository Javadoc clarified: the two-level entity graph yields a Cartesian product of roles × privileges that Hibernate de-duplicates via Sets; acceptable for a single user.
Breaking Changes
- Refined HTTP statuses on WebAuthn credential-management re-authentication failures
- In 5.0.0, missing/incorrect/locked cases all returned 400; in 5.0.1 they return 400/401/423 respectively.
- These endpoints are new as of 5.0.0. Most clients won’t break if they handled any 4xx generically; update only if you explicitly expected 400 for all re-auth failures.
Documentation
- CHANGELOG and MIGRATION updated to document:
- New WebAuthn re-authentication status codes (400 missing, 401 incorrect, 423 locked) and endpoint list.
- Role.privileges reverted to EAGER; guidance focuses on laziness only for User.roles.
- Clarified that the entity-graph load is a “single round trip” (bounded, typically one query).
- JPA note: @EntityGraph fetch results in a roles × privileges Cartesian product, de-duplicated by Sets; fine for single-user loads, not intended for bulk loads.
- Tokens (PasswordResetToken, VerificationToken): getUser() is EAGER for detached access; the associated User’s roles remain LAZY—use findWithRolesByEmail when authorities are needed.
- README updates for 5.0.0 release:
- Installation snippets bumped to 5.0.0; compatibility table reworked (Spring Boot 4.0.x–4.1.x ↔ framework 5.0.x).
- Notes that framework versioning is independent of Spring Boot’s major; 5.0.0 is a breaking release with a reverse-proxy configuration requirement (user.security.appUrl or trustedHosts).
- Spring Boot badge updated to 4.0 | 4.1; spring-retry doc dependency adjusted to 2.0.13.
Testing
- Added WebAuthnManagementAPIAdviceTest to verify precise exception-to-status mapping:
- Base WebAuthnException → 400, WebAuthnReauthenticationException → 401, WebAuthnAccountLockedException → 423, WebAuthnUserNotFoundException → 404.
- Tightened WebAuthn API unit tests to expect specific exception subtypes:
- Locked accounts now raise WebAuthnAccountLockedException; wrong currentPassword raises WebAuthnReauthenticationException.
- Verified loginAttemptService integration: wrong password increments failures; success resets.
- Updated integration tests:
- Incorrect currentPassword paths now assert 401 (previously 400) for delete/rename credential flows.
- AppUrlResolverTest
- New case-insensitivity test to confirm mixed-case allow-list and forwarded host match correctly.
- Repository tests
- UserRepositoryEntityGraphTest Javadoc/comments corrected to reflect Role.privileges EAGER and User.roles LAZY; continues to verify that plain findByEmail leaves roles uninitialized.
3.6.0 — Security Maintenance (Spring Boot 3.5 / Java 17)
Maven Central: com.digitalsanctuary:ds-spring-user-framework:3.6.0
Security-maintenance release for the Spring Boot 3.5 / Java 17 line. This backports the security-critical
fixes from the 4.4.0 and 5.0.x (Spring Boot 4 / Java 21) releases that apply to code present in the 3.5.x line.
It deliberately does not include the larger architectural changes, new features (WebAuthn, MFA, GDPR service,
token-at-rest hashing), or breaking HTTP-contract changes from those releases. The 3.x line is now in
security-maintenance only — new development happens on the 5.0.x line (Spring Boot 4.0/4.1, Java 21). Java-21
users should migrate to 5.0.x.
Features
- None
Fixes
- Security: Optional canonical app URL to mitigate host-header poisoning (CWE-640)
- Added new configuration property user.security.appUrl. When set (e.g. https://app.example.com), password-reset and email-verification links are built from this value and the X-Forwarded-Host header is ignored.
- Implementation details:
- UserAPI now injects user.security.appUrl and uses a new resolveAppUrl(HttpServletRequest) helper.
- Trailing slash is stripped; if the property is blank, behavior falls back to the legacy UserUtils.getAppUrl(request).
- All link creation paths now call resolveAppUrl: registration verification email, forgot-password email, and registration event publishing.
- Spring configuration metadata updated to document the new property.
- Default remains unchanged (blank), preserving prior behavior. Recommended to set this property when running behind a reverse proxy to close the poisoning vector.
- Security: Prevent audit log forging/corruption via CR/LF and delimiter injection
- FileAuditLogWriter now sanitizes all attacker-influenceable fields (action, status, userId, email, IP, sessionId, message, userAgent, extraData) by replacing CR, LF, and | with spaces before writing the pipe-delimited record.
- Ensures each record remains exactly one line with 10 fields, preventing newline-based record forging and delimiter-based column shifting.
- Security: Reject OAuth2/OIDC logins where providers explicitly report unverified emails
- Google (OAuth2 userinfo): getUserFromGoogleOAuth2User now rejects when email_verified is explicitly false (Boolean or case-insensitive String "false"). Absent claim remains trusted.
- OIDC providers: rejects when standard email_verified claim is explicitly false.
- Facebook: getUserFromFacebookOAuth2User now rejects when either email_verified (newer Graph API) or verified (older) claims are explicitly false. Absent claims remain trusted.
- All three paths throw an OAuth2AuthenticationException with error code email_not_verified and log a warning.
- Security: Sanitize OAuth2 failure messaging to avoid sensitive data leakage
- Introduced SanitizingOAuth2AuthenticationFailureHandler; on authentication failure it:
- Logs full exception details server-side.
- Stores only a generic, user-safe message in session attribute error.message, or a specific non-sensitive message when the error is email_not_verified.
- Redirects back to the configured login page.
- WebSecurityConfig now wires this handler into oauth2Login(). This prevents raw exception messages (which may include account emails or provider details) from reaching the browser.
- Introduced SanitizingOAuth2AuthenticationFailureHandler; on authentication failure it:
- Concurrency: Atomic failed-login increment to close lockout-evasion race
- New repository method UserRepository.incrementFailedAttempts(email) performs a single JPQL UPDATE u.failedLoginAttempts = u.failedLoginAttempts + 1 where u.email = :email with @Modifying(clearAutomatically = true, flushAutomatically = true).
- LoginAttemptService.loginFailed now:
- Uses the atomic increment to avoid lost updates under parallel failures.
- Re-reads the user, and if the threshold is reached, locks and timestamps the account (idempotent lock).
- Prevents attackers from evading lockout by generating concurrent failures that previously could lose increments.
- Security: Reduce secret leakage in logs
- User.toString() now excludes the password field via @ToString.Exclude (prevents bcrypt hash from appearing in logs).
- Logs no longer include raw tokens or full principals:
- Sensitive tokens are logged via a short fingerprint (first 6 chars + ellipsis; short tokens masked as ****).
- Authentication/principal logs report only usernames/emails or principal names; attribute dumps replaced with attribute key sets.
- Affects logging in UserActionController, UserVerificationService, DSOAuth2UserService, DSOidcUserService, LoginSuccessService, LogoutSuccessService, UserEmailService, UserService, and JpaAuditingConfig.
Breaking Changes
- OAuth2 login failure UI messaging is now generic
- Previously, raw AuthenticationException messages were stored in the session and could be displayed by the UI. Now only a generic error or the non-sensitive “email not verified” message is stored.
- If your UI or integration logic depended on specific raw exception text, update it to rely on generic messaging or server-side logs for detail.
Refactoring
- Internal cleanup in LoginAttemptService:
- Removed the old read-modify-write incrementFailedAttempts(User) method in favor of the repository-level atomic increment.
- Logging refactors to reduce sensitive output (see Fixes).
Documentation
- CHANGELOG.md: New 3.6.0 section summarizing backported security fixes and maintenance-only posture for the Spring Boot 3.5 / Java 17 line.
- README.md:
- Updated installation coordinates to version 3.6.0.
- Added a maintenance-only banner directing Java 21/Spring Boot 4 users to the 5.0.x line.
- CONFIG.md:
- Documented the new user.security.appUrl property, including guidance on when to enable it and its security implications.
- gradle.properties: Bumped to 3.6.0-SNAPSHOT for development (release plugin produces 3.6.0).
Testing
- UserAPIUnitTest: Added “App URL resolution (CWE-640)” tests:
- Verifies configured appUrl overrides X-Forwarded-Host, trailing slash stripping, and fallback to request-derived URL when blank.
- UserRepositoryTest: New repository slice tests for incrementFailedAttempts:
- Confirms row count returns and atomic increments via two successive updates; verifies zero rows for non-existent emails.
- LoginAttemptServiceTest: Updated and expanded:
- Asserts atomic increment usage, correct locking at threshold, no lock below threshold, behavior when user missing, and when lockout is disabled.
- SanitizingOAuth2AuthenticationFailureHandlerTest: Ensures generic/non-leaking messages are stored in session and redirects occur, including the specific mapping for email_not_verified.
- DSOAuth2UserServiceTest and DSOidcUserServiceTest:
- Added comprehensive tests covering acceptance (true/absent) and rejection (explicit false) of email_verified claims for Google and OIDC providers.
- UserToStringTest: Verifies that User.toString() does not include the password hash.
Other Changes
- Build: Gradle release configuration widened to allow release/* branches
- build.gradle now sets net.researchgate.release git.requireBranch to main|release/.* so security-maintenance releases off release/* (e.g., release/3.6.0) can run ./gradlew release.
- Still accepts exactly main.
5.0.0
Maven Central: com.digitalsanctuary:ds-spring-user-framework:5.0.0
Major release — contains breaking changes across the Java API, HTTP/response contracts, database schema, required configuration, and bean/auto-configuration structure. Read
MIGRATION.md("Migrating to 5.0.x") before upgrading.
⚠️ ACTION REQUIRED if you run behind a reverse proxy: password-reset and email-verification links are now built from a configured canonical URL and ignoreX-Forwarded-Hostunless it is explicitly allow-listed. Setuser.security.appUrl(recommended) oruser.security.trustedHosts, otherwise your reset/verification links will point at the wrong host. SeeMIGRATION.md.
Features
-
Canonical app URL resolution for security emails
- Introduced AppUrlResolver and new properties user.security.appUrl and user.security.trustedHosts.
- Password reset and email verification links are now built from the configured canonical URL. If unset, X-Forwarded-Host is honored only when the first value in the header is on the trusted allow-list; otherwise the server name is used.
- Forwarded port handling derives the port from X-Forwarded-Port or, if absent, from the forwarded scheme to avoid leaking internal ports. Default ports (80/443) are omitted. Trailing slashes in configured URLs are stripped to prevent double slashes in links.
- Provided a default AppUrlResolver bean (customizable by consumers).
-
Auto-configuration entry point and toggleable cross-cutting features
- UserConfiguration is now a proper @autoConfiguration.
- Added independent opt-out toggles (default true): user.async.enabled, user.retry.enabled, user.scheduling.enabled, user.method-security.enabled.
-
Performance: authority loading without N+1
- Switched User.roles and Role.privileges to LAZY and added UserRepository.findWithRolesByEmail with @EntityGraph to fetch roles and privileges in a single query on the authentication path (DSUserDetailsService, OAuth2/OIDC paths).
- Adjusted dependent services (e.g., UserService authWithoutPassword) to avoid touching lazy collections outside a session.
Fixes
-
Anti-enumeration hardening for registration flows
- /user/registration and /user/registration/passwordless now always return the same generic 200 success-shaped body for both new and already-registered emails.
- /user/resendRegistrationToken always returns a uniform 200 response, regardless of unknown email or already-verified accounts.
- Added audit events (Registration/PasswordlessRegistration “Failure”) for guard denials and duplicate-email attempts; no registration event is published in duplicate cases.
-
Require current password for sensitive WebAuthn operations
- Credential-altering operations (remove password, delete/rename passkey) now require currentPassword when the account has a password set; requests missing/incorrect passwords are rejected with 400.
- Integrated with lockout: locked accounts are rejected, wrong passwords are reported to LoginAttemptService (count toward lockout), and success resets the counter.
- For passwordless accounts, currentPassword is not required (documented residual risk remains unchanged).
-
Hardened forwarded header handling in AppUrlResolver (CWE-640)
- Parses multi-valued X-Forwarded-Host (uses first value), validates X-Forwarded-Proto to http/https only, and sanitizes attacker-controlled values before logging.
-
Validation error handling improvements
- GlobalValidationExceptionHandler now:
- Scopes to library controllers only (doesn’t override consumer apps’ error handling).
- Returns 400 for class-level constraints (e.g., @PasswordMatches) and handles ConstraintViolationException with a structured body.
- GlobalValidationExceptionHandler now:
-
Role/privilege setup robustness
- Startup setup is idempotent and concurrency-safe for multi-node startup:
- Unique constraints added to role.name and privilege.name.
- Insertions run in REQUIRES_NEW, catch unique violations, and re-read existing rows (first-writer-wins).
- Null/blank privilege names from configuration are now guarded and filtered with warnings.
- Startup setup is idempotent and concurrency-safe for multi-node startup:
-
Message bundle registration no longer overrides consumers
- Replaced hard-coded spring.messages.basename with a MessageSource EnvironmentPostProcessor that appends the library bundle (messages/dsspringusermessages) additively and de-duplicates entries. Consumer keys win on collisions.
-
Minor stability and correctness
- Stripped trailing slash from configured appUrl to honor “no trailing slash” contract.
- Marked RolePrivilegeSetupService.alreadySetup volatile to ensure visibility across threads.
Breaking Changes
-
Security email host resolution
- Reset/verification links no longer trust X-Forwarded-Host by default. Configure user.security.appUrl (recommended) or user.security.trustedHosts; otherwise links may target the internal host.
-
HTTP/response contract changes (anti-enumeration)
- /user/registration and /user/registration/passwordless for existing emails return a generic 200 success body (previously 409).
- /user/resendRegistrationToken now always returns a generic 200 success body (previously 409 for already-verified or 500 for unknown email).
- Clients must stop branching on the old 409/500 codes.
-
Re-authentication requirement for credential changes
- DELETE /user/webauthn/password now accepts a JSON body {"currentPassword": "..."} and requires it for password-holding accounts.
- DELETE /user/webauthn/credentials/{id} and PUT /user/webauthn/credentials/{id}/label require currentPassword in the body when the account has a password.
- Update clients to supply currentPassword where applicable.
-
Database schema changes
- Added UNIQUE + NOT NULL on password_reset_token.token and verification_token.token.
- Added UNIQUE + NOT NULL on role.name and privilege.name.
- Consumers managing schema manually must apply DDL and reconcile duplicates/nulls ahead of time.
-
Bean namespacing to avoid collisions
- Many library beans now use explicit ds*-prefixed names (e.g., dsUserService, dsMailService, dsUserEmailService, dsUserDetailsService, dsLoginAttemptService, dsSessionInvalidationService, dsPasswordPolicyService, dsAuthorityService, dsRolePrivilegeSetupService, dsMailContentBuilder, dsUserAPI, dsGdprAPI, dsMfaAPI, dsWebAuthnManagementAPI, dsUserActionController, dsUserPageController). By-type injection is unaffected; update any by-name @Qualifier/@Resource/@dependsOn references.
-
Configuration model
- UserConfiguration is now @autoConfiguration and should not be directly @Import-ed or picked up by consumer @componentscan. Use the new opt-out properties for cross-cutting features as needed.
-
Event payloads no longer carry entities
- OnRegistrationCompleteEvent, UserPreDeleteEvent, and ConsentChangedEvent now carry ids/scalars (userId, userEmail, etc.) instead of live JPA User entities. Update listeners to use the new accessors and load entities as needed within a transaction.
-
Entity equals/hashCode semantics
- User, PasswordResetToken, VerificationToken, PasswordHistoryEntry, WebAuthnCredential, and WebAuthnUserEntity now use identity-based (id-only) equality (WebAuthnCredential/WebAuthnUserEntity use their assigned String keys).
- toString excludes collections/associations and secrets (passwords, tokens, keys).
- Avoid using unsaved (id == null) entities as Set/Map keys.
-
Package consolidation
- GlobalValidationExceptionHandler moved from com.digitalsanctuary.spring.user.exception to com.digitalsanctuary.spring.user.exceptions.
Refactoring
- Event data model refactor to id/DTO-based payloads to eliminate detached-entity hazards in async listeners. Adjusted RegistrationListener and UserEmailService to reload User by id.
- Replaced @DaTa on entities with explicit id-only equals/hashCode and safer toString (no secret leakage).
- Moved EnvironmentPostProcessor registration from spring.factories to the modern .imports file.
- Continued bean namespacing (ds* names) across remaining high-collision services and helpers.
Documentation
- Migration Guide (MIGRATION.md) expanded for 5.0.x:
- Reverse-proxy configuration (ACTION REQUIRED).
- Schema migrations for token and role/privilege uniqueness.
- Anti-enumeration response changes (including passwordless registration).
- Re-authentication requirements for credential changes.
- Lazy-loading guidance and remediation patterns.
- Entity id-based equality caveats.
- Event payload changes.
- Validation advice scope change.
- Package consolidation.
- Bean name namespaces.
- Auto-configuration toggles.
- CHANGELOG.md updated with all of the above and clarified scope of security hardening.
- README dependency instructions updated to 5.0.0, with a Spring Boot compatibility note.
Testing
- Added comprehensive tests:
- AppUrlResolver: trusted hosts, multi-valued forwarded headers, proto validation, port derivation, IPv6, context path, trailing slash behavior.
- WebAuthnManagementAPI: currentPassword requirement, lockout integration, success/denial paths; integration tests for delete/rename flows and error messaging.
- Registration anti-enumeration and guard behavior: uniform responses, event/audit expectations for both form and passwordless endpoints.
- MessageSourceEnvironmentPostProcessor: additive basename merge and message resolution across consumer and library bundles.
- Validation advice scope and behavior: ensures only library controllers are targeted; asserts 400 handling for class-level constraints.
- Role/Privilege setup: concurrency/idempotency behavior and uniqueness enforcement; guards for null/blank privilege names.
- Repository entity-graph: verifies single-query fetch of roles/privileges, no LazyInitializationException after detach, bounded query count.
- Entity equality/toString: id-only equality contract, exclusion of secrets from toString.
- GDPR export: reload with entity-graph to out...
4.4.0 - Security Hardening
Features
- Token security hardened: verification and password-reset tokens are now hashed at rest (HMAC-SHA-256 when user.security.tokenHashSecret is set; plain SHA-256 otherwise). Dual-read ensures pre-upgrade plaintext tokens still work until expiry. Only one active token per user is kept; generators delete any previous token before issuing a new one. New properties:
- user.security.tokenHashSecret
- user.security.passwordResetTokenValidityMinutes
- user.registration.verificationTokenValidityMinutes
- Session UX and security during password changes:
- New default behavior preserves the current session and regenerates its ID after a self‑service password change (or removing a password), invalidating only other sessions. Toggle with user.session.invalidation.keep-current-session-on-password-change (default true). Token-based resets still invalidate all sessions.
- Extensibility and override model:
- Security core beans (PasswordEncoder, SessionRegistry, RoleHierarchy, DaoAuthenticationProvider) moved to a dedicated auto-configuration with @ConditionalOnMissingBean so consumers can override them safely.
- AuditLogWriter and MailService moved to an auto-configuration and guarded by @ConditionalOnMissingBean; MailService created via a bean (still lifecycle-managed), allowing full replacement.
- SecurityFilterChain is now contributed via auto-configuration and ordered at low precedence; it coexists with additional, differently named consumer chains. Full replacement requires defining a bean named securityFilterChain.
- MFA improvements:
- Factor merging enabled using a public-API BeanPostProcessor that sets mfaEnabled=true on authentication-processing filters when user.mfa.enabled=true. This fixes the “second factor replaces first” issue by merging factor authorities across steps.
- When MFA is enabled, the configured factor entry-point URIs (passwordEntryPointUri and webauthnEntryPointUri) are auto-unprotected to prevent redirect loops.
- WebAuthn integration polish:
- Success handler now converts the principal to DSUserDetails and publishes a second InteractiveAuthenticationSuccessEvent with the converted auth so profile listeners run for passkey logins too.
- Audit logging improvements:
- Optional file rotation (maxFileSizeMb, maxFiles) and explicit durability tradeoffs documented; configurable flushOnWrite and flushRate; bounded query memory (ring buffer limited by user.audit.maxQueryResults).
- Log writer sanitizes CR/LF and pipe characters in all fields to prevent log forging and parsing issues.
- Mail delivery hardening:
- Dedicated bounded executor (dsMailExecutor) for @async mail to avoid starving the shared pool during SMTP stalls; configurable override by bean name.
- Mail is now genuinely optional: when no JavaMailSender is available, a single startup warning is logged and send operations are safely no‑ops (email-dependent features degrade gracefully).
- Session clustering: User, Role, Privilege are now Serializable, enabling distributed/persistent sessions (e.g., Spring Session).
Fixes
- Token replay under concurrency:
- Verification and password reset flows now use conditional DELETE-by-token as the atomic single-use guard. Exactly one concurrent caller can consume a token; losers see invalid/expired results. Expired tokens are also cleaned up on validate.
- GDPR deletion atomicity and event timing:
- The deletion runs inside a real @transactional boundary (self-proxied call) and publishes UserDeletedEvent only after commit via TransactionSynchronization.afterCommit. Failures during contributor cleanup roll back the entire operation and publish no event.
- Audit safety and visibility:
- Sanitize audit fields (strip CR/LF and ‘|’); corrected comments and defaults. Rotation defaults disabled to avoid hiding older events from readers until multi-file reading is implemented.
- Security filter-chain coexistence:
- Library filter chain no longer disappears when a consumer adds a second chain. The back-off is now by bean name (securityFilterChain), allowing additional chains to coexist. Regression tests added.
- OAuth2/OIDC hardening:
- Enforce account status uniformly: locked/disabled accounts are rejected on OAuth2/OIDC/WebAuthn paths (not just form login).
- Validate email_verified on Google, OIDC, and Facebook (both email_verified and verified claims); explicitly false rejects; absent claims are trusted.
- Failure handler no longer logs PII or raw messages; stores only a generic safe message in session; maps email_not_verified to a specific generic message; uses getSession(false) so scanners cannot force session creation.
- Session security:
- SessionRegistry is now wired into the filter chain; session invalidation and concurrent session counting work as intended.
- Programmatic login rotates the session ID to prevent fixation; logout now uses LogoutSuccessService so a Logout audit event is published.
- Registration robustness:
- Duplicate registration is serialized using SERIALIZABLE isolation and unique email constraint; race losers receive a UserAlreadyExistException instead of raw DB errors.
- Internal persist* methods now protected to ensure Spring proxies can override them across packages in Spring Framework 7 (fixes NPEs due to package-private methods not being proxied).
- Brute-force lockout correctness under load:
- Failed attempts increment is now an atomic DB UPDATE; prevents race conditions allowing attackers to avoid lockout.
- Thymeleaf templates and mail defaults:
- Removed test-only CSRF exemption from default properties; user.mail.fromAddress now defaults to blank with a startup warning if a sender exists but no from address is set.
- Passay upgrade compatibility:
- PasswordPolicyService adjusted for Passay 2.0 API and package changes.
- Authorities correctness:
- Granted authorities now include role names (e.g., ROLE_ADMIN) in addition to privilege names; hasRole() checks now work correctly.
Breaking Changes
- SecurityFilterChain override model:
- Changing the back-off to bean-name based means additional consumer chains will now coexist with the library chain. To fully replace the library chain, define a bean named securityFilterChain. Existing apps that relied on any chain suppressing the library chain may now see both chains active; adjust bean naming or reproduce desired rules as needed.
- UserEmailService constructor signature:
- Now requires a TokenHasher. If you subclass UserEmailService, update your constructor to accept and pass the TokenHasher to super().
- Passay upgrade to 2.0.0:
- Package relocations (rules under org.passay.rule, character data under org.passay.data), PasswordValidator API changes. If you depend on Passay directly, update imports and ensure you don’t downgrade passay via your BOM.
- Transactional behavior of user registration/password changes:
- Password hashing now happens outside DB transactions; DB writes run in short, separate transactions. As a result, registerNewUserAccount, changeUserPassword, and setInitialPassword no longer enlist in caller transactions. Outer transaction rollbacks won’t roll back the registration/password change; adjust flows if you previously depended on the old semantics.
- Default session handling on password change:
- New default preserves/regenerates current session and invalidates others. Set user.session.invalidation.keep-current-session-on-password-change=false to restore prior “invalidate everything” behavior.
Refactoring
- Moved MFA factor-merging BeanPostProcessor into a small, dependency-free MfaFilterMergingConfiguration to avoid early instantiation side-effects; extensive Javadoc added warning that enabling MFA flips mfaEnabled for all AbstractAuthenticationProcessingFilter beans (including consumer-defined filters).
- Moved unconditional security beans (methodSecurityExpressionHandler, HttpSessionEventPublisher, AuthenticationEventPublisher) into an auto-configuration and guarded with @ConditionalOnMissingBean for safe consumer override; relocated @EnableWebSecurity to the filter-chain auto-configuration.
Documentation
- MIGRATION.md updated:
- Name-based SecurityFilterChain replacement guidance.
- Passay 2.0 migration notes.
- UserEmailService TokenHasher change.
- New session handling default and toggle.
- Notes on hashing outside transactions and implications.
- CONFIG.md expanded:
- Audit durability, flush rates, rotation settings, bounded query scope.
- Mail executor details and override by bean name.
- JPA auditing opt-out (user.jpa.auditing.enabled) to avoid hijacking consumer auditing setup.
- PROFILE.md updated with warnings that @scope is not inherited and to use the provided @SessionScopedProfile meta-annotation.
Testing
- Concurrency and DB-integration:
- Token replay guard proven on PostgreSQL and MariaDB Testcontainers with two racing threads; exactly one consumption succeeds and token is removed.
- Duplicate registration serialization verified on PostgreSQL and MariaDB Testcontainers; one success, one UserAlreadyExistException, never two rows.
- GDPR deletion:
- After-commit behavior verified: event delivered only after deletion commits; rollback path publishes no event and keeps user.
- Web security:
- Authorization matrix tests for defaultAction=deny/allow and fail-closed on invalid values.
- CSRF filter behavior verified (protected URIs require token; exemptions work).
- Session invalidation integration test ensures SessionRegistry is populated after real login.
- Logout audit integration test asserts LogoutSuccessService is wired and publishes a Logout audit event.
- OAuth2/OIDC and WebAuthn:
- Failure handler sanitized behavior covered (generic messages, email_not_verified mapping, no forced session).
- Account-status enforcement tests for OAuth2/OIDC/WebAuthn paths.
- WebAuthn success publishes converted InteractiveAuthenticationSuccessEvent.
- User API (re-ena...
4.3.2
Features
- HTMX-aware AuthenticationEntryPoint for session expiry handling (#294)
- When HTMX requests (
HX-Request: trueheader) hit an expired session, the framework now returns a 401 JSON response with anHX-Redirectheader instead of the default 302 redirect that causes HTMX to swap login page HTML into fragment targets - New classes:
HtmxAwareAuthenticationEntryPoint,HtmxAwareAuthenticationEntryPointConfiguration WebSecurityConfignow always configuresexceptionHandling()with the injected entry point (previously only configured when OAuth2 was enabled)- Consumer override: define any
AuthenticationEntryPointbean to replace the default - Respects
server.servlet.context-pathfor non-root deployments - 100% backward-compatible: non-HTMX browser requests get the same 302 redirect as before
- When HTMX requests (
Maintenance
- Fix Groovy space-assignment deprecation in build.gradle (Gradle 10 compatibility)
- Suppress removal warning on deprecated method test in UserEmailServiceTest
Maven Central
<dependency>
<groupId>com.digitalsanctuary</groupId>
<artifactId>ds-spring-user-framework</artifactId>
<version>4.3.2</version>
</dependency>4.3.1
Bug Fixes
- WebAuthn MariaDB compatibility: Use
Length.LONG32forbyte[]columns (attestationObject,attestationClientDataJson,publicKey) to fix MariaDB row size limit errors when using inlineVARBINARY(#286, #287) - OAuth2 DSUserDetails attributes: Populate
DSUserDetails.getAttributes()from the OAuth2 provider response, fixing cases where attributes were empty after login (#285) - Dependencies: Remove redundant
webauthn4j-coredirect dependency that was already transitively included
Maintenance
- Lombok: Bump from 1.18.42 to 1.18.44
- Tests: Add Testcontainers schema validation tests for MariaDB and PostgreSQL to verify column type mappings
Full Changelog
4.3.0
What's New
RegistrationGuard SPI
A new Service Provider Interface for gating user registration across all registration paths (form, passwordless, OAuth2, OIDC). Implement RegistrationGuard to add custom pre-registration logic like invite-only access, domain whitelisting, or rate limiting.
RegistrationGuardinterface with@FunctionalInterfacesupport for lambda implementationsRegistrationContextrecord providing email, source, and provider nameRegistrationDecisionrecord withallow()/deny(reason)factory methods- Default permit-all guard auto-configured via
@ConditionalOnMissingBean - Full documentation in
REGISTRATION-GUARD.md
OIDC Service Alignment
Fixed four inconsistencies in DSOidcUserService to match DSOAuth2UserService behavior:
- Email normalization: Email lookup now uses
trim().toLowerCase(Locale.ROOT)beforefindByEmail(), preventing case-sensitive and locale-dependent mismatches - Audit events: New OIDC user registration now publishes an
"OIDC Registration Success"audit event - Transactional boundaries: Added
@Transactionalat class level for proper database operation handling - Login helper integration:
loadUser()now routes throughLoginHelperService.userLoginHelper()to updatelastActivityDateand run lockout checks
Additional Improvements
DSUserDetailsremains immutable — OIDC tokens are set via a newLoginHelperServiceoverload rather than mutable settersOAuth2Errornow includes the denial reason in the error description field for programmatic access byAuthenticationFailureHandlerimplementations- Audit events are published after
save()to prevent false-positive audit records if persistence fails RegistrationDecision.deny()usesString.isBlank()(Java 17+) and a namedDEFAULT_DENIAL_REASONconstant
Full Changelog
4.2.2
What's Changed
- chore(deps): bump com.webauthn4j:webauthn4j-core from 0.31.0.RELEASE to 0.31.1.RELEASE by @dependabot[bot] in #269
- chore(deps): bump nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect from 3.4.0 to 4.0.0 by @dependabot[bot] in #270
- feat: Wrap Spring Security 7 MFA in simple user.mfa.* properties by @devondragon in #272
- chore(deps): bump gradle-wrapper from 9.3.1 to 9.4.0 by @dependabot[bot] in #273
- Fix non-portable BLOB columnDefinition in WebAuthnCredential by @devondragon in #275
Full Changelog: 4.2.1...4.2.2
4.2.1 - Passwordless Passkey-Only Accounts
What's Changed
New Features
- feat: add passwordless passkey-only account support (#254) by @devondragon in #267
- feat(dev): add DevLoginAutoConfiguration for local development
Bug Fixes
- fix(auth): publish InteractiveAuthenticationSuccessEvent from authWithoutPassword()
- fix: remove dead code and document passwordless endpoint requirements
Improvements
- refactor: migrate @ConfigurationProperties from @component to @EnableConfigurationProperties (#264)
- refactor(security): extract WebAuthn ObjectPostProcessor into helper method (#261)
Dependencies
- chore(deps): bump org.springframework.boot from 4.0.2 to 4.0.3 (#266)
- chore(deps): bump com.webauthn4j:webauthn4j-core (#265)
Full Changelog: 4.2.0...4.2.1
4.2.0 - Passkey WebAuthn Support
What's Changed
- feat: add WebAuthn/Passkey authentication support by @devondragon in #256
- fix(webauthn): apply PR #256 review fixes by @devondragon in #258
- fix(webauthn): address PR #258 review feedback by @devondragon in #259
Full Changelog: 4.1.0...4.2.0