Browse Source

Add test for rendering "request token" form in OneTimeTokenLoginConfigurerTests

Daniel Garnier-Moiroux 11 months ago
parent
commit
6428bf2bd8

+ 212 - 0
config/src/test/java/org/springframework/security/config/annotation/web/configurers/ott/OneTimeTokenLoginConfigurerTests.java

@@ -41,12 +41,17 @@ import org.springframework.security.web.SecurityFilterChain;
 import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
 import org.springframework.security.web.authentication.ott.GeneratedOneTimeTokenHandler;
 import org.springframework.security.web.authentication.ott.RedirectGeneratedOneTimeTokenHandler;
+import org.springframework.security.web.csrf.CsrfToken;
+import org.springframework.security.web.csrf.DefaultCsrfToken;
+import org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository;
 import org.springframework.test.web.servlet.MockMvc;
 
+import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatException;
 import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
 import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated;
 import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.unauthenticated;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
 import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl;
 import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@@ -59,6 +64,143 @@ public class OneTimeTokenLoginConfigurerTests {
 	@Autowired(required = false)
 	MockMvc mvc;
 
+	public static final String EXPECTED_HTML_HEAD = """
+			<!DOCTYPE html>
+			<html lang="en">
+			  <head>
+			    <meta charset="utf-8">
+			    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
+			    <meta name="description" content="">
+			    <meta name="author" content="">
+			    <title>Please sign in</title>
+			    <style>
+			    /* General layout */
+			    body {
+			      font-family: system-ui, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
+			      background-color: #eee;
+			      padding: 40px 0;
+			      margin: 0;
+			      line-height: 1.5;
+			    }
+			\s\s\s\s
+			    h2 {
+			      margin-top: 0;
+			      margin-bottom: 0.5rem;
+			      font-size: 2rem;
+			      font-weight: 500;
+			      line-height: 2rem;
+			    }
+			\s\s\s\s
+			    .content {
+			      margin-right: auto;
+			      margin-left: auto;
+			      padding-right: 15px;
+			      padding-left: 15px;
+			      width: 100%;
+			      box-sizing: border-box;
+			    }
+			\s\s\s\s
+			    @media (min-width: 800px) {
+			      .content {
+			        max-width: 760px;
+			      }
+			    }
+			\s\s\s\s
+			    /* Components */
+			    a,
+			    a:visited {
+			      text-decoration: none;
+			      color: #06f;
+			    }
+			\s\s\s\s
+			    a:hover {
+			      text-decoration: underline;
+			      color: #003c97;
+			    }
+			\s\s\s\s
+			    input[type="text"],
+			    input[type="password"] {
+			      height: auto;
+			      width: 100%;
+			      font-size: 1rem;
+			      padding: 0.5rem;
+			      box-sizing: border-box;
+			    }
+			\s\s\s\s
+			    button {
+			      padding: 0.5rem 1rem;
+			      font-size: 1.25rem;
+			      line-height: 1.5;
+			      border: none;
+			      border-radius: 0.1rem;
+			      width: 100%;
+			    }
+			\s\s\s\s
+			    button.primary {
+			      color: #fff;
+			      background-color: #06f;
+			    }
+			\s\s\s\s
+			    .alert {
+			      padding: 0.75rem 1rem;
+			      margin-bottom: 1rem;
+			      line-height: 1.5;
+			      border-radius: 0.1rem;
+			      width: 100%;
+			      box-sizing: border-box;
+			      border-width: 1px;
+			      border-style: solid;
+			    }
+			\s\s\s\s
+			    .alert.alert-danger {
+			      color: #6b1922;
+			      background-color: #f7d5d7;
+			      border-color: #eab6bb;
+			    }
+			\s\s\s\s
+			    .alert.alert-success {
+			      color: #145222;
+			      background-color: #d1f0d9;
+			      border-color: #c2ebcb;
+			    }
+			\s\s\s\s
+			    .screenreader {
+			      position: absolute;
+			      clip: rect(0 0 0 0);
+			      height: 1px;
+			      width: 1px;
+			      padding: 0;
+			      border: 0;
+			      overflow: hidden;
+			    }
+			\s\s\s\s
+			    table {
+			      width: 100%;
+			      max-width: 100%;
+			      margin-bottom: 2rem;
+			    }
+			\s\s\s\s
+			    .table-striped tr:nth-of-type(2n + 1) {
+			      background-color: #e1e1e1;
+			    }
+			\s\s\s\s
+			    td {
+			      padding: 0.75rem;
+			      vertical-align: top;
+			    }
+			\s\s\s\s
+			    /* Login / logout layouts */
+			    .login-form,
+			    .logout-form {
+			      max-width: 340px;
+			      padding: 0 15px 15px 15px;
+			      margin: 0 auto 2rem auto;
+			      box-sizing: border-box;
+			    }
+			    </style>
+			  </head>
+			""";
+
 	@Test
 	void oneTimeTokenWhenCorrectTokenThenCanAuthenticate() throws Exception {
 		this.spring.register(OneTimeTokenDefaultConfig.class).autowire();
@@ -110,6 +252,54 @@ public class OneTimeTokenLoginConfigurerTests {
 			.andExpectAll(status().isFound(), redirectedUrl("/login?error"), unauthenticated());
 	}
 
+	@Test
+	void oneTimeTokenWhenFormLoginConfiguredThenRendersRequestTokenForm() throws Exception {
+		this.spring.register(OneTimeTokenFormLoginConfig.class).autowire();
+		CsrfToken csrfToken = new DefaultCsrfToken("X-CSRF-TOKEN", "_csrf", "BaseSpringSpec_CSRFTOKEN");
+		String csrfAttributeName = HttpSessionCsrfTokenRepository.class.getName().concat(".CSRF_TOKEN");
+		//@formatter:off
+		this.mvc.perform(get("/login").sessionAttr(csrfAttributeName, csrfToken))
+				.andExpect((result) -> {
+					CsrfToken token = (CsrfToken) result.getRequest().getAttribute(CsrfToken.class.getName());
+					assertThat(result.getResponse().getContentAsString()).isEqualTo(
+						EXPECTED_HTML_HEAD +
+						"""
+						  <body>
+						    <div class="content">
+						      <form class="login-form" method="post" action="/login">
+						        <h2>Please sign in</h2>
+						       \s
+						        <p>
+						          <label for="username" class="screenreader">Username</label>
+						          <input type="text" id="username" name="username" placeholder="Username" required autofocus>
+						        </p>
+						        <p>
+						          <label for="password" class="screenreader">Password</label>
+						          <input type="password" id="password" name="password" placeholder="Password" required>
+						        </p>
+
+						<input name="_csrf" type="hidden" value="%s" />
+						        <button type="submit" class="primary">Sign in</button>
+						      </form>
+						      <form id="ott-form" class="login-form" method="post" action="/ott/generate">
+						        <h2>Request a One-Time Token</h2>
+						     \s
+						        <p>
+						          <label for="ott-username" class="screenreader">Username</label>
+						          <input type="text" id="ott-username" name="username" placeholder="Username" required>
+						        </p>
+						      <input name="_csrf" type="hidden" value="%s" />
+						        <button class="primary" type="submit" form="ott-form">Send Token</button>
+						      </form>
+
+
+						    </div>
+						  </body>
+						</html>""".formatted(token.getToken(), token.getToken()));
+				});
+		//@formatter:on
+	}
+
 	@Test
 	void oneTimeTokenWhenNoGeneratedOneTimeTokenHandlerThenException() {
 		assertThatException()
@@ -167,6 +357,28 @@ public class OneTimeTokenLoginConfigurerTests {
 
 	}
 
+	@Configuration(proxyBeanMethods = false)
+	@EnableWebSecurity
+	@Import(UserDetailsServiceConfig.class)
+	static class OneTimeTokenFormLoginConfig {
+
+		@Bean
+		SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
+			// @formatter:off
+			http
+					.authorizeHttpRequests((authz) -> authz
+							.anyRequest().authenticated()
+					)
+					.formLogin(Customizer.withDefaults())
+					.oneTimeTokenLogin((ott) -> ott
+							.generatedOneTimeTokenHandler(new TestGeneratedOneTimeTokenHandler())
+					);
+			// @formatter:on
+			return http.build();
+		}
+
+	}
+
 	@Configuration(proxyBeanMethods = false)
 	@EnableWebSecurity
 	@Import(UserDetailsServiceConfig.class)