Prepopulate Username When Known

Closes gh-17935
This commit is contained in:
Josh Cummings 2025-09-17 14:28:00 -06:00
parent e813aad82b
commit 42376e2eee
No known key found for this signature in database
GPG Key ID: 869B37A20E876129
3 changed files with 84 additions and 3 deletions

View File

@ -68,6 +68,7 @@ public final class DefaultLoginPageConfigurer<H extends HttpSecurityBuilder<H>>
@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);

View File

@ -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.
* <p>
* 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}}
<p>
<label for="username" class="screenreader">Username</label>
<input type="text" id="username" name="{{usernameParameter}}" placeholder="Username" required autofocus>
{{usernameInput}}
</p>
<p>
<label for="password" class="screenreader">Password</label>
@ -522,6 +558,14 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
<button type="submit" class="primary">Sign in</button>
</form>""";
private static final String FORM_READONLY_USERNAME_INPUT = """
<input type="text" id="username" name="{{usernameParameter}}" value="{{username}}" placeholder="Username" required readonly>
""";
private static final String FORM_USERNAME_INPUT = """
<input type="text" id="username" name="{{usernameParameter}}" placeholder="Username" required autofocus>
""";
private static final String HIDDEN_HTML_INPUT_TEMPLATE = """
<input name="{{name}}" type="hidden" value="{{value}}" />
""";
@ -554,11 +598,19 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
{{errorMessage}}{{logoutMessage}}
<p>
<label for="ott-username" class="screenreader">Username</label>
<input type="text" id="ott-username" name="username" placeholder="Username" required>
{{usernameInput}}
</p>
{{hiddenInputs}}
<button class="primary" type="submit" form="ott-form">Send Token</button>
</form>
""";
private static final String ONE_TIME_READONLY_USERNAME_INPUT = """
<input type="text" id="ott-username" name="username" value="{{username}}" placeholder="Username" required readonly>
""";
private static final String ONE_TIME_USERNAME_INPUT = """
<input type="text" id="ott-username" name="username" placeholder="Username" required>
""";
}

View File

@ -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(
"""
<input type="text" id="ott-username" name="username" value="user" placeholder="Username" required readonly>
""");
assertThat(response.getContentAsString()).contains("""
<input type="text" id="username" name="username" value="user" placeholder="Username" required readonly>
""");
}
@Test
void generatesThenRenders() throws ServletException, IOException {
DefaultLoginPageGeneratingFilter filter = new DefaultLoginPageGeneratingFilter(