Kaynağa Gözat

Allow Token Introspection to be customized

Closes gh-493
Gaurav Tiwari 3 yıl önce
ebeveyn
işleme
7160290aaf

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

@@ -53,7 +53,6 @@ import org.springframework.security.oauth2.server.authorization.web.OAuth2Author
 import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenIntrospectionEndpointFilter;
 import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenRevocationEndpointFilter;
 import org.springframework.security.oauth2.server.authorization.web.ProviderContextFilter;
-import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
 import org.springframework.security.web.authentication.HttpStatusEntryPoint;
 import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;
 import org.springframework.security.web.context.HttpRequestResponseHolder;
@@ -73,6 +72,7 @@ import org.springframework.util.Assert;
  * @author Daniel Garnier-Moiroux
  * @author Gerardo Roza
  * @author Ovidiu Popa
+ * @author Gaurav Tiwari
  * @since 0.0.1
  * @see AbstractHttpConfigurer
  * @see OAuth2ClientAuthenticationConfigurer
@@ -100,7 +100,7 @@ public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBui
 			getRequestMatcher(OAuth2TokenEndpointConfigurer.class).matches(request) ||
 			getRequestMatcher(OAuth2TokenRevocationEndpointConfigurer.class).matches(request) ||
 			getRequestMatcher(OidcConfigurer.class).matches(request) ||
-			this.tokenIntrospectionEndpointMatcher.matches(request) ||
+			getRequestMatcher(OAuth2TokenIntrospectionConfigurer.class).matches(request) ||
 			this.jwkSetEndpointMatcher.matches(request) ||
 			this.authorizationServerMetadataEndpointMatcher.matches(request);
 
@@ -221,6 +221,19 @@ public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBui
 		return this;
 	}
 
