mirror of
https://github.com/spring-projects/spring-security.git
synced 2025-06-26 13:53: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.logout.LogoutFilter;
|
||||
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.x509.X509AuthenticationFilter;
|
||||
import org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter;
|
||||
@ -101,6 +102,7 @@ final class FilterOrderRegistration {
|
||||
"org.springframework.security.saml2.provider.service.web.authentication.Saml2WebSsoAuthenticationFilter",
|
||||
order.next());
|
||||
put(UsernamePasswordAuthenticationFilter.class, order.next());
|
||||
put(OneTimeTokenAuthenticationFilter.class, order.next());
|
||||
order.next(); // gh-8105
|
||||
put(DefaultResourcesFilter.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.web.authentication.AuthenticationConverter;
|
||||
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.SavedRequestAwareAuthenticationSuccessHandler;
|
||||
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.GenerateOneTimeTokenRequestResolver;
|
||||
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.ui.DefaultLoginPageGeneratingFilter;
|
||||
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 String loginProcessingUrl = "/login/ott";
|
||||
private String loginProcessingUrl = OneTimeTokenAuthenticationFilter.DEFAULT_LOGIN_PROCESSING_URL;
|
||||
|
||||
private String tokenGeneratingUrl = "/ott/generate";
|
||||
|
||||
@ -119,12 +119,15 @@ public final class OneTimeTokenLoginConfigurer<H extends HttpSecurityBuilder<H>>
|
||||
|
||||
private void configureOttAuthenticationFilter(H http) {
|
||||
AuthenticationManager authenticationManager = http.getSharedObject(AuthenticationManager.class);
|
||||
AuthenticationFilter oneTimeTokenAuthenticationFilter = new AuthenticationFilter(authenticationManager,
|
||||
this.authenticationConverter);
|
||||
OneTimeTokenAuthenticationFilter oneTimeTokenAuthenticationFilter = new OneTimeTokenAuthenticationFilter();
|
||||
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.setRequestMatcher(antMatcher(HttpMethod.POST, this.loginProcessingUrl));
|
||||
oneTimeTokenAuthenticationFilter.setFailureHandler(getAuthenticationFailureHandler());
|
||||
oneTimeTokenAuthenticationFilter.setSuccessHandler(this.authenticationSuccessHandler);
|
||||
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