Add CredentialRecordOwnerAuthorizationManager

Add CredentialRecordOwnerAuthorizationManager that verifies the
credential being deleted is owned by the currently authenticated user.
Also add an AuthorizationManager<Bytes> to WebAuthnRegistrationFilter
for the delete credential operation, defaulting to deny all, and wire it
up in WebAuthnConfigurer.

Per the WebAuthn specification [1], credential ids contain at least 16
bytes with at least 100 bits of entropy, making them practically
unguessable. The specification also advises that credential ids should
be kept private, as exposing them can leak personally identifying
information [2]. The CredentialRecordOwnerAuthorizationManager serves as
defense in depth: even if a credential id were somehow exposed, an
unauthorized user could not delete another user's credential.

[1] https://www.w3.org/TR/webauthn-3/#credential-id
[2] https://www.w3.org/TR/webauthn-3/#sctn-credential-id-privacy-leak
This commit is contained in:
Robert Winch 2026-03-29 21:45:18 -05:00
parent 95b2cdf7f4
commit a856baa6a8
No known key found for this signature in database
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)