Explorar o código

Support Custom RequestMatchers for WebAuthn

Closes gh-16517

Signed-off-by: topiam <support@topiam.cn>
topiam hai 6 meses
pai
achega
85f0f3f34a

+ 11 - 0
web/src/main/java/org/springframework/security/web/webauthn/authentication/PublicKeyCredentialRequestOptionsFilter.java

@@ -75,6 +75,17 @@ public class PublicKeyCredentialRequestOptionsFilter extends OncePerRequestFilte
 		this.rpOptions = rpOptions;
 	}
 
+	/**
+	 * Sets the {@link RequestMatcher} used to trigger this filter. By default, the
+	 * {@link RequestMatcher} is {@code POST /webauthn/authenticate/options}.
+	 * @param requestMatcher the {@link RequestMatcher} to use
+	 * @since 6.5
+	 */
+	public void setRequestMatcher(RequestMatcher requestMatcher) {
+		Assert.notNull(requestMatcher, "requestMatcher cannot be null");
+		this.matcher = requestMatcher;
+	}
+
 	@Override
 	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
 			throws ServletException, IOException {

+ 12 - 0
web/src/main/java/org/springframework/security/web/webauthn/registration/PublicKeyCredentialCreationOptionsFilter.java

@@ -82,6 +82,18 @@ public class PublicKeyCredentialCreationOptionsFilter extends OncePerRequestFilt
 		this.rpOperations = rpOperations;
 	}
 
