浏览代码

Allow configurable scope validation strategy in OAuth2ClientCredentialsAuthenticationProvider

Closes gh-1377
adamleantech 1 年之前
父节点
当前提交
5c3f1cb691
共有 10 个文件被更改,包括 480 次插入43 次删除
  1. 105 0
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientCredentialsAuthenticationContext.java
  2. 58 0
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientCredentialsAuthenticationException.java
  3. 30 15
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientCredentialsAuthenticationProvider.java
  4. 93 0
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientCredentialsAuthenticationValidator.java
  5. 1 2
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2TokenEndpointConfigurer.java
  6. 88 0
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientCredentialsAuthenticationContextTests.java
  7. 16 6
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientCredentialsAuthenticationProviderTests.java
  8. 70 0
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientCredentialsAuthenticationValidatorTest.java
  9. 9 7
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/client/TestRegisteredClients.java
  10. 10 13
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcClientRegistrationTests.java

+ 105 - 0
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientCredentialsAuthenticationContext.java

@@ -0,0 +1,105 @@
+/*
+ * 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.springframework.lang.Nullable;
+import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
+import org.springframework.util.Assert;
+
+import java.util.Map;
+import java.util.function.Consumer;
+
+/**
+ * An {@link OAuth2AuthenticationContext} that holds an {@link OAuth2ClientCredentialsAuthenticationToken} and additional information
+ * and is used when validating the OAuth 2.0 Authorization Request used in the Client Credentials Grant.
+ *
+ * @author Adam Pilling
+ * @since 1.3.0
+ * @see OAuth2AuthenticationContext
+ * @see OAuth2ClientCredentialsAuthenticationToken
+ * @see OAuth2ClientCredentialsAuthenticationProvider#setAuthenticationValidator(Consumer)
+ */
+public final class OAuth2ClientCredentialsAuthenticationContext implements OAuth2AuthenticationContext {
+	private final Map<Object, Object> context;
+
+	private OAuth2ClientCredentialsAuthenticationContext(Map<Object, Object> context) {
+		this.context = Map.copyOf(context);
+	}
+
+	@SuppressWarnings("unchecked")
+	@Nullable
+	@Override
+	public <V> V get(Object key) {
+		return hasKey(key) ? (V) this.context.get(key) : null;
+	}
+
+	@Override
+	public boolean hasKey(Object key) {
+		Assert.notNull(key, "key cannot be null");
+		return this.context.containsKey(key);
+	}
+
+	/**
+	 * Returns the {@link RegisteredClient registered client}.
+	 *
+	 * @return the {@link RegisteredClient}
+	 */
+	public RegisteredClient getRegisteredClient() {
+		return get(RegisteredClient.class);
+	}
+
+	/**
+	 * Constructs a new {@link Builder} with the provided {@link OAuth2ClientCredentialsAuthenticationToken}.
+	 *
+	 * @param authentication the {@link OAuth2ClientCredentialsAuthenticationToken}
+	 * @return the {@link Builder}
+	 */
+	public static Builder with(OAuth2ClientCredentialsAuthenticationToken authentication) {
+		return new Builder(authentication);
+	}
+
+	/**
+	 * A builder for {@link OAuth2ClientCredentialsAuthenticationContext}.
+	 */
+	public static final class Builder extends AbstractBuilder<OAuth2ClientCredentialsAuthenticationContext, Builder> {
+
+		private Builder(OAuth2ClientCredentialsAuthenticationToken authentication) {
+			super(authentication);
+		}
+
+		/**
+		 * Sets the {@link RegisteredClient registered client}.
+		 *
+		 * @param registeredClient the {@link RegisteredClient}
+		 * @return the {@link Builder} for further configuration
+		 */
+		public Builder registeredClient(RegisteredClient registeredClient) {
+			return put(RegisteredClient.class, registeredClient);
+		}
+
+		/**
+		 * Builds a new {@link OAuth2ClientCredentialsAuthenticationContext}.
+		 *
+		 * @return the {@link OAuth2ClientCredentialsAuthenticationContext}
+		 */
+		public OAuth2ClientCredentialsAuthenticationContext build() {
+			Assert.notNull(get(RegisteredClient.class), "registeredClient cannot be null");
+			return new OAuth2ClientCredentialsAuthenticationContext(getContext());
+		}
+
+	}
+
+}