+	/**
+	 * Configures the OAuth 2.0 Token Introspection Endpoint.
+	 *
+	 * @param tokenIntrospectionEndpointCustomizer the {@link Customizer} providing access to the {@link OAuth2TokenIntrospectionConfigurer}
+	 * @return the {@link OAuth2AuthorizationServerConfigurer} for further configuration
+	 * @since 0.2.3
+	 */
+	public OAuth2AuthorizationServerConfigurer<B> tokenIntrospectionEndpoint(Customizer<OAuth2TokenIntrospectionConfigurer> tokenIntrospectionEndpointCustomizer) {
+		tokenIntrospectionEndpointCustomizer.customize(getConfigurer(OAuth2TokenIntrospectionConfigurer.class));
+		return this;
+	}
+
+
 	/**
 	 * Returns a {@link RequestMatcher} for the authorization server endpoints.
 	 *
@@ -387,12 +400,6 @@ public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBui
 		ProviderContextFilter providerContextFilter = new ProviderContextFilter(providerSettings);
 		builder.addFilterAfter(postProcess(providerContextFilter), SecurityContextPersistenceFilter.class);
 
-		OAuth2TokenIntrospectionEndpointFilter tokenIntrospectionEndpointFilter =
-				new OAuth2TokenIntrospectionEndpointFilter(
-						authenticationManager,
-						providerSettings.getTokenIntrospectionEndpoint());
-		builder.addFilterAfter(postProcess(tokenIntrospectionEndpointFilter), FilterSecurityInterceptor.class);
-
 		JWKSource<com.nimbusds.jose.proc.SecurityContext> jwkSource = OAuth2ConfigurerUtils.getJwkSource(builder);
 		if (jwkSource != null) {
 			NimbusJwkSetEndpointFilter jwkSetEndpointFilter = new NimbusJwkSetEndpointFilter(
@@ -412,6 +419,7 @@ public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBui
 		configurers.put(OAuth2TokenEndpointConfigurer.class, new OAuth2TokenEndpointConfigurer(this::postProcess));
 		configurers.put(OAuth2TokenRevocationEndpointConfigurer.class, new OAuth2TokenRevocationEndpointConfigurer(this::postProcess));
 		configurers.put(OidcConfigurer.class, new OidcConfigurer(this::postProcess));
+		configurers.put(OAuth2TokenIntrospectionConfigurer.class, new OAuth2TokenIntrospectionConfigurer(this::postProcess));
 		return configurers;
 	}
 

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

@@ -0,0 +1,162 @@
+/*
+ * 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.config.annotation.web.configurers.oauth2.server.authorization;
+
+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.OAuth2Error;
+import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationGrantAuthenticationToken;
+import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenIntrospectionAuthenticationProvider;
+import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenIntrospectionAuthenticationToken;
+import org.springframework.security.oauth2.server.authorization.config.ProviderSettings;
+import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenIntrospectionEndpointFilter;
+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;
+import org.springframework.util.Assert;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * Configurer for OAuth 2.0 Token Introspection.
+ *
+ * @author Gaurav Tiwari
+ * @since 0.2.3
+ */
+public class OAuth2TokenIntrospectionConfigurer extends AbstractOAuth2Configurer {
+
+	private RequestMatcher requestMatcher;
+	private AuthenticationConverter accessTokenRequestConverter;
+	private final List<AuthenticationProvider> authenticationProviders = new LinkedList<>();
+	private AuthenticationSuccessHandler tokenIntrospectionResponseHandler;
+	private AuthenticationFailureHandler errorResponseHandler;
+
+	/**
+	 * Restrict for internal use only.
+	 */
+	OAuth2TokenIntrospectionConfigurer(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 OAuth2TokenIntrospectionConfigurer} for further configuration
+	 */
+	public OAuth2TokenIntrospectionConfigurer accessTokenRequestConverter(AuthenticationConverter accessTokenRequestConverter) {
+		this.accessTokenRequestConverter = accessTokenRequestConverter;
+		return this;
+	}
+
+	/**
+	 * Adds an {@link AuthenticationProvider} used for authenticating a type of {@link OAuth2TokenIntrospectionAuthenticationToken}.
+	 *
+	 * @param authenticationProvider an {@link AuthenticationProvider} used for authenticating a type of {@link OAuth2TokenIntrospectionAuthenticationToken}
+	 * @return the {@link OAuth2TokenIntrospectionConfigurer} for further configuration
+	 */
+	public OAuth2TokenIntrospectionConfigurer authenticationProvider(AuthenticationProvider authenticationProvider) {
+		Assert.notNull(authenticationProvider, "authenticationProvider cannot be null");
+		this.authenticationProviders.add(authenticationProvider);
+		return this;
+	}
+
+	/**
+	 * Sets the {@link AuthenticationSuccessHandler} used for handling an {@link OAuth2TokenIntrospectionAuthenticationToken}.
+	 *
+	 * @param tokenIntrospectionResponseHandler the {@link AuthenticationSuccessHandler} used for handling an {@link OAuth2TokenIntrospectionAuthenticationToken}
+	 * @return the {@link OAuth2TokenEndpointConfigurer} for further configuration
+	 */
+	public OAuth2TokenIntrospectionConfigurer accessTokenResponseHandler(AuthenticationSuccessHandler tokenIntrospectionResponseHandler) {
+		this.tokenIntrospectionResponseHandler = tokenIntrospectionResponseHandler;
+		return this;
+	}
+
+	/**
+	 * Sets the {@link AuthenticationFailureHandler} used for handling an {@link org.springframework.security.oauth2.core.OAuth2AuthenticationException}
+	 * and returning the {@link OAuth2Error Error Response}.
+	 *
+	 * @param errorResponseHandler the {@link AuthenticationFailureHandler} used for handling an {@link org.springframework.security.oauth2.core.OAuth2AuthenticationException}
+	 * @return the {@link OAuth2TokenIntrospectionConfigurer} for further configuration
+	 */
+	public OAuth2TokenIntrospectionConfigurer 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.getTokenIntrospectionEndpoint(), HttpMethod.POST.name());
+
+		List<AuthenticationProvider> authenticationProviders = this.authenticationProviders.isEmpty() ? createDefaultAuthenticationProviders(builder) : this.authenticationProviders;
+		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);
+
+		OAuth2TokenIntrospectionEndpointFilter introspectionEndpointFilter =
+				new OAuth2TokenIntrospectionEndpointFilter(authenticationManager, providerSettings.getTokenIntrospectionEndpoint());
+
+		if (accessTokenRequestConverter != null) {
+			introspectionEndpointFilter.setAuthenticationConverter(this.accessTokenRequestConverter);
+		}
+
+		if (this.tokenIntrospectionResponseHandler != null) {
+			introspectionEndpointFilter.setAuthenticationSuccessHandler(this.tokenIntrospectionResponseHandler);
+		}
+
+		if (this.errorResponseHandler != null) {
+			introspectionEndpointFilter.setAuthenticationFailureHandler(this.errorResponseHandler);
+		}
+
+		builder.addFilterAfter(postProcess(introspectionEndpointFilter), FilterSecurityInterceptor.class);
+	}
+
+	@Override
+	public RequestMatcher getRequestMatcher() {
+		return this.requestMatcher;
+	}
+
+	private <B extends HttpSecurityBuilder<B>> List<AuthenticationProvider> createDefaultAuthenticationProviders(B builder) {
+		List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
+
+		OAuth2TokenIntrospectionAuthenticationProvider tokenIntrospectionAuthenticationProvider
+				= new OAuth2TokenIntrospectionAuthenticationProvider(
+				OAuth2ConfigurerUtils.getRegisteredClientRepository(builder),
+				OAuth2ConfigurerUtils.getAuthorizationService(builder)
+		);
+
+		authenticationProviders.add(tokenIntrospectionAuthenticationProvider);
+
+		return authenticationProviders;
+
+	}
+}

