mirror of
https://github.com/spring-projects/spring-security.git
synced 2026-03-30 14:08:11 +00:00
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:
parent
95b2cdf7f4
commit
a856baa6a8
@ -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) {
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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()));
|
||||
}
|
||||
|
||||
}
|
||||
@ -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());
|
||||
}
|
||||
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@ -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)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user