Merge pull request #19004 from rwinch/CredentialRecordOwnerAuthorizationManager

Add CredentialRecordOwnerAuthorizationManager
This commit is contained in:
Rob Winch 2026-03-29 23:46:03 -04:00 committed by GitHub
commit 5a4ada04ac
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 408 additions and 1 deletions

View File

@ -36,6 +36,7 @@ import org.springframework.security.web.webauthn.api.PublicKeyCredentialRpEntity
import org.springframework.security.web.webauthn.authentication.PublicKeyCredentialRequestOptionsFilter;
import org.springframework.security.web.webauthn.authentication.WebAuthnAuthenticationFilter;
import org.springframework.security.web.webauthn.authentication.WebAuthnAuthenticationProvider;
import org.springframework.security.web.webauthn.management.CredentialRecordOwnerAuthorizationManager;
import org.springframework.security.web.webauthn.management.MapPublicKeyCredentialUserEntityRepository;
import org.springframework.security.web.webauthn.management.MapUserCredentialRepository;
import org.springframework.security.web.webauthn.management.PublicKeyCredentialUserEntityRepository;
@ -166,6 +167,8 @@ public class WebAuthnConfigurer<H extends HttpSecurityBuilder<H>>
new ProviderManager(new WebAuthnAuthenticationProvider(rpOperations, userDetailsService)));
WebAuthnRegistrationFilter webAuthnRegistrationFilter = new WebAuthnRegistrationFilter(userCredentials,
rpOperations);
webAuthnRegistrationFilter.setDeleteCredentialAuthorizationManager(
new CredentialRecordOwnerAuthorizationManager(userCredentials, userEntities));
PublicKeyCredentialCreationOptionsFilter creationOptionsFilter = new PublicKeyCredentialCreationOptionsFilter(
rpOperations);
if (creationOptionsRepository != null) {

View File

@ -42,8 +42,15 @@ import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.FilterChainProxy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.ui.DefaultResourcesFilter;
import org.springframework.security.web.webauthn.api.Bytes;
import org.springframework.security.web.webauthn.api.ImmutablePublicKeyCredentialUserEntity;
import org.springframework.security.web.webauthn.api.PublicKeyCredentialCreationOptions;
import org.springframework.security.web.webauthn.api.TestCredentialRecords;
import org.springframework.security.web.webauthn.api.TestPublicKeyCredentialCreationOptions;
import org.springframework.security.web.webauthn.management.MapPublicKeyCredentialUserEntityRepository;
import org.springframework.security.web.webauthn.management.MapUserCredentialRepository;
import org.springframework.security.web.webauthn.management.PublicKeyCredentialUserEntityRepository;
import org.springframework.security.web.webauthn.management.UserCredentialRepository;
import org.springframework.security.web.webauthn.management.WebAuthnRelyingPartyOperations;
import org.springframework.security.web.webauthn.registration.HttpSessionPublicKeyCredentialCreationOptionsRepository;
import org.springframework.test.web.servlet.MockMvc;
@ -56,6 +63,7 @@ import static org.mockito.BDDMockito.willAnswer;
import static org.mockito.Mockito.mock;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
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.content;
@ -247,6 +255,24 @@ public class WebAuthnConfigurerTests {
.andExpect(content().string(expectedBody));
}
@Test
void webauthnWhenDeleteAndCredentialBelongsToUserThenNoContent() throws Exception {
this.spring.register(DeleteCredentialConfiguration.class).autowire();
this.mvc
.perform(delete("/webauthn/register/" + DeleteCredentialConfiguration.CREDENTIAL_ID_BASE64URL)
.with(authentication(new TestingAuthenticationToken("user", "password", "ROLE_USER"))))
.andExpect(status().isNoContent());
}
@Test
void webauthnWhenDeleteAndCredentialBelongsToDifferentUserThenForbidden() throws Exception {
this.spring.register(DeleteCredentialConfiguration.class).autowire();
this.mvc
.perform(delete("/webauthn/register/" + DeleteCredentialConfiguration.CREDENTIAL_ID_BASE64URL)
.with(authentication(new TestingAuthenticationToken("other-user", "password", "ROLE_USER"))))
.andExpect(status().isForbidden());
}
@Configuration
@EnableWebSecurity
static class ConfigCredentialCreationOptionsRepository {
@ -417,4 +443,47 @@ public class WebAuthnConfigurerTests {
}
@Configuration
@EnableWebSecurity
static class DeleteCredentialConfiguration {
static final String CREDENTIAL_ID_BASE64URL = "NauGCN7bZ5jEBwThcde51g";
static final Bytes USER_ENTITY_ID = Bytes.fromBase64("vKBFhsWT3gQnn-gHdT4VXIvjDkVXVYg5w8CLGHPunMM");
@Bean
UserDetailsService userDetailsService() {
return new InMemoryUserDetailsManager();
}
@Bean
WebAuthnRelyingPartyOperations webAuthnRelyingPartyOperations() {
return mock(WebAuthnRelyingPartyOperations.class);
}
@Bean
UserCredentialRepository userCredentialRepository() {
MapUserCredentialRepository repository = new MapUserCredentialRepository();
repository.save(TestCredentialRecords.userCredential().build());
return repository;
}
@Bean
PublicKeyCredentialUserEntityRepository userEntityRepository() {
MapPublicKeyCredentialUserEntityRepository repository = new MapPublicKeyCredentialUserEntityRepository();
repository.save(ImmutablePublicKeyCredentialUserEntity.builder()
.name("user")
.id(USER_ENTITY_ID)
.displayName("User")
.build());
return repository;
}
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http.csrf(AbstractHttpConfigurer::disable).webAuthn(Customizer.withDefaults()).build();
}
}
}