+ 58 - 0
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientCredentialsAuthenticationException.java

@@ -0,0 +1,58 @@
+/*
+ * Copyright 2020-2021 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.springframework.lang.Nullable;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
+import org.springframework.security.oauth2.core.OAuth2Error;
+
+/**
+ * This exception is thrown by {@link OAuth2ClientCredentialsAuthenticationProvider}
+ * when an attempt to authenticate the OAuth 2.0 Authorization Request (or Consent) fails.
+ *
+ * @author Adam Pilling
+ * @since 1.3.0
+ * @see OAuth2ClientCredentialsAuthenticationToken
+ * @see OAuth2ClientCredentialsAuthenticationProvider
+ */
+public class OAuth2ClientCredentialsAuthenticationException extends OAuth2AuthenticationException {
+	private final OAuth2ClientCredentialsAuthenticationToken authorizationCodeRequestAuthentication;
+
+	/**
+	 * Constructs an {@code OAuth2ClientCredentialsAuthenticationException} using the provided parameters.
+	 *
+	 * @param error the {@link OAuth2Error OAuth 2.0 Error}
+	 * @param authorizationCodeRequestAuthentication the {@link Authentication} instance of the OAuth 2.0 Authorization Request (or Consent)
+	 */
+	public OAuth2ClientCredentialsAuthenticationException(
+			OAuth2Error error,
+			@Nullable OAuth2ClientCredentialsAuthenticationToken authorizationCodeRequestAuthentication) {
+		super(error);
+		this.authorizationCodeRequestAuthentication = authorizationCodeRequestAuthentication;
+	}
+
+	/**
+	 * Returns the {@link Authentication} instance of the OAuth 2.0 Authorization Request (or Consent), or {@code null} if not available.
+	 *
+	 * @return the {@link OAuth2AuthorizationCodeRequestAuthenticationToken}
+	 */
+	@Nullable
+	public OAuth2ClientCredentialsAuthenticationToken getClientCredentialsAuthentication() {
+		return this.authorizationCodeRequestAuthentication;
+	}
+
+}

+ 30 - 15
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientCredentialsAuthenticationProvider.java

@@ -15,13 +15,8 @@
  */
 package org.springframework.security.oauth2.server.authorization.authentication;
 
-import java.util.Collections;
-import java.util.LinkedHashSet;
-import java.util.Set;
-
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
-
 import org.springframework.security.authentication.AuthenticationProvider;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.AuthenticationException;
@@ -41,7 +36,9 @@ import org.springframework.security.oauth2.server.authorization.token.DefaultOAu
 import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext;
 import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
 import org.springframework.util.Assert;
-import org.springframework.util.CollectionUtils;
+
+import java.util.Set;
+import java.util.function.Consumer;
 
 import static org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthenticationProviderUtils.getAuthenticatedClientElseThrowInvalidClient;
 
@@ -63,6 +60,8 @@ public final class OAuth2ClientCredentialsAuthenticationProvider implements Auth
 	private final Log logger = LogFactory.getLog(getClass());
 	private final OAuth2AuthorizationService authorizationService;
 	private final OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator;
