Browse Source

Provide configuration for the token endpoint

Closes gh-319
Joe Grandja 4 years ago
parent
commit
1f4b369912

+ 45 - 0
oauth2-authorization-server/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/AbstractOAuth2Configurer.java

@@ -0,0 +1,45 @@
+/*
+ * 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.config.annotation.web.configurers.oauth2.server.authorization;
+
+import org.springframework.security.config.annotation.ObjectPostProcessor;
+import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
+import org.springframework.security.web.util.matcher.RequestMatcher;
+
+/**
+ * Base configurer for an OAuth 2.0 component (e.g. protocol endpoint).
+ *
+ * @author Joe Grandja
+ * @since 0.1.2
+ */
+abstract class AbstractOAuth2Configurer {
+	private final ObjectPostProcessor<Object> objectPostProcessor;
+
+	AbstractOAuth2Configurer(ObjectPostProcessor<Object> objectPostProcessor) {
+		this.objectPostProcessor = objectPostProcessor;
+	}
+
+	abstract <B extends HttpSecurityBuilder<B>> void init(B builder);
+
+	abstract <B extends HttpSecurityBuilder<B>> void configure(B builder);
+
+	abstract RequestMatcher getRequestMatcher();
+
+	protected <T> T postProcess(T object) {
+		return (T) this.objectPostProcessor.postProcess(object);
+	}
+
+}

+ 39 - 51
oauth2-authorization-server/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2AuthorizationServerConfigurer.java

@@ -16,6 +16,8 @@
 package org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization;
 
 import java.net.URI;
+import java.util.LinkedHashMap;
+import java.util.Map;
 
 import com.nimbusds.jose.jwk.source.JWKSource;
 import com.nimbusds.jose.proc.SecurityContext;
@@ -23,19 +25,14 @@ import com.nimbusds.jose.proc.SecurityContext;
 import org.springframework.http.HttpMethod;
 import org.springframework.http.HttpStatus;
 import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.config.Customizer;
 import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
 import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
 import org.springframework.security.config.annotation.web.configurers.ExceptionHandlingConfigurer;
 import org.springframework.security.crypto.password.PasswordEncoder;
-import org.springframework.security.oauth2.jwt.JwtEncoder;
-import org.springframework.security.oauth2.server.authorization.JwtEncodingContext;
 import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
 import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
-import org.springframework.security.oauth2.server.authorization.OAuth2TokenCustomizer;
-import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeAuthenticationProvider;
 import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationProvider;
-import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientCredentialsAuthenticationProvider;
-import org.springframework.security.oauth2.server.authorization.authentication.OAuth2RefreshTokenAuthenticationProvider;
 import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenIntrospectionAuthenticationProvider;
 import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenRevocationAuthenticationProvider;
 import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
@@ -47,7 +44,6 @@ import org.springframework.security.oauth2.server.authorization.web.NimbusJwkSet
 import org.springframework.security.oauth2.server.authorization.web.OAuth2AuthorizationEndpointFilter;
 import org.springframework.security.oauth2.server.authorization.web.OAuth2AuthorizationServerMetadataEndpointFilter;
 import org.springframework.security.oauth2.server.authorization.web.OAuth2ClientAuthenticationFilter;
-import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenEndpointFilter;
 import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenIntrospectionEndpointFilter;
 import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenRevocationEndpointFilter;
 import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
@@ -68,11 +64,11 @@ import org.springframework.util.StringUtils;
  * @author Ovidiu Popa
  * @since 0.0.1
  * @see AbstractHttpConfigurer
+ * @see OAuth2TokenEndpointConfigurer
  * @see RegisteredClientRepository
  * @see OAuth2AuthorizationService
  * @see OAuth2AuthorizationConsentService
  * @see OAuth2AuthorizationEndpointFilter
- * @see OAuth2TokenEndpointFilter
  * @see OAuth2TokenIntrospectionEndpointFilter
  * @see OAuth2TokenRevocationEndpointFilter
  * @see NimbusJwkSetEndpointFilter
