From 42376e2eee825cf8a9c0f1bbae2d1a6cb9e2169b Mon Sep 17 00:00:00 2001 From: Josh Cummings <3627351+jzheaux@users.noreply.github.com> Date: Wed, 17 Sep 2025 14:28:00 -0600 Subject: [PATCH] Prepopulate Username When Known Closes gh-17935 --- .../DefaultLoginPageConfigurer.java | 1 + .../ui/DefaultLoginPageGeneratingFilter.java | 58 ++++++++++++++++++- ...DefaultLoginPageGeneratingFilterTests.java | 28 +++++++++ 3 files changed, 84 insertions(+), 3 deletions(-) diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/DefaultLoginPageConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/DefaultLoginPageConfigurer.java index 43d913f54a..dd73b14bf6 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/DefaultLoginPageConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/DefaultLoginPageConfigurer.java @@ -68,6 +68,7 @@ public final class DefaultLoginPageConfigurer> @Override public void init(H http) { + this.loginPageGeneratingFilter.setSecurityContextHolderStrategy(getSecurityContextHolderStrategy()); this.loginPageGeneratingFilter.setResolveHiddenInputs(DefaultLoginPageConfigurer.this::hiddenInputs); this.logoutPageGeneratingFilter.setResolveHiddenInputs(DefaultLoginPageConfigurer.this::hiddenInputs); http.setSharedObject(DefaultLoginPageGeneratingFilter.class, this.loginPageGeneratingFilter); diff --git a/web/src/main/java/org/springframework/security/web/authentication/ui/DefaultLoginPageGeneratingFilter.java b/web/src/main/java/org/springframework/security/web/authentication/ui/DefaultLoginPageGeneratingFilter.java index 05adc4b58b..e782deddbc 100644 --- a/web/src/main/java/org/springframework/security/web/authentication/ui/DefaultLoginPageGeneratingFilter.java +++ b/web/src/main/java/org/springframework/security/web/authentication/ui/DefaultLoginPageGeneratingFilter.java @@ -59,6 +59,9 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean { public static final String ERROR_PARAMETER_NAME = "error"; + private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder + .getContextHolderStrategy(); + private @Nullable String loginPageUrl; private @Nullable String logoutSuccessUrl; @@ -118,6 +121,18 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean { } } + /** + * Use this {@link SecurityContextHolderStrategy} to retrieve authenticated users. + *

+ * Uses {@link SecurityContextHolder#getContextHolderStrategy()} by default. + * @param securityContextHolderStrategy the strategy to use + * @since 7.0 + */ + public void setSecurityContextHolderStrategy(SecurityContextHolderStrategy securityContextHolderStrategy) { + Assert.notNull(securityContextHolderStrategy, "securityContextHolderStrategy cannot be null"); + this.securityContextHolderStrategy = securityContextHolderStrategy; + } + /** * Sets a Function used to resolve a Map of the hidden inputs where the key is the * name of the input and the value is the value of the input. Typically this is used @@ -307,6 +322,13 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean { return ""; } + String username = getUsername(); + String usernameInput = ((username != null) + ? HtmlTemplates.fromTemplate(FORM_READONLY_USERNAME_INPUT).withValue("username", username) + : HtmlTemplates.fromTemplate(FORM_USERNAME_INPUT)) + .withValue("usernameParameter", this.usernameParameter) + .render(); + String hiddenInputs = this.resolveHiddenInputs.apply(request) .entrySet() .stream() @@ -317,7 +339,7 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean { .withValue("loginUrl", contextPath + this.authenticationUrl) .withRawHtml("errorMessage", renderError(loginError, errorMsg)) .withRawHtml("logoutMessage", renderSuccess(logoutSuccess)) - .withValue("usernameParameter", this.usernameParameter) + .withRawHtml("usernameInput", usernameInput) .withValue("passwordParameter", this.passwordParameter) .withRawHtml("rememberMeInput", renderRememberMe(this.rememberMeParameter)) .withRawHtml("hiddenInputs", hiddenInputs) @@ -337,11 +359,17 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean { .map((inputKeyValue) -> renderHiddenInput(inputKeyValue.getKey(), inputKeyValue.getValue())) .collect(Collectors.joining("\n")); + String username = getUsername(); + String usernameInput = (username != null) + ? HtmlTemplates.fromTemplate(ONE_TIME_READONLY_USERNAME_INPUT).withValue("username", username).render() + : ONE_TIME_USERNAME_INPUT; + return HtmlTemplates.fromTemplate(ONE_TIME_TEMPLATE) .withValue("generateOneTimeTokenUrl", contextPath + this.generateOneTimeTokenUrl) .withRawHtml("errorMessage", renderError(loginError, errorMsg)) .withRawHtml("logoutMessage", renderSuccess(logoutSuccess)) .withRawHtml("hiddenInputs", hiddenInputs) + .withRawHtml("usernameInput", usernameInput) .render(); } @@ -410,6 +438,14 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean { .render(); } + private @Nullable String getUsername() { + Authentication authentication = this.securityContextHolderStrategy.getContext().getAuthentication(); + if (authentication != null && authentication.isAuthenticated()) { + return authentication.getName(); + } + return null; + } + private boolean isLogoutSuccess(HttpServletRequest request) { return this.logoutSuccessUrl != null && matches(request, this.logoutSuccessUrl); } @@ -511,7 +547,7 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean { {{errorMessage}}{{logoutMessage}}

- + {{usernameInput}}

@@ -522,6 +558,14 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean { """; + private static final String FORM_READONLY_USERNAME_INPUT = """ + + """; + + private static final String FORM_USERNAME_INPUT = """ + + """; + private static final String HIDDEN_HTML_INPUT_TEMPLATE = """ """; @@ -554,11 +598,19 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean { {{errorMessage}}{{logoutMessage}}

- + {{usernameInput}}

{{hiddenInputs}} """; + private static final String ONE_TIME_READONLY_USERNAME_INPUT = """ + + """; + + private static final String ONE_TIME_USERNAME_INPUT = """ + + """; + } diff --git a/web/src/test/java/org/springframework/security/web/authentication/DefaultLoginPageGeneratingFilterTests.java b/web/src/test/java/org/springframework/security/web/authentication/DefaultLoginPageGeneratingFilterTests.java index fc469f4f3c..ee38d177b0 100644 --- a/web/src/test/java/org/springframework/security/web/authentication/DefaultLoginPageGeneratingFilterTests.java +++ b/web/src/test/java/org/springframework/security/web/authentication/DefaultLoginPageGeneratingFilterTests.java @@ -26,11 +26,15 @@ import org.junit.jupiter.api.Test; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.TestAuthentication; +import org.springframework.security.core.context.SecurityContextHolderStrategy; +import org.springframework.security.core.context.SecurityContextImpl; import org.springframework.security.web.WebAttributes; import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter; import org.springframework.security.web.servlet.TestMockHttpServletRequests; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; /** @@ -246,6 +250,30 @@ public class DefaultLoginPageGeneratingFilterTests { assertThat(response.getContentAsString()).contains("Password"); } + @Test + public void generateWhenAuthenticatedThenReadOnlyUsername() throws Exception { + SecurityContextHolderStrategy strategy = mock(SecurityContextHolderStrategy.class); + DefaultLoginPageGeneratingFilter filter = new DefaultLoginPageGeneratingFilter(); + filter.setLoginPageUrl(DefaultLoginPageGeneratingFilter.DEFAULT_LOGIN_PAGE_URL); + filter.setFormLoginEnabled(true); + filter.setUsernameParameter("username"); + filter.setPasswordParameter("password"); + filter.setOneTimeTokenEnabled(true); + filter.setOneTimeTokenGenerationUrl("/ott/authenticate"); + filter.setSecurityContextHolderStrategy(strategy); + given(strategy.getContext()).willReturn(new SecurityContextImpl(TestAuthentication.authenticatedUser())); + MockHttpServletResponse response = new MockHttpServletResponse(); + filter.doFilter(TestMockHttpServletRequests.get("/login").build(), response, this.chain); + assertThat(response.getContentAsString()).contains("Request a One-Time Token"); + assertThat(response.getContentAsString()).contains( + """ + + """); + assertThat(response.getContentAsString()).contains(""" + + """); + } + @Test void generatesThenRenders() throws ServletException, IOException { DefaultLoginPageGeneratingFilter filter = new DefaultLoginPageGeneratingFilter(