Skip to content

Allow configurable scope validation strategy in OAuth2ClientCredentialsAuthenticationProvider #1377

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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());
}

}

}
Original file line number Diff line number Diff line change
@@ -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;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -41,7 +36,9 @@
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;

Expand All @@ -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.
Expand Down Expand Up @@ -96,20 +95,18 @@ public Authentication authenticate(Authentication authentication) throws Authent
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)
Expand Down Expand Up @@ -168,4 +165,22 @@ public boolean supports(Class<?> authentication) {
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;
}

}
Original file line number Diff line number Diff line change
@@ -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);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
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;
Expand Down Expand Up @@ -215,7 +214,7 @@ private static List<AuthenticationConverter> createDefaultAuthenticationConverte
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);
Expand Down
Loading