+	/**
+	 * Sets the {@link RequestMatcher} used to trigger this filter.
+	 * <p>
+	 * By default, the {@link RequestMatcher} is {@code POST /webauthn/register/options}.
+	 * @param requestMatcher the {@link RequestMatcher} to use
+	 * @since 6.5
+	 */
+	public void setRequestMatcher(RequestMatcher requestMatcher) {
+		Assert.notNull(requestMatcher, "requestMatcher cannot be null");
+		this.matcher = requestMatcher;
+	}
+
 	@Override
 	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
 			throws ServletException, IOException {

+ 26 - 0
web/src/main/java/org/springframework/security/web/webauthn/registration/WebAuthnRegistrationFilter.java

@@ -105,6 +105,32 @@ public class WebAuthnRegistrationFilter extends OncePerRequestFilter {
 		this.rpOptions = rpOptions;
 	}
 
+	/**
+	 * Sets the {@link RequestMatcher} to trigger this filter's the credential
+	 * registration operation .
+	 * <p/>
+	 * By default, the {@link RequestMatcher} is {@code POST /webauthn/register}.
+	 * @param registerCredentialMatcher the {@link RequestMatcher} to use
+	 * @since 6.5
+	 */
+	public void setRegisterCredentialMatcher(RequestMatcher registerCredentialMatcher) {
+		Assert.notNull(registerCredentialMatcher, "registerCredentialMatcher cannot be null");
+		this.registerCredentialMatcher = registerCredentialMatcher;
+	}
+
+	/**
+	 * Sets the {@link RequestMatcher} to trigger this filter's the credential removal
+	 * operation .
+	 * <p/>
+	 * By default, the {@link RequestMatcher} is {@code DELETE /webauthn/register/{id}}.
+	 * @param removeCredentialMatcher the {@link RequestMatcher} to use
+	 * @since 6.5
+	 */
+	public void setRemoveCredentialMatcher(RequestMatcher removeCredentialMatcher) {
+		Assert.notNull(removeCredentialMatcher, "removeCredentialMatcher cannot be null");
+		this.removeCredentialMatcher = removeCredentialMatcher;
+	}
+
 	@Override
 	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
 			throws ServletException, IOException {

+ 21 - 0
web/src/test/java/org/springframework/security/web/webauthn/authentication/PublicKeyCredentialRequestOptionsFilterTests.java

@@ -18,6 +18,7 @@ package org.springframework.security.web.webauthn.authentication;
 
 import java.nio.charset.StandardCharsets;
 
+import jakarta.servlet.FilterChain;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
@@ -30,10 +31,13 @@ import org.skyscreamer.jsonassert.JSONAssert;
 
 import org.springframework.http.converter.HttpMessageConverter;
 import org.springframework.http.server.ServletServerHttpResponse;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.mock.web.MockHttpServletResponse;
 import org.springframework.security.authentication.TestingAuthenticationToken;
 import org.springframework.security.core.context.SecurityContextHolder;
 import org.springframework.security.core.context.SecurityContextHolderStrategy;
 import org.springframework.security.core.context.SecurityContextImpl;
+import org.springframework.security.web.util.matcher.RequestMatcher;
 import org.springframework.security.web.webauthn.api.PublicKeyCredentialCreationOptions;
 import org.springframework.security.web.webauthn.api.PublicKeyCredentialRequestOptions;
 import org.springframework.security.web.webauthn.api.TestPublicKeyCredentialRequestOptions;
@@ -48,6 +52,8 @@ import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.BDDMockito.given;
 import static org.mockito.BDDMockito.verifyNoInteractions;
 import static org.mockito.BDDMockito.willAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
 import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@@ -75,6 +81,10 @@ class PublicKeyCredentialRequestOptionsFilterTests {
 
 	private PublicKeyCredentialRequestOptionsFilter filter;
 
+	private MockHttpServletRequest request;
+
+	private MockHttpServletResponse response;
+
 	private MockMvc mockMvc;
 
 	@BeforeEach
@@ -82,6 +92,8 @@ class PublicKeyCredentialRequestOptionsFilterTests {
 		this.filter = new PublicKeyCredentialRequestOptionsFilter(this.relyingPartyOperations);
 		this.filter.setRequestOptionsRepository(this.requestOptionsRepository);
 		this.mockMvc = MockMvcBuilders.standaloneSetup().addFilter(this.filter).build();
+		this.request = new MockHttpServletRequest();
+		this.response = new MockHttpServletResponse();
 	}
 
 	@AfterEach
@@ -89,6 +101,15 @@ class PublicKeyCredentialRequestOptionsFilterTests {
 		SecurityContextHolder.clearContext();
 	}
 
+	@Test
+	void doFilterWhenCustomRequestMatcherThenUses() throws Exception {
+		RequestMatcher requestMatcher = mock(RequestMatcher.class);
+		this.filter.setRequestMatcher(requestMatcher);
+		FilterChain mock = mock(FilterChain.class);
+		this.filter.doFilter(this.request, this.response, mock);
+		verify(requestMatcher).matches(any());
+	}
+
 	@Test
 	void constructorWhenNull() {
 		assertThatExceptionOfType(IllegalArgumentException.class)

+ 33 - 0
web/src/test/java/org/springframework/security/web/webauthn/registration/PublicKeyCredentialCreationOptionsFilterTests.java

@@ -18,7 +18,9 @@ package org.springframework.security.web.webauthn.registration;
 
 import java.util.Arrays;
 
+import jakarta.servlet.FilterChain;
 import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
 import org.mockito.Mock;
@@ -27,12 +29,14 @@ import org.mockito.junit.jupiter.MockitoExtension;
 import org.springframework.http.HttpHeaders;
 import org.springframework.http.HttpStatus;
 import org.springframework.http.MediaType;
+import org.springframework.mock.web.MockHttpServletRequest;
 import org.springframework.mock.web.MockHttpServletResponse;
 import org.springframework.security.authentication.AnonymousAuthenticationToken;
 import org.springframework.security.authentication.TestingAuthenticationToken;
 import org.springframework.security.core.authority.AuthorityUtils;
 import org.springframework.security.core.context.SecurityContextHolder;
 import org.springframework.security.core.context.SecurityContextImpl;
+import org.springframework.security.web.util.matcher.RequestMatcher;
 import org.springframework.security.web.webauthn.api.AuthenticatorTransport;
 import org.springframework.security.web.webauthn.api.Bytes;
 import org.springframework.security.web.webauthn.api.PublicKeyCredentialCreationOptions;
@@ -47,6 +51,8 @@ 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;
@@ -68,11 +74,38 @@ class PublicKeyCredentialCreationOptionsFilterTests {
 	@Mock
 	private WebAuthnRelyingPartyOperations rpOperations;
 
+	private PublicKeyCredentialCreationOptionsFilter filter;
+
+	private MockHttpServletRequest request;
+
+	private MockHttpServletResponse response;
+
+	@BeforeEach
+	void setup() {
+		this.filter = new PublicKeyCredentialCreationOptionsFilter(this.rpOperations);
+		this.request = new MockHttpServletRequest();
+		this.response = new MockHttpServletResponse();
+	}
+
 	@AfterEach
 	void clear() {
 		SecurityContextHolder.clearContext();
 	}
 
+	@Test
+	void doFilterWhenCustomRequestMatcherThenUses() throws Exception {
+		RequestMatcher requestMatcher = mock(RequestMatcher.class);
+		this.filter.setRequestMatcher(requestMatcher);
+		FilterChain mock = mock(FilterChain.class);
+		this.filter.doFilter(this.request, this.response, mock);
+		verify(requestMatcher).matches(any());
+	}
+
+	@Test
+	void setRequestMatcherWhenNullThenIllegalArgument() {
+		assertThatIllegalArgumentException().isThrownBy(() -> this.filter.setRequestMatcher(null));
+	}
+
 	@Test
 	void constructorWhenRpOperationsIsNullThenIllegalArgumentException() {
 		assertThatIllegalArgumentException().isThrownBy(() -> new PublicKeyCredentialCreationOptionsFilter(null))

+ 34 - 0
web/src/test/java/org/springframework/security/web/webauthn/registration/WebAuthnRegistrationFilterTests.java

@@ -30,6 +30,7 @@ import org.springframework.http.converter.GenericHttpMessageConverter;
 import org.springframework.mock.web.MockHttpServletRequest;
 import org.springframework.mock.web.MockHttpServletResponse;
 import org.springframework.mock.web.MockServletContext;
+import org.springframework.security.web.util.matcher.RequestMatcher;
 import org.springframework.security.web.webauthn.api.ImmutableCredentialRecord;
 import org.springframework.security.web.webauthn.api.PublicKeyCredentialCreationOptions;
 import org.springframework.security.web.webauthn.api.TestCredentialRecord;
@@ -100,9 +101,42 @@ class WebAuthnRegistrationFilterTests {
 
 	private WebAuthnRegistrationFilter filter;
 
+	private MockHttpServletRequest request;
+
 	@BeforeEach
 	void setup() {
 		this.filter = new WebAuthnRegistrationFilter(this.userCredentials, this.operations);
+		this.request = new MockHttpServletRequest();
+		this.response = new MockHttpServletResponse();
+		this.chain = mock(FilterChain.class);
+	}
+
+	@Test
+	void doFilterWhenCustomRequestRegisterCredentialMatcherThenUses() throws Exception {
+		RequestMatcher requestMatcher = mock(RequestMatcher.class);
+		this.filter.setRegisterCredentialMatcher(requestMatcher);
+		FilterChain mock = mock(FilterChain.class);
+		this.filter.doFilter(this.request, this.response, mock);
+		verify(requestMatcher).matches(any());
+	}
+
+	@Test
+	void doFilterWhenCustomRequestRemoveCredentialMatcherThenUses() throws Exception {
+		RequestMatcher requestMatcher = mock(RequestMatcher.class);
+		this.filter.setRemoveCredentialMatcher(requestMatcher);
+		FilterChain mock = mock(FilterChain.class);
+		this.filter.doFilter(this.request, this.response, mock);
+		verify(requestMatcher).matches(any());
+	}
+
+	@Test
+	void setRequestRegisterCredentialWhenNullThenIllegalArgument() {
+		assertThatIllegalArgumentException().isThrownBy(() -> this.filter.setRegisterCredentialMatcher(null));
+	}
+
+	@Test
+	void setRequestRemoveCredentialWhenNullThenIllegalArgument() {
+		assertThatIllegalArgumentException().isThrownBy(() -> this.filter.setRemoveCredentialMatcher(null));
 	}
 
 	@Test