You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
user.security.* is the only framework config area still wired as ~40 scattered @Value injections across 14 classes — every other area (MFA, WebAuthn, Captcha, GDPR, Audit, DevLogin, Roles) is already a typed @ConfigurationProperties. Consequences:
Defaults are split three ways and already drift: config/dsspringuserconfig.properties, inline @Value(":default") fragments, and 48 hand-maintained entries in additional-spring-configuration-metadata.json. Two live disagreements today: bcryptStrength (inline 10 vs shipped 12) and password.history-count (inline 0 vs shipped 3).
Config metadata is hand-maintained instead of generated.
Then: migrate all 14 internal @Value consumers to the beans; replace the 48 hand-maintained metadata entries with generated metadata; and expose a secret-free, immutable view record (URIs + copyrightFirstYear) to templates as @ModelAttribute("userSecurity"), mirroring CaptchaSiteKeyControllerAdvice, so consumers can drop ${@environment...}.
Constraints
Additive — zero config-key changes for consumers (relaxed binding maps the existing camelCase keys).
Do not trim the shipped dsspringuserconfig.properties — it is @PropertySource-loaded and therefore Environment-visible API; field initializers never reach the Environment.
Never expose the raw properties bean to templates (tokenHashSecret leak); @ToString.Exclude the secrets.
Key reorganization, password/passay redesign, and the demo app changes (a separate follow-up PR adopts Boot 4.1.0 and switches templates to ${userSecurity.*}, closing DemoApp#82).
Problem
user.security.*is the only framework config area still wired as ~40 scattered@Valueinjections across 14 classes — every other area (MFA, WebAuthn, Captcha, GDPR, Audit, DevLogin, Roles) is already a typed@ConfigurationProperties. Consequences:config/dsspringuserconfig.properties, inline@Value(":default")fragments, and 48 hand-maintained entries inadditional-spring-configuration-metadata.json. Two live disagreements today:bcryptStrength(inline 10 vs shipped 12) andpassword.history-count(inline 0 vs shipped 3).${@environment.getProperty(...)}), which Thymeleaf 3.1.5 (Spring Boot 4.1.0) rejects in the restricted expression context used duringthymeleaf-layout-dialectdecoration. This is what breaks the demo app — see Adopt Spring Boot 4.1.0: migrate ${@environment.getProperty(...)} template pattern for Thymeleaf 3.1.5 restricted expressions SpringUserFrameworkDemoApp#82.Approach (design approved)
Convert
user.security.*to a cohesive family of typed@ConfigurationProperties:UserSecurityConfigProperties(prefix="user.security")— flat URIs/action paths, URI lists, and scalars.PasswordPolicyConfigProperties(prefix="user.security.password")— 11 password-policy fields.RememberMeConfigProperties(prefix="user.security.remember-me")— 7 remember-me fields.Then: migrate all 14 internal
@Valueconsumers to the beans; replace the 48 hand-maintained metadata entries with generated metadata; and expose a secret-free, immutable view record (URIs +copyrightFirstYear) to templates as@ModelAttribute("userSecurity"), mirroringCaptchaSiteKeyControllerAdvice, so consumers can drop${@environment...}.Constraints
dsspringuserconfig.properties— it is@PropertySource-loaded and therefore Environment-visible API; field initializers never reach the Environment.tokenHashSecretleak);@ToString.Excludethe secrets.@Validated/JSR-380 (would change startup behavior; contradictsdefaultAction's deliberate runtime degrade-to-denyAll).Out of scope
Key reorganization, password/passay redesign, and the demo app changes (a separate follow-up PR adopts Boot 4.1.0 and switches templates to
${userSecurity.*}, closing DemoApp#82).Design doc
docs/design/2026-08-13-user-security-config-properties-design.md(branchfeature/user-security-config-properties).