Skip to content

Commit c8d3490

Browse files
committed
Prepopulate Username When Known
Closes gh-17935
1 parent aa9151a commit c8d3490

File tree

3 files changed

+76
-3
lines changed

3 files changed

+76
-3
lines changed

config/src/main/java/org/springframework/security/config/annotation/web/configurers/DefaultLoginPageConfigurer.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ public final class DefaultLoginPageConfigurer<H extends HttpSecurityBuilder<H>>
6868

6969
@Override
7070
public void init(H http) {
71+
this.loginPageGeneratingFilter.setSecurityContextHolderStrategy(getSecurityContextHolderStrategy());
7172
this.loginPageGeneratingFilter.setResolveHiddenInputs(DefaultLoginPageConfigurer.this::hiddenInputs);
7273
this.logoutPageGeneratingFilter.setResolveHiddenInputs(DefaultLoginPageConfigurer.this::hiddenInputs);
7374
http.setSharedObject(DefaultLoginPageGeneratingFilter.class, this.loginPageGeneratingFilter);

web/src/main/java/org/springframework/security/web/authentication/ui/DefaultLoginPageGeneratingFilter.java

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,9 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
5959

6060
public static final String ERROR_PARAMETER_NAME = "error";
6161

62+
private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder
63+
.getContextHolderStrategy();
64+
6265
private @Nullable String loginPageUrl;
6366

6467
private @Nullable String logoutSuccessUrl;
@@ -118,6 +121,10 @@ private void initAuthFilter(UsernamePasswordAuthenticationFilter authFilter) {
118121
}
119122
}
120123

