Add test for rendering "request token" form in OneTimeTokenLoginConfigurerTests
This commit is contained in:
		
							parent
							
								
									803c32eb4e
								
							
						
					
					
						commit
						6428bf2bd8
					
				| 
						 | 
				
			
			@ -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)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue