diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientCredentialsAuthenticationContext.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientCredentialsAuthenticationContext.java new file mode 100644 index 00000000..1bf80d93 --- /dev/null +++ b/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 context; + + private OAuth2ClientCredentialsAuthenticationContext(Map context) { + this.context = Map.copyOf(context); + } + + @SuppressWarnings("unchecked") + @Nullable + @Override + public 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 { + + 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()); + } + + } + +} diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientCredentialsAuthenticationException.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientCredentialsAuthenticationException.java new file mode 100644 index 00000000..fca40b5c --- /dev/null +++ b/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; + } + +} diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientCredentialsAuthenticationProvider.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientCredentialsAuthenticationProvider.java index dc0fcff1..6399321b 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientCredentialsAuthenticationProvider.java +++ b/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 tokenGenerator; + private Consumer 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 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 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}. + * + *

+ * NOTE: 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 authenticationValidator) { + Assert.notNull(authenticationValidator, "authenticationValidator cannot be null"); + this.authenticationValidator = authenticationValidator; + } + } diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientCredentialsAuthenticationValidator.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientCredentialsAuthenticationValidator.java new file mode 100644 index 00000000..82181a5d --- /dev/null +++ b/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. + * + *

+ * 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 { + 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 DEFAULT_SCOPE_VALIDATOR = + OAuth2ClientCredentialsAuthenticationValidator::validateScope; + + private final Consumer 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 requestedScopes = clientCredentialsAuthenticationToken.getScopes(); + Set 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); + } + +} diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2TokenEndpointConfigurer.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2TokenEndpointConfigurer.java index fa23a173..12857847 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2TokenEndpointConfigurer.java +++ b/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 createDefaultAuthenticationProviders(HttpSecurity httpSecurity) { + private List createDefaultAuthenticationProviders(HttpSecurity httpSecurity) { List authenticationProviders = new ArrayList<>(); OAuth2AuthorizationService authorizationService = OAuth2ConfigurerUtils.getAuthorizationService(httpSecurity); diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientCredentialsAuthenticationContextTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientCredentialsAuthenticationContextTests.java new file mode 100644 index 00000000..2ceb51e1 --- /dev/null +++ b/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.getAuthentication()).isEqualTo(this.authorizationConsentAuthentication); + assertThat(context.getRegisteredClient()).isEqualTo(this.registeredClient); + assertThat(context.get("custom-key-1")).isEqualTo("custom-value-1"); + assertThat(context.get("custom-key-2")).isEqualTo("custom-value-2"); + } + +} diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientCredentialsAuthenticationProviderTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientCredentialsAuthenticationProviderTests.java index 8f66092b..36712a9a 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientCredentialsAuthenticationProviderTests.java +++ b/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 validator = mock(Consumer.class); + this.authenticationProvider.setAuthenticationValidator(validator); + + authenticateWhenScopeRequestedThenAccessTokenContainsScope(); + + verify(validator).accept(any(OAuth2ClientCredentialsAuthenticationContext.class)); + } + @Test public void authenticateWhenNoScopeRequestedThenAccessTokenDoesNotContainScope() { RegisteredClient registeredClient = TestRegisteredClients.registeredClient2().build(); diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientCredentialsAuthenticationValidatorTest.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientCredentialsAuthenticationValidatorTest.java new file mode 100644 index 00000000..18a3dd6d --- /dev/null +++ b/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 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 validScopes() { + return Stream.of(Arguments.of(new HashSet<>()), Arguments.of(Set.of(SCOPE_1))); + } +} diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/client/TestRegisteredClients.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/client/TestRegisteredClients.java index 90151201..ac640192 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/client/TestRegisteredClients.java +++ b/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()); } } diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcClientRegistrationTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcClientRegistrationTests.java index be1ea066..0a828c3e 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcClientRegistrationTests.java +++ b/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;