Skip to content

Commit 42376e2

Browse files
committed
Prepopulate Username When Known
Closes spring-projectsgh-17935
1 parent e813aad commit 42376e2

File tree

3 files changed

+84
-3
lines changed

3 files changed

+84
-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: 55 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,18 @@ private void initAuthFilter(UsernamePasswordAuthenticationFilter authFilter) {
118121
}
119122
}
120123

124+
/**
125+
* Use this {@link SecurityContextHolderStrategy} to retrieve authenticated users.
126+
* <p>
127+
* Uses {@link SecurityContextHolder#getContextHolderStrategy()} by default.
128+
* @param securityContextHolderStrategy the strategy to use
129+
* @since 7.0
130+
*/
131+
public void setSecurityContextHolderStrategy(SecurityContextHolderStrategy securityContextHolderStrategy) {
132+
Assert.notNull(securityContextHolderStrategy, "securityContextHolderStrategy cannot be null");
133+
this.securityContextHolderStrategy = securityContextHolderStrategy;
134+
}
135+
121136
/**
122137
* Sets a Function used to resolve a Map of the hidden inputs where the key is the
123138
* name of the input and the value is the value of the input. Typically this is used
@@ -307,6 +322,13 @@ private String renderFormLogin(HttpServletRequest request, boolean loginError, b
307322
return "";
308323
}
309324

325+
String username = getUsername();
326+
String usernameInput = ((username != null)
327+
? HtmlTemplates.fromTemplate(FORM_READONLY_USERNAME_INPUT).withValue("username", username)
328+
: HtmlTemplates.fromTemplate(FORM_USERNAME_INPUT))
329+
.withValue("usernameParameter", this.usernameParameter)
330+
.render();
331+
310332
String hiddenInputs = this.resolveHiddenInputs.apply(request)
311333
.entrySet()
312334
.stream()
@@ -317,7 +339,7 @@ private String renderFormLogin(HttpServletRequest request, boolean loginError, b
317339
.withValue("loginUrl", contextPath + this.authenticationUrl)
318340
.withRawHtml("errorMessage", renderError(loginError, errorMsg))
319341
.withRawHtml("logoutMessage", renderSuccess(logoutSuccess))
320-
.withValue("usernameParameter", this.usernameParameter)
342+
.withRawHtml("usernameInput", usernameInput)
321343
.withValue("passwordParameter", this.passwordParameter)
322344
.withRawHtml("rememberMeInput", renderRememberMe(this.rememberMeParameter))
323345
.withRawHtml("hiddenInputs", hiddenInputs)
@@ -337,11 +359,17 @@ private String renderOneTimeTokenLogin(HttpServletRequest request, boolean login
337359
.map((inputKeyValue) -> renderHiddenInput(inputKeyValue.getKey(), inputKeyValue.getValue()))
338360
.collect(Collectors.joining("\n"));
339361

362+
String username = getUsername();
363+
String usernameInput = (username != null)
364+
? HtmlTemplates.fromTemplate(ONE_TIME_READONLY_USERNAME_INPUT).withValue("username", username).render()
365+
: ONE_TIME_USERNAME_INPUT;
366+
340367
return HtmlTemplates.fromTemplate(ONE_TIME_TEMPLATE)
341368
.withValue("generateOneTimeTokenUrl", contextPath + this.generateOneTimeTokenUrl)
342369
.withRawHtml("errorMessage", renderError(loginError, errorMsg))
343370
.withRawHtml("logoutMessage", renderSuccess(logoutSuccess))
344371
.withRawHtml("hiddenInputs", hiddenInputs)
372+
.withRawHtml("usernameInput", usernameInput)
345373
.render();
346374
}
347375

@@ -410,6 +438,14 @@ private String renderRememberMe(@Nullable String paramName) {
410438
.render();
411439
}
412440

441+
private @Nullable String getUsername() {
442+
Authentication authentication = this.securityContextHolderStrategy.getContext().getAuthentication();
443+
if (authentication != null && authentication.isAuthenticated()) {
444+
return authentication.getName();
445+
}
446+
return null;
447+
}
448+
413449
private boolean isLogoutSuccess(HttpServletRequest request) {
414450
return this.logoutSuccessUrl != null && matches(request, this.logoutSuccessUrl);
415451
}
@@ -511,7 +547,7 @@ private boolean matches(HttpServletRequest request, @Nullable String url) {
511547
{{errorMessage}}{{logoutMessage}}
512548
<p>
513549
<label for="username" class="screenreader">Username</label>
514-
<input type="text" id="username" name="{{usernameParameter}}" placeholder="Username" required autofocus>
550+
{{usernameInput}}
515551
</p>
516552
<p>
517553
<label for="password" class="screenreader">Password</label>
@@ -522,6 +558,14 @@ private boolean matches(HttpServletRequest request, @Nullable String url) {
522558
<button type="submit" class="primary">Sign in</button>
523559
</form>""";
524560

561+
private static final String FORM_READONLY_USERNAME_INPUT = """
562+
<input type="text" id="username" name="{{usernameParameter}}" value="{{username}}" placeholder="Username" required readonly>
563+
""";
564+
565+
private static final String FORM_USERNAME_INPUT = """
566+
<input type="text" id="username" name="{{usernameParameter}}" placeholder="Username" required autofocus>
567+
""";
568+
525569
private static final String HIDDEN_HTML_INPUT_TEMPLATE = """
526570
<input name="{{name}}" type="hidden" value="{{value}}" />
527571
""";
@@ -554,11 +598,19 @@ private boolean matches(HttpServletRequest request, @Nullable String url) {
554598
{{errorMessage}}{{logoutMessage}}
555599
<p>
556600
<label for="ott-username" class="screenreader">Username</label>
557-
<input type="text" id="ott-username" name="username" placeholder="Username" required>
601+
{{usernameInput}}
558602
</p>
559603
{{hiddenInputs}}
560604
<button class="primary" type="submit" form="ott-form">Send Token</button>
561605
</form>
562606
""";
563607

608+
private static final String ONE_TIME_READONLY_USERNAME_INPUT = """
609+
<input type="text" id="ott-username" name="username" value="{{username}}" placeholder="Username" required readonly>
610+
""";
611+
612+
private static final String ONE_TIME_USERNAME_INPUT = """
613+
<input type="text" id="ott-username" name="username" placeholder="Username" required>
614+
""";
615+
564616
}

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)