+ 32 - 9
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/OAuth2TokenIntrospection.java

@@ -34,6 +34,7 @@ import org.springframework.util.Assert;
  *
  * @author Gerardo Roza
  * @author Joe Grandja
+ * @author Gaurav Tiwari
  * @since 0.1.1
  * @see OAuth2TokenIntrospectionClaimAccessor
  * @see <a target="_blank" href="https://tools.ietf.org/html/rfc7662#section-2.2">Section 2.2 Introspection Response</a>
@@ -257,6 +258,28 @@ public final class OAuth2TokenIntrospection implements OAuth2TokenIntrospectionC
 			return this;
 		}
 
+		/**
+		 * Adds custom claims if corresponding keys don't exist in present set of claims.
+		 *
+		 * @since 0.2.3
+		 * @param presentedClaims map of all claims
+		 * @return the {@link Builder} for further configuration
+		 */
+		public Builder withCustomClaims(Map<String, Object> presentedClaims) {
+
+			if (presentedClaims != null && !presentedClaims.isEmpty()) {
+
+				presentedClaims.keySet().forEach(key -> {
+					if (!this.claims.containsKey(key)) {
+						this.claim(key, presentedClaims.get(key));
+					}
+				});
+
+			}
+
+			return this;
+		}
+
 		/**
 		 * Provides access to every {@link #claim(String, Object)} declared so far with
 		 * the possibility to add, replace, or remove.
@@ -312,15 +335,6 @@ public final class OAuth2TokenIntrospection implements OAuth2TokenIntrospectionC
 			((List<String>) this.claims.get(name)).add(value);
 		}
 
-		@SuppressWarnings("unchecked")
-		private void acceptClaimValues(String name, Consumer<List<String>> valuesConsumer) {
-			Assert.hasText(name, "name cannot be empty");
-			Assert.notNull(valuesConsumer, "valuesConsumer cannot be null");
-			this.claims.computeIfAbsent(name, k -> new LinkedList<String>());
-			List<String> values = (List<String>) this.claims.get(name);
-			valuesConsumer.accept(values);
-		}
-
 		private static void validateURL(Object url, String errorMessage) {
 			if (URL.class.isAssignableFrom(url.getClass())) {
 				return;
@@ -332,5 +346,14 @@ public final class OAuth2TokenIntrospection implements OAuth2TokenIntrospectionC
 				throw new IllegalArgumentException(errorMessage, ex);
 			}
 		}
+
+		@SuppressWarnings("unchecked")
+		private void acceptClaimValues(String name, Consumer<List<String>> valuesConsumer) {
+			Assert.hasText(name, "name cannot be empty");
+			Assert.notNull(valuesConsumer, "valuesConsumer cannot be null");
+			this.claims.computeIfAbsent(name, k -> new LinkedList<String>());
+			List<String> values = (List<String>) this.claims.get(name);
+			valuesConsumer.accept(values);
+		}
 	}
 }

+ 3 - 0
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2TokenIntrospectionAuthenticationProvider.java

@@ -41,6 +41,7 @@ import static org.springframework.security.oauth2.server.authorization.authentic
  *
  * @author Gerardo Roza
  * @author Joe Grandja
+ * @author Gaurav Tiwari
  * @since 0.1.1
  * @see OAuth2TokenIntrospectionAuthenticationToken
  * @see RegisteredClientRepository
@@ -143,6 +144,8 @@ public final class OAuth2TokenIntrospectionAuthenticationProvider implements Aut
 			}
 		}
 
+		tokenClaims.withCustomClaims(authorizedToken.getClaims());
+
 		return tokenClaims.build();
 	}
 }

+ 54 - 9
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2TokenIntrospectionEndpointFilter.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2020-2021 the original author or authors.
+ * 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.
@@ -24,13 +24,13 @@ import javax.servlet.ServletException;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
-import org.springframework.core.convert.converter.Converter;
 import org.springframework.http.HttpMethod;
 import org.springframework.http.HttpStatus;
 import org.springframework.http.converter.HttpMessageConverter;
 import org.springframework.http.server.ServletServerHttpResponse;
 import org.springframework.security.authentication.AuthenticationManager;
 import org.springframework.security.core.Authentication;
+import org.springframework.security.core.AuthenticationException;
 import org.springframework.security.core.context.SecurityContextHolder;
 import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
 import org.springframework.security.oauth2.core.OAuth2Error;
@@ -41,6 +41,9 @@ import org.springframework.security.oauth2.core.http.converter.OAuth2ErrorHttpMe
 import org.springframework.security.oauth2.core.http.converter.OAuth2TokenIntrospectionHttpMessageConverter;
 import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenIntrospectionAuthenticationProvider;
 import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenIntrospectionAuthenticationToken;
+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;
 import org.springframework.util.Assert;
@@ -53,6 +56,7 @@ import org.springframework.web.filter.OncePerRequestFilter;
  *
  * @author Gerardo Roza
  * @author Joe Grandja
+ * @author Gaurav Tiwari
  * @see OAuth2TokenIntrospectionAuthenticationProvider
  * @see <a target="_blank" href="https://tools.ietf.org/html/rfc7662#section-2">Section 2 Introspection Endpoint</a>
  * @see <a target="_blank" href="https://tools.ietf.org/html/rfc7662#section-2.1">Section 2.1 Introspection Request</a>
@@ -66,11 +70,13 @@ public final class OAuth2TokenIntrospectionEndpointFilter extends OncePerRequest
 
 	private final AuthenticationManager authenticationManager;
 	private final RequestMatcher tokenIntrospectionEndpointMatcher;
-	private final Converter<HttpServletRequest, Authentication> tokenIntrospectionAuthenticationConverter =
+	private AuthenticationConverter tokenIntrospectionAuthenticationConverter =
 			new DefaultTokenIntrospectionAuthenticationConverter();
 	private final HttpMessageConverter<OAuth2TokenIntrospection> tokenIntrospectionHttpResponseConverter =
 			new OAuth2TokenIntrospectionHttpMessageConverter();
 	private final HttpMessageConverter<OAuth2Error> errorHttpResponseConverter = new OAuth2ErrorHttpMessageConverter();
+	private AuthenticationSuccessHandler authenticationSuccessHandler = this::sendTokenIntrospectionResponse;;
+	private AuthenticationFailureHandler authenticationFailureHandler = this::sendErrorResponse;
 
 	/**
 	 * Constructs an {@code OAuth2TokenIntrospectionEndpointFilter} using the provided parameters.
@@ -112,21 +118,60 @@ public final class OAuth2TokenIntrospectionEndpointFilter extends OncePerRequest
 			OAuth2TokenIntrospectionAuthenticationToken tokenIntrospectionAuthenticationResult =
 					(OAuth2TokenIntrospectionAuthenticationToken) this.authenticationManager.authenticate(tokenIntrospectionAuthentication);
 
-			OAuth2TokenIntrospection tokenClaims = tokenIntrospectionAuthenticationResult.getTokenClaims();
-			sendTokenIntrospectionResponse(response, tokenClaims);
+			this.authenticationSuccessHandler.onAuthenticationSuccess(request, response, tokenIntrospectionAuthenticationResult);
 
 		} catch (OAuth2AuthenticationException ex) {
 			SecurityContextHolder.clearContext();
-			sendErrorResponse(response, ex.getError());
+			this.authenticationFailureHandler.onAuthenticationFailure(request, response, ex);
 		}
 	}
 
-	private void sendTokenIntrospectionResponse(HttpServletResponse response, OAuth2TokenIntrospection tokenClaims) throws IOException {
+	/**
+	 * Sets the {@link AuthenticationConverter} used when attempting to extract a Token Introspection Request from
+	 * {@link HttpServletRequest} to an instance of {@link OAuth2TokenIntrospectionAuthenticationToken} used for authenticating the request.
+	 *
+	 * @param authenticationConverter the {@link AuthenticationConverter} used when attempting to extract a Token Introspection Request from {@link HttpServletRequest}
+	 * @since 0.2.3
+	 */
+	public void setAuthenticationConverter(AuthenticationConverter authenticationConverter) {
+		Assert.notNull(authenticationConverter, "authenticationConverter cannot be null.");
+		this.tokenIntrospectionAuthenticationConverter = authenticationConverter;
+	}
+
+	/**
+	 * Sets the {@link AuthenticationSuccessHandler} used for handling an {@link OAuth2TokenIntrospectionAuthenticationToken}
+	 *
+	 * @param authenticationSuccessHandler the {@link AuthenticationSuccessHandler} used for handling an {@link OAuth2TokenIntrospectionAuthenticationToken}
+	 * @since 0.2.3
+	 */
+	public void setAuthenticationSuccessHandler(AuthenticationSuccessHandler authenticationSuccessHandler) {
+		Assert.notNull(authenticationSuccessHandler, "authenticationSuccessHandler cannot be null.");
+		this.authenticationSuccessHandler = authenticationSuccessHandler;
+	}
+
+	/**
+	 * Sets the {@link AuthenticationFailureHandler} used for handling an {@link OAuth2AuthenticationException} and
+	 * returning {@link OAuth2Error Error Resonse}.
+	 *
+	 * @param authenticationFailureHandler the {@link .AuthenticationFailureHandler} used for handling {@link OAuth2AuthenticationException}
+	 * @since 0.2.3
+	 */
+	public void setAuthenticationFailureHandler(AuthenticationFailureHandler authenticationFailureHandler) {
+		Assert.notNull(authenticationFailureHandler, "authenticationFailureHandler cannot be null.");
+		this.authenticationFailureHandler = authenticationFailureHandler;
+	}
+
+	private void sendTokenIntrospectionResponse(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
+
+		OAuth2TokenIntrospectionAuthenticationToken tokenIntrospectionAuthenticationResult = (OAuth2TokenIntrospectionAuthenticationToken) authentication;
+		OAuth2TokenIntrospection tokenClaims = tokenIntrospectionAuthenticationResult.getTokenClaims();
+
 		ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
 		this.tokenIntrospectionHttpResponseConverter.write(tokenClaims, null, httpResponse);
 	}
 
