Allow configurable scope validation strategy in OAuth2ClientCredentialsAuthenticationProvider

Closes gh-1377
This commit is contained in:
adamleantech
2023-10-02 10:15:57 +01:00
committed by Joe Grandja
parent 168077be24
commit 5c3f1cb691
10 changed files with 480 additions and 43 deletions

View File

@@ -0,0 +1,88 @@
/*
* Copyright 2020-2022 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.oauth2.server.authorization.authentication;
import java.security.Principal;
import java.util.Map;
import java.util.Set;
import org.junit.jupiter.api.Test;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
import org.springframework.security.oauth2.server.authorization.TestOAuth2Authorizations;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
/**
* Tests for {@link OAuth2ClientCredentialsAuthenticationContext}.
*
* @author Steve Riesenberg
* @author Joe Grandja
*/
public class OAuth2ClientCredentialsAuthenticationContextTests {
private final RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
private final OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(this.registeredClient).build();
private final Authentication principal = this.authorization.getAttribute(Principal.class.getName());
private final OAuth2ClientCredentialsAuthenticationToken authorizationConsentAuthentication =
new OAuth2ClientCredentialsAuthenticationToken(this.principal, Set.of("a_scope"), Map.of("a_key", "a_value"));
@Test
public void withWhenAuthenticationNullThenThrowIllegalArgumentException() {
assertThatThrownBy(() -> OAuth2ClientCredentialsAuthenticationContext.with(null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("authentication cannot be null");
}
@Test
public void setWhenValueNullThenThrowIllegalArgumentException() {
OAuth2ClientCredentialsAuthenticationContext.Builder builder =
OAuth2ClientCredentialsAuthenticationContext.with(this.authorizationConsentAuthentication);
assertThatThrownBy(() -> builder.registeredClient(null))
.isInstanceOf(IllegalArgumentException.class);
assertThatThrownBy(() -> builder.put(null, ""))
.isInstanceOf(IllegalArgumentException.class);
}
@Test
public void buildWhenRequiredValueNullThenThrowIllegalArgumentException() {
OAuth2ClientCredentialsAuthenticationContext.Builder builder =
OAuth2ClientCredentialsAuthenticationContext.with(this.authorizationConsentAuthentication);
assertThatThrownBy(builder::build)
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("registeredClient cannot be null");
}
@Test
public void buildWhenAllValuesProvidedThenAllValuesAreSet() {
OAuth2ClientCredentialsAuthenticationContext context =
OAuth2ClientCredentialsAuthenticationContext.with(this.authorizationConsentAuthentication)
.registeredClient(this.registeredClient)
.put("custom-key-1", "custom-value-1")
.context(ctx -> ctx.put("custom-key-2", "custom-value-2"))
.build();
assertThat(context.<Authentication>getAuthentication()).isEqualTo(this.authorizationConsentAuthentication);
assertThat(context.getRegisteredClient()).isEqualTo(this.registeredClient);
assertThat(context.<String>get("custom-key-1")).isEqualTo("custom-value-1");
assertThat(context.<String>get("custom-key-2")).isEqualTo("custom-value-2");
}
}

View File

@@ -15,16 +15,10 @@
*/
package org.springframework.security.oauth2.server.authorization.authentication;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Collections;
import java.util.Set;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
@@ -56,6 +50,12 @@ import org.springframework.security.oauth2.server.authorization.token.OAuth2Toke
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Collections;
import java.util.Set;
import java.util.function.Consumer;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
@@ -211,6 +211,16 @@ public class OAuth2ClientCredentialsAuthenticationProviderTests {
assertThat(accessTokenAuthentication.getAccessToken().getScopes()).isEqualTo(requestedScope);
}
@Test
public void authenticateWhenCustomAuthenticationValidatorThenInvokeValidator() {
Consumer<OAuth2ClientCredentialsAuthenticationContext> validator = mock(Consumer.class);
this.authenticationProvider.setAuthenticationValidator(validator);
authenticateWhenScopeRequestedThenAccessTokenContainsScope();
verify(validator).accept(any(OAuth2ClientCredentialsAuthenticationContext.class));
}
@Test
public void authenticateWhenNoScopeRequestedThenAccessTokenDoesNotContainScope() {
RegisteredClient registeredClient = TestRegisteredClients.registeredClient2().build();

View File

@@ -0,0 +1,70 @@
/*
* Copyright 2020-2022 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.oauth2.server.authorization.authentication;
import org.junit.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
import org.springframework.security.oauth2.server.authorization.TestOAuth2Authorizations;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients;
import java.security.Principal;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatNoException;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients.SCOPE_1;
import static org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients.SCOPE_2;
public class OAuth2ClientCredentialsAuthenticationValidatorTest {
private final RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
private final OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(this.registeredClient).build();
private final Authentication principal = this.authorization.getAttribute(Principal.class.getName());
private final OAuth2ClientCredentialsAuthenticationValidator validator = new OAuth2ClientCredentialsAuthenticationValidator();
@ParameterizedTest
@MethodSource("validScopes")
public void acceptWhenRequestScopesAreEmptyOrValidThenDoesNotThrowException(Set<String> testScopes) {
OAuth2ClientCredentialsAuthenticationToken token =
new OAuth2ClientCredentialsAuthenticationToken(this.principal, testScopes, Map.of());
OAuth2ClientCredentialsAuthenticationContext context = OAuth2ClientCredentialsAuthenticationContext.with(token).registeredClient(registeredClient).build();
assertThatNoException().isThrownBy(() -> validator.accept(context));
}
@Test
public void acceptWhenRequestScopesAreNotAllValidThenThrowException() {
OAuth2ClientCredentialsAuthenticationToken token =
new OAuth2ClientCredentialsAuthenticationToken(this.principal, Set.of(SCOPE_1, SCOPE_2), Map.of());
OAuth2ClientCredentialsAuthenticationContext context = OAuth2ClientCredentialsAuthenticationContext.with(token).registeredClient(registeredClient).build();
assertThatThrownBy(() -> validator.accept(context))
.isInstanceOfSatisfying(OAuth2ClientCredentialsAuthenticationException.class,
t -> assertThat(t.getClientCredentialsAuthentication()).isEqualTo(token));
}
static Stream<Arguments> validScopes() {
return Stream.of(Arguments.of(new HashSet<>()), Arguments.of(Set.of(SCOPE_1)));
}
}

View File

@@ -15,17 +15,19 @@
*/
package org.springframework.security.oauth2.server.authorization.client;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
/**
* @author Anoop Garlapati
*/
public class TestRegisteredClients {
public static final String SCOPE_1 = "scope1";
public static final String SCOPE_2 = "scope2";
public static RegisteredClient.Builder registeredClient() {
return RegisteredClient.withId("registration-1")
@@ -39,7 +41,7 @@ public class TestRegisteredClients {
.redirectUri("https://example.com/callback-2")
.redirectUri("https://example.com/callback-3")
.postLogoutRedirectUri("https://example.com/oidc-post-logout")
.scope("scope1");
.scope(SCOPE_1);
}
public static RegisteredClient.Builder registeredClient2() {
@@ -54,8 +56,8 @@ public class TestRegisteredClients {
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST)
.redirectUri("https://example.com")
.postLogoutRedirectUri("https://example.com/oidc-post-logout")
.scope("scope1")
.scope("scope2");
.scope(SCOPE_1)
.scope(SCOPE_2);
}
public static RegisteredClient.Builder registeredPublicClient() {
@@ -65,7 +67,7 @@ public class TestRegisteredClients {
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.clientAuthenticationMethod(ClientAuthenticationMethod.NONE)
.redirectUri("https://example.com")
.scope("scope1")
.scope(SCOPE_1)
.clientSettings(ClientSettings.builder().requireProofKey(true).build());
}
}

View File

@@ -15,21 +15,10 @@
*/
package org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import javax.crypto.spec.SecretKeySpec;
import jakarta.servlet.http.HttpServletResponse;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import jakarta.servlet.http.HttpServletResponse;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import org.junit.jupiter.api.AfterAll;
@@ -39,7 +28,6 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -111,6 +99,15 @@ import org.springframework.test.web.servlet.MvcResult;
import org.springframework.util.CollectionUtils;
import org.springframework.web.util.UriComponentsBuilder;
import javax.crypto.spec.SecretKeySpec;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.CoreMatchers.containsString;
import static org.mockito.ArgumentMatchers.any;