Add support for One-Time Token Login
Closes gh-15114
This commit is contained in:
@@ -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<>!@#"\" placeholder=\"Token\" required=\"true\" autofocus=\"autofocus\"/>");
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user