@@ -84,8 +80,8 @@ import org.springframework.util.StringUtils;
 public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBuilder<B>>
 		extends AbstractHttpConfigurer<OAuth2AuthorizationServerConfigurer<B>, B> {
 
+	private final Map<Class<? extends AbstractOAuth2Configurer>, AbstractOAuth2Configurer> configurers = createConfigurers();
 	private RequestMatcher authorizationEndpointMatcher;
-	private RequestMatcher tokenEndpointMatcher;
 	private RequestMatcher tokenIntrospectionEndpointMatcher;
 	private RequestMatcher tokenRevocationEndpointMatcher;
 	private RequestMatcher jwkSetEndpointMatcher;
@@ -94,7 +90,7 @@ public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBui
 	private RequestMatcher oidcClientRegistrationEndpointMatcher;
 	private final RequestMatcher endpointsMatcher = (request) ->
 			this.authorizationEndpointMatcher.matches(request) ||
-			this.tokenEndpointMatcher.matches(request) ||
+			getRequestMatcher(OAuth2TokenEndpointConfigurer.class).matches(request) ||
 			this.tokenIntrospectionEndpointMatcher.matches(request) ||
 			this.tokenRevocationEndpointMatcher.matches(request) ||
 			this.jwkSetEndpointMatcher.matches(request) ||
@@ -187,6 +183,17 @@ public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBui
 		return this;
 	}
 