-	private void sendErrorResponse(HttpServletResponse response, OAuth2Error error) throws IOException {
+	private void sendErrorResponse(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException {
+		OAuth2Error error = ((OAuth2AuthenticationException) exception).getError();
 		ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
 		httpResponse.setStatusCode(HttpStatus.BAD_REQUEST);
 		this.errorHttpResponseConverter.write(error, null, httpResponse);
@@ -139,7 +184,7 @@ public final class OAuth2TokenIntrospectionEndpointFilter extends OncePerRequest
 	}
 
 	private static class DefaultTokenIntrospectionAuthenticationConverter
-			implements Converter<HttpServletRequest, Authentication> {
+			implements AuthenticationConverter {
 
 		@Override
 		public Authentication convert(HttpServletRequest request) {

+ 56 - 0
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2TokenIntrospectionAuthenticationProviderTests.java

@@ -54,6 +54,7 @@ import static org.mockito.Mockito.when;
  *
  * @author Gerardo Roza
  * @author Joe Grandja
+ * @author Gaurav Tiwari
  */
 public class OAuth2TokenIntrospectionAuthenticationProviderTests {
 	private RegisteredClientRepository registeredClientRepository;
@@ -261,6 +262,61 @@ public class OAuth2TokenIntrospectionAuthenticationProviderTests {
 		assertThat(tokenClaims.getId()).isEqualTo(claimsSet.getId());
 	}
 
+	@Test
+	public void authenticateWhenValidAccessTokenAndCustomClaimThenActiveAndCustomClaimInResponse() {
+		RegisteredClient authorizedClient = TestRegisteredClients.registeredClient().build();
+		Instant issuedAt = Instant.now();
+		Instant expiresAt = issuedAt.plus(Duration.ofHours(1));
+		OAuth2AccessToken accessToken = new OAuth2AccessToken(
+				OAuth2AccessToken.TokenType.BEARER, "access-token", issuedAt, expiresAt,
+				new HashSet<>(Arrays.asList("scope1", "scope2")));
+
+		// @formatter:off
+		OAuth2TokenClaimsSet claimsSet = OAuth2TokenClaimsSet.builder()
+				.issuer("https://provider.com")
+				.subject("subject")
+				.audience(Collections.singletonList(authorizedClient.getClientId()))
+				.issuedAt(issuedAt)
+				.notBefore(issuedAt)
+				.expiresAt(expiresAt)
+				.claim("custom-claim", "custom-claim-value")
+				.id("id")
+				.build();
+		// @formatter:on
+
+		OAuth2Authorization authorization = TestOAuth2Authorizations
+				.authorization(authorizedClient, accessToken, claimsSet.getClaims())
+				.build();
+		when(this.authorizationService.findByToken(eq(accessToken.getTokenValue()), isNull()))
+				.thenReturn(authorization);
+		when(this.registeredClientRepository.findById(eq(authorizedClient.getId()))).thenReturn(authorizedClient);
+		RegisteredClient registeredClient = TestRegisteredClients.registeredClient2().build();
+		OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(
+				registeredClient, ClientAuthenticationMethod.CLIENT_SECRET_BASIC, registeredClient.getClientSecret());
+
+		OAuth2TokenIntrospectionAuthenticationToken authentication = new OAuth2TokenIntrospectionAuthenticationToken(
+				accessToken.getTokenValue(), clientPrincipal, null, null);
+		OAuth2TokenIntrospectionAuthenticationToken authenticationResult =
+				(OAuth2TokenIntrospectionAuthenticationToken) this.authenticationProvider.authenticate(authentication);
+
+		verify(this.authorizationService).findByToken(eq(authentication.getToken()), isNull());
+		verify(this.registeredClientRepository).findById(eq(authorizedClient.getId()));
+		assertThat(authenticationResult.isAuthenticated()).isTrue();
+		OAuth2TokenIntrospection tokenClaims = authenticationResult.getTokenClaims();
+		assertThat(tokenClaims.isActive()).isTrue();
+		assertThat(tokenClaims.getClientId()).isEqualTo(authorizedClient.getClientId());
+		assertThat(tokenClaims.getIssuedAt()).isEqualTo(accessToken.getIssuedAt());
+		assertThat(tokenClaims.getExpiresAt()).isEqualTo(accessToken.getExpiresAt());
+		assertThat(tokenClaims.getScopes()).containsExactlyInAnyOrderElementsOf(accessToken.getScopes());
+		assertThat(tokenClaims.getTokenType()).isEqualTo(accessToken.getTokenType().getValue());
+		assertThat(tokenClaims.getNotBefore()).isEqualTo(claimsSet.getNotBefore());
+		assertThat(tokenClaims.getSubject()).isEqualTo(claimsSet.getSubject());
+		assertThat(tokenClaims.getAudience()).containsExactlyInAnyOrderElementsOf(claimsSet.getAudience());
+		assertThat(tokenClaims.getIssuer()).isEqualTo(claimsSet.getIssuer());
+		assertThat(tokenClaims.getId()).isEqualTo(claimsSet.getId());
+		assertThat((String) tokenClaims.getClaim("custom-claim")).isEqualTo("custom-claim-value");
+	}
+
 	@Test
 	public void authenticateWhenValidRefreshTokenThenActive() {
 		RegisteredClient authorizedClient = TestRegisteredClients.registeredClient().build();