View File

@ -0,0 +1,91 @@
/*
* Copyright 2004-present 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.webauthn.management;
import java.util.function.Supplier;
import org.springframework.security.authorization.AuthenticatedAuthorizationManager;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.webauthn.api.Bytes;
import org.springframework.security.web.webauthn.api.CredentialRecord;
import org.springframework.security.web.webauthn.api.PublicKeyCredentialUserEntity;
import org.springframework.util.Assert;
/**
* An {@link AuthorizationManager} that grants access when the {@link CredentialRecord}
* identified by the provided credential id is owned by the currently authenticated user.
*
* <p>
* Per the <a href="https://www.w3.org/TR/webauthn-3/#credential-id">WebAuthn
* specification</a>, a credential id must contain at least 16 bytes with at least 100
* bits of entropy, making it practically unguessable. The specification also advises that
* credential ids should be kept private, as exposing them can leak personally identifying
* information (see
* <a href="https://www.w3.org/TR/webauthn-3/#sctn-credential-id-privacy-leak">§ 14.6.3
* Privacy leak via credential IDs</a>). This {@link AuthorizationManager} is therefore
* intended as defense in depth: even if a credential id were somehow exposed, an
* unauthorized user could not delete another user's credential.
*
* @author Rob Winch
* @since 6.5.10
*/
public final class CredentialRecordOwnerAuthorizationManager implements AuthorizationManager<Bytes> {
private final AuthenticatedAuthorizationManager<Bytes> authenticatedAuthorizationManager = AuthenticatedAuthorizationManager
.authenticated();
private final UserCredentialRepository userCredentials;
private final PublicKeyCredentialUserEntityRepository userEntities;
/**
* Creates a new instance.
* @param userCredentials the {@link UserCredentialRepository} to use
* @param userEntities the {@link PublicKeyCredentialUserEntityRepository} to use
*/
public CredentialRecordOwnerAuthorizationManager(UserCredentialRepository userCredentials,
PublicKeyCredentialUserEntityRepository userEntities) {
Assert.notNull(userCredentials, "userCredentials cannot be null");
Assert.notNull(userEntities, "userEntities cannot be null");
this.userCredentials = userCredentials;
this.userEntities = userEntities;
}
@Override
public AuthorizationDecision check(Supplier<Authentication> authentication, Bytes credentialId) {
AuthorizationDecision decision = this.authenticatedAuthorizationManager.check(authentication, credentialId);
if (!decision.isGranted()) {
return decision;
}
Authentication auth = authentication.get();
CredentialRecord credential = this.userCredentials.findByCredentialId(credentialId);
if (credential == null) {
return new AuthorizationDecision(false);
}
if (credential.getUserEntityUserId() == null) {
return new AuthorizationDecision(false);
}
PublicKeyCredentialUserEntity userEntity = this.userEntities.findByUsername(auth.getName());
if (userEntity == null) {
return new AuthorizationDecision(false);
}
return new AuthorizationDecision(credential.getUserEntityUserId().equals(userEntity.getId()));
}
}

View File

