Merge pull request #19005 from rwinch/7.0.x-CredentialRecordOwnerAuthorizationManager
Merge Add CredentialRecordOwnerAuthorizationManager
This commit is contained in:
+3
@@ -39,6 +39,7 @@ import org.springframework.security.web.webauthn.api.PublicKeyCredentialRpEntity
|
|||||||
import org.springframework.security.web.webauthn.authentication.PublicKeyCredentialRequestOptionsFilter;
|
import org.springframework.security.web.webauthn.authentication.PublicKeyCredentialRequestOptionsFilter;
|
||||||
import org.springframework.security.web.webauthn.authentication.WebAuthnAuthenticationFilter;
|
import org.springframework.security.web.webauthn.authentication.WebAuthnAuthenticationFilter;
|
||||||
import org.springframework.security.web.webauthn.authentication.WebAuthnAuthenticationProvider;
|
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.MapPublicKeyCredentialUserEntityRepository;
|
||||||
import org.springframework.security.web.webauthn.management.MapUserCredentialRepository;
|
import org.springframework.security.web.webauthn.management.MapUserCredentialRepository;
|
||||||
import org.springframework.security.web.webauthn.management.PublicKeyCredentialUserEntityRepository;
|
import org.springframework.security.web.webauthn.management.PublicKeyCredentialUserEntityRepository;
|
||||||
@@ -180,6 +181,8 @@ public class WebAuthnConfigurer<H extends HttpSecurityBuilder<H>>
|
|||||||
webAuthnAuthnFilter = postProcess(webAuthnAuthnFilter);
|
webAuthnAuthnFilter = postProcess(webAuthnAuthnFilter);
|
||||||
WebAuthnRegistrationFilter webAuthnRegistrationFilter = new WebAuthnRegistrationFilter(userCredentials,
|
WebAuthnRegistrationFilter webAuthnRegistrationFilter = new WebAuthnRegistrationFilter(userCredentials,
|
||||||
rpOperations);
|
rpOperations);
|
||||||
|
webAuthnRegistrationFilter.setDeleteCredentialAuthorizationManager(
|
||||||
|
new CredentialRecordOwnerAuthorizationManager(userCredentials, userEntities));
|
||||||
PublicKeyCredentialCreationOptionsFilter creationOptionsFilter = new PublicKeyCredentialCreationOptionsFilter(
|
PublicKeyCredentialCreationOptionsFilter creationOptionsFilter = new PublicKeyCredentialCreationOptionsFilter(
|
||||||
rpOperations);
|
rpOperations);
|
||||||
if (creationOptionsRepository != null) {
|
if (creationOptionsRepository != null) {
|
||||||
|
|||||||
+69
@@ -43,9 +43,16 @@ import org.springframework.security.provisioning.InMemoryUserDetailsManager;
|
|||||||
import org.springframework.security.web.FilterChainProxy;
|
import org.springframework.security.web.FilterChainProxy;
|
||||||
import org.springframework.security.web.SecurityFilterChain;
|
import org.springframework.security.web.SecurityFilterChain;
|
||||||
import org.springframework.security.web.authentication.ui.DefaultResourcesFilter;
|
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.PublicKeyCredentialCreationOptions;
|
||||||
|
import org.springframework.security.web.webauthn.api.TestCredentialRecords;
|
||||||
import org.springframework.security.web.webauthn.api.TestPublicKeyCredentialCreationOptions;
|
import org.springframework.security.web.webauthn.api.TestPublicKeyCredentialCreationOptions;
|
||||||
import org.springframework.security.web.webauthn.authentication.WebAuthnAuthenticationFilter;
|
import org.springframework.security.web.webauthn.authentication.WebAuthnAuthenticationFilter;
|
||||||
|
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.management.WebAuthnRelyingPartyOperations;
|
||||||
import org.springframework.security.web.webauthn.registration.HttpSessionPublicKeyCredentialCreationOptionsRepository;
|
import org.springframework.security.web.webauthn.registration.HttpSessionPublicKeyCredentialCreationOptionsRepository;
|
||||||
import org.springframework.test.web.servlet.MockMvc;
|
import org.springframework.test.web.servlet.MockMvc;
|
||||||
@@ -58,6 +65,7 @@ import static org.mockito.BDDMockito.willAnswer;
|
|||||||
import static org.mockito.Mockito.mock;
|
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.authentication;
|
||||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
|
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.get;
|
||||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
|
||||||
@@ -257,6 +265,24 @@ public class WebAuthnConfigurerTests {
|
|||||||
.andExpect(content().string(expectedBody));
|
.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
|
@Configuration
|
||||||
@EnableWebSecurity
|
@EnableWebSecurity
|
||||||
static class ConfigCredentialCreationOptionsRepository {
|
static class ConfigCredentialCreationOptionsRepository {
|
||||||
@@ -475,4 +501,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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+95
@@ -0,0 +1,95 @@
|
|||||||
|
/*
|
||||||
|
* 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.jspecify.annotations.Nullable;
|
||||||
|
|
||||||
|
import org.springframework.security.authorization.AuthenticatedAuthorizationManager;
|
||||||
|
import org.springframework.security.authorization.AuthorizationDecision;
|
||||||
|
import org.springframework.security.authorization.AuthorizationManager;
|
||||||
|
import org.springframework.security.authorization.AuthorizationResult;
|
||||||
|
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 AuthorizationResult authorize(Supplier<? extends @Nullable Authentication> authentication,
|
||||||
|
Bytes credentialId) {
|
||||||
|
AuthorizationResult decision = this.authenticatedAuthorizationManager.authorize(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()));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+58
-1
@@ -17,6 +17,7 @@
|
|||||||
package org.springframework.security.web.webauthn.registration;
|
package org.springframework.security.web.webauthn.registration;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
import jakarta.servlet.FilterChain;
|
import jakarta.servlet.FilterChain;
|
||||||
import jakarta.servlet.ServletException;
|
import jakarta.servlet.ServletException;
|
||||||
@@ -35,6 +36,12 @@ import org.springframework.http.converter.HttpMessageConverter;
|
|||||||
import org.springframework.http.converter.json.JacksonJsonHttpMessageConverter;
|
import org.springframework.http.converter.json.JacksonJsonHttpMessageConverter;
|
||||||
import org.springframework.http.server.ServletServerHttpRequest;
|
import org.springframework.http.server.ServletServerHttpRequest;
|
||||||
import org.springframework.http.server.ServletServerHttpResponse;
|
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.servlet.util.matcher.PathPatternRequestMatcher;
|
||||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||||
import org.springframework.security.web.webauthn.api.Bytes;
|
import org.springframework.security.web.webauthn.api.Bytes;
|
||||||
@@ -88,6 +95,9 @@ public class WebAuthnRegistrationFilter extends OncePerRequestFilter {
|
|||||||
|
|
||||||
private final UserCredentialRepository userCredentials;
|
private final UserCredentialRepository userCredentials;
|
||||||
|
|
||||||
|
private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder
|
||||||
|
.getContextHolderStrategy();
|
||||||
|
|
||||||
private HttpMessageConverter<Object> converter = new JacksonJsonHttpMessageConverter(
|
private HttpMessageConverter<Object> converter = new JacksonJsonHttpMessageConverter(
|
||||||
JsonMapper.builder().addModule(new WebauthnJacksonModule()).build());
|
JsonMapper.builder().addModule(new WebauthnJacksonModule()).build());
|
||||||
|
|
||||||
@@ -99,6 +109,9 @@ public class WebAuthnRegistrationFilter extends OncePerRequestFilter {
|
|||||||
private RequestMatcher removeCredentialMatcher = PathPatternRequestMatcher.withDefaults()
|
private RequestMatcher removeCredentialMatcher = PathPatternRequestMatcher.withDefaults()
|
||||||
.matcher(HttpMethod.DELETE, "/webauthn/register/{id}");
|
.matcher(HttpMethod.DELETE, "/webauthn/register/{id}");
|
||||||
|
|
||||||
|
private AuthorizationManager<Bytes> deleteCredentialAuthorizationManager = SingleResultAuthorizationManager
|
||||||
|
.denyAll();
|
||||||
|
|
||||||
public WebAuthnRegistrationFilter(UserCredentialRepository userCredentials,
|
public WebAuthnRegistrationFilter(UserCredentialRepository userCredentials,
|
||||||
WebAuthnRelyingPartyOperations rpOptions) {
|
WebAuthnRelyingPartyOperations rpOptions) {
|
||||||
Assert.notNull(userCredentials, "userCredentials must not be null");
|
Assert.notNull(userCredentials, "userCredentials must not be null");
|
||||||
@@ -133,6 +146,42 @@ public class WebAuthnRegistrationFilter extends OncePerRequestFilter {
|
|||||||
this.removeCredentialMatcher = removeCredentialMatcher;
|
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
|
@Override
|
||||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
||||||
throws ServletException, IOException {
|
throws ServletException, IOException {
|
||||||
@@ -204,7 +253,15 @@ public class WebAuthnRegistrationFilter extends OncePerRequestFilter {
|
|||||||
|
|
||||||
private void removeCredential(HttpServletRequest request, HttpServletResponse response, @Nullable String id)
|
private void removeCredential(HttpServletRequest request, HttpServletResponse response, @Nullable String id)
|
||||||
throws IOException {
|
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());
|
response.setStatus(HttpStatus.NO_CONTENT.value());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+154
@@ -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.AuthorizationResult;
|
||||||
|
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();
|
||||||
|
AuthorizationResult decision = manager.authorize(() -> 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);
|
||||||
|
AuthorizationResult decision = manager.authorize(() -> 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");
|
||||||
|
AuthorizationResult decision = manager.authorize(() -> 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");
|
||||||
|
AuthorizationResult decision = manager.authorize(() -> 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");
|
||||||
|
AuthorizationResult decision = manager.authorize(() -> 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");
|
||||||
|
AuthorizationResult decision = manager.authorize(() -> 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");
|
||||||
|
AuthorizationResult decision = manager.authorize(() -> authentication, credentialId);
|
||||||
|
assertThat(decision.isGranted()).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+33
@@ -31,7 +31,11 @@ import org.springframework.mock.web.MockFilterChain;
|
|||||||
import org.springframework.mock.web.MockHttpServletRequest;
|
import org.springframework.mock.web.MockHttpServletRequest;
|
||||||
import org.springframework.mock.web.MockHttpServletResponse;
|
import org.springframework.mock.web.MockHttpServletResponse;
|
||||||
import org.springframework.mock.web.MockServletContext;
|
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.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.ImmutableCredentialRecord;
|
||||||
import org.springframework.security.web.webauthn.api.PublicKeyCredentialCreationOptions;
|
import org.springframework.security.web.webauthn.api.PublicKeyCredentialCreationOptions;
|
||||||
import org.springframework.security.web.webauthn.api.TestCredentialRecords;
|
import org.springframework.security.web.webauthn.api.TestCredentialRecords;
|
||||||
@@ -213,12 +217,41 @@ class WebAuthnRegistrationFilterTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void doFilterWhenDeleteSuccessThenNoContent() throws Exception {
|
void doFilterWhenDeleteSuccessThenNoContent() throws Exception {
|
||||||
|
this.filter.setDeleteCredentialAuthorizationManager(SingleResultAuthorizationManager.permitAll());
|
||||||
MockHttpServletRequest request = MockMvcRequestBuilders.delete("/webauthn/register/123456")
|
MockHttpServletRequest request = MockMvcRequestBuilders.delete("/webauthn/register/123456")
|
||||||
.buildRequest(new MockServletContext());
|
.buildRequest(new MockServletContext());
|
||||||
this.filter.doFilter(request, this.response, this.chain);
|
this.filter.doFilter(request, this.response, this.chain);
|
||||||
assertThat(this.response.getStatus()).isEqualTo(HttpStatus.NO_CONTENT.value());
|
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) {
|
private static MockHttpServletRequest registerCredentialRequest(String body) {
|
||||||
return MockMvcRequestBuilders.post(WebAuthnRegistrationFilter.DEFAULT_REGISTER_CREDENTIAL_URL)
|
return MockMvcRequestBuilders.post(WebAuthnRegistrationFilter.DEFAULT_REGISTER_CREDENTIAL_URL)
|
||||||
.content(body)
|
.content(body)
|
||||||
|
|||||||
Reference in New Issue
Block a user