+	/**
+	 * Configures the OAuth 2.0 Token Endpoint.
+	 *
+	 * @param tokenEndpointCustomizer the {@link Customizer} providing access to the {@link OAuth2TokenEndpointConfigurer}
+	 * @return the {@link OAuth2AuthorizationServerConfigurer} for further configuration
+	 */
+	public OAuth2AuthorizationServerConfigurer<B> tokenEndpoint(Customizer<OAuth2TokenEndpointConfigurer> tokenEndpointCustomizer) {
+		tokenEndpointCustomizer.customize(getConfigurer(OAuth2TokenEndpointConfigurer.class));
+		return this;
+	}
+
 	/**
 	 * Returns a {@link RequestMatcher} for the authorization server endpoints.
 	 *
@@ -202,6 +209,8 @@ public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBui
 		validateProviderSettings(providerSettings);
 		initEndpointMatchers(providerSettings);
 
+		this.configurers.values().forEach(configurer -> configurer.init(builder));
+
 		OAuth2ClientAuthenticationProvider clientAuthenticationProvider =
 				new OAuth2ClientAuthenticationProvider(
 						OAuth2ConfigurerUtils.getRegisteredClientRepository(builder),
@@ -212,36 +221,6 @@ public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBui
 		}
 		builder.authenticationProvider(postProcess(clientAuthenticationProvider));
 
-		JwtEncoder jwtEncoder = OAuth2ConfigurerUtils.getJwtEncoder(builder);
-		OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer = OAuth2ConfigurerUtils.getJwtCustomizer(builder);
-
-		OAuth2AuthorizationCodeAuthenticationProvider authorizationCodeAuthenticationProvider =
-				new OAuth2AuthorizationCodeAuthenticationProvider(
-						OAuth2ConfigurerUtils.getAuthorizationService(builder),
-						jwtEncoder);
-		if (jwtCustomizer != null) {
-			authorizationCodeAuthenticationProvider.setJwtCustomizer(jwtCustomizer);
-		}
-		builder.authenticationProvider(postProcess(authorizationCodeAuthenticationProvider));
-
-		OAuth2RefreshTokenAuthenticationProvider refreshTokenAuthenticationProvider =
-				new OAuth2RefreshTokenAuthenticationProvider(
-						OAuth2ConfigurerUtils.getAuthorizationService(builder),
-						jwtEncoder);
-		if (jwtCustomizer != null) {
-			refreshTokenAuthenticationProvider.setJwtCustomizer(jwtCustomizer);
-		}
-		builder.authenticationProvider(postProcess(refreshTokenAuthenticationProvider));
-
-		OAuth2ClientCredentialsAuthenticationProvider clientCredentialsAuthenticationProvider =
-				new OAuth2ClientCredentialsAuthenticationProvider(
-						OAuth2ConfigurerUtils.getAuthorizationService(builder),
-						jwtEncoder);
-		if (jwtCustomizer != null) {
-			clientCredentialsAuthenticationProvider.setJwtCustomizer(jwtCustomizer);
-		}
-		builder.authenticationProvider(postProcess(clientCredentialsAuthenticationProvider));
-
 		OAuth2TokenIntrospectionAuthenticationProvider tokenIntrospectionAuthenticationProvider =
 				new OAuth2TokenIntrospectionAuthenticationProvider(
 						OAuth2ConfigurerUtils.getRegisteredClientRepository(builder),
@@ -265,7 +244,7 @@ public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBui
 			exceptionHandling.defaultAuthenticationEntryPointFor(
 					new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED),
 					new OrRequestMatcher(
-							this.tokenEndpointMatcher,
+							getRequestMatcher(OAuth2TokenEndpointConfigurer.class),
 							this.tokenIntrospectionEndpointMatcher,
 							this.tokenRevocationEndpointMatcher)
 			);
@@ -274,6 +253,8 @@ public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBui
 
 	@Override
 	public void configure(B builder) {
+		this.configurers.values().forEach(configurer -> configurer.configure(builder));
+
 		ProviderSettings providerSettings = OAuth2ConfigurerUtils.getProviderSettings(builder);
 		if (providerSettings.issuer() != null) {
 			OidcProviderConfigurationEndpointFilter oidcProviderConfigurationEndpointFilter =
@@ -297,7 +278,7 @@ public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBui
 				new OAuth2ClientAuthenticationFilter(
 						authenticationManager,
 						new OrRequestMatcher(
-								this.tokenEndpointMatcher,
+								getRequestMatcher(OAuth2TokenEndpointConfigurer.class),
 								this.tokenIntrospectionEndpointMatcher,
 								this.tokenRevocationEndpointMatcher));
 		builder.addFilterAfter(postProcess(clientAuthenticationFilter), AbstractPreAuthenticatedProcessingFilter.class);
@@ -313,17 +294,11 @@ public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBui
 		}
 		builder.addFilterBefore(postProcess(authorizationEndpointFilter), AbstractPreAuthenticatedProcessingFilter.class);
 
-		OAuth2TokenEndpointFilter tokenEndpointFilter =
-				new OAuth2TokenEndpointFilter(
-						authenticationManager,
-						providerSettings.tokenEndpoint());
-		builder.addFilterAfter(postProcess(tokenEndpointFilter), FilterSecurityInterceptor.class);
-
 		OAuth2TokenIntrospectionEndpointFilter tokenIntrospectionEndpointFilter =
 				new OAuth2TokenIntrospectionEndpointFilter(
 						authenticationManager,
 						providerSettings.tokenIntrospectionEndpoint());
-		builder.addFilterAfter(postProcess(tokenIntrospectionEndpointFilter), OAuth2TokenEndpointFilter.class);
+		builder.addFilterAfter(postProcess(tokenIntrospectionEndpointFilter), FilterSecurityInterceptor.class);
 
 		OAuth2TokenRevocationEndpointFilter tokenRevocationEndpointFilter =
 				new OAuth2TokenRevocationEndpointFilter(
@@ -339,6 +314,21 @@ public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBui
 		builder.addFilterAfter(postProcess(oidcClientRegistrationEndpointFilter), OAuth2TokenRevocationEndpointFilter.class);
 	}
 
+	private Map<Class<? extends AbstractOAuth2Configurer>, AbstractOAuth2Configurer> createConfigurers() {
+		Map<Class<? extends AbstractOAuth2Configurer>, AbstractOAuth2Configurer> configurers = new LinkedHashMap<>();
+		configurers.put(OAuth2TokenEndpointConfigurer.class, new OAuth2TokenEndpointConfigurer(this::postProcess));
+		return configurers;
+	}
+
+	@SuppressWarnings("unchecked")
+	private <T> T getConfigurer(Class<T> type) {
+		return (T) this.configurers.get(type);
+	}
+
+	private <T extends AbstractOAuth2Configurer> RequestMatcher getRequestMatcher(Class<T> configurerType) {
+		return getConfigurer(configurerType).getRequestMatcher();
+	}
+
 	private void initEndpointMatchers(ProviderSettings providerSettings) {
 		this.authorizationEndpointMatcher = new OrRequestMatcher(
 				new AntPathRequestMatcher(
@@ -347,8 +337,6 @@ public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBui
 				new AntPathRequestMatcher(
 						providerSettings.authorizationEndpoint(),
 						HttpMethod.POST.name()));
-		this.tokenEndpointMatcher = new AntPathRequestMatcher(
-				providerSettings.tokenEndpoint(), HttpMethod.POST.name());
 		this.tokenIntrospectionEndpointMatcher = new AntPathRequestMatcher(
 				providerSettings.tokenIntrospectionEndpoint(), HttpMethod.POST.name());
 		this.tokenRevocationEndpointMatcher = new AntPathRequestMatcher(

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

@@ -0,0 +1,194 @@
+/*
+ * 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.config.annotation.web.configurers.oauth2.server.authorization;
+
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.springframework.http.HttpMethod;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.AuthenticationProvider;
+import org.springframework.security.config.annotation.ObjectPostProcessor;
+import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
+import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
+import org.springframework.security.oauth2.core.OAuth2Error;
+import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
+import org.springframework.security.oauth2.jwt.JwtEncoder;
+import org.springframework.security.oauth2.server.authorization.JwtEncodingContext;
+import org.springframework.security.oauth2.server.authorization.OAuth2TokenCustomizer;
+import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AccessTokenAuthenticationToken;
+import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeAuthenticationProvider;
+import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationGrantAuthenticationToken;
+import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientCredentialsAuthenticationProvider;
+import org.springframework.security.oauth2.server.authorization.authentication.OAuth2RefreshTokenAuthenticationProvider;
+import org.springframework.security.oauth2.server.authorization.config.ProviderSettings;
+import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenEndpointFilter;
+import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
+import org.springframework.security.web.authentication.AuthenticationConverter;
+import org.springframework.security.web.authentication.AuthenticationFailureHandler;
+import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
+import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
+import org.springframework.security.web.util.matcher.RequestMatcher;
+
+/**
+ * Configurer for the OAuth 2.0 Token Endpoint.
+ *
+ * @author Joe Grandja
+ * @since 0.1.2
+ * @see OAuth2AuthorizationServerConfigurer#tokenEndpoint
+ * @see OAuth2TokenEndpointFilter
+ */
+public final class OAuth2TokenEndpointConfigurer extends AbstractOAuth2Configurer {
+	private RequestMatcher requestMatcher;
+	private AuthenticationConverter accessTokenRequestConverter;
+	private final List<AuthenticationProvider> authenticationProviders = new LinkedList<>();
+	private AuthenticationSuccessHandler accessTokenResponseHandler;
+	private AuthenticationFailureHandler errorResponseHandler;
+
+	/**
+	 * Restrict for internal use only.
+	 */
+	OAuth2TokenEndpointConfigurer(ObjectPostProcessor<Object> objectPostProcessor) {
+		super(objectPostProcessor);
+	}
+
+	/**
+	 * Sets the {@link AuthenticationConverter} used when attempting to extract an Access Token Request from {@link HttpServletRequest}
+	 * to an instance of {@link OAuth2AuthorizationGrantAuthenticationToken} used for authenticating the authorization grant.
+	 *
+	 * @param accessTokenRequestConverter the {@link AuthenticationConverter} used when attempting to extract an Access Token Request from {@link HttpServletRequest}
+	 * @return the {@link OAuth2TokenEndpointConfigurer} for further configuration
+	 */
+	public OAuth2TokenEndpointConfigurer accessTokenRequestConverter(AuthenticationConverter accessTokenRequestConverter) {
+		this.accessTokenRequestConverter = accessTokenRequestConverter;
+		return this;
+	}
+
+	/**
+	 * Adds an {@link AuthenticationProvider} used for authenticating a type of {@link OAuth2AuthorizationGrantAuthenticationToken}.
+	 *
+	 * @param authenticationProvider an {@link AuthenticationProvider} used for authenticating a type of {@link OAuth2AuthorizationGrantAuthenticationToken}
+	 * @return the {@link OAuth2TokenEndpointConfigurer} for further configuration
+	 */
+	public OAuth2TokenEndpointConfigurer authenticationProvider(AuthenticationProvider authenticationProvider) {
+		this.authenticationProviders.add(authenticationProvider);
+		return this;
+	}
+
+	/**
+	 * Sets the {@link AuthenticationSuccessHandler} used for handling an {@link OAuth2AccessTokenAuthenticationToken}
+	 * and returning the {@link OAuth2AccessTokenResponse Access Token Response}.
+	 *
+	 * @param accessTokenResponseHandler the {@link AuthenticationSuccessHandler} used for handling an {@link OAuth2AccessTokenAuthenticationToken}
+	 * @return the {@link OAuth2TokenEndpointConfigurer} for further configuration
+	 */
+	public OAuth2TokenEndpointConfigurer accessTokenResponseHandler(AuthenticationSuccessHandler accessTokenResponseHandler) {
+		this.accessTokenResponseHandler = accessTokenResponseHandler;
+		return this;
+	}
+
+	/**
+	 * Sets the {@link AuthenticationFailureHandler} used for handling an {@link OAuth2AuthenticationException}
+	 * and returning the {@link OAuth2Error Error Response}.
+	 *
+	 * @param errorResponseHandler the {@link AuthenticationFailureHandler} used for handling an {@link OAuth2AuthenticationException}
+	 * @return the {@link OAuth2TokenEndpointConfigurer} for further configuration
+	 */
+	public OAuth2TokenEndpointConfigurer errorResponseHandler(AuthenticationFailureHandler errorResponseHandler) {
+		this.errorResponseHandler = errorResponseHandler;
+		return this;
+	}
+
+	@Override
+	<B extends HttpSecurityBuilder<B>> void init(B builder) {
+		ProviderSettings providerSettings = OAuth2ConfigurerUtils.getProviderSettings(builder);
+		this.requestMatcher = new AntPathRequestMatcher(
+				providerSettings.tokenEndpoint(), HttpMethod.POST.name());
+
+		List<AuthenticationProvider> authenticationProviders =
+				!this.authenticationProviders.isEmpty() ?
+						this.authenticationProviders :
+						createDefaultAuthenticationProviders(builder);
+		authenticationProviders.forEach(authenticationProvider ->
+				builder.authenticationProvider(postProcess(authenticationProvider)));
+	}
+
+	@Override
+	<B extends HttpSecurityBuilder<B>> void configure(B builder) {
+		AuthenticationManager authenticationManager = builder.getSharedObject(AuthenticationManager.class);
+		ProviderSettings providerSettings = OAuth2ConfigurerUtils.getProviderSettings(builder);
+
+		OAuth2TokenEndpointFilter tokenEndpointFilter =
+				new OAuth2TokenEndpointFilter(
+						authenticationManager,
+						providerSettings.tokenEndpoint());
+		if (this.accessTokenRequestConverter != null) {
+			tokenEndpointFilter.setAuthenticationConverter(this.accessTokenRequestConverter);
+		}
+		if (this.accessTokenResponseHandler != null) {
+			tokenEndpointFilter.setAuthenticationSuccessHandler(this.accessTokenResponseHandler);
+		}
+		if (this.errorResponseHandler != null) {
+			tokenEndpointFilter.setAuthenticationFailureHandler(this.errorResponseHandler);
+		}
+		builder.addFilterAfter(postProcess(tokenEndpointFilter), FilterSecurityInterceptor.class);
+	}
+
+	@Override
+	RequestMatcher getRequestMatcher() {
+		return this.requestMatcher;
+	}
+
+	private <B extends HttpSecurityBuilder<B>> List<AuthenticationProvider> createDefaultAuthenticationProviders(B builder) {
+		List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
+
+		JwtEncoder jwtEncoder = OAuth2ConfigurerUtils.getJwtEncoder(builder);
+		OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer = OAuth2ConfigurerUtils.getJwtCustomizer(builder);
+
+		OAuth2AuthorizationCodeAuthenticationProvider authorizationCodeAuthenticationProvider =
+				new OAuth2AuthorizationCodeAuthenticationProvider(
+						OAuth2ConfigurerUtils.getAuthorizationService(builder),
+						jwtEncoder);
+		if (jwtCustomizer != null) {
+			authorizationCodeAuthenticationProvider.setJwtCustomizer(jwtCustomizer);
+		}
+		authenticationProviders.add(authorizationCodeAuthenticationProvider);
+
+		OAuth2RefreshTokenAuthenticationProvider refreshTokenAuthenticationProvider =
+				new OAuth2RefreshTokenAuthenticationProvider(
+						OAuth2ConfigurerUtils.getAuthorizationService(builder),
+						jwtEncoder);
+		if (jwtCustomizer != null) {
+			refreshTokenAuthenticationProvider.setJwtCustomizer(jwtCustomizer);
+		}
+		authenticationProviders.add(refreshTokenAuthenticationProvider);
+
+		OAuth2ClientCredentialsAuthenticationProvider clientCredentialsAuthenticationProvider =
+				new OAuth2ClientCredentialsAuthenticationProvider(
+						OAuth2ConfigurerUtils.getAuthorizationService(builder),
+						jwtEncoder);
+		if (jwtCustomizer != null) {
+			clientCredentialsAuthenticationProvider.setJwtCustomizer(jwtCustomizer);
+		}
+		authenticationProviders.add(clientCredentialsAuthenticationProvider);
+
+		return authenticationProviders;
+	}
+
+}