+	private Consumer<OAuth2ClientCredentialsAuthenticationContext> authenticationValidator =
+			new OAuth2ClientCredentialsAuthenticationValidator();
 
 	/**
 	 * Constructs an {@code OAuth2ClientCredentialsAuthenticationProvider} using the provided parameters.
@@ -96,20 +95,18 @@ public final class OAuth2ClientCredentialsAuthenticationProvider implements Auth
 			throw new OAuth2AuthenticationException(OAuth2ErrorCodes.UNAUTHORIZED_CLIENT);
 		}
 
-		Set<String> authorizedScopes = Collections.emptySet();
-		if (!CollectionUtils.isEmpty(clientCredentialsAuthentication.getScopes())) {
-			for (String requestedScope : clientCredentialsAuthentication.getScopes()) {
-				if (!registeredClient.getScopes().contains(requestedScope)) {
-					throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_SCOPE);
-				}
-			}
-			authorizedScopes = new LinkedHashSet<>(clientCredentialsAuthentication.getScopes());
-		}
+		OAuth2ClientCredentialsAuthenticationContext authenticationContext =
+				OAuth2ClientCredentialsAuthenticationContext.with(clientCredentialsAuthentication)
+						.registeredClient(registeredClient)
+						.build();
+		authenticationValidator.accept(authenticationContext);
 
 		if (this.logger.isTraceEnabled()) {
 			this.logger.trace("Validated token request parameters");
 		}
 
+		Set<String> authorizedScopes = Set.copyOf(clientCredentialsAuthentication.getScopes());
+
 		// @formatter:off
 		OAuth2TokenContext tokenContext = DefaultOAuth2TokenContext.builder()
 				.registeredClient(registeredClient)
@@ -168,4 +165,22 @@ public final class OAuth2ClientCredentialsAuthenticationProvider implements Auth
 		return OAuth2ClientCredentialsAuthenticationToken.class.isAssignableFrom(authentication);
 	}
 
+	/**
+	 * Sets the {@code Consumer} providing access to the {@link OAuth2ClientCredentialsAuthenticationContext}
+	 * and is responsible for validating specific OAuth 2.0 Client Credentials parameters
+	 * associated in the {@link OAuth2ClientCredentialsAuthenticationToken}.
+	 * The default authentication validator is {@link OAuth2ClientCredentialsAuthenticationValidator}.
+	 *
+	 * <p>
+	 * <b>NOTE:</b> The authentication validator MUST throw {@link OAuth2ClientCredentialsAuthenticationException} if validation fails.
+	 *
+	 * @param authenticationValidator the {@code Consumer} providing access to the {@link OAuth2ClientCredentialsAuthenticationContext}
+	 *                                   and is responsible for validating specific OAuth 2.0 Authorization Request parameters
+	 * @since 1.3.0
+	 */
+	public void setAuthenticationValidator(Consumer<OAuth2ClientCredentialsAuthenticationContext> authenticationValidator) {
+		Assert.notNull(authenticationValidator, "authenticationValidator cannot be null");
+		this.authenticationValidator = authenticationValidator;
+	}
+
 }

+ 93 - 0
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientCredentialsAuthenticationValidator.java

