10 changed files with 480 additions and 43 deletions
@ -0,0 +1,105 @@
@@ -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()); |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,58 @@
@@ -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; |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,93 @@
@@ -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); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,88 @@
@@ -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"); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,70 @@
@@ -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))); |
||||
} |
||||
} |
||||
Loading…
Reference in new issue