1
0
mirror of synced 2026-08-04 09:17:02 +00:00

Add support for One-Time Token Login

Closes gh-15114
This commit is contained in:
Marcus Hert Da Coregio
2024-07-18 09:37:03 -03:00
parent 5c56bddbdd
commit 00e4a8fb54
28 changed files with 2116 additions and 2 deletions
@@ -0,0 +1,94 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.authentication.ott;
import java.io.IOException;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.ott.GenerateOneTimeTokenRequest;
import org.springframework.security.authentication.ott.OneTimeToken;
import org.springframework.security.authentication.ott.OneTimeTokenService;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import static org.springframework.security.web.util.matcher.AntPathRequestMatcher.antMatcher;
/**
* Filter that process a One-Time Token generation request.
*
* @author Marcus da Coregio
* @since 6.4
* @see OneTimeTokenService
*/
public final class GenerateOneTimeTokenFilter extends OncePerRequestFilter {
private final OneTimeTokenService oneTimeTokenService;
private RequestMatcher requestMatcher = antMatcher(HttpMethod.POST, "/ott/generate");
private GeneratedOneTimeTokenHandler generatedOneTimeTokenHandler = new RedirectGeneratedOneTimeTokenHandler(
"/login/ott");
public GenerateOneTimeTokenFilter(OneTimeTokenService oneTimeTokenService) {
Assert.notNull(oneTimeTokenService, "oneTimeTokenService cannot be null");
this.oneTimeTokenService = oneTimeTokenService;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
if (!this.requestMatcher.matches(request)) {
filterChain.doFilter(request, response);
return;
}
String username = request.getParameter("username");
if (!StringUtils.hasText(username)) {
filterChain.doFilter(request, response);
return;
}
GenerateOneTimeTokenRequest generateRequest = new GenerateOneTimeTokenRequest(username);
OneTimeToken ott = this.oneTimeTokenService.generate(generateRequest);
this.generatedOneTimeTokenHandler.handle(request, response, ott);
}
/**
* Use the given {@link RequestMatcher} to match the request.
* @param requestMatcher
*/
public void setRequestMatcher(RequestMatcher requestMatcher) {
Assert.notNull(requestMatcher, "requestMatcher cannot be null");
this.requestMatcher = requestMatcher;
}
/**
* Specifies {@link GeneratedOneTimeTokenHandler} to be used to handle generated
* one-time tokens
* @param generatedOneTimeTokenHandler
*/
public void setGeneratedOneTimeTokenHandler(GeneratedOneTimeTokenHandler generatedOneTimeTokenHandler) {
Assert.notNull(generatedOneTimeTokenHandler, "generatedOneTimeTokenHandler cannot be null");
this.generatedOneTimeTokenHandler = generatedOneTimeTokenHandler;
}
}
@@ -0,0 +1,42 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.authentication.ott;
import java.io.IOException;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.ott.OneTimeToken;
/**
* Defines a strategy to handle generated one-time tokens.
*
* @author Marcus da Coregio
* @since 6.4
*/
@FunctionalInterface
public interface GeneratedOneTimeTokenHandler {
/**
* Handles generated one-time tokens
*/
void handle(HttpServletRequest request, HttpServletResponse response, OneTimeToken oneTimeToken)
throws IOException, ServletException;
}
@@ -0,0 +1,51 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.authentication.ott;
import jakarta.servlet.http.HttpServletRequest;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.security.authentication.ott.OneTimeTokenAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.util.StringUtils;
/**
* An implementation of {@link AuthenticationConverter} that detects if the request
* contains a {@code token} parameter and constructs a
* {@link OneTimeTokenAuthenticationToken} with it.
*
* @author Marcus da Coregio
* @since 6.4
* @see GenerateOneTimeTokenFilter
*/
public class OneTimeTokenAuthenticationConverter implements AuthenticationConverter {
private final Log logger = LogFactory.getLog(getClass());
@Override
public Authentication convert(HttpServletRequest request) {
String token = request.getParameter("token");
if (!StringUtils.hasText(token)) {
this.logger.debug("No token found in request");
return null;
}
return OneTimeTokenAuthenticationToken.unauthenticated(token);
}
}
@@ -0,0 +1,56 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.authentication.ott;
import java.io.IOException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.ott.OneTimeToken;
import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.RedirectStrategy;
import org.springframework.util.Assert;
/**
* A {@link GeneratedOneTimeTokenHandler} that performs a redirect to a specific location
*
* @author Marcus da Coregio
* @since 6.4
*/
public final class RedirectGeneratedOneTimeTokenHandler implements GeneratedOneTimeTokenHandler {
private final RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
private final String redirectUrl;
/**
* Constructs an instance of this class that redirects to the specified URL.
* @param redirectUrl
*/
public RedirectGeneratedOneTimeTokenHandler(String redirectUrl) {
Assert.hasText(redirectUrl, "redirectUrl cannot be empty or null");
this.redirectUrl = redirectUrl;
}
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, OneTimeToken oneTimeToken)
throws IOException {
this.redirectStrategy.sendRedirect(request, response, this.redirectUrl);
}
}
@@ -68,8 +68,12 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
private boolean saml2LoginEnabled;
private boolean oneTimeTokenEnabled;
private String authenticationUrl;
private String generateOneTimeTokenUrl;
private String usernameParameter;
private String passwordParameter;
@@ -142,6 +146,10 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
this.oauth2LoginEnabled = oauth2LoginEnabled;
}
public void setOneTimeTokenEnabled(boolean oneTimeTokenEnabled) {
this.oneTimeTokenEnabled = oneTimeTokenEnabled;
}
public void setSaml2LoginEnabled(boolean saml2LoginEnabled) {
this.saml2LoginEnabled = saml2LoginEnabled;
}
@@ -150,6 +158,10 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
this.authenticationUrl = authenticationUrl;
}
public void setGenerateOneTimeTokenUrl(String generateOneTimeTokenUrl) {
this.generateOneTimeTokenUrl = generateOneTimeTokenUrl;
}
public void setUsernameParameter(String usernameParameter) {
this.usernameParameter = usernameParameter;
}
@@ -224,6 +236,19 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
sb.append(" <button type=\"submit\" class=\"primary\">Sign in</button>\n");
sb.append(" </form>\n");
}
if (this.oneTimeTokenEnabled) {
sb.append(" <form id=\"ott-form\" class=\"login-form\" method=\"post\" action=\"" + contextPath
+ this.generateOneTimeTokenUrl + "\">\n");
sb.append(" <h2>Request a One-Time Token</h2>\n");
sb.append(createError(loginError, errorMsg) + createLogoutSuccess(logoutSuccess) + "<p>\n");
sb.append(" <label for=\"ott-username\" class=\"screenreader\">Username</label>\n");
sb.append(" <input type=\"text\" id=\"ott-username\" name=\"" + this.usernameParameter
+ "\" placeholder=\"Username\" required>\n");
sb.append(" </p>\n");
sb.append(renderHiddenInputs(request));
sb.append(" <button class=\"primary\" type=\"submit\" form=\"ott-form\">Send Token</button>\n");
sb.append(" </form>\n");
}
if (this.oauth2LoginEnabled) {
sb.append("<h2>Login with OAuth 2.0</h2>");
sb.append(createError(loginError, errorMsg));
@@ -0,0 +1,138 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.authentication.ui;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.Map;
import java.util.function.Function;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.web.util.CssUtils;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.HtmlUtils;
/**
* Creates a default one-time token submit page. If the request contains a {@code token}
* query param the page will automatically fill the form with the token value.
*
* @author Marcus da Coregio
* @since 6.4
*/
public final class DefaultOneTimeTokenSubmitPageGeneratingFilter extends OncePerRequestFilter {
private RequestMatcher requestMatcher = new AntPathRequestMatcher("/login/ott", "GET");
private Function<HttpServletRequest, Map<String, String>> resolveHiddenInputs = (request) -> Collections.emptyMap();
private String loginProcessingUrl = "/login/ott";
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
if (!this.requestMatcher.matches(request)) {
filterChain.doFilter(request, response);
return;
}
String html = generateHtml(request);
response.setContentType("text/html;charset=UTF-8");
response.setContentLength(html.getBytes(StandardCharsets.UTF_8).length);
response.getWriter().write(html);
}
private String generateHtml(HttpServletRequest request) {
String token = request.getParameter("token");
String inputValue = StringUtils.hasText(token) ? HtmlUtils.htmlEscape(token) : "";
String input = "<input type=\"text\" id=\"token\" name=\"token\" value=\"" + inputValue + "\""
+ " placeholder=\"Token\" required=\"true\" autofocus=\"autofocus\"/>";
return """
<!DOCTYPE html>
<html lang="en">
<head>
<title>One-Time Token Login</title>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"/>
<meta http-equiv="Content-Security-Policy" content="script-src 'sha256-oZhLbc2kO8b8oaYLrUc7uye1MgVKMyLtPqWR4WtKF+c='"/>
"""
+ CssUtils.getCssStyleBlock().indent(4)
+ """
</head>
<body>
<noscript>
<p>
<strong>Note:</strong> Since your browser does not support JavaScript, you must press the Sign In button once to proceed.
</p>
</noscript>
<div class="container">
"""
+ "<form class=\"login-form\" action=\"" + this.loginProcessingUrl + "\" method=\"post\">" + """
<h2>Please input the token</h2>
<p>
<label for="token" class="screenreader">Token</label>
""" + input + """
</p>
<button class="primary" type="submit">Sign in</button>
""" + renderHiddenInputs(request) + """
</form>
</div>
</body>
</html>
""";
}
private String renderHiddenInputs(HttpServletRequest request) {
StringBuilder sb = new StringBuilder();
for (Map.Entry<String, String> input : this.resolveHiddenInputs.apply(request).entrySet()) {
sb.append("<input name=\"");
sb.append(input.getKey());
sb.append("\" type=\"hidden\" value=\"");
sb.append(input.getValue());
sb.append("\" />\n");
}
return sb.toString();
}
public void setResolveHiddenInputs(Function<HttpServletRequest, Map<String, String>> resolveHiddenInputs) {
Assert.notNull(resolveHiddenInputs, "resolveHiddenInputs cannot be null");
this.resolveHiddenInputs = resolveHiddenInputs;
}
public void setRequestMatcher(RequestMatcher requestMatcher) {
Assert.notNull(requestMatcher, "requestMatcher cannot be null");
this.requestMatcher = requestMatcher;
}
/**
* Specifies the URL that the submit form should POST to. Defaults to
* {@code /login/ott}.
* @param loginProcessingUrl
*/
public void setLoginProcessingUrl(String loginProcessingUrl) {
Assert.hasText(loginProcessingUrl, "loginProcessingUrl cannot be null or empty");
this.loginProcessingUrl = loginProcessingUrl;
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -185,4 +185,25 @@ public class DefaultLoginPageGeneratingFilterTests {
assertThat(response.getContentAsString()).contains("Invalid credentials");
}
@Test
public void generateWhenOneTimeTokenLoginThenOttForm() throws Exception {
DefaultLoginPageGeneratingFilter filter = new DefaultLoginPageGeneratingFilter();
filter.setLoginPageUrl(DefaultLoginPageGeneratingFilter.DEFAULT_LOGIN_PAGE_URL);
filter.setOneTimeTokenEnabled(true);
filter.setGenerateOneTimeTokenUrl("/ott/authenticate");
MockHttpServletResponse response = new MockHttpServletResponse();
filter.doFilter(new MockHttpServletRequest("GET", "/login"), response, this.chain);
assertThat(response.getContentAsString()).contains("Request a One-Time Token");
assertThat(response.getContentAsString()).contains("""
<form id="ott-form" class="login-form" method="post" action="/ott/authenticate">
<h2>Request a One-Time Token</h2>
<p>
<label for="ott-username" class="screenreader">Username</label>
<input type="text" id="ott-username" name="null" placeholder="Username" required>
</p>
<button class="primary" type="submit" form="ott-form">Send Token</button>
</form>
""");
}
}
@@ -0,0 +1,72 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.authentication.ott;
import org.junit.jupiter.api.Test;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.security.authentication.ott.OneTimeTokenAuthenticationToken;
import org.springframework.security.core.Authentication;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link OneTimeTokenAuthenticationConverter}
*
* @author Marcus da Coregio
*/
class OneTimeTokenAuthenticationConverterTests {
private final OneTimeTokenAuthenticationConverter converter = new OneTimeTokenAuthenticationConverter();
@Test
void convertWhenTokenParameterThenReturnOneTimeTokenAuthenticationToken() {
MockHttpServletRequest request = new MockHttpServletRequest();
request.setParameter("token", "1234");
OneTimeTokenAuthenticationToken authentication = (OneTimeTokenAuthenticationToken) this.converter
.convert(request);
assertThat(authentication).isNotNull();
assertThat(authentication.getTokenValue()).isEqualTo("1234");
assertThat(authentication.getPrincipal()).isNull();
}
@Test
void convertWhenTokenAndUsernameParameterThenReturnOneTimeTokenAuthenticationTokenWithUsername() {
MockHttpServletRequest request = new MockHttpServletRequest();
request.setParameter("token", "1234");
OneTimeTokenAuthenticationToken authentication = (OneTimeTokenAuthenticationToken) this.converter
.convert(request);
assertThat(authentication).isNotNull();
assertThat(authentication.getTokenValue()).isEqualTo("1234");
}
@Test
void convertWhenOnlyUsernameParameterThenReturnNull() {
MockHttpServletRequest request = new MockHttpServletRequest();
request.setParameter("username", "josh");
OneTimeTokenAuthenticationToken authentication = (OneTimeTokenAuthenticationToken) this.converter
.convert(request);
assertThat(authentication).isNull();
}
@Test
void convertWhenNoTokenParameterThenNull() {
Authentication authentication = this.converter.convert(new MockHttpServletRequest());
assertThat(authentication).isNull();
}
}
@@ -0,0 +1,62 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.authentication.ott;
import java.io.IOException;
import java.time.Instant;
import org.junit.jupiter.api.Test;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.security.authentication.ott.DefaultOneTimeToken;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
/**
* Tests for {@link RedirectGeneratedOneTimeTokenHandler}
*
* @author Marcus da Coregio
*/
class RedirectGeneratedOneTimeTokenHandlerTests {
@Test
void handleThenRedirectToDefaultLocation() throws IOException {
RedirectGeneratedOneTimeTokenHandler handler = new RedirectGeneratedOneTimeTokenHandler("/login/ott");
MockHttpServletResponse response = new MockHttpServletResponse();
handler.handle(new MockHttpServletRequest(), response, new DefaultOneTimeToken("token", "user", Instant.now()));
assertThat(response.getRedirectedUrl()).isEqualTo("/login/ott");
}
@Test
void handleWhenUrlChangedThenRedirectToUrl() throws IOException {
MockHttpServletResponse response = new MockHttpServletResponse();
RedirectGeneratedOneTimeTokenHandler handler = new RedirectGeneratedOneTimeTokenHandler("/redirected");
handler.handle(new MockHttpServletRequest(), response, new DefaultOneTimeToken("token", "user", Instant.now()));
assertThat(response.getRedirectedUrl()).isEqualTo("/redirected");
}
@Test
void setRedirectUrlWhenNullOrEmptyThenException() {
assertThatIllegalArgumentException().isThrownBy(() -> new RedirectGeneratedOneTimeTokenHandler(null))
.withMessage("redirectUrl cannot be empty or null");
assertThatIllegalArgumentException().isThrownBy(() -> new RedirectGeneratedOneTimeTokenHandler(""))
.withMessage("redirectUrl cannot be empty or null");
}
}
@@ -0,0 +1,88 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.authentication.ui;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.mock.web.MockFilterChain;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
/**
* Tests for {@link DefaultOneTimeTokenSubmitPageGeneratingFilter}
*
* @author Marcus da Coregio
*/
class DefaultOneTimeTokenSubmitPageGeneratingFilterTests {
DefaultOneTimeTokenSubmitPageGeneratingFilter filter = new DefaultOneTimeTokenSubmitPageGeneratingFilter();
MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();
MockFilterChain filterChain = new MockFilterChain();
@BeforeEach
void setup() {
this.request.setMethod("GET");
this.request.setServletPath("/login/ott");
}
@Test
void filterWhenTokenQueryParamThenShouldIncludeJavascriptToAutoSubmitFormAndInputHasTokenValue() throws Exception {
this.request.setParameter("token", "1234");
this.filter.doFilterInternal(this.request, this.response, this.filterChain);
String response = this.response.getContentAsString();
assertThat(response).contains(
"<input type=\"text\" id=\"token\" name=\"token\" value=\"1234\" placeholder=\"Token\" required=\"true\" autofocus=\"autofocus\"/>");
}
@Test
void setRequestMatcherWhenNullThenException() {
assertThatIllegalArgumentException().isThrownBy(() -> this.filter.setRequestMatcher(null));
}
@Test
void setLoginProcessingUrlWhenNullOrEmptyThenException() {
assertThatIllegalArgumentException().isThrownBy(() -> this.filter.setLoginProcessingUrl(null));
assertThatIllegalArgumentException().isThrownBy(() -> this.filter.setLoginProcessingUrl(""));
}
@Test
void setLoginProcessingUrlThenUseItForFormAction() throws Exception {
this.filter.setLoginProcessingUrl("/login/another");
this.filter.doFilterInternal(this.request, this.response, this.filterChain);
String response = this.response.getContentAsString();
assertThat(response).contains(
"<form class=\"login-form\" action=\"/login/another\" method=\"post\">\t<h2>Please input the token</h2>");
}
@Test
void filterWhenTokenQueryParamUsesSpecialCharactersThenValueIsEscaped() throws Exception {
this.request.setParameter("token", "this<>!@#\"");
this.filter.doFilterInternal(this.request, this.response, this.filterChain);
String response = this.response.getContentAsString();
assertThat(response).contains(
"<input type=\"text\" id=\"token\" name=\"token\" value=\"this&lt;&gt;!@#&quot;\" placeholder=\"Token\" required=\"true\" autofocus=\"autofocus\"/>");
}
}