mirror of
https://github.com/spring-projects/spring-security.git
synced 2025-07-02 16:52:14 +00:00
Introduce OneTimeTokenAuthenticationFilter
closes gh-16539 Signed-off-by: Daniel Garnier-Moiroux <git@garnier.wf>
This commit is contained in:
parent
8e2a4bf356
commit
5ee6b83953
@ -31,6 +31,7 @@ import org.springframework.security.web.authentication.AuthenticationFilter;
|
|||||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||||
import org.springframework.security.web.authentication.logout.LogoutFilter;
|
import org.springframework.security.web.authentication.logout.LogoutFilter;
|
||||||
import org.springframework.security.web.authentication.ott.GenerateOneTimeTokenFilter;
|
import org.springframework.security.web.authentication.ott.GenerateOneTimeTokenFilter;
|
||||||
|
import org.springframework.security.web.authentication.ott.OneTimeTokenAuthenticationFilter;
|
||||||
import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;
|
import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;
|
||||||
import org.springframework.security.web.authentication.preauth.x509.X509AuthenticationFilter;
|
import org.springframework.security.web.authentication.preauth.x509.X509AuthenticationFilter;
|
||||||
import org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter;
|
import org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter;
|
||||||
@ -101,6 +102,7 @@ final class FilterOrderRegistration {
|
|||||||
"org.springframework.security.saml2.provider.service.web.authentication.Saml2WebSsoAuthenticationFilter",
|
"org.springframework.security.saml2.provider.service.web.authentication.Saml2WebSsoAuthenticationFilter",
|
||||||
order.next());
|
order.next());
|
||||||
put(UsernamePasswordAuthenticationFilter.class, order.next());
|
put(UsernamePasswordAuthenticationFilter.class, order.next());
|
||||||
|
put(OneTimeTokenAuthenticationFilter.class, order.next());
|
||||||
order.next(); // gh-8105
|
order.next(); // gh-8105
|
||||||
put(DefaultResourcesFilter.class, order.next());
|
put(DefaultResourcesFilter.class, order.next());
|
||||||
put(DefaultLoginPageGeneratingFilter.class, order.next());
|
put(DefaultLoginPageGeneratingFilter.class, order.next());
|
||||||
|
@ -37,7 +37,6 @@ import org.springframework.security.core.Authentication;
|
|||||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||||
import org.springframework.security.web.authentication.AuthenticationConverter;
|
import org.springframework.security.web.authentication.AuthenticationConverter;
|
||||||
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
|
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
|
||||||
import org.springframework.security.web.authentication.AuthenticationFilter;
|
|
||||||
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
|
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
|
||||||
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
|
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
|
||||||
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
|
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
|
||||||
@ -45,6 +44,7 @@ import org.springframework.security.web.authentication.ott.DefaultGenerateOneTim
|
|||||||
import org.springframework.security.web.authentication.ott.GenerateOneTimeTokenFilter;
|
import org.springframework.security.web.authentication.ott.GenerateOneTimeTokenFilter;
|
||||||
import org.springframework.security.web.authentication.ott.GenerateOneTimeTokenRequestResolver;
|
import org.springframework.security.web.authentication.ott.GenerateOneTimeTokenRequestResolver;
|
||||||
import org.springframework.security.web.authentication.ott.OneTimeTokenAuthenticationConverter;
|
import org.springframework.security.web.authentication.ott.OneTimeTokenAuthenticationConverter;
|
||||||
|
import org.springframework.security.web.authentication.ott.OneTimeTokenAuthenticationFilter;
|
||||||
import org.springframework.security.web.authentication.ott.OneTimeTokenGenerationSuccessHandler;
|
import org.springframework.security.web.authentication.ott.OneTimeTokenGenerationSuccessHandler;
|
||||||
import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter;
|
import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter;
|
||||||
import org.springframework.security.web.authentication.ui.DefaultOneTimeTokenSubmitPageGeneratingFilter;
|
import org.springframework.security.web.authentication.ui.DefaultOneTimeTokenSubmitPageGeneratingFilter;
|
||||||
@ -74,7 +74,7 @@ public final class OneTimeTokenLoginConfigurer<H extends HttpSecurityBuilder<H>>
|
|||||||
|
|
||||||
private boolean submitPageEnabled = true;
|
private boolean submitPageEnabled = true;
|
||||||
|
|
||||||
private String loginProcessingUrl = "/login/ott";
|
private String loginProcessingUrl = OneTimeTokenAuthenticationFilter.DEFAULT_LOGIN_PROCESSING_URL;
|
||||||
|
|
||||||
private String tokenGeneratingUrl = "/ott/generate";
|
private String tokenGeneratingUrl = "/ott/generate";
|
||||||
|
|
||||||
@ -119,12 +119,15 @@ public final class OneTimeTokenLoginConfigurer<H extends HttpSecurityBuilder<H>>
|
|||||||
|
|
||||||
private void configureOttAuthenticationFilter(H http) {
|
private void configureOttAuthenticationFilter(H http) {
|
||||||
AuthenticationManager authenticationManager = http.getSharedObject(AuthenticationManager.class);
|
AuthenticationManager authenticationManager = http.getSharedObject(AuthenticationManager.class);
|
||||||
AuthenticationFilter oneTimeTokenAuthenticationFilter = new AuthenticationFilter(authenticationManager,
|
OneTimeTokenAuthenticationFilter oneTimeTokenAuthenticationFilter = new OneTimeTokenAuthenticationFilter();
|
||||||
this.authenticationConverter);
|
oneTimeTokenAuthenticationFilter.setAuthenticationManager(authenticationManager);
|
||||||
|
if (this.loginProcessingUrl != null) {
|
||||||
|
oneTimeTokenAuthenticationFilter
|
||||||
|
.setRequiresAuthenticationRequestMatcher(antMatcher(HttpMethod.POST, this.loginProcessingUrl));
|
||||||
|
}
|
||||||
|
oneTimeTokenAuthenticationFilter.setAuthenticationSuccessHandler(this.authenticationSuccessHandler);
|
||||||
|
oneTimeTokenAuthenticationFilter.setAuthenticationFailureHandler(getAuthenticationFailureHandler());
|
||||||
oneTimeTokenAuthenticationFilter.setSecurityContextRepository(getSecurityContextRepository(http));
|
oneTimeTokenAuthenticationFilter.setSecurityContextRepository(getSecurityContextRepository(http));
|
||||||
oneTimeTokenAuthenticationFilter.setRequestMatcher(antMatcher(HttpMethod.POST, this.loginProcessingUrl));
|
|
||||||
oneTimeTokenAuthenticationFilter.setFailureHandler(getAuthenticationFailureHandler());
|
|
||||||
oneTimeTokenAuthenticationFilter.setSuccessHandler(this.authenticationSuccessHandler);
|
|
||||||
http.addFilter(postProcess(oneTimeTokenAuthenticationFilter));
|
http.addFilter(postProcess(oneTimeTokenAuthenticationFilter));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,73 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2025 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.BadCredentialsException;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.AuthenticationException;
|
||||||
|
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
|
||||||
|
import org.springframework.security.web.authentication.AuthenticationConverter;
|
||||||
|
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter that processes a one-time token for log in.
|
||||||
|
* <p>
|
||||||
|
* By default, it uses {@link OneTimeTokenAuthenticationConverter} to extract the token
|
||||||
|
* from the request.
|
||||||
|
*
|
||||||
|
* @author Daniel Garnier-Moiroux
|
||||||
|
* @since 6.5
|
||||||
|
*/
|
||||||
|
public final class OneTimeTokenAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
|
||||||
|
|
||||||
|
public static final String DEFAULT_LOGIN_PROCESSING_URL = "/login/ott";
|
||||||
|
|
||||||
|
private AuthenticationConverter authenticationConverter = new OneTimeTokenAuthenticationConverter();
|
||||||
|
|
||||||
|
public OneTimeTokenAuthenticationFilter() {
|
||||||
|
super(new AntPathRequestMatcher(DEFAULT_LOGIN_PROCESSING_URL, "POST"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
|
||||||
|
throws AuthenticationException, IOException, ServletException {
|
||||||
|
Authentication authentication = this.authenticationConverter.convert(request);
|
||||||
|
if (authentication == null) {
|
||||||
|
throw new BadCredentialsException("Unable to authenticate with the one-time token");
|
||||||
|
}
|
||||||
|
return getAuthenticationManager().authenticate(authentication);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use this {@link AuthenticationConverter} when converting incoming requests to an
|
||||||
|
* {@link Authentication}. By default, the {@link OneTimeTokenAuthenticationConverter}
|
||||||
|
* is used.
|
||||||
|
* @param authenticationConverter the {@link AuthenticationConverter} to use
|
||||||
|
*/
|
||||||
|
public void setAuthenticationConverter(AuthenticationConverter authenticationConverter) {
|
||||||
|
Assert.notNull(authenticationConverter, "authenticationConverter cannot be null");
|
||||||
|
this.authenticationConverter = authenticationConverter;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,125 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2025 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.HttpServletResponse;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.mock.web.MockHttpServletResponse;
|
||||||
|
import org.springframework.security.authentication.AuthenticationManager;
|
||||||
|
import org.springframework.security.authentication.BadCredentialsException;
|
||||||
|
import org.springframework.security.authentication.ott.OneTimeTokenAuthenticationToken;
|
||||||
|
import org.springframework.security.core.authority.AuthorityUtils;
|
||||||
|
import org.springframework.security.web.servlet.MockServletContext;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.BDDMockito.given;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.verifyNoInteractions;
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link OneTimeTokenAuthenticationFilter}.
|
||||||
|
*
|
||||||
|
* @author Daniel Garnier-Moiroux
|
||||||
|
* @since 6.5
|
||||||
|
*/
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class OneTimeTokenAuthenticationFilterTests {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private FilterChain chain;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private AuthenticationManager authenticationManager;
|
||||||
|
|
||||||
|
private final OneTimeTokenAuthenticationFilter filter = new OneTimeTokenAuthenticationFilter();
|
||||||
|
|
||||||
|
private final HttpServletResponse response = new MockHttpServletResponse();
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
this.filter.setAuthenticationManager(this.authenticationManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void setAuthenticationConverterWhenNullThenIllegalArgumentException() {
|
||||||
|
assertThatIllegalArgumentException().isThrownBy(() -> this.filter.setAuthenticationConverter(null));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void doFilterWhenUrlDoesNotMatchThenContinues() throws ServletException, IOException {
|
||||||
|
OneTimeTokenAuthenticationConverter converter = mock(OneTimeTokenAuthenticationConverter.class);
|
||||||
|
HttpServletResponse response = mock(HttpServletResponse.class);
|
||||||
|
this.filter.setAuthenticationConverter(converter);
|
||||||
|
this.filter.doFilter(post("/nomatch").buildRequest(new MockServletContext()), response, this.chain);
|
||||||
|
verifyNoInteractions(converter, response);
|
||||||
|
verify(this.chain).doFilter(any(), any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void doFilterWhenMethodDoesNotMatchThenContinues() throws ServletException, IOException {
|
||||||
|
OneTimeTokenAuthenticationConverter converter = mock(OneTimeTokenAuthenticationConverter.class);
|
||||||
|
HttpServletResponse response = mock(HttpServletResponse.class);
|
||||||
|
this.filter.setAuthenticationConverter(converter);
|
||||||
|
this.filter.doFilter(get("/login/ott").buildRequest(new MockServletContext()), response, this.chain);
|
||||||
|
verifyNoInteractions(converter, response);
|
||||||
|
verify(this.chain).doFilter(any(), any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void doFilterWhenMissingTokenThenUnauthorized() throws ServletException, IOException {
|
||||||
|
this.filter.doFilter(post("/login/ott").buildRequest(new MockServletContext()), this.response, this.chain);
|
||||||
|
assertThat(this.response.getStatus()).isEqualTo(HttpStatus.UNAUTHORIZED.value());
|
||||||
|
verifyNoInteractions(this.chain);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void doFilterWhenInvalidTokenThenUnauthorized() throws ServletException, IOException {
|
||||||
|
given(this.authenticationManager.authenticate(any())).willThrow(new BadCredentialsException("invalid token"));
|
||||||
|
this.filter.doFilter(
|
||||||
|
post("/login/ott").param("token", "some-token-value").buildRequest(new MockServletContext()),
|
||||||
|
this.response, this.chain);
|
||||||
|
assertThat(this.response.getStatus()).isEqualTo(HttpStatus.UNAUTHORIZED.value());
|
||||||
|
verifyNoInteractions(this.chain);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void doFilterWhenValidThenRedirectsToSavedRequest() throws ServletException, IOException {
|
||||||
|
given(this.authenticationManager.authenticate(any()))
|
||||||
|
.willReturn(OneTimeTokenAuthenticationToken.authenticated("username", AuthorityUtils.NO_AUTHORITIES));
|
||||||
|
this.filter.doFilter(
|
||||||
|
post("/login/ott").param("token", "some-token-value").buildRequest(new MockServletContext()),
|
||||||
|
this.response, this.chain);
|
||||||
|
assertThat(this.response.getStatus()).isEqualTo(HttpStatus.FOUND.value());
|
||||||
|
assertThat(this.response.getHeader("location")).endsWith("/");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user