@@ -0,0 +1,93 @@
+/*
+ * Copyright 2020-2023 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.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.core.log.LogMessage;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.oauth2.core.OAuth2Error;
+import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
+import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
+import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
+
+import java.util.Set;
+import java.util.function.Consumer;
+
+/**
+ * A {@code Consumer} providing access to the {@link OAuth2ClientCredentialsAuthenticationContext}
+ * containing an {@link OAuth2ClientCredentialsAuthenticationToken}
+ * and is the default {@link OAuth2ClientCredentialsAuthenticationProvider#setAuthenticationValidator(Consumer) authentication validator}
+ * used for validating specific OAuth 2.0 Client Credentials parameters used in the Client Credentials Grant.
+ *
+ * <p>
+ * The default compares the provided scopes with those configured in the RegisteredClient.
+ * If validation fails, an {@link OAuth2ClientCredentialsAuthenticationException} is thrown.
+ *
+ * @author Adam Pilling
+ * @since 1.3.0
+ * @see OAuth2ClientCredentialsAuthenticationContext
+ * @see RegisteredClient
+ * @see OAuth2ClientCredentialsAuthenticationToken
+ * @see OAuth2ClientCredentialsAuthenticationProvider#setAuthenticationValidator(Consumer)
+ */
+public final class OAuth2ClientCredentialsAuthenticationValidator implements Consumer<OAuth2ClientCredentialsAuthenticationContext> {
+	private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1";
+	private static final Log LOGGER = LogFactory.getLog(OAuth2ClientCredentialsAuthenticationValidator.class);
+
+	/**
+	 * The default validator for {@link OAuth2ClientCredentialsAuthenticationToken#getScopes()}.
+	 */
+	public static final Consumer<OAuth2ClientCredentialsAuthenticationContext> DEFAULT_SCOPE_VALIDATOR =
+			OAuth2ClientCredentialsAuthenticationValidator::validateScope;
+
+	private final Consumer<OAuth2ClientCredentialsAuthenticationContext> authenticationValidator = DEFAULT_SCOPE_VALIDATOR;
+
+	@Override
+	public void accept(OAuth2ClientCredentialsAuthenticationContext authenticationContext) {
+		this.authenticationValidator.accept(authenticationContext);
+	}
+
+	private static void validateScope(OAuth2ClientCredentialsAuthenticationContext authenticationContext) {
+		OAuth2ClientCredentialsAuthenticationToken clientCredentialsAuthenticationToken =
+				authenticationContext.getAuthentication();
+		RegisteredClient registeredClient = authenticationContext.getRegisteredClient();
+
+		Set<String> requestedScopes = clientCredentialsAuthenticationToken.getScopes();
+		Set<String> allowedScopes = registeredClient.getScopes();
+		if (!requestedScopes.isEmpty() && !allowedScopes.containsAll(requestedScopes)) {
+			if (LOGGER.isDebugEnabled()) {
+				LOGGER.debug(LogMessage.format("Invalid request: requested scope is not allowed" +
+						" for registered client '%s'", registeredClient.getId()));
+			}
+			throwError(OAuth2ErrorCodes.INVALID_SCOPE, OAuth2ParameterNames.SCOPE, clientCredentialsAuthenticationToken);
+		}
+	}
+
+	private static void throwError(String errorCode, String parameterName,
+			OAuth2ClientCredentialsAuthenticationToken clientCredentialsAuthenticationToken) {
+		OAuth2Error error = new OAuth2Error(errorCode, "OAuth 2.0 Parameter: " + parameterName, ERROR_URI);
+		OAuth2ClientCredentialsAuthenticationToken authorizationCodeRequestAuthenticationResult =
+				new OAuth2ClientCredentialsAuthenticationToken(
+						(Authentication) clientCredentialsAuthenticationToken.getPrincipal(),
+						clientCredentialsAuthenticationToken.getScopes(),
+						clientCredentialsAuthenticationToken.getAdditionalParameters());
+		authorizationCodeRequestAuthenticationResult.setAuthenticated(true);
+
+		throw new OAuth2ClientCredentialsAuthenticationException(error, authorizationCodeRequestAuthenticationResult);
+	}
+
+}

+ 1 - 2
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2TokenEndpointConfigurer.java

@@ -20,7 +20,6 @@ import java.util.List;
 import java.util.function.Consumer;
 
 import jakarta.servlet.http.HttpServletRequest;
-
 import org.springframework.http.HttpMethod;
 import org.springframework.security.authentication.AuthenticationManager;
 import org.springframework.security.authentication.AuthenticationProvider;
@@ -217,7 +216,7 @@ public final class OAuth2TokenEndpointConfigurer extends AbstractOAuth2Configure
 		return authenticationConverters;
 	}
 
-	private static List<AuthenticationProvider> createDefaultAuthenticationProviders(HttpSecurity httpSecurity) {
+	private List<AuthenticationProvider> createDefaultAuthenticationProviders(HttpSecurity httpSecurity) {
 		List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
 
 		OAuth2AuthorizationService authorizationService = OAuth2ConfigurerUtils.getAuthorizationService(httpSecurity);

+ 88 - 0
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientCredentialsAuthenticationContextTests.java

@@ -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");
+	}
+
+}

+ 16 - 6
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientCredentialsAuthenticationProviderTests.java

@@ -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();

+ 70 - 0
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientCredentialsAuthenticationValidatorTest.java

@@ -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)));
+	}
+}

+ 9 - 7
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/client/TestRegisteredClients.java

@@ -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());
 	}
 }

+ 10 - 13
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcClientRegistrationTests.java

@@ -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;