@ -17,6 +17,7 @@
package org.springframework.security.web.webauthn.registration;
import java.io.IOException;
import java.util.function.Supplier;
import com.fasterxml.jackson.databind.json.JsonMapper;
import jakarta.servlet.FilterChain;
@ -34,6 +35,12 @@ import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.authorization.AuthorizationResult;
import org.springframework.security.authorization.SingleResultAuthorizationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.context.SecurityContextHolderStrategy;
import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.security.web.webauthn.api.Bytes;
@ -87,6 +94,9 @@ public class WebAuthnRegistrationFilter extends OncePerRequestFilter {
private final UserCredentialRepository userCredentials;
private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder
.getContextHolderStrategy();
private HttpMessageConverter<Object> converter = new MappingJackson2HttpMessageConverter(
JsonMapper.builder().addModule(new WebauthnJackson2Module()).build());
@ -98,6 +108,9 @@ public class WebAuthnRegistrationFilter extends OncePerRequestFilter {
private RequestMatcher removeCredentialMatcher = PathPatternRequestMatcher.withDefaults()
.matcher(HttpMethod.DELETE, "/webauthn/register/{id}");
private AuthorizationManager<Bytes> deleteCredentialAuthorizationManager = SingleResultAuthorizationManager
.denyAll();
public WebAuthnRegistrationFilter(UserCredentialRepository userCredentials,
WebAuthnRelyingPartyOperations rpOptions) {
Assert.notNull(userCredentials, "userCredentials must not be null");
@ -132,6 +145,42 @@ public class WebAuthnRegistrationFilter extends OncePerRequestFilter {
this.removeCredentialMatcher = removeCredentialMatcher;
}
/**
* Sets the {@link AuthorizationManager} used to authorize the delete credential
* operation. The object being authorized is the credential id as {@link Bytes}. By
* default, all delete requests are denied.
*
* <p>
* Per the <a href="https://www.w3.org/TR/webauthn-3/#credential-id">WebAuthn
* specification</a>, a credential id must contain at least 16 bytes with at least 100
* bits of entropy, making it practically unguessable. The specification also advises
* that credential ids should be kept private, as exposing them can leak personally
* identifying information (see
* <a href="https://www.w3.org/TR/webauthn-3/#sctn-credential-id-privacy-leak">§
* 14.6.3 Privacy leak via credential IDs</a>). This {@link AuthorizationManager} is
* therefore intended as defense in depth: even if a credential id were somehow
* exposed, an unauthorized user could not delete another user's credential.
* @param deleteCredentialAuthorizationManager the {@link AuthorizationManager} to use
* @since 6.5.10
*/
public void setDeleteCredentialAuthorizationManager(
AuthorizationManager<Bytes> deleteCredentialAuthorizationManager) {
Assert.notNull(deleteCredentialAuthorizationManager, "deleteCredentialAuthorizationManager cannot be null");
this.deleteCredentialAuthorizationManager = deleteCredentialAuthorizationManager;
}
/**
* Sets the {@link SecurityContextHolderStrategy} to use. The default is
* {@link SecurityContextHolder#getContextHolderStrategy()}.
* @param securityContextHolderStrategy the {@link SecurityContextHolderStrategy} to
* use
* @since 6.5.10
*/
public void setSecurityContextHolderStrategy(SecurityContextHolderStrategy securityContextHolderStrategy) {
Assert.notNull(securityContextHolderStrategy, "securityContextHolderStrategy cannot be null");
this.securityContextHolderStrategy = securityContextHolderStrategy;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
@ -203,7 +252,15 @@ public class WebAuthnRegistrationFilter extends OncePerRequestFilter {
private void removeCredential(HttpServletRequest request, HttpServletResponse response, String id)
throws IOException {
this.userCredentials.delete(Bytes.fromBase64(id));
Bytes credentialId = Bytes.fromBase64(id);
Supplier<Authentication> authentication = () -> this.securityContextHolderStrategy.getContext()
.getAuthentication();
AuthorizationResult result = this.deleteCredentialAuthorizationManager.authorize(authentication, credentialId);
if (result != null && !result.isGranted()) {
response.setStatus(HttpStatus.FORBIDDEN.value());
return;
}
this.userCredentials.delete(credentialId);
response.setStatus(HttpStatus.NO_CONTENT.value());
}

View File

@ -0,0 +1,154 @@
/*
* Copyright 2004-present 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.webauthn.management;
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.security.authentication.TestingAuthenticationToken;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.web.webauthn.api.Bytes;
import org.springframework.security.web.webauthn.api.ImmutablePublicKeyCredentialUserEntity;
import org.springframework.security.web.webauthn.api.PublicKeyCredentialUserEntity;
import org.springframework.security.web.webauthn.api.TestBytes;
import org.springframework.security.web.webauthn.api.TestCredentialRecords;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.mockito.BDDMockito.given;
/**
* Tests for {@link CredentialRecordOwnerAuthorizationManager}.
*
* @author Rob Winch
* @since 6.5.10
*/
@ExtendWith(MockitoExtension.class)
class CredentialRecordOwnerAuthorizationManagerTests {
@Mock
private UserCredentialRepository userCredentials;
@Mock
private PublicKeyCredentialUserEntityRepository userEntities;
@Test
void constructorWhenNullUserCredentialsThenIllegalArgument() {
assertThatIllegalArgumentException()
.isThrownBy(() -> new CredentialRecordOwnerAuthorizationManager(null, this.userEntities));
}
@Test
void constructorWhenNullUserEntitiesTonIllegalArgument() {
assertThatIllegalArgumentException()
.isThrownBy(() -> new CredentialRecordOwnerAuthorizationManager(this.userCredentials, null));
}
@Test
void checkWhenAuthenticationNullThenDenied() {
CredentialRecordOwnerAuthorizationManager manager = new CredentialRecordOwnerAuthorizationManager(
this.userCredentials, this.userEntities);
Bytes credentialId = TestCredentialRecords.userCredential().build().getCredentialId();
AuthorizationDecision decision = manager.check(() -> null, credentialId);
assertThat(decision.isGranted()).isFalse();
}
@Test
void checkWhenNotAuthenticatedThenDenied() {
CredentialRecordOwnerAuthorizationManager manager = new CredentialRecordOwnerAuthorizationManager(
this.userCredentials, this.userEntities);
Bytes credentialId = TestCredentialRecords.userCredential().build().getCredentialId();
TestingAuthenticationToken authentication = new TestingAuthenticationToken("user", "password");
authentication.setAuthenticated(false);
AuthorizationDecision decision = manager.check(() -> authentication, credentialId);
assertThat(decision.isGranted()).isFalse();
}
@Test
void checkWhenCredentialNotFoundThenDenied() {
CredentialRecordOwnerAuthorizationManager manager = new CredentialRecordOwnerAuthorizationManager(
this.userCredentials, this.userEntities);
Bytes credentialId = TestCredentialRecords.userCredential().build().getCredentialId();
TestingAuthenticationToken authentication = new TestingAuthenticationToken("user", "password", "USER");
AuthorizationDecision decision = manager.check(() -> authentication, credentialId);
assertThat(decision.isGranted()).isFalse();
}
@Test
void checkWhenCredentialUserEntityUserIdNullThenDenied() {
CredentialRecordOwnerAuthorizationManager manager = new CredentialRecordOwnerAuthorizationManager(
this.userCredentials, this.userEntities);
Bytes credentialId = TestCredentialRecords.userCredential().build().getCredentialId();
given(this.userCredentials.findByCredentialId(credentialId))
.willReturn(TestCredentialRecords.userCredential().userEntityUserId(null).build());
TestingAuthenticationToken authentication = new TestingAuthenticationToken("user", "password", "USER");
AuthorizationDecision decision = manager.check(() -> authentication, credentialId);
assertThat(decision.isGranted()).isFalse();
}
@Test
void checkWhenUserEntityNotFoundThenDenied() {
CredentialRecordOwnerAuthorizationManager manager = new CredentialRecordOwnerAuthorizationManager(
this.userCredentials, this.userEntities);
Bytes credentialId = TestCredentialRecords.userCredential().build().getCredentialId();
given(this.userCredentials.findByCredentialId(credentialId))
.willReturn(TestCredentialRecords.userCredential().build());
TestingAuthenticationToken authentication = new TestingAuthenticationToken("user", "password", "USER");
AuthorizationDecision decision = manager.check(() -> authentication, credentialId);
assertThat(decision.isGranted()).isFalse();
}
@Test
void checkWhenCredentialBelongsToUserThenGranted() {
CredentialRecordOwnerAuthorizationManager manager = new CredentialRecordOwnerAuthorizationManager(
this.userCredentials, this.userEntities);
Bytes credentialId = TestCredentialRecords.userCredential().build().getCredentialId();
Bytes userId = TestCredentialRecords.userCredential().build().getUserEntityUserId();
given(this.userCredentials.findByCredentialId(credentialId))
.willReturn(TestCredentialRecords.userCredential().build());
PublicKeyCredentialUserEntity userEntity = ImmutablePublicKeyCredentialUserEntity.builder()
.name("user")
.id(userId)
.displayName("User")
.build();
given(this.userEntities.findByUsername("user")).willReturn(userEntity);
TestingAuthenticationToken authentication = new TestingAuthenticationToken("user", "password", "USER");
AuthorizationDecision decision = manager.check(() -> authentication, credentialId);
assertThat(decision.isGranted()).isTrue();
}
@Test
void checkWhenCredentialBelongsToDifferentUserThenDenied() {
CredentialRecordOwnerAuthorizationManager manager = new CredentialRecordOwnerAuthorizationManager(
this.userCredentials, this.userEntities);
Bytes credentialId = TestCredentialRecords.userCredential().build().getCredentialId();
given(this.userCredentials.findByCredentialId(credentialId))
.willReturn(TestCredentialRecords.userCredential().build());
PublicKeyCredentialUserEntity otherUserEntity = ImmutablePublicKeyCredentialUserEntity.builder()
.name("user")
.id(TestBytes.get())
.displayName("User")
.build();
given(this.userEntities.findByUsername("user")).willReturn(otherUserEntity);
TestingAuthenticationToken authentication = new TestingAuthenticationToken("user", "password", "USER");
AuthorizationDecision decision = manager.check(() -> authentication, credentialId);
assertThat(decision.isGranted()).isFalse();
}
}

View File

@ -31,7 +31,11 @@ import org.springframework.mock.web.MockFilterChain;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.mock.web.MockServletContext;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.authorization.SingleResultAuthorizationManager;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.security.web.webauthn.api.Bytes;
import org.springframework.security.web.webauthn.api.ImmutableCredentialRecord;
import org.springframework.security.web.webauthn.api.PublicKeyCredentialCreationOptions;
import org.springframework.security.web.webauthn.api.TestCredentialRecords;
@ -213,12 +217,41 @@ class WebAuthnRegistrationFilterTests {
@Test
void doFilterWhenDeleteSuccessThenNoContent() throws Exception {
this.filter.setDeleteCredentialAuthorizationManager(SingleResultAuthorizationManager.permitAll());
MockHttpServletRequest request = MockMvcRequestBuilders.delete("/webauthn/register/123456")
.buildRequest(new MockServletContext());
this.filter.doFilter(request, this.response, this.chain);
assertThat(this.response.getStatus()).isEqualTo(HttpStatus.NO_CONTENT.value());
}
@Test
void setDeleteCredentialAuthorizationManagerWhenNullThenIllegalArgument() {
assertThatIllegalArgumentException()
.isThrownBy(() -> this.filter.setDeleteCredentialAuthorizationManager(null));
}
@Test
void doFilterWhenDeleteAndCustomAuthorizationManagerThenUses() throws Exception {
AuthorizationManager<Bytes> authorizationManager = mock(AuthorizationManager.class);
given(authorizationManager.authorize(any(), any())).willReturn(new AuthorizationDecision(true));
this.filter.setDeleteCredentialAuthorizationManager(authorizationManager);
MockHttpServletRequest request = MockMvcRequestBuilders.delete("/webauthn/register/123456")
.buildRequest(new MockServletContext());
this.filter.doFilter(request, this.response, this.chain);
verify(authorizationManager).authorize(any(), any());
}
@Test
void doFilterWhenDeleteAndAuthorizationDeniedThenForbidden() throws Exception {
AuthorizationManager<Bytes> authorizationManager = mock(AuthorizationManager.class);
given(authorizationManager.authorize(any(), any())).willReturn(new AuthorizationDecision(false));
this.filter.setDeleteCredentialAuthorizationManager(authorizationManager);
MockHttpServletRequest request = MockMvcRequestBuilders.delete("/webauthn/register/123456")
.buildRequest(new MockServletContext());
this.filter.doFilter(request, this.response, this.chain);
assertThat(this.response.getStatus()).isEqualTo(HttpStatus.FORBIDDEN.value());
}
private static MockHttpServletRequest registerCredentialRequest(String body) {
return MockMvcRequestBuilders.post(WebAuthnRegistrationFilter.DEFAULT_REGISTER_CREDENTIAL_URL)
.content(body)