+ 83 - 0
oauth2-authorization-server/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2ClientCredentialsGrantTests.java

@@ -17,6 +17,8 @@ package org.springframework.security.config.annotation.web.configurers.oauth2.se
 
 import java.net.URLEncoder;
 import java.nio.charset.StandardCharsets;
+import java.time.Duration;
+import java.time.Instant;
 import java.util.Base64;
 
 import com.nimbusds.jose.jwk.JWKSet;
@@ -31,21 +33,32 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Import;
 import org.springframework.http.HttpHeaders;
+import org.springframework.security.authentication.AuthenticationProvider;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
 import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
 import org.springframework.security.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
 import org.springframework.security.config.test.SpringTestRule;
 import org.springframework.security.crypto.password.NoOpPasswordEncoder;
 import org.springframework.security.crypto.password.PasswordEncoder;
 import org.springframework.security.oauth2.core.AuthorizationGrantType;
+import org.springframework.security.oauth2.core.OAuth2AccessToken;
 import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
 import org.springframework.security.oauth2.jose.TestJwks;
 import org.springframework.security.oauth2.server.authorization.JwtEncodingContext;
 import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
 import org.springframework.security.oauth2.server.authorization.OAuth2TokenCustomizer;
+import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AccessTokenAuthenticationToken;
+import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken;
+import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientCredentialsAuthenticationToken;
 import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
 import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
 import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients;
 import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenEndpointFilter;
