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

View File

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

View File

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

View File

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

View File

@@ -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\"/>");
}
}