124+
public void setSecurityContextHolderStrategy(SecurityContextHolderStrategy securityContextHolderStrategy) {
125+
this.securityContextHolderStrategy = securityContextHolderStrategy;
126+
}
127+
121128
/**
122129
* Sets a Function used to resolve a Map of the hidden inputs where the key is the
123130
* name of the input and the value is the value of the input. Typically this is used
@@ -307,6 +314,13 @@ private String renderFormLogin(HttpServletRequest request, boolean loginError, b
307314
return "";
308315
}
309316

317+
String username = getUsername();
318+
String usernameInput = ((username != null)
319+
? HtmlTemplates.fromTemplate(FORM_READONLY_USERNAME_INPUT).withValue("username", username)
320+
: HtmlTemplates.fromTemplate(FORM_USERNAME_INPUT))
321+
.withValue("usernameParameter", this.usernameParameter)
322+
.render();
323+
310324
String hiddenInputs = this.resolveHiddenInputs.apply(request)
311325
.entrySet()
312326
.stream()
@@ -317,7 +331,7 @@ private String renderFormLogin(HttpServletRequest request, boolean loginError, b
317331
.withValue("loginUrl", contextPath + this.authenticationUrl)
318332
.withRawHtml("errorMessage", renderError(loginError, errorMsg))
319333
.withRawHtml("logoutMessage", renderSuccess(logoutSuccess))
320-
.withValue("usernameParameter", this.usernameParameter)
334+
.withRawHtml("usernameInput", usernameInput)
321335
.withValue("passwordParameter", this.passwordParameter)
322336
.withRawHtml("rememberMeInput", renderRememberMe(this.rememberMeParameter))
323337
.withRawHtml("hiddenInputs", hiddenInputs)
@@ -337,11 +351,17 @@ private String renderOneTimeTokenLogin(HttpServletRequest request, boolean login
337351
.map((inputKeyValue) -> renderHiddenInput(inputKeyValue.getKey(), inputKeyValue.getValue()))
338352
.collect(Collectors.joining("\n"));
339353

354+
String username = getUsername();
355+
String usernameInput = (username != null)
356+
? HtmlTemplates.fromTemplate(ONE_TIME_READONLY_USERNAME_INPUT).withValue("username", username).render()
357+
: ONE_TIME_USERNAME_INPUT;
358+
340359
return HtmlTemplates.fromTemplate(ONE_TIME_TEMPLATE)
341360
.withValue("generateOneTimeTokenUrl", contextPath + this.generateOneTimeTokenUrl)
342361
.withRawHtml("errorMessage", renderError(loginError, errorMsg))
343362
.withRawHtml("logoutMessage", renderSuccess(logoutSuccess))
344363
.withRawHtml("hiddenInputs", hiddenInputs)
364+
.withRawHtml("usernameInput", usernameInput)
345365
.render();
346366
}
347367

@@ -410,6 +430,14 @@ private String renderRememberMe(@Nullable String paramName) {
410430
.render();
411431
}
412432

433+
private @Nullable String getUsername() {
434+
Authentication authentication = this.securityContextHolderStrategy.getContext().getAuthentication();
435+
if (authentication != null && authentication.isAuthenticated()) {
436+
return authentication.getName();
437+
}
438+
return null;
439+
}
440+
413441
private boolean isLogoutSuccess(HttpServletRequest request) {
414442
return this.logoutSuccessUrl != null && matches(request, this.logoutSuccessUrl);
415443
}
@@ -511,7 +539,7 @@ private boolean matches(HttpServletRequest request, @Nullable String url) {
511539
{{errorMessage}}{{logoutMessage}}
512540
<p>
513541
<label for="username" class="screenreader">Username</label>
514-
<input type="text" id="username" name="{{usernameParameter}}" placeholder="Username" required autofocus>
542+
{{usernameInput}}
515543
</p>
516544
<p>
517545
<label for="password" class="screenreader">Password</label>
@@ -522,6 +550,14 @@ private boolean matches(HttpServletRequest request, @Nullable String url) {
522550
<button type="submit" class="primary">Sign in</button>
523551
</form>""";
524552

553+
private static final String FORM_READONLY_USERNAME_INPUT = """
554+
<input type="text" id="username" name="{{usernameParameter}}" value="{{username}}" placeholder="Username" required readonly>
555+
""";
556+
557+
private static final String FORM_USERNAME_INPUT = """
558+
<input type="text" id="username" name="{{usernameParameter}}" placeholder="Username" required autofocus>
559+
""";
560+
525561
private static final String HIDDEN_HTML_INPUT_TEMPLATE = """
526562
<input name="{{name}}" type="hidden" value="{{value}}" />
527563
""";
@@ -554,11 +590,19 @@ private boolean matches(HttpServletRequest request, @Nullable String url) {
554590
{{errorMessage}}{{logoutMessage}}
555591
<p>
556592
<label for="ott-username" class="screenreader">Username</label>
557-
<input type="text" id="ott-username" name="username" placeholder="Username" required>
593+
{{usernameInput}}
558594
</p>
559595
{{hiddenInputs}}
560596
<button class="primary" type="submit" form="ott-form">Send Token</button>
561597
</form>
562598
""";
563599

600+
private static final String ONE_TIME_READONLY_USERNAME_INPUT = """
601+
<input type="text" id="ott-username" name="username" value="{{username}}" placeholder="Username" required readonly>
602+
""";
603+
604+
private static final String ONE_TIME_USERNAME_INPUT = """
605+
<input type="text" id="ott-username" name="username" placeholder="Username" required>
606+
""";
607+
564608
}

web/src/test/java/org/springframework/security/web/authentication/DefaultLoginPageGeneratingFilterTests.java

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,15 @@
2626
import org.springframework.mock.web.MockHttpServletRequest;
2727
import org.springframework.mock.web.MockHttpServletResponse;
2828
import org.springframework.security.authentication.BadCredentialsException;
29+
import org.springframework.security.authentication.TestAuthentication;
30+
import org.springframework.security.core.context.SecurityContextHolderStrategy;
31+
import org.springframework.security.core.context.SecurityContextImpl;
2932
import org.springframework.security.web.WebAttributes;
3033
import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter;
3134
import org.springframework.security.web.servlet.TestMockHttpServletRequests;
3235

3336
import static org.assertj.core.api.Assertions.assertThat;
37+
import static org.mockito.BDDMockito.given;
3438
import static org.mockito.Mockito.mock;
3539

3640
/**
@@ -246,6 +250,30 @@ public void generateWhenTwoAuthoritiesRequestedThenBothForms() throws Exception
246250
assertThat(response.getContentAsString()).contains("Password");
247251
}
248252

253+
@Test
254+
public void generateWhenAuthenticatedThenReadOnlyUsername() throws Exception {
255+
SecurityContextHolderStrategy strategy = mock(SecurityContextHolderStrategy.class);
256+
DefaultLoginPageGeneratingFilter filter = new DefaultLoginPageGeneratingFilter();
257+
filter.setLoginPageUrl(DefaultLoginPageGeneratingFilter.DEFAULT_LOGIN_PAGE_URL);
258+
filter.setFormLoginEnabled(true);
259+
filter.setUsernameParameter("username");
260+
filter.setPasswordParameter("password");
261+
filter.setOneTimeTokenEnabled(true);
262+
filter.setOneTimeTokenGenerationUrl("/ott/authenticate");
263+
filter.setSecurityContextHolderStrategy(strategy);
264+
given(strategy.getContext()).willReturn(new SecurityContextImpl(TestAuthentication.authenticatedUser()));
265+
MockHttpServletResponse response = new MockHttpServletResponse();
266+
filter.doFilter(TestMockHttpServletRequests.get("/login").build(), response, this.chain);
267+
assertThat(response.getContentAsString()).contains("Request a One-Time Token");
268+
assertThat(response.getContentAsString()).contains(
269+
"""
270+
<input type="text" id="ott-username" name="username" value="user" placeholder="Username" required readonly>
271+
""");
272+
assertThat(response.getContentAsString()).contains("""
273+
<input type="text" id="username" name="username" value="user" placeholder="Username" required readonly>
274+
""");
275+
}
276+
249277
@Test
250278
void generatesThenRenders() throws ServletException, IOException {
251279
DefaultLoginPageGeneratingFilter filter = new DefaultLoginPageGeneratingFilter(

0 commit comments

Comments
 (0)