+import org.springframework.security.web.SecurityFilterChain;
+import org.springframework.security.web.authentication.AuthenticationConverter;
+import org.springframework.security.web.authentication.AuthenticationFailureHandler;
+import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
+import org.springframework.security.web.util.matcher.RequestMatcher;
 import org.springframework.test.web.servlet.MockMvc;
 import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
 
@@ -64,12 +77,17 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
  * Integration tests for the OAuth 2.0 Client Credentials Grant.
  *
  * @author Alexey Nesterov
+ * @author Joe Grandja
  */
 public class OAuth2ClientCredentialsGrantTests {
 	private static RegisteredClientRepository registeredClientRepository;
 	private static OAuth2AuthorizationService authorizationService;
 	private static JWKSource<SecurityContext> jwkSource;
 	private static OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer;
+	private static AuthenticationConverter accessTokenRequestConverter;
+	private static AuthenticationProvider authenticationProvider;
+	private static AuthenticationSuccessHandler accessTokenResponseHandler;
+	private static AuthenticationFailureHandler errorResponseHandler;
 
 	@Rule
 	public final SpringTestRule spring = new SpringTestRule();
@@ -84,6 +102,10 @@ public class OAuth2ClientCredentialsGrantTests {
 		JWKSet jwkSet = new JWKSet(TestJwks.DEFAULT_RSA_JWK);
 		jwkSource = (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
 		jwtCustomizer = mock(OAuth2TokenCustomizer.class);
+		accessTokenRequestConverter = mock(AuthenticationConverter.class);
+		authenticationProvider = mock(AuthenticationProvider.class);
+		accessTokenResponseHandler = mock(AuthenticationSuccessHandler.class);
+		errorResponseHandler = mock(AuthenticationFailureHandler.class);
 	}
 
 	@SuppressWarnings("unchecked")
@@ -150,6 +172,38 @@ public class OAuth2ClientCredentialsGrantTests {
 		verify(authorizationService).save(any());
 	}
 
+	@Test
+	public void requestWhenTokenEndpointCustomizedThenUsed() throws Exception {
+		this.spring.register(AuthorizationServerConfigurationCustomTokenEndpoint.class).autowire();
+
+		RegisteredClient registeredClient = TestRegisteredClients.registeredClient2().build();
+		when(registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
+				.thenReturn(registeredClient);
+
+		OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient);
+		OAuth2ClientCredentialsAuthenticationToken clientCredentialsAuthentication =
+				new OAuth2ClientCredentialsAuthenticationToken(clientPrincipal, null, null);
+		when(accessTokenRequestConverter.convert(any())).thenReturn(clientCredentialsAuthentication);
+
+		OAuth2AccessToken accessToken = new OAuth2AccessToken(
+				OAuth2AccessToken.TokenType.BEARER, "token",
+				Instant.now(), Instant.now().plus(Duration.ofHours(1)));
+		OAuth2AccessTokenAuthenticationToken accessTokenAuthentication =
+				new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, accessToken);
+		when(authenticationProvider.supports(eq(OAuth2ClientCredentialsAuthenticationToken.class))).thenReturn(true);
+		when(authenticationProvider.authenticate(any())).thenReturn(accessTokenAuthentication);
+
+		this.mvc.perform(post(OAuth2TokenEndpointFilter.DEFAULT_TOKEN_ENDPOINT_URI)
+				.param(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())
+				.header(HttpHeaders.AUTHORIZATION, "Basic " + encodeBasicAuth(
+						registeredClient.getClientId(), registeredClient.getClientSecret())))
+				.andExpect(status().isOk());
+
+		verify(accessTokenRequestConverter).convert(any());
+		verify(authenticationProvider).authenticate(eq(clientCredentialsAuthentication));
+		verify(accessTokenResponseHandler).onAuthenticationSuccess(any(), any(), eq(accessTokenAuthentication));
+	}
+
 	private static String encodeBasicAuth(String clientId, String secret) throws Exception {
 		clientId = URLEncoder.encode(clientId, StandardCharsets.UTF_8.name());
 		secret = URLEncoder.encode(secret, StandardCharsets.UTF_8.name());
@@ -188,4 +242,33 @@ public class OAuth2ClientCredentialsGrantTests {
 		}
 
 	}
+
+	@EnableWebSecurity
+	static class AuthorizationServerConfigurationCustomTokenEndpoint extends AuthorizationServerConfiguration {
+		// @formatter:off
+		@Bean
+		public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
+			OAuth2AuthorizationServerConfigurer<HttpSecurity> authorizationServerConfigurer =
+					new OAuth2AuthorizationServerConfigurer<>();
+			authorizationServerConfigurer
+					.tokenEndpoint(tokenEndpoint ->
+							tokenEndpoint
+									.accessTokenRequestConverter(accessTokenRequestConverter)
+									.authenticationProvider(authenticationProvider)
+									.accessTokenResponseHandler(accessTokenResponseHandler)
+									.errorResponseHandler(errorResponseHandler));
+			RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher();
+
+			http
+					.requestMatcher(endpointsMatcher)
+					.authorizeRequests(authorizeRequests ->
+							authorizeRequests.anyRequest().authenticated()
+					)
+					.csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher))
+					.apply(authorizationServerConfigurer);
+			return http.build();
+		}
+		// @formatter:on
+	}
+
 }