Ver Fonte

Prepopulate Username When Known

Closes gh-17935
Josh Cummings há 2 meses atrás
pai
commit
42376e2eee

+ 1 - 0
config/src/main/java/org/springframework/security/config/annotation/web/configurers/DefaultLoginPageConfigurer.java

@@ -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);

+ 55 - 3
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.
+	 * <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>
+			""";
+
 }

+ 28 - 0
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(
+				"""
+						<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(