瀏覽代碼

Polish gh-161

Joe Grandja 4 年之前
父節點
當前提交
9a45ae9804
共有 19 個文件被更改,包括 1506 次插入1250 次删除
  1. 12 11
      oauth2-authorization-server/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2AuthorizationServerConfigurer.java
  2. 336 0
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/OAuth2TokenIntrospection.java
  3. 41 55
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/OAuth2TokenIntrospectionClaimAccessor.java
  4. 102 0
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/OAuth2TokenIntrospectionClaimNames.java
  5. 0 296
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/OAuth2TokenIntrospectionClaims.java
  6. 0 181
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/http/converter/OAuth2TokenIntrospectionClaimsHttpMessageConverter.java
  7. 203 0
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/http/converter/OAuth2TokenIntrospectionHttpMessageConverter.java
  8. 33 0
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/OAuth2Authorization.java
  9. 72 67
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2TokenIntrospectionAuthenticationProvider.java
  10. 42 33
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2TokenIntrospectionAuthenticationToken.java
  11. 42 32
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2TokenIntrospectionEndpointFilter.java
  12. 116 99
      oauth2-authorization-server/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2TokenIntrospectionTests.java
  13. 0 193
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/core/http/converter/OAuth2TokenIntrospectionClaimsHttpMessageConverterTests.java
  14. 170 0
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/core/http/converter/OAuth2TokenIntrospectionHttpMessageConverterTests.java
  15. 25 6
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/TestOAuth2Authorizations.java
  16. 136 110
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2TokenIntrospectionAuthenticationProviderTests.java
  17. 46 39
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2TokenIntrospectionAuthenticationTokenTests.java
  18. 13 1
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/ProviderSettingsTests.java
  19. 117 127
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2TokenIntrospectionEndpointFilterTests.java

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

@@ -184,16 +184,17 @@ public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBui
 		}
 		builder.authenticationProvider(postProcess(clientCredentialsAuthenticationProvider));
 
-		OAuth2TokenRevocationAuthenticationProvider tokenRevocationAuthenticationProvider =
-				new OAuth2TokenRevocationAuthenticationProvider(
-						getAuthorizationService(builder));
-		builder.authenticationProvider(postProcess(tokenRevocationAuthenticationProvider));
-
 		OAuth2TokenIntrospectionAuthenticationProvider tokenIntrospectionAuthenticationProvider =
 				new OAuth2TokenIntrospectionAuthenticationProvider(
+						getRegisteredClientRepository(builder),
 						getAuthorizationService(builder));
 		builder.authenticationProvider(postProcess(tokenIntrospectionAuthenticationProvider));
 
+		OAuth2TokenRevocationAuthenticationProvider tokenRevocationAuthenticationProvider =
+				new OAuth2TokenRevocationAuthenticationProvider(
+						getAuthorizationService(builder));
+		builder.authenticationProvider(postProcess(tokenRevocationAuthenticationProvider));
+
 		ExceptionHandlingConfigurer<B> exceptionHandling = builder.getConfigurer(ExceptionHandlingConfigurer.class);
 		if (exceptionHandling != null) {
 			exceptionHandling.defaultAuthenticationEntryPointFor(
@@ -245,17 +246,17 @@ public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBui
 						providerSettings.tokenEndpoint());
 		builder.addFilterAfter(postProcess(tokenEndpointFilter), FilterSecurityInterceptor.class);
 
-		OAuth2TokenRevocationEndpointFilter tokenRevocationEndpointFilter =
-				new OAuth2TokenRevocationEndpointFilter(
-						authenticationManager,
-						providerSettings.tokenRevocationEndpoint());
-		builder.addFilterAfter(postProcess(tokenRevocationEndpointFilter), OAuth2TokenEndpointFilter.class);
-
 		OAuth2TokenIntrospectionEndpointFilter tokenIntrospectionEndpointFilter =
 				new OAuth2TokenIntrospectionEndpointFilter(
 						authenticationManager,
 						providerSettings.tokenIntrospectionEndpoint());
 		builder.addFilterAfter(postProcess(tokenIntrospectionEndpointFilter), OAuth2TokenEndpointFilter.class);
+
+		OAuth2TokenRevocationEndpointFilter tokenRevocationEndpointFilter =
+				new OAuth2TokenRevocationEndpointFilter(
+						authenticationManager,
+						providerSettings.tokenRevocationEndpoint());
+		builder.addFilterAfter(postProcess(tokenRevocationEndpointFilter), OAuth2TokenIntrospectionEndpointFilter.class);
 	}
 
 	private void initEndpointMatchers(ProviderSettings providerSettings) {

+ 336 - 0
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/OAuth2TokenIntrospection.java

@@ -0,0 +1,336 @@
+/*
+ * 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.core;
+
+import java.io.Serializable;
+import java.net.URI;
+import java.net.URL;
+import java.time.Instant;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Consumer;
+
+import org.springframework.util.Assert;
+
+/**
+ * A representation of the claims returned in an OAuth 2.0 Token Introspection Response.
+ *
+ * @author Gerardo Roza
+ * @author Joe Grandja
+ * @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>
+ */
+public final class OAuth2TokenIntrospection implements OAuth2TokenIntrospectionClaimAccessor, Serializable {
+	private static final long serialVersionUID = Version.SERIAL_VERSION_UID;
+	private final Map<String, Object> claims;
+
+	private OAuth2TokenIntrospection(Map<String, Object> claims) {
+		this.claims = Collections.unmodifiableMap(new LinkedHashMap<>(claims));
+	}
+
+	/**
+	 * Returns the claims in the Token Introspection Response.
+	 *
+	 * @return a {@code Map} of the claims
+	 */
+	@Override
+	public Map<String, Object> getClaims() {
+		return this.claims;
+	}
+
+	/**
+	 * Constructs a new {@link Builder} initialized with the {@link #isActive() active} claim to {@code false}.
+	 *
+	 * @return the {@link Builder}
+	 */
+	public static Builder builder() {
+		return builder(false);
+	}
+
+	/**
+	 * Constructs a new {@link Builder} initialized with the provided {@link #isActive() active} claim.
+	 *
+	 * @param active {@code true} if the token is currently active, {@code false} otherwise
+	 * @return the {@link Builder}
+	 */
+	public static Builder builder(boolean active) {
+		return new Builder(active);
+	}
+
+	/**
+	 * Constructs a new {@link Builder} initialized with the provided claims.
+	 *
+	 * @param claims the claims to initialize the builder
+	 * @return the {@link Builder}
+	 */
+	public static Builder withClaims(Map<String, Object> claims) {
+		Assert.notEmpty(claims, "claims cannot be empty");
+		return builder().claims(c -> c.putAll(claims));
+	}
+
+	/**
+	 * A builder for {@link OAuth2TokenIntrospection}.
+	 */
+	public static class Builder {
+		private final Map<String, Object> claims = new LinkedHashMap<>();
+
+		private Builder(boolean active) {
+			active(active);
+		}
+
+		/**
+		 * Sets the indicator of whether or not the presented token is currently active, REQUIRED.
+		 *
+		 * @param active {@code true} if the token is currently active, {@code false} otherwise
+		 * @return the {@link Builder} for further configuration
+		 */
+		public Builder active(boolean active) {
+			return claim(OAuth2TokenIntrospectionClaimNames.ACTIVE, active);
+		}
+
+		/**
+		 * Add the scope associated with this token, OPTIONAL.
+		 *
+		 * @param scope the scope associated with this token
+		 * @return the {@link Builder} for further configuration
+		 */
+		public Builder scope(String scope) {
+			addClaimToClaimList(OAuth2TokenIntrospectionClaimNames.SCOPE, scope);
+			return this;
+		}
+
+		/**
+		 * A {@code Consumer} of the scope(s) associated with this token,
+		 * allowing the ability to add, replace, or remove, OPTIONAL.
+		 *
+		 * @param scopesConsumer a {@code Consumer} of the scope(s) associated with this token
+		 * @return the {@link Builder} for further configuration
+		 */
+		public Builder scopes(Consumer<List<String>> scopesConsumer) {
+			acceptClaimValues(OAuth2TokenIntrospectionClaimNames.SCOPE, scopesConsumer);
+			return this;
+		}
+
+		/**
+		 * Sets the client identifier for the OAuth 2.0 client that requested this token, OPTIONAL.
+		 *
+		 * @param clientId the client identifier for the OAuth 2.0 client that requested this token
+		 * @return the {@link Builder} for further configuration
+		 */
+		public Builder clientId(String clientId) {
+			return claim(OAuth2TokenIntrospectionClaimNames.CLIENT_ID, clientId);
+		}
+
+		/**
+		 * Sets the human-readable identifier for the resource owner who authorized this token, OPTIONAL.
+		 *
+		 * @param username the human-readable identifier for the resource owner who authorized this token
+		 * @return the {@link Builder} for further configuration
+		 */
+		public Builder username(String username) {
+			return claim(OAuth2TokenIntrospectionClaimNames.USERNAME, username);
+		}
+
+		/**
+		 * Sets the token type (e.g. bearer), OPTIONAL.
+		 *
+		 * @param tokenType the token type
+		 * @return the {@link Builder} for further configuration
+		 */
+		public Builder tokenType(String tokenType) {
+			return claim(OAuth2TokenIntrospectionClaimNames.TOKEN_TYPE, tokenType);
+		}
+
+		/**
+		 * Sets the time indicating when this token will expire, OPTIONAL.
+		 *
+		 * @param expiresAt the time indicating when this token will expire
+		 * @return the {@link Builder} for further configuration
+		 */
+		public Builder expiresAt(Instant expiresAt) {
+			return claim(OAuth2TokenIntrospectionClaimNames.EXP, expiresAt);
+		}
+
+		/**
+		 * Sets the time indicating when this token was originally issued, OPTIONAL.
+		 *
+		 * @param issuedAt the time indicating when this token was originally issued
+		 * @return the {@link Builder} for further configuration
+		 */
+		public Builder issuedAt(Instant issuedAt) {
+			return claim(OAuth2TokenIntrospectionClaimNames.IAT, issuedAt);
+		}
+
+		/**
+		 * Sets the time indicating when this token is not to be used before, OPTIONAL.
+		 *
+		 * @param notBefore the time indicating when this token is not to be used before
+		 * @return the {@link Builder} for further configuration
+		 */
+		public Builder notBefore(Instant notBefore) {
+			return claim(OAuth2TokenIntrospectionClaimNames.NBF, notBefore);
+		}
+
+		/**
+		 * Sets the subject of the token, usually a machine-readable identifier
+		 * of the resource owner who authorized this token, OPTIONAL.
+		 *
+		 * @param subject the subject of the token
+		 * @return the {@link Builder} for further configuration
+		 */
+		public Builder subject(String subject) {
+			return claim(OAuth2TokenIntrospectionClaimNames.SUB, subject);
+		}
+
+		/**
+		 * Add the identifier representing the intended audience for this token, OPTIONAL.
+		 *
+		 * @param audience the identifier representing the intended audience for this token
+		 * @return the {@link Builder} for further configuration
+		 */
+		public Builder audience(String audience) {
+			addClaimToClaimList(OAuth2TokenIntrospectionClaimNames.AUD, audience);
+			return this;
+		}
+
+		/**
+		 * A {@code Consumer} of the intended audience(s) for this token,
+		 * allowing the ability to add, replace, or remove, OPTIONAL.
+		 *
+		 * @param audiencesConsumer a {@code Consumer} of the intended audience(s) for this token
+		 * @return the {@link Builder} for further configuration
+		 */
+		public Builder audiences(Consumer<List<String>> audiencesConsumer) {
+			acceptClaimValues(OAuth2TokenIntrospectionClaimNames.AUD, audiencesConsumer);
+			return this;
+		}
+
+		/**
+		 * Sets the issuer of this token, OPTIONAL.
+		 *
+		 * @param issuer the issuer of this token
+		 * @return the {@link Builder} for further configuration
+		 */
+		public Builder issuer(String issuer) {
+			return claim(OAuth2TokenIntrospectionClaimNames.ISS, issuer);
+		}
+
+		/**
+		 * Sets the identifier for the token, OPTIONAL.
+		 *
+		 * @param jti the identifier for the token
+		 * @return the {@link Builder} for further configuration
+		 */
+		public Builder id(String jti) {
+			return claim(OAuth2TokenIntrospectionClaimNames.JTI, jti);
+		}
+
+		/**
+		 * Sets the claim.
+		 *
+		 * @param name the claim name
+		 * @param value the claim value
+		 * @return the {@link Builder} for further configuration
+		 */
+		public Builder claim(String name, Object value) {
+			Assert.hasText(name, "name cannot be empty");
+			Assert.notNull(value, "value cannot be null");
+			this.claims.put(name, value);
+			return this;
+		}
+
+		/**
+		 * Provides access to every {@link #claim(String, Object)} declared so far with
+		 * the possibility to add, replace, or remove.
+		 *
+		 * @param claimsConsumer a {@code Consumer} of the claims
+		 * @return the {@link Builder} for further configurations
+		 */
+		public Builder claims(Consumer<Map<String, Object>> claimsConsumer) {
+			claimsConsumer.accept(this.claims);
+			return this;
+		}
+
+		/**
+		 * Validate the claims and build the {@link OAuth2TokenIntrospection}.
+		 * <p>
+		 * The following claims are REQUIRED: {@code active}
+		 *
+		 * @return the {@link OAuth2TokenIntrospection}
+		 */
+		public OAuth2TokenIntrospection build() {
+			validateClaims();
+			return new OAuth2TokenIntrospection(this.claims);
+		}
+
+		private void validateClaims() {
+			Assert.notNull(this.claims.get(OAuth2TokenIntrospectionClaimNames.ACTIVE), "active cannot be null");
+			Assert.isInstanceOf(Boolean.class, this.claims.get(OAuth2TokenIntrospectionClaimNames.ACTIVE), "active must be of type boolean");
+			if (this.claims.containsKey(OAuth2TokenIntrospectionClaimNames.SCOPE)) {
+				Assert.isInstanceOf(List.class, this.claims.get(OAuth2TokenIntrospectionClaimNames.SCOPE), "scope must be of type List");
+			}
+			if (this.claims.containsKey(OAuth2TokenIntrospectionClaimNames.EXP)) {
+				Assert.isInstanceOf(Instant.class, this.claims.get(OAuth2TokenIntrospectionClaimNames.EXP), "exp must be of type Instant");
+			}
+			if (this.claims.containsKey(OAuth2TokenIntrospectionClaimNames.IAT)) {
+				Assert.isInstanceOf(Instant.class, this.claims.get(OAuth2TokenIntrospectionClaimNames.IAT), "iat must be of type Instant");
+			}
+			if (this.claims.containsKey(OAuth2TokenIntrospectionClaimNames.NBF)) {
+				Assert.isInstanceOf(Instant.class, this.claims.get(OAuth2TokenIntrospectionClaimNames.NBF), "nbf must be of type Instant");
+			}
+			if (this.claims.containsKey(OAuth2TokenIntrospectionClaimNames.AUD)) {
+				Assert.isInstanceOf(List.class, this.claims.get(OAuth2TokenIntrospectionClaimNames.AUD), "aud must be of type List");
+			}
+			if (this.claims.containsKey(OAuth2TokenIntrospectionClaimNames.ISS)) {
+				validateURL(this.claims.get(OAuth2TokenIntrospectionClaimNames.ISS), "iss must be a valid URL");
+			}
+		}
+
+		@SuppressWarnings("unchecked")
+		private void addClaimToClaimList(String name, String value) {
+			Assert.hasText(name, "name cannot be empty");
+			Assert.notNull(value, "value cannot be null");
+			this.claims.computeIfAbsent(name, k -> new LinkedList<String>());
+			((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;
+			}
+
+			try {
+				new URI(url.toString()).toURL();
+			} catch (Exception ex) {
+				throw new IllegalArgumentException(errorMessage, ex);
+			}
+		}
+	}
+}

+ 41 - 55
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/OAuth2TokenIntrospectionClaimAccessor.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2020-2021 the original author or authors.
+ * Copyright 2002-2020 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.
@@ -16,147 +16,133 @@
 
 package org.springframework.security.oauth2.core;
 
-import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames.CLIENT_ID;
-import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames.SCOPE;
-import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames.TOKEN_TYPE;
-import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames.USERNAME;
-import static org.springframework.security.oauth2.jwt.JwtClaimNames.AUD;
-import static org.springframework.security.oauth2.jwt.JwtClaimNames.EXP;
-import static org.springframework.security.oauth2.jwt.JwtClaimNames.IAT;
-import static org.springframework.security.oauth2.jwt.JwtClaimNames.ISS;
-import static org.springframework.security.oauth2.jwt.JwtClaimNames.JTI;
-import static org.springframework.security.oauth2.jwt.JwtClaimNames.NBF;
-import static org.springframework.security.oauth2.jwt.JwtClaimNames.SUB;
-
 import java.net.URL;
 import java.time.Instant;
 import java.util.List;
 
-/**
- * TODO This class is a copy from Spring Security (Resource Server) with the difference that we rely on the existing
- * {@code OAuth2ParameterNames} and {@code JwtClaimNames} claims. It should be consolidated when merging this codebase into Spring
- * Security.
+/*
+ * TODO
+ * This class is "mostly" a copy from Spring Security and should be removed after upgrading to Spring Security 5.6.0 GA.
+ * The major changes made between the Spring Security class and this one are:
+ *	1) Class renamed from `OAuth2IntrospectionClaimAccessor` to `OAuth2TokenIntrospectionClaimAccessor`
+ *	2) Moved from package `org.springframework.security.oauth2.server.resource.introspection` to `org.springframework.security.oauth2.core`
  *
- * A {@link ClaimAccessor} for the &quot;claims&quot; that may be contained in the Introspection Response.
+ * gh-9647 Move and rename OAuth2IntrospectionClaimAccessor/Names
+ * https://github.com/spring-projects/spring-security/issues/9647
+ */
+
+/**
+ * A {@link ClaimAccessor} for the &quot;claims&quot; that may be contained in the
+ * Introspection Response.
  *
  * @author David Kovac
- * @author Gerardo Roza
- * @since 0.1.1
+ * @since 5.4
  * @see ClaimAccessor
- * @see OAuth2IntrospectionClaimNames
- * @see OAuth2IntrospectionAuthenticatedPrincipal
- * @see <a target="_blank" href= "https://tools.ietf.org/html/rfc7662#section-2.2">Introspection Response</a>
+ * @see OAuth2TokenIntrospectionClaimNames
+ * @see <a target="_blank" href=
+ * "https://tools.ietf.org/html/rfc7662#section-2.2">Introspection Response</a>
  */
 public interface OAuth2TokenIntrospectionClaimAccessor extends ClaimAccessor {
 
-	String ACTIVE = "active";
-
 	/**
 	 * Returns the indicator {@code (active)} whether or not the token is currently active
-	 *
 	 * @return the indicator whether or not the token is currently active
 	 */
 	default boolean isActive() {
-		return Boolean.TRUE.equals(this.getClaimAsBoolean(ACTIVE));
+		return Boolean.TRUE.equals(getClaimAsBoolean(OAuth2TokenIntrospectionClaimNames.ACTIVE));
 	}
 
 	/**
 	 * Returns the scopes {@code (scope)} associated with the token
-	 *
 	 * @return the scopes associated with the token
 	 */
-	default String getScope() {
-		return this.getClaimAsString(SCOPE);
+	default List<String> getScope() {
+		return getClaimAsStringList(OAuth2TokenIntrospectionClaimNames.SCOPE);
 	}
 
 	/**
 	 * Returns the client identifier {@code (client_id)} for the token
-	 *
 	 * @return the client identifier for the token
 	 */
 	default String getClientId() {
-		return this.getClaimAsString(CLIENT_ID);
+		return getClaimAsString(OAuth2TokenIntrospectionClaimNames.CLIENT_ID);
 	}
 
 	/**
-	 * Returns a human-readable identifier {@code (username)} for the resource owner that authorized the token
-	 *
-	 * @return a human-readable identifier for the resource owner that authorized the token
+	 * Returns a human-readable identifier {@code (username)} for the resource owner that
+	 * authorized the token
+	 * @return a human-readable identifier for the resource owner that authorized the
+	 * token
 	 */
 	default String getUsername() {
-		return this.getClaimAsString(USERNAME);
+		return getClaimAsString(OAuth2TokenIntrospectionClaimNames.USERNAME);
 	}
 
 	/**
 	 * Returns the type of the token {@code (token_type)}, for example {@code bearer}.
-	 *
 	 * @return the type of the token, for example {@code bearer}.
 	 */
 	default String getTokenType() {
-		return this.getClaimAsString(TOKEN_TYPE);
+		return getClaimAsString(OAuth2TokenIntrospectionClaimNames.TOKEN_TYPE);
 	}
 
 	/**
 	 * Returns a timestamp {@code (exp)} indicating when the token expires
-	 *
 	 * @return a timestamp indicating when the token expires
 	 */
 	default Instant getExpiresAt() {
-		return this.getClaimAsInstant(EXP);
+		return getClaimAsInstant(OAuth2TokenIntrospectionClaimNames.EXP);
 	}
 
 	/**
 	 * Returns a timestamp {@code (iat)} indicating when the token was issued
-	 *
 	 * @return a timestamp indicating when the token was issued
 	 */
 	default Instant getIssuedAt() {
-		return this.getClaimAsInstant(IAT);
+		return getClaimAsInstant(OAuth2TokenIntrospectionClaimNames.IAT);
 	}
 
 	/**
-	 * Returns a timestamp {@code (nbf)} indicating when the token is not to be used before
-	 *
+	 * Returns a timestamp {@code (nbf)} indicating when the token is not to be used
+	 * before
 	 * @return a timestamp indicating when the token is not to be used before
 	 */
 	default Instant getNotBefore() {
-		return this.getClaimAsInstant(NBF);
+		return getClaimAsInstant(OAuth2TokenIntrospectionClaimNames.NBF);
 	}
 
 	/**
-	 * Returns usually a machine-readable identifier {@code (sub)} of the resource owner who authorized the token
-	 *
-	 * @return usually a machine-readable identifier of the resource owner who authorized the token
+	 * Returns usually a machine-readable identifier {@code (sub)} of the resource owner
+	 * who authorized the token
+	 * @return usually a machine-readable identifier of the resource owner who authorized
+	 * the token
 	 */
 	default String getSubject() {
-		return this.getClaimAsString(SUB);
+		return getClaimAsString(OAuth2TokenIntrospectionClaimNames.SUB);
 	}
 
 	/**
 	 * Returns the intended audience {@code (aud)} for the token
-	 *
 	 * @return the intended audience for the token
 	 */
 	default List<String> getAudience() {
-		return this.getClaimAsStringList(AUD);
+		return getClaimAsStringList(OAuth2TokenIntrospectionClaimNames.AUD);
 	}
 
 	/**
 	 * Returns the issuer {@code (iss)} of the token
-	 *
 	 * @return the issuer of the token
 	 */
 	default URL getIssuer() {
-		return this.getClaimAsURL(ISS);
+		return getClaimAsURL(OAuth2TokenIntrospectionClaimNames.ISS);
 	}
 
 	/**
 	 * Returns the identifier {@code (jti)} for the token
-	 *
 	 * @return the identifier for the token
 	 */
 	default String getId() {
-		return this.getClaimAsString(JTI);
+		return getClaimAsString(OAuth2TokenIntrospectionClaimNames.JTI);
 	}
 
 }

+ 102 - 0
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/OAuth2TokenIntrospectionClaimNames.java

@@ -0,0 +1,102 @@
+/*
+ * Copyright 2002-2019 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.core;
+
+/*
+ * TODO
+ * This class is "mostly" a copy from Spring Security and should be removed after upgrading to Spring Security 5.6.0 GA.
+ * The major changes made between the Spring Security class and this one are:
+ *	1) Class renamed from `OAuth2IntrospectionClaimNames` to `OAuth2TokenIntrospectionClaimNames`
+ *	2) Moved from package `org.springframework.security.oauth2.server.resource.introspection` to `org.springframework.security.oauth2.core`
+ *
+ * gh-9647 Move and rename OAuth2IntrospectionClaimAccessor/Names
+ * https://github.com/spring-projects/spring-security/issues/9647
+ */
+
+/**
+ * The names of the &quot;Introspection Claims&quot; defined by an
+ * <a target="_blank" href="https://tools.ietf.org/html/rfc7662#section-2.2">Introspection
+ * Response</a>.
+ *
+ * @author Josh Cummings
+ * @since 5.2
+ */
+public interface OAuth2TokenIntrospectionClaimNames {
+
+	/**
+	 * {@code active} - Indicator whether or not the token is currently active
+	 */
+	String ACTIVE = "active";
+
+	/**
+	 * {@code scope} - The scopes for the token
+	 */
+	String SCOPE = "scope";
+
+	/**
+	 * {@code client_id} - The Client identifier for the token
+	 */
+	String CLIENT_ID = "client_id";
+
+	/**
+	 * {@code username} - A human-readable identifier for the resource owner that
+	 * authorized the token
+	 */
+	String USERNAME = "username";
+
+	/**
+	 * {@code token_type} - The type of the token, for example {@code bearer}.
+	 */
+	String TOKEN_TYPE = "token_type";
+
+	/**
+	 * {@code exp} - A timestamp indicating when the token expires
+	 */
+	String EXP = "exp";
+
+	/**
+	 * {@code iat} - A timestamp indicating when the token was issued
+	 */
+	String IAT = "iat";
+
+	/**
+	 * {@code nbf} - A timestamp indicating when the token is not to be used before
+	 */
+	String NBF = "nbf";
+
+	/**
+	 * {@code sub} - Usually a machine-readable identifier of the resource owner who
+	 * authorized the token
+	 */
+	String SUB = "sub";
+
+	/**
+	 * {@code aud} - The intended audience for the token
+	 */
+	String AUD = "aud";
+
+	/**
+	 * {@code iss} - The issuer of the token
+	 */
+	String ISS = "iss";
+
+	/**
+	 * {@code jti} - The identifier for the token
+	 */
+	String JTI = "jti";
+
+}

+ 0 - 296
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/OAuth2TokenIntrospectionClaims.java

@@ -1,296 +0,0 @@
-/*
- * 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.core;
-
-import static java.util.stream.Collectors.toMap;
-import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames.CLIENT_ID;
-import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames.SCOPE;
-import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames.TOKEN_TYPE;
-import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames.USERNAME;
-import static org.springframework.security.oauth2.jwt.JwtClaimNames.AUD;
-import static org.springframework.security.oauth2.jwt.JwtClaimNames.EXP;
-import static org.springframework.security.oauth2.jwt.JwtClaimNames.IAT;
-import static org.springframework.security.oauth2.jwt.JwtClaimNames.ISS;
-import static org.springframework.security.oauth2.jwt.JwtClaimNames.JTI;
-import static org.springframework.security.oauth2.jwt.JwtClaimNames.NBF;
-import static org.springframework.security.oauth2.jwt.JwtClaimNames.SUB;
-
-import org.springframework.security.oauth2.core.OAuth2AccessToken.TokenType;
-import org.springframework.util.Assert;
-
-import java.io.Serializable;
-import java.time.Instant;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.function.Consumer;
-
-/**
- * A representation of an OAuth 2.0 Introspection Token Response Claims.
- *
- * @author Gerardo Roza
- * @since 0.1.1
- * @see <a target="_blank" href="https://tools.ietf.org/html/rfc7662#section-2.2">Section 2.2 - Introspection Response</a>
- */
-public class OAuth2TokenIntrospectionClaims implements OAuth2TokenIntrospectionClaimAccessor, Serializable {
-	private static final long serialVersionUID = Version.SERIAL_VERSION_UID;
-	private static final Collection<String> SUPPORTED_FIELDS = Arrays
-			.asList(ACTIVE, CLIENT_ID, SCOPE, TOKEN_TYPE, USERNAME, AUD, EXP, IAT, ISS, JTI, NBF, SUB);
-
-	private final Map<String, Object> claims;
-
-	private OAuth2TokenIntrospectionClaims(Map<String, Object> claims) {
-		this.claims = Collections.unmodifiableMap(new LinkedHashMap<>(claims));
-	}
-
-	/**
-	 * Returns the populated Token Introspection Response claims.
-	 *
-	 * @return the claims
-	 */
-	@Override
-	public Map<String, Object> getClaims() {
-		return this.claims;
-	}
-
-	/**
-	 * Constructs a new {@link Builder} with the provided parameters.
-	 *
-	 * @param claims the params to initialize the builder
-	 */
-	public static Builder withClaims(Map<String, Object> claims) {
-		Assert.notEmpty(claims, "claims cannot be empty");
-		Builder builder = new Builder().claims(c -> c.putAll(claims));
-		Optional.ofNullable(claims.get(IAT)).filter(Instant.class::isInstance).map(Instant.class::cast)
-				.ifPresent(builder::issuedAt);
-		Optional.ofNullable(claims.get(EXP)).filter(Instant.class::isInstance).map(Instant.class::cast)
-				.ifPresent(builder::expirationTime);
-		Optional.ofNullable(claims.get(NBF)).filter(Instant.class::isInstance).map(Instant.class::cast)
-				.ifPresent(builder::notBefore);
-		Optional.ofNullable(claims.get(TOKEN_TYPE)).filter(TokenType.class::isInstance).map(TokenType.class::cast)
-				.ifPresent(builder::tokenType);
-		return builder;
-	}
-
-	/**
-	 * Constructs a new {@link Builder} with the required active field.
-	 *
-	 * @param active boolean indicating whether the introspected token is active or not to initialize the builder
-	 */
-	public static Builder builder(boolean active) {
-		return new Builder(active);
-	}
-
-	/**
-	 * A builder for {@link OAuth2TokenIntrospectionClaims}.
-	 */
-	public static final class Builder {
-
-		private final Map<String, Object> claims = new LinkedHashMap<>();
-
-		private Builder() {
-		}
-
-		/**
-		 * Helps configure a basic {@link OAuth2TokenIntrospectionClaims}.
-		 *
-		 * @param active boolean indicating whether the introspected token is active or not
-		 */
-		public Builder(boolean active) {
-			claim(ACTIVE, active);
-		}
-
-		/**
-		 * Populates the 'active' field.
-		 *
-		 * @param active boolean indicating whether the introspected token is active or not
-		 * @return the {@link Builder} for further configurations
-		 */
-		public Builder active(boolean active) {
-			claim(ACTIVE, active);
-			return this;
-		}
-
-		/**
-		 * Populates the 'scope' field.
-		 *
-		 * @param scope string containing a space-separated list of scopes associated with this token
-		 * @return the {@link Builder} for further configurations
-		 */
-		public Builder scope(String scope) {
-			claim(SCOPE, scope);
-			return this;
-		}
-
-		/**
-		 * Populates the 'client_id' field.
-		 *
-		 * @param clientId identifier for the OAuth 2.0 client that requested this token
-		 * @return the {@link Builder} for further configurations
-		 */
-		public Builder clientId(String clientId) {
-			claim(CLIENT_ID, clientId);
-			return this;
-		}
-
-		/**
-		 * Populates the 'username' field.
-		 *
-		 * @param username Human-readable identifier for the resource owner who authorized this token
-		 * @return the {@link Builder} for further configurations
-		 */
-		public Builder username(String username) {
-			claim(USERNAME, username);
-			return this;
-		}
-
-		/**
-		 * Populates the 'token_type' field.
-		 *
-		 * @param tokenType {@link TokenType} indicating the type of the token
-		 * @return the {@link Builder} for further configurations
-		 */
-		public Builder tokenType(OAuth2AccessToken.TokenType tokenType) {
-			claim(TOKEN_TYPE, tokenType.getValue());
-			return this;
-		}
-
-		/**
-		 * Populates the 'exp' (Expiration Time) field.
-		 *
-		 * @param expirationTime {@link Instant} indicating when this token will expire
-		 * @return the {@link Builder} for further configurations
-		 */
-		public Builder expirationTime(Instant expirationTime) {
-			claim(EXP, expirationTime.getEpochSecond());
-			return this;
-		}
-
-		/**
-		 * Populates the 'iat' (Issued At) field.
-		 *
-		 * @param issuedAt {@link Instant} indicating when this token was originally issued
-		 * @return the {@link Builder} for further configurations
-		 */
-		public Builder issuedAt(Instant issuedAt) {
-			claim(IAT, issuedAt.getEpochSecond());
-			return this;
-		}
-
-		/**
-		 * Populates the 'nbf' (Not Before) field.
-		 *
-		 * @param notBefore {@link Instant} indicating when this token is not to be used before
-		 * @return the {@link Builder} for further configurations
-		 */
-		public Builder notBefore(Instant notBefore) {
-			claim(NBF, notBefore.getEpochSecond());
-			return this;
-		}
-
-		/**
-		 * Populates the 'sub' (Subject) field.
-		 *
-		 * @param subject usually a machine-readable identifier of the resource owner who authorized this token
-		 * @return the {@link Builder} for further configurations
-		 */
-		public Builder subject(String subject) {
-			claim(SUB, subject);
-			return this;
-		}
-
-		/**
-		 * Populates the 'aud' (Audience) field.
-		 *
-		 * @param audience service-specific string identifier or list of string identifiers representing the intended audience for this
-		 * token
-		 * @return the {@link Builder} for further configurations
-		 */
-		public Builder audience(List<String> audience) {
-			claim(AUD, audience);
-			return this;
-		}
-
-		/**
-		 * Populates the 'iss' (Issuer) field.
-		 *
-		 * @param issuer of this token
-		 * @return the {@link Builder} for further configurations
-		 */
-		public Builder issuer(String issuer) {
-			claim(ISS, issuer);
-			return this;
-		}
-
-		/**
-		 * Populates the 'jti' (JWT ID) field.
-		 *
-		 * @param jwtId identifier for the token
-		 * @return the {@link Builder} for further configurations
-		 */
-		public Builder jwtId(String jwtId) {
-			claim(JTI, jwtId);
-			return this;
-		}
-
-		/**
-		 * Use this claim in the resulting {@link OAuth2TokenIntrospectionClaims}.
-		 *
-		 * @param name the claim name
-		 * @param value the claim value
-		 * @return the {@link Builder} for further configuration
-		 */
-		public Builder claim(String name, Object value) {
-			Assert.hasText(name, "name cannot be empty");
-			Assert.notNull(value, "value cannot be null");
-			this.claims.put(name, value);
-			return this;
-		}
-
-		/**
-		 * Provides access to every {@link #claim(String, Object)} declared so far with the possibility to add, replace, or remove.
-		 *
-		 * @param claimsConsumer a {@code Consumer} of the claims
-		 * @return the {@link Builder} for further configurations
-		 */
-		public Builder claims(Consumer<Map<String, Object>> claimsConsumer) {
-			claimsConsumer.accept(this.claims);
-			return this;
-		}
-
-		private void validateClaims() {
-			Assert.notNull(this.claims.get(ACTIVE), "active cannot be null");
-		}
-
-		/**
-		 * Build the {@link OAuth2TokenIntrospectionClaims}
-		 *
-		 * @return The constructed {@link OAuth2TokenIntrospectionClaims}
-		 */
-		public OAuth2TokenIntrospectionClaims build() {
-			Map<String, Object> responseClaims = this.claims.entrySet().stream()
-					.filter(entry -> SUPPORTED_FIELDS.contains(entry.getKey()))
-					.collect(toMap(Map.Entry::getKey, Map.Entry::getValue));
-			validateClaims();
-			return new OAuth2TokenIntrospectionClaims(responseClaims);
-		}
-	}
-}

+ 0 - 181
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/http/converter/OAuth2TokenIntrospectionClaimsHttpMessageConverter.java

@@ -1,181 +0,0 @@
-/*
- * 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.core.http.converter;
-
-import static org.springframework.security.oauth2.core.OAuth2TokenIntrospectionClaimAccessor.ACTIVE;
-import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames.CLIENT_ID;
-import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames.SCOPE;
-import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames.TOKEN_TYPE;
-import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames.USERNAME;
-import static org.springframework.security.oauth2.jwt.JwtClaimNames.AUD;
-import static org.springframework.security.oauth2.jwt.JwtClaimNames.EXP;
-import static org.springframework.security.oauth2.jwt.JwtClaimNames.IAT;
-import static org.springframework.security.oauth2.jwt.JwtClaimNames.ISS;
-import static org.springframework.security.oauth2.jwt.JwtClaimNames.JTI;
-import static org.springframework.security.oauth2.jwt.JwtClaimNames.NBF;
-import static org.springframework.security.oauth2.jwt.JwtClaimNames.SUB;
-
-import org.springframework.core.ParameterizedTypeReference;
-import org.springframework.core.convert.TypeDescriptor;
-import org.springframework.core.convert.converter.Converter;
-import org.springframework.http.HttpInputMessage;
-import org.springframework.http.HttpOutputMessage;
-import org.springframework.http.MediaType;
-import org.springframework.http.converter.AbstractHttpMessageConverter;
-import org.springframework.http.converter.GenericHttpMessageConverter;
-import org.springframework.http.converter.HttpMessageConverter;
-import org.springframework.http.converter.HttpMessageNotReadableException;
-import org.springframework.http.converter.HttpMessageNotWritableException;
-import org.springframework.security.oauth2.core.OAuth2TokenIntrospectionClaims;
-import org.springframework.security.oauth2.core.converter.ClaimConversionService;
-import org.springframework.security.oauth2.core.converter.ClaimTypeConverter;
-import org.springframework.util.Assert;
-
-import java.time.Instant;
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.Map;
-
-/**
- * A {@link HttpMessageConverter} for an {@link OAuth2TokenIntrospectionClaims} Token Introspection Response.
- *
- * @author Gerardo Roza
- * @since 0.1.1
- * @see AbstractHttpMessageConverter
- * @see OAuth2TokenIntrospectionClaims
- */
-public class OAuth2TokenIntrospectionClaimsHttpMessageConverter
-		extends AbstractHttpMessageConverter<OAuth2TokenIntrospectionClaims> {
-
-	private static final ParameterizedTypeReference<Map<String, Object>> STRING_OBJECT_MAP = new ParameterizedTypeReference<Map<String, Object>>() {
-	};
-
-	private GenericHttpMessageConverter<Object> jsonMessageConverter = HttpMessageConverters.getJsonMessageConverter();
-
-	private Converter<Map<String, Object>, OAuth2TokenIntrospectionClaims> tokenIntrospectionResponseConverter = new OAuth2TokenIntrospectionResponseConverter();
-	private Converter<OAuth2TokenIntrospectionClaims, Map<String, Object>> tokenIntrospectionResponseParametersConverter = OAuth2TokenIntrospectionClaims::getClaims;
-
-	public OAuth2TokenIntrospectionClaimsHttpMessageConverter() {
-		super(MediaType.APPLICATION_JSON, new MediaType("application", "*+json"));
-	}
-
-	@Override
-	protected boolean supports(Class<?> clazz) {
-		return OAuth2TokenIntrospectionClaims.class.isAssignableFrom(clazz);
-	}
-
-	@Override
-	@SuppressWarnings("unchecked")
-	protected OAuth2TokenIntrospectionClaims readInternal(Class<? extends OAuth2TokenIntrospectionClaims> clazz,
-			HttpInputMessage inputMessage) throws HttpMessageNotReadableException {
-		try {
-			Map<String, Object> tokenIntrospectionResponseParameters = (Map<String, Object>) this.jsonMessageConverter
-					.read(STRING_OBJECT_MAP.getType(), null, inputMessage);
-			return this.tokenIntrospectionResponseConverter.convert(tokenIntrospectionResponseParameters);
-		} catch (Exception ex) {
-			throw new HttpMessageNotReadableException(
-					"An error occurred reading the Token Introspection Response: " + ex.getMessage(), ex, inputMessage);
-		}
-	}
-
-	@Override
-	protected void writeInternal(OAuth2TokenIntrospectionClaims tokenIntrospectionResponse,
-			HttpOutputMessage outputMessage) {
-		try {
-
-			Map<String, Object> tokenIntrospectionResponseParameters = this.tokenIntrospectionResponseParametersConverter
-					.convert(tokenIntrospectionResponse);
-			this.jsonMessageConverter.write(
-					tokenIntrospectionResponseParameters, STRING_OBJECT_MAP.getType(), MediaType.APPLICATION_JSON,
-					outputMessage);
-		} catch (Exception ex) {
-			throw new HttpMessageNotWritableException(
-					"An error occurred writing the Token Introspection Response: " + ex.getMessage(), ex);
-		}
-	}
-
-	/**
-	 * Sets the {@link Converter} used for converting the Token Introspection parameters to an {@link OAuth2TokenIntrospectionClaims}.
-	 *
-	 * @param tokenIntrospectionResponseConverter the {@link Converter} used for converting to an
-	 * {@link OAuth2TokenIntrospectionClaims}
-	 */
-	public void setTokenIntrospectionResponseConverter(
-			Converter<Map<String, Object>, OAuth2TokenIntrospectionClaims> tokenIntrospectionResponseConverter) {
-		Assert.notNull(tokenIntrospectionResponseConverter, "tokenIntrospectionResponseConverter cannot be null");
-		this.tokenIntrospectionResponseConverter = tokenIntrospectionResponseConverter;
-	}
-
-	/**
-	 * Sets the {@link Converter} used for converting the {@link OAuth2TokenIntrospectionClaims} to a {@code Map} representation of
-	 * the Token Introspection Response.
-	 *
-	 * @param tokenIntrospectionResponseParametersConverter the {@link Converter} used for converting to a {@code Map} representation
-	 * of the Token Introspection Response
-	 */
-	public void setTokenIntrospectionResponseParametersConverter(
-			Converter<OAuth2TokenIntrospectionClaims, Map<String, Object>> tokenIntrospectionResponseParametersConverter) {
-		Assert.notNull(
-				tokenIntrospectionResponseParametersConverter,
-				"tokenIntrospectionResponseParametersConverter cannot be null");
-		this.tokenIntrospectionResponseParametersConverter = tokenIntrospectionResponseParametersConverter;
-	}
-
-	private static final class OAuth2TokenIntrospectionResponseConverter
-			implements Converter<Map<String, Object>, OAuth2TokenIntrospectionClaims> {
-		private static final ClaimConversionService CLAIM_CONVERSION_SERVICE = ClaimConversionService
-				.getSharedInstance();
-		private static final TypeDescriptor OBJECT_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(Object.class);
-		private static final TypeDescriptor BOOLEAN_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(Boolean.class);
-		private static final TypeDescriptor INSTANT_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(Instant.class);
-		private static final TypeDescriptor STRING_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(String.class);
-		private final ClaimTypeConverter parameterTypeConverter;
-
-		private OAuth2TokenIntrospectionResponseConverter() {
-			Converter<Object, ?> collectionStringConverter = getConverter(
-					TypeDescriptor.collection(Collection.class, STRING_TYPE_DESCRIPTOR));
-			Converter<Object, ?> stringConverter = getConverter(STRING_TYPE_DESCRIPTOR);
-			Converter<Object, ?> booleanConverter = getConverter(BOOLEAN_TYPE_DESCRIPTOR);
-			Converter<Object, ?> instantConverter = getConverter(INSTANT_TYPE_DESCRIPTOR);
-
-			Map<String, Converter<Object, ?>> claimConverters = new HashMap<>();
-			claimConverters.put(ACTIVE, booleanConverter);
-			claimConverters.put(SCOPE, stringConverter);
-			claimConverters.put(CLIENT_ID, stringConverter);
-			claimConverters.put(TOKEN_TYPE, stringConverter);
-			claimConverters.put(USERNAME, stringConverter);
-			claimConverters.put(AUD, collectionStringConverter);
-			claimConverters.put(EXP, instantConverter);
-			claimConverters.put(IAT, instantConverter);
-			claimConverters.put(ISS, stringConverter);
-			claimConverters.put(JTI, stringConverter);
-			claimConverters.put(NBF, instantConverter);
-			claimConverters.put(SUB, stringConverter);
-			this.parameterTypeConverter = new ClaimTypeConverter(claimConverters);
-		}
-
-		@Override
-		public OAuth2TokenIntrospectionClaims convert(Map<String, Object> source) {
-			Map<String, Object> parsedClaims = this.parameterTypeConverter.convert(source);
-			return OAuth2TokenIntrospectionClaims.withClaims(parsedClaims).build();
-		}
-
-		private static Converter<Object, ?> getConverter(TypeDescriptor targetDescriptor) {
-			return (source) -> CLAIM_CONVERSION_SERVICE.convert(source, OBJECT_TYPE_DESCRIPTOR, targetDescriptor);
-		}
-	}
-}

+ 203 - 0
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/http/converter/OAuth2TokenIntrospectionHttpMessageConverter.java

@@ -0,0 +1,203 @@
+/*
+ * 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.core.http.converter;
+
+import java.net.URL;
+import java.time.Instant;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.springframework.core.ParameterizedTypeReference;
+import org.springframework.core.convert.TypeDescriptor;
+import org.springframework.core.convert.converter.Converter;
+import org.springframework.http.HttpInputMessage;
+import org.springframework.http.HttpOutputMessage;
+import org.springframework.http.MediaType;
+import org.springframework.http.converter.AbstractHttpMessageConverter;
+import org.springframework.http.converter.GenericHttpMessageConverter;
+import org.springframework.http.converter.HttpMessageConverter;
+import org.springframework.http.converter.HttpMessageNotReadableException;
+import org.springframework.http.converter.HttpMessageNotWritableException;
+import org.springframework.security.oauth2.core.OAuth2TokenIntrospection;
+import org.springframework.security.oauth2.core.OAuth2TokenIntrospectionClaimNames;
+import org.springframework.security.oauth2.core.converter.ClaimConversionService;
+import org.springframework.security.oauth2.core.converter.ClaimTypeConverter;
+import org.springframework.util.Assert;
+import org.springframework.util.CollectionUtils;
+import org.springframework.util.StringUtils;
+
+/**
+ * A {@link HttpMessageConverter} for an {@link OAuth2TokenIntrospection OAuth 2.0 Token Introspection Response}.
+ *
+ * @author Gerardo Roza
+ * @author Joe Grandja
+ * @since 0.1.1
+ * @see AbstractHttpMessageConverter
+ * @see OAuth2TokenIntrospection
+ */
+public class OAuth2TokenIntrospectionHttpMessageConverter extends AbstractHttpMessageConverter<OAuth2TokenIntrospection> {
+
+	private static final ParameterizedTypeReference<Map<String, Object>> STRING_OBJECT_MAP = new ParameterizedTypeReference<Map<String, Object>>() {
+	};
+
+	private final GenericHttpMessageConverter<Object> jsonMessageConverter = HttpMessageConverters.getJsonMessageConverter();
+
+	private Converter<Map<String, Object>, OAuth2TokenIntrospection> tokenIntrospectionConverter = new MapOAuth2TokenIntrospectionConverter();
+	private Converter<OAuth2TokenIntrospection, Map<String, Object>> tokenIntrospectionParametersConverter = new OAuth2TokenIntrospectionMapConverter();
+
+	public OAuth2TokenIntrospectionHttpMessageConverter() {
+		super(MediaType.APPLICATION_JSON, new MediaType("application", "*+json"));
+	}
+
+	@Override
+	protected boolean supports(Class<?> clazz) {
+		return OAuth2TokenIntrospection.class.isAssignableFrom(clazz);
+	}
+
+	@Override
+	@SuppressWarnings("unchecked")
+	protected OAuth2TokenIntrospection readInternal(Class<? extends OAuth2TokenIntrospection> clazz, HttpInputMessage inputMessage)
+			throws HttpMessageNotReadableException {
+		try {
+			Map<String, Object> tokenIntrospectionParameters = (Map<String, Object>) this.jsonMessageConverter
+					.read(STRING_OBJECT_MAP.getType(), null, inputMessage);
+			return this.tokenIntrospectionConverter.convert(tokenIntrospectionParameters);
+		} catch (Exception ex) {
+			throw new HttpMessageNotReadableException(
+					"An error occurred reading the Token Introspection Response: " + ex.getMessage(), ex, inputMessage);
+		}
+	}
+
+	@Override
+	protected void writeInternal(OAuth2TokenIntrospection tokenIntrospection, HttpOutputMessage outputMessage)
+			throws HttpMessageNotWritableException {
+		try {
+			Map<String, Object> tokenIntrospectionResponseParameters = this.tokenIntrospectionParametersConverter
+					.convert(tokenIntrospection);
+			this.jsonMessageConverter.write(tokenIntrospectionResponseParameters, STRING_OBJECT_MAP.getType(),
+					MediaType.APPLICATION_JSON, outputMessage);
+		} catch (Exception ex) {
+			throw new HttpMessageNotWritableException(
+					"An error occurred writing the Token Introspection Response: " + ex.getMessage(), ex);
+		}
+	}
+
+	/**
+	 * Sets the {@link Converter} used for converting the Token Introspection Response parameters to an {@link OAuth2TokenIntrospection}.
+	 *
+	 * @param tokenIntrospectionConverter the {@link Converter} used for converting to an {@link OAuth2TokenIntrospection}
+	 */
+	public final void setTokenIntrospectionConverter(
+			Converter<Map<String, Object>, OAuth2TokenIntrospection> tokenIntrospectionConverter) {
+		Assert.notNull(tokenIntrospectionConverter, "tokenIntrospectionConverter cannot be null");
+		this.tokenIntrospectionConverter = tokenIntrospectionConverter;
+	}
+
+	/**
+	 * Sets the {@link Converter} used for converting an {@link OAuth2TokenIntrospection}
+	 * to a {@code Map} representation of the Token Introspection Response parameters.
+	 *
+	 * @param tokenIntrospectionParametersConverter the {@link Converter} used for converting to a
+	 * {@code Map} representation of the Token Introspection Response parameters
+	 */
+	public final void setTokenIntrospectionParametersConverter(
+			Converter<OAuth2TokenIntrospection, Map<String, Object>> tokenIntrospectionParametersConverter) {
+		Assert.notNull(tokenIntrospectionParametersConverter, "tokenIntrospectionParametersConverter cannot be null");
+		this.tokenIntrospectionParametersConverter = tokenIntrospectionParametersConverter;
+	}
+
+	private static final class MapOAuth2TokenIntrospectionConverter
+			implements Converter<Map<String, Object>, OAuth2TokenIntrospection> {
+
+		private static final ClaimConversionService CLAIM_CONVERSION_SERVICE = ClaimConversionService.getSharedInstance();
+		private static final TypeDescriptor OBJECT_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(Object.class);
+		private static final TypeDescriptor BOOLEAN_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(Boolean.class);
+		private static final TypeDescriptor STRING_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(String.class);
+		private static final TypeDescriptor INSTANT_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(Instant.class);
+		private static final TypeDescriptor URL_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(URL.class);
+		private final ClaimTypeConverter claimTypeConverter;
+
+		private MapOAuth2TokenIntrospectionConverter() {
+			Converter<Object, ?> booleanConverter = getConverter(BOOLEAN_TYPE_DESCRIPTOR);
+			Converter<Object, ?> stringConverter = getConverter(STRING_TYPE_DESCRIPTOR);
+			Converter<Object, ?> instantConverter = getConverter(INSTANT_TYPE_DESCRIPTOR);
+			Converter<Object, ?> collectionStringConverter = getConverter(
+					TypeDescriptor.collection(Collection.class, STRING_TYPE_DESCRIPTOR));
+			Converter<Object, ?> urlConverter = getConverter(URL_TYPE_DESCRIPTOR);
+
+			Map<String, Converter<Object, ?>> claimConverters = new HashMap<>();
+			claimConverters.put(OAuth2TokenIntrospectionClaimNames.ACTIVE, booleanConverter);
+			claimConverters.put(OAuth2TokenIntrospectionClaimNames.SCOPE, MapOAuth2TokenIntrospectionConverter::convertScope);
+			claimConverters.put(OAuth2TokenIntrospectionClaimNames.CLIENT_ID, stringConverter);
+			claimConverters.put(OAuth2TokenIntrospectionClaimNames.USERNAME, stringConverter);
+			claimConverters.put(OAuth2TokenIntrospectionClaimNames.TOKEN_TYPE, stringConverter);
+			claimConverters.put(OAuth2TokenIntrospectionClaimNames.EXP, instantConverter);
+			claimConverters.put(OAuth2TokenIntrospectionClaimNames.IAT, instantConverter);
+			claimConverters.put(OAuth2TokenIntrospectionClaimNames.NBF, instantConverter);
+			claimConverters.put(OAuth2TokenIntrospectionClaimNames.SUB, stringConverter);
+			claimConverters.put(OAuth2TokenIntrospectionClaimNames.AUD, collectionStringConverter);
+			claimConverters.put(OAuth2TokenIntrospectionClaimNames.ISS, urlConverter);
+			claimConverters.put(OAuth2TokenIntrospectionClaimNames.JTI, stringConverter);
+			this.claimTypeConverter = new ClaimTypeConverter(claimConverters);
+		}
+
+		@Override
+		public OAuth2TokenIntrospection convert(Map<String, Object> source) {
+			Map<String, Object> parsedClaims = this.claimTypeConverter.convert(source);
+			return OAuth2TokenIntrospection.withClaims(parsedClaims).build();
+		}
+
+		private static Converter<Object, ?> getConverter(TypeDescriptor targetDescriptor) {
+			return (source) -> CLAIM_CONVERSION_SERVICE.convert(source, OBJECT_TYPE_DESCRIPTOR, targetDescriptor);
+		}
+
+		private static List<String> convertScope(Object scope) {
+			if (scope == null) {
+				return Collections.emptyList();
+			}
+			return Arrays.asList(StringUtils.delimitedListToStringArray(scope.toString(), " "));
+		}
+	}
+
+	private static final class OAuth2TokenIntrospectionMapConverter
+			implements Converter<OAuth2TokenIntrospection, Map<String, Object>> {
+
+		@Override
+		public Map<String, Object> convert(OAuth2TokenIntrospection source) {
+			Map<String, Object> responseClaims = new LinkedHashMap<>(source.getClaims());
+			if (!CollectionUtils.isEmpty(source.getScope())) {
+				responseClaims.put(OAuth2TokenIntrospectionClaimNames.SCOPE, StringUtils.collectionToDelimitedString(source.getScope(), " "));
+			}
+			if (source.getExpiresAt() != null) {
+				responseClaims.put(OAuth2TokenIntrospectionClaimNames.EXP, source.getExpiresAt().getEpochSecond());
+			}
+			if (source.getIssuedAt() != null) {
+				responseClaims.put(OAuth2TokenIntrospectionClaimNames.IAT, source.getIssuedAt().getEpochSecond());
+			}
+			if (source.getNotBefore() != null) {
+				responseClaims.put(OAuth2TokenIntrospectionClaimNames.NBF, source.getNotBefore().getEpochSecond());
+			}
+			return responseClaims;
+		}
+	}
+
+}

+ 33 - 0
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/OAuth2Authorization.java

@@ -16,6 +16,7 @@
 package org.springframework.security.oauth2.server.authorization;
 
 import java.io.Serializable;
+import java.time.Instant;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
@@ -32,6 +33,7 @@ import org.springframework.security.oauth2.core.OAuth2RefreshToken;
 import org.springframework.security.oauth2.core.OAuth2RefreshToken2;
 import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
 import org.springframework.util.Assert;
+import org.springframework.util.CollectionUtils;
 import org.springframework.util.StringUtils;
 
 /**
@@ -280,6 +282,37 @@ public class OAuth2Authorization implements Serializable {
 			return Boolean.TRUE.equals(getMetadata(INVALIDATED_METADATA_NAME));
 		}
 
+		/**
+		 * Returns {@code true} if the token has expired.
+		 *
+		 * @return {@code true} if the token has expired, {@code false} otherwise
+		 */
+		public boolean isExpired() {
+			return getToken().getExpiresAt() != null && Instant.now().isAfter(getToken().getExpiresAt());
+		}
+
+		/**
+		 * Returns {@code true} if the token is before the time it can be used.
+		 *
+		 * @return {@code true} if the token is before the time it can be used, {@code false} otherwise
+		 */
+		public boolean isBeforeUse() {
+			Instant notBefore = null;
+			if (!CollectionUtils.isEmpty(getClaims())) {
+				notBefore = (Instant) getClaims().get("nbf");
+			}
+			return notBefore != null && Instant.now().isBefore(notBefore);
+		}
+
+		/**
+		 * Returns {@code true} if the token is currently active.
+		 *
+		 * @return {@code true} if the token is currently active, {@code false} otherwise
+		 */
+		public boolean isActive() {
+			return !isInvalidated() && !isExpired() && !isBeforeUse();
+		}
+
 		/**
 		 * Returns the claims associated to the token.
 		 *

+ 72 - 67
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2TokenIntrospectionAuthenticationProvider.java

@@ -15,85 +15,83 @@
  */
 package org.springframework.security.oauth2.server.authorization.authentication;
 
-import static org.springframework.security.oauth2.core.OAuth2TokenIntrospectionClaimAccessor.ACTIVE;
-import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames.CLIENT_ID;
-import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames.SCOPE;
-import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames.TOKEN_TYPE;
-import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames.USERNAME;
-import static org.springframework.security.oauth2.jwt.JwtClaimNames.EXP;
-import static org.springframework.security.oauth2.jwt.JwtClaimNames.IAT;
-import static org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthenticationProviderUtils.getAuthenticatedClientElseThrowInvalidClient;
+import java.time.Instant;
+import java.util.List;
+import java.util.Map;
 
 import org.springframework.security.authentication.AuthenticationProvider;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.AuthenticationException;
 import org.springframework.security.oauth2.core.AbstractOAuth2Token;
 import org.springframework.security.oauth2.core.OAuth2AccessToken;
-import org.springframework.security.oauth2.jwt.JwtClaimNames;
+import org.springframework.security.oauth2.core.OAuth2TokenIntrospection;
+import org.springframework.security.oauth2.jwt.JwtClaimAccessor;
 import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
-import org.springframework.security.oauth2.server.authorization.OAuth2Authorization.Token;
 import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
 import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
+import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
 import org.springframework.util.Assert;
+import org.springframework.util.CollectionUtils;
+import org.springframework.util.StringUtils;
 
-import java.time.Instant;
-import java.util.Collection;
-import java.util.LinkedHashMap;
-import java.util.Map;
-import java.util.Optional;
+import static org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthenticationProviderUtils.getAuthenticatedClientElseThrowInvalidClient;
 
 /**
  * An {@link AuthenticationProvider} implementation for OAuth 2.0 Token Introspection.
  *
  * @author Gerardo Roza
+ * @author Joe Grandja
  * @since 0.1.1
  * @see OAuth2TokenIntrospectionAuthenticationToken
+ * @see RegisteredClientRepository
  * @see OAuth2AuthorizationService
- * @see <a target="_blank" href="https://tools.ietf.org/html/rfc7662#section-2.1">Section 2.1 - Introspection Request</a>
+ * @see <a target="_blank" href="https://tools.ietf.org/html/rfc7662#section-2.1">Section 2.1 Introspection Request</a>
  */
 public class OAuth2TokenIntrospectionAuthenticationProvider implements AuthenticationProvider {
+	private final RegisteredClientRepository registeredClientRepository;
 	private final OAuth2AuthorizationService authorizationService;
 
 	/**
 	 * Constructs an {@code OAuth2TokenIntrospectionAuthenticationProvider} using the provided parameters.
 	 *
+	 * @param registeredClientRepository the repository of registered clients
 	 * @param authorizationService the authorization service
 	 */
-	public OAuth2TokenIntrospectionAuthenticationProvider(OAuth2AuthorizationService authorizationService) {
+	public OAuth2TokenIntrospectionAuthenticationProvider(RegisteredClientRepository registeredClientRepository,
+			OAuth2AuthorizationService authorizationService) {
+		Assert.notNull(registeredClientRepository, "registeredClientRepository cannot be null");
 		Assert.notNull(authorizationService, "authorizationService cannot be null");
+		this.registeredClientRepository = registeredClientRepository;
 		this.authorizationService = authorizationService;
 	}
 
 	@Override
 	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
-		OAuth2TokenIntrospectionAuthenticationToken tokenIntrospectionAuthentication = (OAuth2TokenIntrospectionAuthenticationToken) authentication;
+		OAuth2TokenIntrospectionAuthenticationToken tokenIntrospectionAuthentication =
+				(OAuth2TokenIntrospectionAuthenticationToken) authentication;
 
-		OAuth2ClientAuthenticationToken clientPrincipal = getAuthenticatedClientElseThrowInvalidClient(
-				tokenIntrospectionAuthentication);
-		RegisteredClient registeredClient = clientPrincipal.getRegisteredClient();
+		OAuth2ClientAuthenticationToken clientPrincipal =
+				getAuthenticatedClientElseThrowInvalidClient(tokenIntrospectionAuthentication);
 
-		OAuth2Authorization authorization = this.authorizationService
-				.findByToken(tokenIntrospectionAuthentication.getTokenValue(), null);
+		OAuth2Authorization authorization = this.authorizationService.findByToken(
+				tokenIntrospectionAuthentication.getToken(), null);
 		if (authorization == null) {
-			return generateAuthenticationTokenForInvalidToken(clientPrincipal, registeredClient);
+			// Return the authentication request when token not found
+			return tokenIntrospectionAuthentication;
 		}
 
-		Token<AbstractOAuth2Token> tokenHolder = authorization
-				.getToken(tokenIntrospectionAuthentication.getTokenValue());
-
-		if (tokenHolder.isInvalidated()) {
-			return generateAuthenticationTokenForInvalidToken(clientPrincipal, registeredClient);
+		OAuth2Authorization.Token<AbstractOAuth2Token> authorizedToken =
+				authorization.getToken(tokenIntrospectionAuthentication.getToken());
+		if (!authorizedToken.isActive()) {
+			return new OAuth2TokenIntrospectionAuthenticationToken(tokenIntrospectionAuthentication.getToken(),
+					clientPrincipal, OAuth2TokenIntrospection.builder().build());
 		}
 
-		if (isExpired(tokenHolder.getToken())
-				|| (tokenHolder.getClaims() != null && hasInvalidClaims(tokenHolder.getClaims()))) {
-			return generateAuthenticationTokenForInvalidToken(clientPrincipal, registeredClient);
-		}
+		RegisteredClient authorizedClient = this.registeredClientRepository.findById(authorization.getRegisteredClientId());
+		OAuth2TokenIntrospection tokenClaims = withActiveTokenClaims(authorizedToken, authorizedClient);
 
-		Map<String, Object> claims = generateTokenIntrospectionClaims(
-				tokenHolder, registeredClient.getClientId(), authorization.getPrincipalName());
-
-		return new OAuth2TokenIntrospectionAuthenticationToken(clientPrincipal, claims);
+		return new OAuth2TokenIntrospectionAuthenticationToken(authorizedToken.getToken().getTokenValue(),
+				clientPrincipal, tokenClaims);
 	}
 
 	@Override
@@ -101,42 +99,49 @@ public class OAuth2TokenIntrospectionAuthenticationProvider implements Authentic
 		return OAuth2TokenIntrospectionAuthenticationToken.class.isAssignableFrom(authentication);
 	}
 
-	private boolean isExpired(AbstractOAuth2Token token) {
-		Instant expiry = token.getExpiresAt();
-		return (expiry != null && Instant.now().isAfter(expiry));
-	}
+	private static OAuth2TokenIntrospection withActiveTokenClaims(
+			OAuth2Authorization.Token<AbstractOAuth2Token> authorizedToken, RegisteredClient authorizedClient) {
+
+		OAuth2TokenIntrospection.Builder tokenClaims = OAuth2TokenIntrospection.builder(true)
+				.clientId(authorizedClient.getClientId());
+
+		// TODO Set "username"
 
-	private boolean hasInvalidClaims(Map<String, Object> claims) {
-		Object notBeforeValue = claims.get(JwtClaimNames.NBF);
-		if (notBeforeValue != null && Instant.class.isAssignableFrom(notBeforeValue.getClass())) {
-			Instant notBefore = (Instant) notBeforeValue;
-			return Instant.now().isBefore(notBefore);
+		AbstractOAuth2Token token = authorizedToken.getToken();
+		if (token.getIssuedAt() != null) {
+			tokenClaims.issuedAt(token.getIssuedAt());
+		}
+		if (token.getExpiresAt() != null) {
+			tokenClaims.expiresAt(token.getExpiresAt());
 		}
-		return false;
-	}
 
-	private Map<String, Object> generateTokenIntrospectionClaims(Token<? extends AbstractOAuth2Token> tokenHolder,
-			String clientId, String username) {
-		Map<String, Object> claims = Optional.ofNullable(tokenHolder.getClaims()).map(LinkedHashMap::new)
-				.orElse(new LinkedHashMap<>());
-		AbstractOAuth2Token token = tokenHolder.getToken();
-		claims.put(ACTIVE, true);
-		claims.put(CLIENT_ID, clientId);
-		claims.put(USERNAME, username);
-		Optional.ofNullable(token.getIssuedAt()).ifPresent(iat -> claims.put(IAT, iat));
-		Optional.ofNullable(token.getExpiresAt()).ifPresent(exp -> claims.put(EXP, exp));
 		if (OAuth2AccessToken.class.isAssignableFrom(token.getClass())) {
-			Collection<String> scopes = ((OAuth2AccessToken) token).getScopes();
-			if (!scopes.isEmpty()) {
-				claims.put(SCOPE, String.join(" ", scopes));
+			OAuth2AccessToken accessToken = (OAuth2AccessToken) token;
+			tokenClaims.scopes(scopes -> scopes.addAll(accessToken.getScopes()));
+			tokenClaims.tokenType(accessToken.getTokenType().getValue());
+
+			Map<String, Object> claims = authorizedToken.getClaims();
+			if (!CollectionUtils.isEmpty(claims)) {
+				// Assuming JWT as it's the only (currently) supported access token format
+				JwtClaimAccessor jwtClaims = () -> claims;
+
+				Instant notBefore = jwtClaims.getNotBefore();
+				if (notBefore != null) {
+					tokenClaims.notBefore(notBefore);
+				}
+				tokenClaims.subject(jwtClaims.getSubject());
+				List<String> audience = jwtClaims.getAudience();
+				if (!CollectionUtils.isEmpty(audience)) {
+					tokenClaims.audiences(audiences -> audiences.addAll(audience));
+				}
+				tokenClaims.issuer(jwtClaims.getIssuer().toExternalForm());
+				String jti = jwtClaims.getId();
+				if (StringUtils.hasText(jti)) {
+					tokenClaims.id(jti);
+				}
 			}
-			claims.put(TOKEN_TYPE, OAuth2AccessToken.TokenType.BEARER);
 		}
-		return claims;
-	}
 
-	private OAuth2TokenIntrospectionAuthenticationToken generateAuthenticationTokenForInvalidToken(
-			Authentication clientPrincipal, RegisteredClient registeredClient) {
-		return new OAuth2TokenIntrospectionAuthenticationToken(clientPrincipal, null);
+		return tokenClaims.build();
 	}
 }

+ 42 - 33
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2TokenIntrospectionAuthenticationToken.java

@@ -15,66 +15,76 @@
  */
 package org.springframework.security.oauth2.server.authorization.authentication;
 
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
 import org.springframework.lang.Nullable;
 import org.springframework.security.authentication.AbstractAuthenticationToken;
 import org.springframework.security.core.Authentication;
+import org.springframework.security.oauth2.core.OAuth2TokenIntrospection;
 import org.springframework.security.oauth2.core.Version;
 import org.springframework.util.Assert;
 
-import java.util.Collections;
-import java.util.Map;
-
 /**
  * An {@link Authentication} implementation used for OAuth 2.0 Token Introspection.
  *
  * @author Gerardo Roza
+ * @author Joe Grandja
  * @since 0.1.1
  * @see AbstractAuthenticationToken
+ * @see OAuth2TokenIntrospection
  * @see OAuth2TokenIntrospectionAuthenticationProvider
  */
 public class OAuth2TokenIntrospectionAuthenticationToken extends AbstractAuthenticationToken {
 	private static final long serialVersionUID = Version.SERIAL_VERSION_UID;
-	private final String tokenValue;
+	private final String token;
 	private final Authentication clientPrincipal;
 	private final String tokenTypeHint;
-	private Map<String, Object> claims;
+	private final Map<String, Object> additionalParameters;
+	private final OAuth2TokenIntrospection tokenClaims;
 
 	/**
 	 * Constructs an {@code OAuth2TokenIntrospectionAuthenticationToken} using the provided parameters.
 	 *
-	 * @param tokenValue the token
+	 * @param token the token
 	 * @param clientPrincipal the authenticated client principal
 	 * @param tokenTypeHint the token type hint
+	 * @param additionalParameters the additional parameters
 	 */
-	public OAuth2TokenIntrospectionAuthenticationToken(String tokenValue, Authentication clientPrincipal,
-			@Nullable String tokenTypeHint) {
+	public OAuth2TokenIntrospectionAuthenticationToken(String token, Authentication clientPrincipal,
+			@Nullable String tokenTypeHint, @Nullable Map<String, Object> additionalParameters) {
 		super(Collections.emptyList());
-		Assert.hasText(tokenValue, "token cannot be empty");
+		Assert.hasText(token, "token cannot be empty");
 		Assert.notNull(clientPrincipal, "clientPrincipal cannot be null");
-		this.tokenValue = tokenValue;
+		this.token = token;
 		this.clientPrincipal = clientPrincipal;
 		this.tokenTypeHint = tokenTypeHint;
-		this.claims = null;
+		this.additionalParameters = Collections.unmodifiableMap(
+				additionalParameters != null ? new HashMap<>(additionalParameters) : Collections.emptyMap());
+		this.tokenClaims = OAuth2TokenIntrospection.builder().build();
 	}
 
 	/**
 	 * Constructs an {@code OAuth2TokenIntrospectionAuthenticationToken} using the provided parameters.
 	 *
-	 * The {@code claims} should be provided only if the token is active.
-	 *
-	 * @param claims the claims obtained from the introspected active token
+	 * @param token the token
 	 * @param clientPrincipal the authenticated client principal
+	 * @param tokenClaims the token claims
 	 */
-	public OAuth2TokenIntrospectionAuthenticationToken(Authentication clientPrincipal,
-			@Nullable Map<String, Object> claims) {
+	public OAuth2TokenIntrospectionAuthenticationToken(String token, Authentication clientPrincipal,
+			OAuth2TokenIntrospection tokenClaims) {
 		super(Collections.emptyList());
+		Assert.hasText(token, "token cannot be empty");
 		Assert.notNull(clientPrincipal, "clientPrincipal cannot be null");
-		this.claims = claims;
+		Assert.notNull(tokenClaims, "tokenClaims cannot be null");
+		this.token = token;
 		this.clientPrincipal = clientPrincipal;
 		this.tokenTypeHint = null;
-		this.tokenValue = null;
-		setAuthenticated(true); // Indicates that the request was authenticated, even though the introspected token might not be
-								// active
+		this.additionalParameters = Collections.emptyMap();
+		this.tokenClaims = tokenClaims;
+		// Indicates that the request was authenticated, even though the token might not be active
+		setAuthenticated(true);
 	}
 
 	@Override
@@ -88,12 +98,12 @@ public class OAuth2TokenIntrospectionAuthenticationToken extends AbstractAuthent
 	}
 
 	/**
-	 * Returns the token value.
+	 * Returns the token.
 	 *
-	 * @return the token value
+	 * @return the token
 	 */
-	public String getTokenValue() {
-		return this.tokenValue;
+	public String getToken() {
+		return this.token;
 	}
 
 	/**
@@ -107,22 +117,21 @@ public class OAuth2TokenIntrospectionAuthenticationToken extends AbstractAuthent
 	}
 
 	/**
-	 * Returns the introspection claims.
+	 * Returns the additional parameters.
 	 *
-	 * @return the claims
+	 * @return the additional parameters
 	 */
-	public Map<String, Object> getClaims() {
-		return claims;
+	public Map<String, Object> getAdditionalParameters() {
+		return this.additionalParameters;
 	}
 
 	/**
-	 * Returns whether the introspected token is active, having in mind only active tokens' claims should be passed to the
-	 * constructor.
+	 * Returns the token claims.
 	 *
-	 * @return whether the introspected token is active or not
+	 * @return the {@link OAuth2TokenIntrospection}
 	 */
-	public boolean isTokenActive() {
-		return this.claims != null;
+	public OAuth2TokenIntrospection getTokenClaims() {
+		return this.tokenClaims;
 	}
 
 }

+ 42 - 32
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2TokenIntrospectionEndpointFilter.java

@@ -15,6 +15,10 @@
  */
 package org.springframework.security.oauth2.server.authorization.web;
 
+import java.io.IOException;
+import java.util.Map;
+import java.util.stream.Collectors;
+
 import javax.servlet.FilterChain;
 import javax.servlet.ServletException;
 import javax.servlet.http.HttpServletRequest;
@@ -31,10 +35,11 @@ import org.springframework.security.core.context.SecurityContextHolder;
 import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
 import org.springframework.security.oauth2.core.OAuth2Error;
 import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
-import org.springframework.security.oauth2.core.OAuth2TokenIntrospectionClaims;
+import org.springframework.security.oauth2.core.OAuth2TokenIntrospection;
 import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames2;
 import org.springframework.security.oauth2.core.http.converter.OAuth2ErrorHttpMessageConverter;
-import org.springframework.security.oauth2.core.http.converter.OAuth2TokenIntrospectionClaimsHttpMessageConverter;
+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.util.matcher.AntPathRequestMatcher;
 import org.springframework.security.web.util.matcher.RequestMatcher;
@@ -43,18 +48,17 @@ import org.springframework.util.MultiValueMap;
 import org.springframework.util.StringUtils;
 import org.springframework.web.filter.OncePerRequestFilter;
 
-import java.io.IOException;
-
 /**
  * A {@code Filter} for the OAuth 2.0 Token Introspection endpoint.
  *
  * @author Gerardo Roza
- * @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>
+ * @author Joe Grandja
+ * @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>
  * @since 0.1.1
  */
 public class OAuth2TokenIntrospectionEndpointFilter extends OncePerRequestFilter {
-
 	/**
 	 * The default endpoint {@code URI} for token introspection requests.
 	 */
@@ -62,9 +66,10 @@ public class OAuth2TokenIntrospectionEndpointFilter extends OncePerRequestFilter
 
 	private final AuthenticationManager authenticationManager;
 	private final RequestMatcher tokenIntrospectionEndpointMatcher;
-	private final Converter<HttpServletRequest, Authentication> tokenIntrospectionAuthenticationConverter = new DefaultTokenIntrospectionAuthenticationConverter();
-	private final HttpMessageConverter<OAuth2TokenIntrospectionClaims> tokenIntrospectionHttpResponseConverter = new OAuth2TokenIntrospectionClaimsHttpMessageConverter();
-
+	private final Converter<HttpServletRequest, Authentication> tokenIntrospectionAuthenticationConverter =
+			new DefaultTokenIntrospectionAuthenticationConverter();
+	private final HttpMessageConverter<OAuth2TokenIntrospection> tokenIntrospectionHttpResponseConverter =
+			new OAuth2TokenIntrospectionHttpMessageConverter();
 	private final HttpMessageConverter<OAuth2Error> errorHttpResponseConverter = new OAuth2ErrorHttpMessageConverter();
 
 	/**
@@ -101,40 +106,34 @@ public class OAuth2TokenIntrospectionEndpointFilter extends OncePerRequestFilter
 		}
 
 		try {
+			OAuth2TokenIntrospectionAuthenticationToken tokenIntrospectionAuthentication =
+					(OAuth2TokenIntrospectionAuthenticationToken) this.tokenIntrospectionAuthenticationConverter.convert(request);
 
-			Authentication authentication = this.tokenIntrospectionAuthenticationConverter.convert(request);
-
-			OAuth2TokenIntrospectionAuthenticationToken tokenIntrospectionAuthentication = (OAuth2TokenIntrospectionAuthenticationToken) this.authenticationManager
-					.authenticate(authentication);
+			OAuth2TokenIntrospectionAuthenticationToken tokenIntrospectionAuthenticationResult =
+					(OAuth2TokenIntrospectionAuthenticationToken) this.authenticationManager.authenticate(tokenIntrospectionAuthentication);
 
-			OAuth2TokenIntrospectionClaims tokenIntrospectionResponse = tokenIntrospectionAuthentication
-					.isTokenActive()
-							? OAuth2TokenIntrospectionClaims.withClaims(tokenIntrospectionAuthentication.getClaims())
-									.build()
-							: OAuth2TokenIntrospectionClaims.builder(false).build();
+			OAuth2TokenIntrospection tokenClaims = tokenIntrospectionAuthenticationResult.getTokenClaims();
+			sendTokenIntrospectionResponse(response, tokenClaims);
 
-			sendTokenIntrospectionResponse(response, tokenIntrospectionResponse);
 		} catch (OAuth2AuthenticationException ex) {
 			SecurityContextHolder.clearContext();
 			sendErrorResponse(response, ex.getError());
 		}
 	}
 
-	private void sendErrorResponse(HttpServletResponse response, OAuth2Error error) throws IOException {
+	private void sendTokenIntrospectionResponse(HttpServletResponse response, OAuth2TokenIntrospection tokenClaims) throws IOException {
 		ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
-		httpResponse.setStatusCode(HttpStatus.BAD_REQUEST);
-		this.errorHttpResponseConverter.write(error, null, httpResponse);
+		this.tokenIntrospectionHttpResponseConverter.write(tokenClaims, null, httpResponse);
 	}
 
-	private void sendTokenIntrospectionResponse(HttpServletResponse response,
-			OAuth2TokenIntrospectionClaims tokenIntrospectionResponse) throws IOException {
+	private void sendErrorResponse(HttpServletResponse response, OAuth2Error error) throws IOException {
 		ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
-		this.tokenIntrospectionHttpResponseConverter.write(tokenIntrospectionResponse, null, httpResponse);
+		httpResponse.setStatusCode(HttpStatus.BAD_REQUEST);
+		this.errorHttpResponseConverter.write(error, null, httpResponse);
 	}
 
 	private static void throwError(String errorCode, String parameterName) {
-		OAuth2Error error = new OAuth2Error(
-				errorCode, "OAuth 2.0 Token Introspection Parameter: " + parameterName,
+		OAuth2Error error = new OAuth2Error(errorCode, "OAuth 2.0 Token Introspection Parameter: " + parameterName,
 				"https://tools.ietf.org/html/rfc7662#section-2.1");
 		throw new OAuth2AuthenticationException(error);
 	}
@@ -150,18 +149,29 @@ public class OAuth2TokenIntrospectionEndpointFilter extends OncePerRequestFilter
 
 			// token (REQUIRED)
 			String token = parameters.getFirst(OAuth2ParameterNames2.TOKEN);
-			if (!StringUtils.hasText(token) || parameters.get(OAuth2ParameterNames2.TOKEN).size() != 1) {
+			if (!StringUtils.hasText(token) ||
+					parameters.get(OAuth2ParameterNames2.TOKEN).size() != 1) {
 				throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames2.TOKEN);
 			}
 
 			// token_type_hint (OPTIONAL)
 			String tokenTypeHint = parameters.getFirst(OAuth2ParameterNames2.TOKEN_TYPE_HINT);
-			if (StringUtils.hasText(tokenTypeHint)
-					&& parameters.get(OAuth2ParameterNames2.TOKEN_TYPE_HINT).size() != 1) {
+			if (StringUtils.hasText(tokenTypeHint) &&
+					parameters.get(OAuth2ParameterNames2.TOKEN_TYPE_HINT).size() != 1) {
 				throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames2.TOKEN_TYPE_HINT);
 			}
 
-			return new OAuth2TokenIntrospectionAuthenticationToken(token, clientPrincipal, tokenTypeHint);
+			// @formatter:off
+			Map<String, Object> additionalParameters = parameters
+					.entrySet()
+					.stream()
+					.filter(e -> !e.getKey().equals(OAuth2ParameterNames2.TOKEN) &&
+							!e.getKey().equals(OAuth2ParameterNames2.TOKEN_TYPE_HINT))
+					.collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().get(0)));
+			// @formatter:on
+
+			return new OAuth2TokenIntrospectionAuthenticationToken(
+					token, clientPrincipal, tokenTypeHint, additionalParameters);
 		}
 	}
 }

+ 116 - 99
oauth2-authorization-server/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2TokenIntrospectionTests.java

@@ -15,62 +15,75 @@
  */
 package org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization;
 
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.ArgumentMatchers.isNull;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.reset;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic;
-import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
-import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Arrays;
+import java.util.HashSet;
 
+import com.nimbusds.jose.jwk.JWKSet;
+import com.nimbusds.jose.jwk.source.JWKSource;
+import com.nimbusds.jose.proc.SecurityContext;
 import org.junit.Before;
 import org.junit.BeforeClass;
 import org.junit.Rule;
 import org.junit.Test;
+
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Import;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.converter.HttpMessageConverter;
+import org.springframework.mock.http.client.MockClientHttpResponse;
+import org.springframework.mock.web.MockHttpServletResponse;
 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.oauth2.core.AbstractOAuth2Token;
 import org.springframework.security.oauth2.core.OAuth2AccessToken;
 import org.springframework.security.oauth2.core.OAuth2RefreshToken;
+import org.springframework.security.oauth2.core.OAuth2TokenIntrospection;
 import org.springframework.security.oauth2.core.OAuth2TokenType;
 import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames2;
+import org.springframework.security.oauth2.core.http.converter.OAuth2TokenIntrospectionHttpMessageConverter;
 import org.springframework.security.oauth2.jose.TestJwks;
+import org.springframework.security.oauth2.jwt.JwtClaimsSet;
+import org.springframework.security.oauth2.jwt.TestJwtClaimsSets;
 import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
 import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
 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.RegisteredClientRepository;
 import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients;
-import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenIntrospectionEndpointFilter;
+import org.springframework.security.oauth2.server.authorization.config.ProviderSettings;
 import org.springframework.test.web.servlet.MockMvc;
-import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
+import org.springframework.test.web.servlet.MvcResult;
 import org.springframework.util.LinkedMultiValueMap;
 import org.springframework.util.MultiValueMap;
 
-import com.nimbusds.jose.jwk.JWKSet;
-import com.nimbusds.jose.jwk.source.JWKSource;
-import com.nimbusds.jose.proc.SecurityContext;
-
-import java.time.Duration;
-import java.time.Instant;
-import java.util.Arrays;
-import java.util.HashSet;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.isNull;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
 
 /**
  * Integration tests for the OAuth 2.0 Token Introspection endpoint.
  *
  * @author Gerardo Roza
+ * @author Joe Grandja
  */
 public class OAuth2TokenIntrospectionTests {
 	private static RegisteredClientRepository registeredClientRepository;
 	private static OAuth2AuthorizationService authorizationService;
 	private static JWKSource<SecurityContext> jwkSource;
+	private static ProviderSettings providerSettings;
+	private final HttpMessageConverter<OAuth2TokenIntrospection> tokenIntrospectionHttpResponseConverter =
+			new OAuth2TokenIntrospectionHttpMessageConverter();
 
 	@Rule
 	public final SpringTestRule spring = new SpringTestRule();
@@ -84,6 +97,7 @@ public class OAuth2TokenIntrospectionTests {
 		authorizationService = mock(OAuth2AuthorizationService.class);
 		JWKSet jwkSet = new JWKSet(TestJwks.DEFAULT_RSA_JWK);
 		jwkSource = (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
+		providerSettings = new ProviderSettings().tokenIntrospectionEndpoint("/test/introspect");
 	}
 
 	@Before
@@ -93,110 +107,101 @@ public class OAuth2TokenIntrospectionTests {
 	}
 
 	@Test
-	public void requestWhenIntrospectValidRefreshTokenThenActiveResponse() throws Exception {
+	public void requestWhenIntrospectValidAccessTokenThenActive() throws Exception {
 		this.spring.register(AuthorizationServerConfiguration.class).autowire();
 
-		RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
-		when(registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
-				.thenReturn(registeredClient);
-
-		OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient).build();
-		OAuth2RefreshToken token = authorization.getRefreshToken().getToken();
-		OAuth2TokenType tokenType = OAuth2TokenType.REFRESH_TOKEN;
-		when(authorizationService.findByToken(eq(token.getTokenValue()), isNull())).thenReturn(authorization);
-
-		// @formatter:off
-		this.mvc.perform(
-				MockMvcRequestBuilders.post(OAuth2TokenIntrospectionEndpointFilter.DEFAULT_TOKEN_INTROSPECTION_ENDPOINT_URI)
-						.params(getTokenIntrospectionRequestParameters(token, tokenType))
-						.with(httpBasic(registeredClient.getClientId(), registeredClient.getClientSecret())))
-				.andExpect(status().isOk())
-				.andExpect(jsonPath("$.active").value(true))
-				.andExpect(jsonPath("$.client_id").value("client-1"))
-				.andExpect(jsonPath("$.iat").isNotEmpty())
-				.andExpect(jsonPath("$.exp").isNotEmpty())
-				.andExpect(jsonPath("$.username").value("principal"));
-		// @formatter:on
-
-		verify(registeredClientRepository).findByClientId(eq(registeredClient.getClientId()));
-		verify(authorizationService).findByToken(eq(token.getTokenValue()), isNull());
-	}
-
-	@Test
-	public void requestWhenIntrospectValidAccessTokenThenActiveResponse() throws Exception {
-		this.spring.register(AuthorizationServerConfiguration.class).autowire();
-
-		RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
-		when(registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
-				.thenReturn(registeredClient);
+		RegisteredClient introspectRegisteredClient = TestRegisteredClients.registeredClient2().build();
+		when(registeredClientRepository.findByClientId(eq(introspectRegisteredClient.getClientId())))
+				.thenReturn(introspectRegisteredClient);
 
+		RegisteredClient authorizedRegisteredClient = TestRegisteredClients.registeredClient().build();
 		Instant issuedAt = Instant.now();
 		Instant expiresAt = issuedAt.plus(Duration.ofHours(1));
 		OAuth2AccessToken accessToken = new OAuth2AccessToken(
-				OAuth2AccessToken.TokenType.BEARER, "token", issuedAt, expiresAt,
-				new HashSet<>(Arrays.asList("scope1", "Scope2")));
-		OAuth2TokenType tokenType = OAuth2TokenType.ACCESS_TOKEN;
-		OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient).token(accessToken)
+				OAuth2AccessToken.TokenType.BEARER, "access-token", issuedAt, expiresAt,
+				new HashSet<>(Arrays.asList("scope1", "scope2")));
+		JwtClaimsSet tokenClaims = TestJwtClaimsSets.jwtClaimsSet().build();
+		OAuth2Authorization authorization = TestOAuth2Authorizations
+				.authorization(authorizedRegisteredClient, accessToken, tokenClaims.getClaims())
 				.build();
-
-		when(authorizationService.findByToken(eq(accessToken.getTokenValue()), isNull())).thenReturn(authorization);
+		when(authorizationService.findByToken(eq(accessToken.getTokenValue()), isNull()))
+				.thenReturn(authorization);
+		when(registeredClientRepository.findById(eq(authorizedRegisteredClient.getId())))
+				.thenReturn(authorizedRegisteredClient);
 
 		// @formatter:off
-		this.mvc.perform(
-				MockMvcRequestBuilders.post(OAuth2TokenIntrospectionEndpointFilter.DEFAULT_TOKEN_INTROSPECTION_ENDPOINT_URI)
-						.params(getTokenIntrospectionRequestParameters(accessToken, tokenType))
-						.with(httpBasic(registeredClient.getClientId(), registeredClient.getClientSecret())))
+		MvcResult mvcResult = this.mvc.perform(post(providerSettings.tokenIntrospectionEndpoint())
+				.params(getTokenIntrospectionRequestParameters(accessToken, OAuth2TokenType.ACCESS_TOKEN))
+				.with(httpBasic(introspectRegisteredClient.getClientId(), introspectRegisteredClient.getClientSecret())))
 				.andExpect(status().isOk())
-				.andExpect(jsonPath("$.active").value(true))
-				.andExpect(jsonPath("$.client_id").value("client-1"))
-				.andExpect(jsonPath("$.scope").isNotEmpty())
-				.andExpect(jsonPath("$.token_type").value(OAuth2AccessToken.TokenType.BEARER.getValue()))
-				.andExpect(jsonPath("$.iat").isNotEmpty())
-				.andExpect(jsonPath("$.exp").isNotEmpty())
-				.andExpect(jsonPath("$.username").value("principal"));
+				.andReturn();
 		// @formatter:on
 
-		verify(registeredClientRepository).findByClientId(eq(registeredClient.getClientId()));
+		verify(registeredClientRepository).findByClientId(eq(introspectRegisteredClient.getClientId()));
 		verify(authorizationService).findByToken(eq(accessToken.getTokenValue()), isNull());
+		verify(registeredClientRepository).findById(eq(authorizedRegisteredClient.getId()));
+
+		OAuth2TokenIntrospection tokenIntrospectionResponse = readTokenIntrospectionResponse(mvcResult);
+		assertThat(tokenIntrospectionResponse.isActive()).isTrue();
+		assertThat(tokenIntrospectionResponse.getClientId()).isEqualTo(authorizedRegisteredClient.getClientId());
+		assertThat(tokenIntrospectionResponse.getUsername()).isNull();
+		assertThat(tokenIntrospectionResponse.getIssuedAt()).isBetween(
+				accessToken.getIssuedAt().minusSeconds(1), accessToken.getIssuedAt().plusSeconds(1));
+		assertThat(tokenIntrospectionResponse.getExpiresAt()).isBetween(
+				accessToken.getExpiresAt().minusSeconds(1), accessToken.getExpiresAt().plusSeconds(1));
+		assertThat(tokenIntrospectionResponse.getScope()).containsExactlyInAnyOrderElementsOf(accessToken.getScopes());
+		assertThat(tokenIntrospectionResponse.getTokenType()).isEqualTo(accessToken.getTokenType().getValue());
+		assertThat(tokenIntrospectionResponse.getNotBefore()).isBetween(
+				tokenClaims.getNotBefore().minusSeconds(1), tokenClaims.getNotBefore().plusSeconds(1));
+		assertThat(tokenIntrospectionResponse.getSubject()).isEqualTo(tokenClaims.getSubject());
+		assertThat(tokenIntrospectionResponse.getAudience()).containsExactlyInAnyOrderElementsOf(tokenClaims.getAudience());
+		assertThat(tokenIntrospectionResponse.getIssuer()).isEqualTo(tokenClaims.getIssuer());
+		assertThat(tokenIntrospectionResponse.getId()).isEqualTo(tokenClaims.getId());
 	}
 
 	@Test
-	public void requestWhenIntrospectTokenIssuedToDifferentClientThenActiveResponse() throws Exception {
+	public void requestWhenIntrospectValidRefreshTokenThenActive() throws Exception {
 		this.spring.register(AuthorizationServerConfiguration.class).autowire();
 
-		RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
-		when(registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
-				.thenReturn(registeredClient);
-
-		RegisteredClient registeredClient2 = TestRegisteredClients.registeredClient2().build();
-		Instant issuedAt = Instant.now();
-		Instant expiresAt = issuedAt.plus(Duration.ofHours(1));
-		OAuth2AccessToken accessToken = new OAuth2AccessToken(
-				OAuth2AccessToken.TokenType.BEARER, "token", issuedAt, expiresAt,
-				new HashSet<>(Arrays.asList("scope1", "Scope2")));
-		OAuth2TokenType tokenType = OAuth2TokenType.ACCESS_TOKEN;
-		OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient2).token(accessToken)
-				.build();
+		RegisteredClient introspectRegisteredClient = TestRegisteredClients.registeredClient2().build();
+		when(registeredClientRepository.findByClientId(eq(introspectRegisteredClient.getClientId())))
+				.thenReturn(introspectRegisteredClient);
 
-		when(authorizationService.findByToken(eq(accessToken.getTokenValue()), isNull())).thenReturn(authorization);
+		RegisteredClient authorizedRegisteredClient = TestRegisteredClients.registeredClient().build();
+		OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(authorizedRegisteredClient).build();
+		OAuth2RefreshToken refreshToken = authorization.getRefreshToken().getToken();
+		when(authorizationService.findByToken(eq(refreshToken.getTokenValue()), isNull()))
+				.thenReturn(authorization);
+		when(registeredClientRepository.findById(eq(authorizedRegisteredClient.getId())))
+				.thenReturn(authorizedRegisteredClient);
 
 		// @formatter:off
-		this.mvc.perform(
-				MockMvcRequestBuilders.post(OAuth2TokenIntrospectionEndpointFilter.DEFAULT_TOKEN_INTROSPECTION_ENDPOINT_URI)
-						.params(getTokenIntrospectionRequestParameters(accessToken, tokenType))
-						.with(httpBasic(registeredClient.getClientId(), registeredClient.getClientSecret())))
+		MvcResult mvcResult = this.mvc.perform(post(providerSettings.tokenIntrospectionEndpoint())
+				.params(getTokenIntrospectionRequestParameters(refreshToken, OAuth2TokenType.REFRESH_TOKEN))
+				.with(httpBasic(introspectRegisteredClient.getClientId(), introspectRegisteredClient.getClientSecret())))
 				.andExpect(status().isOk())
-				.andExpect(jsonPath("$.active").value(true))
-				.andExpect(jsonPath("$.client_id").value("client-1"))
-				.andExpect(jsonPath("$.scope").isNotEmpty())
-				.andExpect(jsonPath("$.token_type").value(OAuth2AccessToken.TokenType.BEARER.getValue()))
-				.andExpect(jsonPath("$.iat").isNotEmpty())
-				.andExpect(jsonPath("$.exp").isNotEmpty())
-				.andExpect(jsonPath("$.username").value("principal"));
+				.andReturn();
 		// @formatter:on
 
-		verify(registeredClientRepository).findByClientId(eq(registeredClient.getClientId()));
-		verify(authorizationService).findByToken(eq(accessToken.getTokenValue()), isNull());
+		verify(registeredClientRepository).findByClientId(eq(introspectRegisteredClient.getClientId()));
+		verify(authorizationService).findByToken(eq(refreshToken.getTokenValue()), isNull());
+		verify(registeredClientRepository).findById(eq(authorizedRegisteredClient.getId()));
+
+		OAuth2TokenIntrospection tokenIntrospectionResponse = readTokenIntrospectionResponse(mvcResult);
+		assertThat(tokenIntrospectionResponse.isActive()).isTrue();
+		assertThat(tokenIntrospectionResponse.getClientId()).isEqualTo(authorizedRegisteredClient.getClientId());
+		assertThat(tokenIntrospectionResponse.getUsername()).isNull();
+		assertThat(tokenIntrospectionResponse.getIssuedAt()).isBetween(
+				refreshToken.getIssuedAt().minusSeconds(1), refreshToken.getIssuedAt().plusSeconds(1));
+		assertThat(tokenIntrospectionResponse.getExpiresAt()).isBetween(
+				refreshToken.getExpiresAt().minusSeconds(1), refreshToken.getExpiresAt().plusSeconds(1));
+		assertThat(tokenIntrospectionResponse.getScope()).isNull();
+		assertThat(tokenIntrospectionResponse.getTokenType()).isNull();
+		assertThat(tokenIntrospectionResponse.getNotBefore()).isNull();
+		assertThat(tokenIntrospectionResponse.getSubject()).isNull();
+		assertThat(tokenIntrospectionResponse.getAudience()).isNull();
+		assertThat(tokenIntrospectionResponse.getIssuer()).isNull();
+		assertThat(tokenIntrospectionResponse.getId()).isNull();
 	}
 
 	private static MultiValueMap<String, String> getTokenIntrospectionRequestParameters(AbstractOAuth2Token token,
@@ -207,6 +212,13 @@ public class OAuth2TokenIntrospectionTests {
 		return parameters;
 	}
 
+	private OAuth2TokenIntrospection readTokenIntrospectionResponse(MvcResult mvcResult) throws Exception {
+		MockHttpServletResponse servletResponse = mvcResult.getResponse();
+		MockClientHttpResponse httpResponse = new MockClientHttpResponse(
+				servletResponse.getContentAsByteArray(), HttpStatus.valueOf(servletResponse.getStatus()));
+		return this.tokenIntrospectionHttpResponseConverter.read(OAuth2TokenIntrospection.class, httpResponse);
+	}
+
 	@EnableWebSecurity
 	@Import(OAuth2AuthorizationServerConfiguration.class)
 	static class AuthorizationServerConfiguration {
@@ -225,5 +237,10 @@ public class OAuth2TokenIntrospectionTests {
 		JWKSource<SecurityContext> jwkSource() {
 			return jwkSource;
 		}
+
+		@Bean
+		ProviderSettings providerSettings() {
+			return providerSettings;
+		}
 	}
 }

+ 0 - 193
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/core/http/converter/OAuth2TokenIntrospectionClaimsHttpMessageConverterTests.java

@@ -1,193 +0,0 @@
-/*
- * 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.core.http.converter;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
-import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
-import static org.assertj.core.api.Assertions.assertThatThrownBy;
-import static org.springframework.security.oauth2.core.OAuth2TokenIntrospectionClaimAccessor.ACTIVE;
-import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames.CLIENT_ID;
-import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames.SCOPE;
-import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames.TOKEN_TYPE;
-import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames.USERNAME;
-import static org.springframework.security.oauth2.jwt.JwtClaimNames.AUD;
-import static org.springframework.security.oauth2.jwt.JwtClaimNames.EXP;
-import static org.springframework.security.oauth2.jwt.JwtClaimNames.IAT;
-import static org.springframework.security.oauth2.jwt.JwtClaimNames.ISS;
-import static org.springframework.security.oauth2.jwt.JwtClaimNames.JTI;
-import static org.springframework.security.oauth2.jwt.JwtClaimNames.NBF;
-import static org.springframework.security.oauth2.jwt.JwtClaimNames.SUB;
-
-import org.assertj.core.api.Condition;
-import org.junit.Test;
-import org.springframework.core.convert.converter.Converter;
-import org.springframework.http.HttpStatus;
-import org.springframework.http.converter.HttpMessageNotReadableException;
-import org.springframework.http.converter.HttpMessageNotWritableException;
-import org.springframework.mock.http.MockHttpOutputMessage;
-import org.springframework.mock.http.client.MockClientHttpResponse;
-import org.springframework.security.oauth2.core.OAuth2AccessToken.TokenType;
-import org.springframework.security.oauth2.core.http.converter.OAuth2TokenIntrospectionClaimsHttpMessageConverter;
-import org.springframework.security.oauth2.core.OAuth2TokenIntrospectionClaims;
-
-import java.time.Instant;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Map;
-
-/**
- * Tests for {@link OAuth2TokenIntrospectionClaimsHttpMessageConverter}
- *
- * @author Gerardo Roza
- */
-public class OAuth2TokenIntrospectionClaimsHttpMessageConverterTests {
-	private final OAuth2TokenIntrospectionClaimsHttpMessageConverter messageConverter = new OAuth2TokenIntrospectionClaimsHttpMessageConverter();
-
-	@Test
-	public void supportsWhenOidcProviderConfigurationThenTrue() {
-		assertThat(this.messageConverter.supports(OAuth2TokenIntrospectionClaims.class)).isTrue();
-	}
-
-	@Test
-	public void setProviderConfigurationParametersConverterWhenNullThenThrowIllegalArgumentException() {
-		assertThatIllegalArgumentException()
-				.isThrownBy(() -> this.messageConverter.setTokenIntrospectionResponseParametersConverter(null));
-	}
-
-	@Test
-	public void setProviderConfigurationConverterWhenNullThenThrowIllegalArgumentException() {
-		assertThatIllegalArgumentException()
-				.isThrownBy(() -> this.messageConverter.setTokenIntrospectionResponseConverter(null));
-	}
-
-	@SuppressWarnings("unchecked")
-	@Test
-	public void readInternalWhenValidParametersThenSuccess() throws Exception {
-		// @formatter:off
-		String tokenIntrospectionResponseBody = "{\n"
-				+ "		\"active\": true,\n"
-				+ "		\"iss\": \"https://example.com/issuer1\",\n"
-				+ "		\"scope\": \"scope1 Scope2\",\n"
-				+ "		\"client_id\": \"clientId1\",\n"
-				+ "		\"token_type\": \"Bearer\",\n"
-				+ "		\"username\": \"username1\",\n"
-				+ "		\"aud\": [\"audience1\", \"audience2\"],\n"
-				+ "		\"exp\": 1607637467,\n"
-				+ "		\"iat\": 1607633867,\n"
-				+ "		\"nbf\": 1607633867,\n"
-				+ "		\"sub\": \"subject1\",\n"
-				+ "		\"jti\": \"jwtId1\"\n"
-				+ "}\n";
-		// @formatter:on
-		MockClientHttpResponse response = new MockClientHttpResponse(
-				tokenIntrospectionResponseBody.getBytes(), HttpStatus.OK);
-		OAuth2TokenIntrospectionClaims tokenIntrospectionResponse = this.messageConverter
-				.readInternal(OAuth2TokenIntrospectionClaims.class, response);
-		Map<String, Object> responseParameters = tokenIntrospectionResponse.getClaims();
-		Condition<Object> collectionContainsCondition = new Condition<>(
-				collection -> Collection.class.isAssignableFrom(collection.getClass())
-						&& ((Collection<String>) collection).contains("audience1")
-						&& ((Collection<String>) collection).contains("audience2"),
-				"collection contains entries");
-
-		// @formatter:off
-		assertThat(responseParameters)
-			.containsEntry(ACTIVE, true)
-			.containsEntry(SCOPE, "scope1 Scope2")
-			.containsEntry(CLIENT_ID, "clientId1")
-			.containsEntry(TOKEN_TYPE, "Bearer")
-			.containsEntry(USERNAME, "username1")
-			.hasEntrySatisfying(AUD, collectionContainsCondition)
-			.containsEntry(EXP, 1607637467L)
-			.containsEntry(IAT, 1607633867L)
-			.containsEntry(NBF, 1607633867L)
-			.containsEntry(ISS, "https://example.com/issuer1")
-			.containsEntry(JTI, "jwtId1")
-			.containsEntry(SUB, "subject1");
-		// @formatter:on
-	}
-
-	@Test
-	public void readInternalWhenFailingConverterThenThrowException() {
-		String errorMessage = "this is not a valid converter";
-		this.messageConverter.setTokenIntrospectionResponseConverter(source -> {
-			throw new RuntimeException(errorMessage);
-		});
-		MockClientHttpResponse response = new MockClientHttpResponse("{}".getBytes(), HttpStatus.OK);
-
-		assertThatExceptionOfType(HttpMessageNotReadableException.class)
-				.isThrownBy(() -> this.messageConverter.readInternal(OAuth2TokenIntrospectionClaims.class, response))
-				.withMessageContaining("An error occurred reading the Token Introspection Response")
-				.withMessageContaining(errorMessage);
-	}
-
-	@Test
-	public void writeInternalWhenTokenIntrospectionResponseThenSuccess() {
-		// @formatter:off
-		OAuth2TokenIntrospectionClaims providerConfiguration = OAuth2TokenIntrospectionClaims.builder(true)
-				.issuer("https://example.com/issuer1")
-				.scope("scope1 Scope2")
-				.clientId("clientId1")
-				.tokenType(TokenType.BEARER)
-				.username("username1")
-				.audience(Arrays.asList("audience1", "audience2"))
-				.expirationTime(Instant.ofEpochSecond(1607637467))
-				.issuedAt(Instant.ofEpochSecond(1607633867))
-				.notBefore(Instant.ofEpochSecond(1607633867))
-				.jwtId("jwtId1")
-				.subject("subject1").build();
-		// @formatter:on
-		MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
-
-		this.messageConverter.writeInternal(providerConfiguration, outputMessage);
-
-		String providerConfigurationResponse = outputMessage.getBodyAsString();
-		// @formatter:off
-		assertThat(providerConfigurationResponse)
-			.contains("\"iss\":\"https://example.com/issuer1\"")
-			.contains("\"active\":true")
-			.contains("\"scope\":\"scope1 Scope2\"")
-			.contains("\"client_id\":\"clientId1\"")
-			.contains("\"token_type\":\"Bearer\"")
-			.contains("\"username\":\"username1\"")
-			.contains("\"aud\":[\"audience1\",\"audience2\"]")
-			.contains("\"exp\":1607637467")
-			.contains("\"iat\":1607633867")
-			.contains("\"nbf\":1607633867")
-			.contains("\"jti\":\"jwtId1\"")
-			.contains("\"sub\":\"subject1\"");
-		// @formatter:on
-	}
-
-	@Test
-	public void writeInternalWhenWriteFailsThenThrowsException() {
-		String errorMessage = "this is not a valid converter";
-		Converter<OAuth2TokenIntrospectionClaims, Map<String, Object>> failingConverter = source -> {
-			throw new RuntimeException(errorMessage);
-		};
-		this.messageConverter.setTokenIntrospectionResponseParametersConverter(failingConverter);
-
-		OAuth2TokenIntrospectionClaims providerConfiguration = OAuth2TokenIntrospectionClaims.builder(true).build();
-
-		MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
-
-		assertThatThrownBy(() -> this.messageConverter.writeInternal(providerConfiguration, outputMessage))
-				.isInstanceOf(HttpMessageNotWritableException.class)
-				.hasMessageContaining("An error occurred writing the Token Introspection Response")
-				.hasMessageContaining(errorMessage);
-	}
-}

+ 170 - 0
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/core/http/converter/OAuth2TokenIntrospectionHttpMessageConverterTests.java

@@ -0,0 +1,170 @@
+/*
+ * 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.core.http.converter;
+
+import java.net.URL;
+import java.time.Instant;
+import java.util.Arrays;
+import java.util.Map;
+
+import org.junit.Test;
+
+import org.springframework.core.convert.converter.Converter;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.converter.HttpMessageNotReadableException;
+import org.springframework.http.converter.HttpMessageNotWritableException;
+import org.springframework.mock.http.MockHttpOutputMessage;
+import org.springframework.mock.http.client.MockClientHttpResponse;
+import org.springframework.security.oauth2.core.OAuth2AccessToken.TokenType;
+import org.springframework.security.oauth2.core.OAuth2TokenIntrospection;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/**
+ * Tests for {@link OAuth2TokenIntrospectionHttpMessageConverter}
+ *
+ * @author Gerardo Roza
+ * @author Joe Grandja
+ */
+public class OAuth2TokenIntrospectionHttpMessageConverterTests {
+	private final OAuth2TokenIntrospectionHttpMessageConverter messageConverter = new OAuth2TokenIntrospectionHttpMessageConverter();
+
+	@Test
+	public void supportsWhenOAuth2TokenIntrospectionThenTrue() {
+		assertThat(this.messageConverter.supports(OAuth2TokenIntrospection.class)).isTrue();
+	}
+
+	@Test
+	public void setTokenIntrospectionParametersConverterWhenNullThenThrowIllegalArgumentException() {
+		assertThatIllegalArgumentException()
+				.isThrownBy(() -> this.messageConverter.setTokenIntrospectionParametersConverter(null));
+	}
+
+	@Test
+	public void setTokenIntrospectionConverterWhenNullThenThrowIllegalArgumentException() {
+		assertThatIllegalArgumentException()
+				.isThrownBy(() -> this.messageConverter.setTokenIntrospectionConverter(null));
+	}
+
+	@Test
+	public void readInternalWhenValidParametersThenSuccess() throws Exception {
+		// @formatter:off
+		String tokenIntrospectionResponseBody = "{\n"
+				+ "		\"active\": true,\n"
+				+ "		\"client_id\": \"clientId1\",\n"
+				+ "		\"username\": \"username1\",\n"
+				+ "		\"iat\": 1607633867,\n"
+				+ "		\"exp\": 1607637467,\n"
+				+ "		\"scope\": \"scope1 scope2\",\n"
+				+ "		\"token_type\": \"Bearer\",\n"
+				+ "		\"nbf\": 1607633867,\n"
+				+ "		\"sub\": \"subject1\",\n"
+				+ "		\"aud\": [\"audience1\", \"audience2\"],\n"
+				+ "		\"iss\": \"https://example.com/issuer1\",\n"
+				+ "		\"jti\": \"jwtId1\"\n"
+				+ "}\n";
+		// @formatter:on
+		MockClientHttpResponse response = new MockClientHttpResponse(
+				tokenIntrospectionResponseBody.getBytes(), HttpStatus.OK);
+		OAuth2TokenIntrospection tokenIntrospectionResponse = this.messageConverter
+				.readInternal(OAuth2TokenIntrospection.class, response);
+
+		assertThat(tokenIntrospectionResponse.isActive()).isTrue();
+		assertThat(tokenIntrospectionResponse.getClientId()).isEqualTo("clientId1");
+		assertThat(tokenIntrospectionResponse.getUsername()).isEqualTo("username1");
+		assertThat(tokenIntrospectionResponse.getIssuedAt()).isEqualTo(Instant.ofEpochSecond(1607633867L));
+		assertThat(tokenIntrospectionResponse.getExpiresAt()).isEqualTo(Instant.ofEpochSecond(1607637467L));
+		assertThat(tokenIntrospectionResponse.getScope()).containsExactlyInAnyOrderElementsOf(Arrays.asList("scope1", "scope2"));
+		assertThat(tokenIntrospectionResponse.getTokenType()).isEqualTo("Bearer");
+		assertThat(tokenIntrospectionResponse.getNotBefore()).isEqualTo(Instant.ofEpochSecond(1607633867L));
+		assertThat(tokenIntrospectionResponse.getSubject()).isEqualTo("subject1");
+		assertThat(tokenIntrospectionResponse.getAudience()).containsExactlyInAnyOrderElementsOf(Arrays.asList("audience1", "audience2"));
+		assertThat(tokenIntrospectionResponse.getIssuer()).isEqualTo(new URL("https://example.com/issuer1"));
+		assertThat(tokenIntrospectionResponse.getId()).isEqualTo("jwtId1");
+	}
+
+	@Test
+	public void readInternalWhenFailingConverterThenThrowException() {
+		String errorMessage = "this is not a valid converter";
+		this.messageConverter.setTokenIntrospectionConverter(source -> {
+			throw new RuntimeException(errorMessage);
+		});
+		MockClientHttpResponse response = new MockClientHttpResponse("{}".getBytes(), HttpStatus.OK);
+
+		assertThatExceptionOfType(HttpMessageNotReadableException.class)
+				.isThrownBy(() -> this.messageConverter.readInternal(OAuth2TokenIntrospection.class, response))
+				.withMessageContaining("An error occurred reading the Token Introspection Response")
+				.withMessageContaining(errorMessage);
+	}
+
+	@Test
+	public void writeInternalWhenTokenIntrospectionThenSuccess() {
+		// @formatter:off
+		OAuth2TokenIntrospection tokenClaims = OAuth2TokenIntrospection.builder(true)
+				.clientId("clientId1")
+				.username("username1")
+				.issuedAt(Instant.ofEpochSecond(1607633867))
+				.expiresAt(Instant.ofEpochSecond(1607637467))
+				.scope("scope1 scope2")
+				.tokenType(TokenType.BEARER.getValue())
+				.notBefore(Instant.ofEpochSecond(1607633867))
+				.subject("subject1")
+				.audience("audience1")
+				.audience("audience2")
+				.issuer("https://example.com/issuer1")
+				.id("jwtId1")
+				.build();
+		// @formatter:on
+		MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
+
+		this.messageConverter.writeInternal(tokenClaims, outputMessage);
+
+		String tokenIntrospectionResponse = outputMessage.getBodyAsString();
+		assertThat(tokenIntrospectionResponse).contains("\"active\":true");
+		assertThat(tokenIntrospectionResponse).contains("\"client_id\":\"clientId1\"");
+		assertThat(tokenIntrospectionResponse).contains("\"username\":\"username1\"");
+		assertThat(tokenIntrospectionResponse).contains("\"iat\":1607633867");
+		assertThat(tokenIntrospectionResponse).contains("\"exp\":1607637467");
+		assertThat(tokenIntrospectionResponse).contains("\"scope\":\"scope1 scope2\"");
+		assertThat(tokenIntrospectionResponse).contains("\"token_type\":\"Bearer\"");
+		assertThat(tokenIntrospectionResponse).contains("\"nbf\":1607633867");
+		assertThat(tokenIntrospectionResponse).contains("\"sub\":\"subject1\"");
+		assertThat(tokenIntrospectionResponse).contains("\"aud\":[\"audience1\",\"audience2\"]");
+		assertThat(tokenIntrospectionResponse).contains("\"iss\":\"https://example.com/issuer1\"");
+		assertThat(tokenIntrospectionResponse).contains("\"jti\":\"jwtId1\"");
+	}
+
+	@Test
+	public void writeInternalWhenWriteFailsThenThrowsException() {
+		String errorMessage = "this is not a valid converter";
+		Converter<OAuth2TokenIntrospection, Map<String, Object>> failingConverter = source -> {
+			throw new RuntimeException(errorMessage);
+		};
+		this.messageConverter.setTokenIntrospectionParametersConverter(failingConverter);
+
+		OAuth2TokenIntrospection tokenClaims = OAuth2TokenIntrospection.builder().build();
+
+		MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
+
+		assertThatThrownBy(() -> this.messageConverter.writeInternal(tokenClaims, outputMessage))
+				.isInstanceOf(HttpMessageNotWritableException.class)
+				.hasMessageContaining("An error occurred writing the Token Introspection Response")
+				.hasMessageContaining(errorMessage);
+	}
+}

+ 25 - 6
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/TestOAuth2Authorizations.java

@@ -30,6 +30,7 @@ import org.springframework.security.oauth2.core.OAuth2RefreshToken2;
 import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
 import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
 import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients;
+import org.springframework.util.CollectionUtils;
 
 /**
  * @author Joe Grandja
@@ -45,12 +46,23 @@ public class TestOAuth2Authorizations {
 		return authorization(registeredClient, Collections.emptyMap());
 	}
 
+	public static OAuth2Authorization.Builder authorization(RegisteredClient registeredClient,
+			OAuth2AccessToken accessToken, Map<String, Object> accessTokenClaims) {
+		return authorization(registeredClient, accessToken, accessTokenClaims, Collections.emptyMap());
+	}
+
 	public static OAuth2Authorization.Builder authorization(RegisteredClient registeredClient,
 			Map<String, Object> authorizationRequestAdditionalParameters) {
-		OAuth2AuthorizationCode authorizationCode = new OAuth2AuthorizationCode(
-				"code", Instant.now(), Instant.now().plusSeconds(120));
 		OAuth2AccessToken accessToken = new OAuth2AccessToken(
 				OAuth2AccessToken.TokenType.BEARER, "access-token", Instant.now(), Instant.now().plusSeconds(300));
+		return authorization(registeredClient, accessToken, Collections.emptyMap(), authorizationRequestAdditionalParameters);
+	}
+
+	private static OAuth2Authorization.Builder authorization(RegisteredClient registeredClient,
+			OAuth2AccessToken accessToken, Map<String, Object> accessTokenClaims,
+			Map<String, Object> authorizationRequestAdditionalParameters) {
+		OAuth2AuthorizationCode authorizationCode = new OAuth2AuthorizationCode(
+				"code", Instant.now(), Instant.now().plusSeconds(120));
 		OAuth2RefreshToken refreshToken = new OAuth2RefreshToken2(
 				"refresh-token", Instant.now(), Instant.now().plus(1, ChronoUnit.HOURS));
 		OAuth2AuthorizationRequest authorizationRequest = OAuth2AuthorizationRequest.authorizationCode()
@@ -66,7 +78,7 @@ public class TestOAuth2Authorizations {
 				.principalName("principal")
 				.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
 				.token(authorizationCode)
-				.token(accessToken, (metadata) -> metadata.putAll(tokenMetadata()))
+				.token(accessToken, (metadata) -> metadata.putAll(tokenMetadata(accessTokenClaims)))
 				.refreshToken(refreshToken)
 				.attribute(OAuth2AuthorizationRequest.class.getName(), authorizationRequest)
 				.attribute(Principal.class.getName(),
@@ -74,14 +86,21 @@ public class TestOAuth2Authorizations {
 				.attribute(OAuth2Authorization.AUTHORIZED_SCOPE_ATTRIBUTE_NAME, authorizationRequest.getScopes());
 	}
 
-	private static Map<String, Object> tokenMetadata() {
+	private static Map<String, Object> tokenMetadata(Map<String, Object> tokenClaims) {
 		Map<String, Object> tokenMetadata = new HashMap<>();
 		tokenMetadata.put(OAuth2Authorization.Token.INVALIDATED_METADATA_NAME, false);
+		if (CollectionUtils.isEmpty(tokenClaims)) {
+			tokenClaims = defaultTokenClaims();
+		}
+		tokenMetadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, tokenClaims);
+		return tokenMetadata;
+	}
+
+	private static Map<String, Object> defaultTokenClaims() {
 		Map<String, Object> claims = new HashMap<>();
 		claims.put("claim1", "value1");
 		claims.put("claim2", "value2");
 		claims.put("claim3", "value3");
-		tokenMetadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, claims);
-		return tokenMetadata;
+		return claims;
 	}
 }

+ 136 - 110
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2TokenIntrospectionAuthenticationProviderTests.java

@@ -15,62 +15,72 @@
  */
 package org.springframework.security.oauth2.server.authorization.authentication;
 
-import static java.time.Duration.ofHours;
-import static java.time.Instant.now;
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.assertThatThrownBy;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.ArgumentMatchers.isNull;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-import static org.springframework.security.oauth2.core.OAuth2TokenIntrospectionClaimAccessor.ACTIVE;
-import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames.CLIENT_ID;
-import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames.SCOPE;
-import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames.TOKEN_TYPE;
-import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames.USERNAME;
-import static org.springframework.security.oauth2.jwt.JwtClaimNames.EXP;
-import static org.springframework.security.oauth2.jwt.JwtClaimNames.IAT;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Map;
 
 import org.junit.Before;
 import org.junit.Test;
+
 import org.springframework.security.authentication.TestingAuthenticationToken;
 import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
 import org.springframework.security.oauth2.core.OAuth2AccessToken;
 import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
 import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
 import org.springframework.security.oauth2.core.OAuth2RefreshToken;
-import org.springframework.security.oauth2.core.OAuth2TokenType;
+import org.springframework.security.oauth2.core.OAuth2TokenIntrospection;
 import org.springframework.security.oauth2.jwt.JwtClaimNames;
+import org.springframework.security.oauth2.jwt.JwtClaimsSet;
+import org.springframework.security.oauth2.jwt.TestJwtClaimsSets;
 import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
 import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
 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.RegisteredClientRepository;
 import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients;
 
-import java.time.Instant;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.HashSet;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.isNull;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 /**
  * Tests for {@link OAuth2TokenIntrospectionAuthenticationProvider}.
  *
  * @author Gerardo Roza
+ * @author Joe Grandja
  */
 public class OAuth2TokenIntrospectionAuthenticationProviderTests {
+	private RegisteredClientRepository registeredClientRepository;
 	private OAuth2AuthorizationService authorizationService;
 	private OAuth2TokenIntrospectionAuthenticationProvider authenticationProvider;
 
 	@Before
 	public void setUp() {
+		this.registeredClientRepository = mock(RegisteredClientRepository.class);
 		this.authorizationService = mock(OAuth2AuthorizationService.class);
-		this.authenticationProvider = new OAuth2TokenIntrospectionAuthenticationProvider(this.authorizationService);
+		this.authenticationProvider = new OAuth2TokenIntrospectionAuthenticationProvider(
+				this.registeredClientRepository, this.authorizationService);
+	}
+
+	@Test
+	public void constructorWhenRegisteredClientRepositoryNullThenThrowIllegalArgumentException() {
+		assertThatThrownBy(() -> new OAuth2TokenIntrospectionAuthenticationProvider(null, this.authorizationService))
+				.isInstanceOf(IllegalArgumentException.class)
+				.hasMessage("registeredClientRepository cannot be null");
 	}
 
 	@Test
 	public void constructorWhenAuthorizationServiceNullThenThrowIllegalArgumentException() {
-		assertThatThrownBy(() -> new OAuth2TokenIntrospectionAuthenticationProvider(null))
-				.isInstanceOf(IllegalArgumentException.class).hasMessage("authorizationService cannot be null");
+		assertThatThrownBy(() -> new OAuth2TokenIntrospectionAuthenticationProvider(this.registeredClientRepository, null))
+				.isInstanceOf(IllegalArgumentException.class)
+				.hasMessage("authorizationService cannot be null");
 	}
 
 	@Test
@@ -83,8 +93,10 @@ public class OAuth2TokenIntrospectionAuthenticationProviderTests {
 		RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
 		TestingAuthenticationToken clientPrincipal = new TestingAuthenticationToken(
 				registeredClient.getClientId(), registeredClient.getClientSecret());
+
 		OAuth2TokenIntrospectionAuthenticationToken authentication = new OAuth2TokenIntrospectionAuthenticationToken(
-				"token", clientPrincipal, OAuth2TokenType.ACCESS_TOKEN.getValue());
+				"token", clientPrincipal, null, null);
+
 		assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
 				.isInstanceOf(OAuth2AuthenticationException.class)
 				.extracting(ex -> ((OAuth2AuthenticationException) ex).getError()).extracting("errorCode")
@@ -95,10 +107,11 @@ public class OAuth2TokenIntrospectionAuthenticationProviderTests {
 	public void authenticateWhenClientPrincipalNotAuthenticatedThenThrowOAuth2AuthenticationException() {
 		RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
 		OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(
-				registeredClient.getClientId(), registeredClient.getClientSecret(), ClientAuthenticationMethod.BASIC,
-				null);
+				registeredClient.getClientId(), registeredClient.getClientSecret(), ClientAuthenticationMethod.BASIC, null);
+
 		OAuth2TokenIntrospectionAuthenticationToken authentication = new OAuth2TokenIntrospectionAuthenticationToken(
-				"token", clientPrincipal, OAuth2TokenType.ACCESS_TOKEN.getValue());
+				"token", clientPrincipal, null, null);
+
 		assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
 				.isInstanceOf(OAuth2AuthenticationException.class)
 				.extracting(ex -> ((OAuth2AuthenticationException) ex).getError()).extracting("errorCode")
@@ -106,144 +119,157 @@ public class OAuth2TokenIntrospectionAuthenticationProviderTests {
 	}
 
 	@Test
-	public void authenticateWhenInvalidOAuth2TokenTypeThenThrowOAuth2AuthenticationException() {
+	public void authenticateWhenInvalidTokenThenNotActive() {
 		RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
 		OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient);
-		OAuth2TokenIntrospectionAuthenticationToken authentication = new OAuth2TokenIntrospectionAuthenticationToken(
-				"token", clientPrincipal, "unsupportedOAuth2TokenType");
-		OAuth2TokenIntrospectionAuthenticationToken authenticationResult = (OAuth2TokenIntrospectionAuthenticationToken) this.authenticationProvider
-				.authenticate(authentication);
-		assertThat(authenticationResult.isAuthenticated()).isTrue();
-		assertThat(authenticationResult.isTokenActive()).isFalse();
-	}
 
-	@Test
-	public void authenticateWhenTokenNotFoundThenAuthenticatedButTokenNotActive() {
-		RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
-		OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient);
 		OAuth2TokenIntrospectionAuthenticationToken authentication = new OAuth2TokenIntrospectionAuthenticationToken(
-				"token", clientPrincipal, OAuth2TokenType.ACCESS_TOKEN.getValue());
-		OAuth2TokenIntrospectionAuthenticationToken authenticationResult = (OAuth2TokenIntrospectionAuthenticationToken) this.authenticationProvider
-				.authenticate(authentication);
-		assertThat(authenticationResult.isAuthenticated()).isTrue();
-		assertThat(authenticationResult.isTokenActive()).isFalse();
+				"token", clientPrincipal, null, null);
+		OAuth2TokenIntrospectionAuthenticationToken authenticationResult =
+				(OAuth2TokenIntrospectionAuthenticationToken) this.authenticationProvider.authenticate(authentication);
+
+		verify(this.authorizationService).findByToken(eq(authentication.getToken()), isNull());
+		assertThat(authenticationResult.isAuthenticated()).isFalse();
+		assertThat(authenticationResult.getTokenClaims().getClaims()).hasSize(1);
+		assertThat(authenticationResult.getTokenClaims().isActive()).isFalse();
 	}
 
 	@Test
-	public void authenticateWhenInvalidatedTokenThenAuthenticatedButTokenNotActive() {
+	public void authenticateWhenTokenInvalidatedThenNotActive() {
 		RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
 		OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient).build();
 		OAuth2AccessToken accessToken = authorization.getAccessToken().getToken();
 		authorization = OAuth2AuthenticationProviderUtils.invalidate(authorization, accessToken);
 		when(this.authorizationService.findByToken(eq(accessToken.getTokenValue()), isNull()))
 				.thenReturn(authorization);
-
 		OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient);
+
 		OAuth2TokenIntrospectionAuthenticationToken authentication = new OAuth2TokenIntrospectionAuthenticationToken(
-				accessToken.getTokenValue(), clientPrincipal, OAuth2TokenType.ACCESS_TOKEN.getValue());
+				accessToken.getTokenValue(), clientPrincipal, null, null);
+		OAuth2TokenIntrospectionAuthenticationToken authenticationResult =
+				(OAuth2TokenIntrospectionAuthenticationToken) this.authenticationProvider.authenticate(authentication);
 
-		OAuth2TokenIntrospectionAuthenticationToken authenticationResult = (OAuth2TokenIntrospectionAuthenticationToken) this.authenticationProvider
-				.authenticate(authentication);
+		verify(this.authorizationService).findByToken(eq(authentication.getToken()), isNull());
 		assertThat(authenticationResult.isAuthenticated()).isTrue();
-		assertThat(authenticationResult.isTokenActive()).isFalse();
+		assertThat(authenticationResult.getTokenClaims().getClaims()).hasSize(1);
+		assertThat(authenticationResult.getTokenClaims().isActive()).isFalse();
 	}
 
 	@Test
-	public void authenticateWhenValidAccessTokenThenActiveWithScopes() {
+	public void authenticateWhenTokenExpiredThenNotActive() {
 		RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
+		Instant issuedAt = Instant.now().minus(Duration.ofHours(1));
+		Instant expiresAt = Instant.now().minus(Duration.ofMinutes(1));
 		OAuth2AccessToken accessToken = new OAuth2AccessToken(
-				OAuth2AccessToken.TokenType.BEARER, "access-token", Instant.now(), Instant.now().plusSeconds(300),
-				new HashSet<>(Arrays.asList("scope1", "Scope2")));
-		OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient)
-				.accessToken(accessToken).build();
+				OAuth2AccessToken.TokenType.BEARER, "access-token", issuedAt, expiresAt);
+		OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient).token(accessToken).build();
 		when(this.authorizationService.findByToken(eq(accessToken.getTokenValue()), isNull()))
 				.thenReturn(authorization);
-
 		OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient);
+
 		OAuth2TokenIntrospectionAuthenticationToken authentication = new OAuth2TokenIntrospectionAuthenticationToken(
-				accessToken.getTokenValue(), clientPrincipal, OAuth2TokenType.ACCESS_TOKEN.getValue());
+				accessToken.getTokenValue(), clientPrincipal, null, null);
+		OAuth2TokenIntrospectionAuthenticationToken authenticationResult =
+				(OAuth2TokenIntrospectionAuthenticationToken) this.authenticationProvider.authenticate(authentication);
 
-		OAuth2TokenIntrospectionAuthenticationToken authenticationResult = (OAuth2TokenIntrospectionAuthenticationToken) this.authenticationProvider
-				.authenticate(authentication);
+		verify(this.authorizationService).findByToken(eq(authentication.getToken()), isNull());
 		assertThat(authenticationResult.isAuthenticated()).isTrue();
-		assertThat(authenticationResult.isTokenActive()).isTrue();
-		assertThat(authenticationResult.getClaims()).containsEntry(ACTIVE, true)
-				.containsEntry(IAT, accessToken.getIssuedAt()).containsEntry(EXP, accessToken.getExpiresAt())
-				.containsEntry(TOKEN_TYPE, OAuth2AccessToken.TokenType.BEARER).containsEntry(CLIENT_ID, "client-1")
-				.containsEntry(USERNAME, "principal").containsKey(SCOPE)
-				.containsAllEntriesOf(authorization.getAccessToken().getClaims());
-
+		assertThat(authenticationResult.getTokenClaims().getClaims()).hasSize(1);
+		assertThat(authenticationResult.getTokenClaims().isActive()).isFalse();
 	}
 
 	@Test
-	public void authenticateWhenValidRefreshTokenThenActive() {
+	public void authenticateWhenTokenBeforeUseThenNotActive() {
 		RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
-		OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient).build();
-		OAuth2RefreshToken refreshToken = authorization.getRefreshToken().getToken();
-		when(this.authorizationService.findByToken(eq(refreshToken.getTokenValue()), isNull()))
+		Instant issuedAt = Instant.now();
+		Instant notBefore = issuedAt.plus(Duration.ofMinutes(5));
+		Instant expiresAt = issuedAt.plus(Duration.ofHours(1));
+		OAuth2AccessToken accessToken = new OAuth2AccessToken(
+				OAuth2AccessToken.TokenType.BEARER, "access-token", issuedAt, expiresAt);
+		Map<String, Object> accessTokenClaims = Collections.singletonMap(JwtClaimNames.NBF, notBefore);
+		OAuth2Authorization authorization = TestOAuth2Authorizations
+				.authorization(registeredClient, accessToken, accessTokenClaims)
+				.build();
+		when(this.authorizationService.findByToken(eq(accessToken.getTokenValue()), isNull()))
 				.thenReturn(authorization);
-
 		OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient);
+
 		OAuth2TokenIntrospectionAuthenticationToken authentication = new OAuth2TokenIntrospectionAuthenticationToken(
-				refreshToken.getTokenValue(), clientPrincipal, OAuth2TokenType.REFRESH_TOKEN.getValue());
+				accessToken.getTokenValue(), clientPrincipal, null, null);
+		OAuth2TokenIntrospectionAuthenticationToken authenticationResult =
+				(OAuth2TokenIntrospectionAuthenticationToken) this.authenticationProvider.authenticate(authentication);
 
-		OAuth2TokenIntrospectionAuthenticationToken authenticationResult = (OAuth2TokenIntrospectionAuthenticationToken) this.authenticationProvider
-				.authenticate(authentication);
+		verify(this.authorizationService).findByToken(eq(authentication.getToken()), isNull());
 		assertThat(authenticationResult.isAuthenticated()).isTrue();
-		assertThat(authenticationResult.isTokenActive()).isTrue();
-		assertThat(authenticationResult.getClaims()).containsEntry(ACTIVE, true)
-				.containsEntry(IAT, refreshToken.getIssuedAt()).containsEntry(EXP, refreshToken.getExpiresAt())
-				.containsEntry(USERNAME, "principal").containsEntry(CLIENT_ID, "client-1").hasSize(5);
+		assertThat(authenticationResult.getTokenClaims().getClaims()).hasSize(1);
+		assertThat(authenticationResult.getTokenClaims().isActive()).isFalse();
 	}
 
 	@Test
-	public void authenticateWhenExpiredTokenThenAuthenticatedButTokenNotActive() {
-		RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
-		Instant expiresAt = Instant.now().minus(ofHours(1));
-		Instant issuedAt = expiresAt.minus(ofHours(1));
+	public void authenticateWhenValidAccessTokenThenActive() {
+		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);
-		OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient).token(accessToken)
+				OAuth2AccessToken.TokenType.BEARER, "access-token", issuedAt, expiresAt,
+				new HashSet<>(Arrays.asList("scope1", "scope2")));
+		JwtClaimsSet jwtClaims = TestJwtClaimsSets.jwtClaimsSet().build();
+		OAuth2Authorization authorization = TestOAuth2Authorizations
+				.authorization(authorizedClient, accessToken, jwtClaims.getClaims())
 				.build();
 		when(this.authorizationService.findByToken(eq(accessToken.getTokenValue()), isNull()))
 				.thenReturn(authorization);
+		when(this.registeredClientRepository.findById(eq(authorizedClient.getId()))).thenReturn(authorizedClient);
+		OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(
+				TestRegisteredClients.registeredClient2().build());
 
-		OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient);
 		OAuth2TokenIntrospectionAuthenticationToken authentication = new OAuth2TokenIntrospectionAuthenticationToken(
-				accessToken.getTokenValue(), clientPrincipal, OAuth2TokenType.ACCESS_TOKEN.getValue());
+				accessToken.getTokenValue(), clientPrincipal, null, null);
+		OAuth2TokenIntrospectionAuthenticationToken authenticationResult =
+				(OAuth2TokenIntrospectionAuthenticationToken) this.authenticationProvider.authenticate(authentication);
 
-		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();
-		assertThat(authenticationResult.isTokenActive()).isFalse();
-		assertThat(authenticationResult.getClaims()).isNull();
+		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.getScope()).containsExactlyInAnyOrderElementsOf(accessToken.getScopes());
+		assertThat(tokenClaims.getTokenType()).isEqualTo(accessToken.getTokenType().getValue());
+		assertThat(tokenClaims.getNotBefore()).isEqualTo(jwtClaims.getNotBefore());
+		assertThat(tokenClaims.getSubject()).isEqualTo(jwtClaims.getSubject());
+		assertThat(tokenClaims.getAudience()).containsExactlyInAnyOrderElementsOf(jwtClaims.getAudience());
+		assertThat(tokenClaims.getIssuer()).isEqualTo(jwtClaims.getIssuer());
+		assertThat(tokenClaims.getId()).isEqualTo(jwtClaims.getId());
 	}
 
 	@Test
-	public void authenticateWhenInvalidNotBeforeClaimThenAuthenticatedButTokenNotActive() {
-		RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
-		Instant issuedAt = Instant.now();
-		Instant expiresAt = issuedAt.plus(ofHours(1));
-		OAuth2AccessToken accessToken = new OAuth2AccessToken(
-				OAuth2AccessToken.TokenType.BEARER, "access-token", issuedAt, expiresAt);
-		OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient)
-				.token(
-						accessToken,
-						(metadata) -> metadata.put(
-								OAuth2Authorization.Token.CLAIMS_METADATA_NAME,
-								Collections.singletonMap(JwtClaimNames.NBF, now().plus(ofHours(1)))))
-				.build();
-		when(this.authorizationService.findByToken(eq(accessToken.getTokenValue()), isNull()))
+	public void authenticateWhenValidRefreshTokenThenActive() {
+		RegisteredClient authorizedClient = TestRegisteredClients.registeredClient().build();
+		OAuth2Authorization authorization = TestOAuth2Authorizations.authorization().build();
+		OAuth2RefreshToken refreshToken = authorization.getRefreshToken().getToken();
+		when(this.authorizationService.findByToken(eq(refreshToken.getTokenValue()), isNull()))
 				.thenReturn(authorization);
+		when(this.registeredClientRepository.findById(eq(authorizedClient.getId()))).thenReturn(authorizedClient);
+		OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(
+				TestRegisteredClients.registeredClient2().build());
 
-		OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient);
 		OAuth2TokenIntrospectionAuthenticationToken authentication = new OAuth2TokenIntrospectionAuthenticationToken(
-				accessToken.getTokenValue(), clientPrincipal, OAuth2TokenType.ACCESS_TOKEN.getValue());
+				refreshToken.getTokenValue(), clientPrincipal, null, null);
+		OAuth2TokenIntrospectionAuthenticationToken authenticationResult =
+				(OAuth2TokenIntrospectionAuthenticationToken) this.authenticationProvider.authenticate(authentication);
 
-		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();
-		assertThat(authenticationResult.isTokenActive()).isFalse();
-		assertThat(authenticationResult.getClaims()).isNull();
+		OAuth2TokenIntrospection tokenClaims = authenticationResult.getTokenClaims();
+		assertThat(tokenClaims.getClaims()).hasSize(4);
+		assertThat(tokenClaims.isActive()).isTrue();
+		assertThat(tokenClaims.getClientId()).isEqualTo(authorizedClient.getClientId());
+		assertThat(tokenClaims.getIssuedAt()).isEqualTo(refreshToken.getIssuedAt());
+		assertThat(tokenClaims.getExpiresAt()).isEqualTo(refreshToken.getExpiresAt());
 	}
+
 }

+ 46 - 39
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2TokenIntrospectionAuthenticationTokenTests.java

@@ -15,84 +15,91 @@
  */
 package org.springframework.security.oauth2.server.authorization.authentication;
 
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import java.util.Collections;
+import java.util.Map;
 
 import org.junit.Test;
+
+import org.springframework.security.oauth2.core.OAuth2TokenIntrospection;
 import org.springframework.security.oauth2.core.OAuth2TokenType;
 import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients;
 
-import java.util.HashMap;
-import java.util.Map;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
 /**
  * Tests for {@link OAuth2TokenIntrospectionAuthenticationToken}.
  *
  * @author Gerardo Roza
+ * @author Joe Grandja
  */
 public class OAuth2TokenIntrospectionAuthenticationTokenTests {
-	private String tokenValue = "tokenValue";
+	private String token = "token";
 	private OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(
 			TestRegisteredClients.registeredClient().build());
-	private String tokenTypeHint = OAuth2TokenType.ACCESS_TOKEN.getValue();
-	private Map<String, Object> claims = new HashMap<>();
+	private OAuth2TokenIntrospection tokenClaims = OAuth2TokenIntrospection.builder(true).build();
 
 	@Test
-	public void constructorWhenTokenValueNullThenThrowIllegalArgumentException() {
-		assertThatThrownBy(
-				() -> new OAuth2TokenIntrospectionAuthenticationToken(null, this.clientPrincipal, this.tokenTypeHint))
-						.isInstanceOf(IllegalArgumentException.class).hasMessage("token cannot be empty");
+	public void constructorWhenTokenNullThenThrowIllegalArgumentException() {
+		assertThatThrownBy(() -> new OAuth2TokenIntrospectionAuthenticationToken(null, this.clientPrincipal, null, null))
+				.isInstanceOf(IllegalArgumentException.class)
+				.hasMessage("token cannot be empty");
 	}
 
 	@Test
 	public void constructorWhenClientPrincipalNullThenThrowIllegalArgumentException() {
-		assertThatThrownBy(
-				() -> new OAuth2TokenIntrospectionAuthenticationToken(this.tokenValue, null, this.tokenTypeHint))
-						.isInstanceOf(IllegalArgumentException.class).hasMessage("clientPrincipal cannot be null");
+		assertThatThrownBy(() -> new OAuth2TokenIntrospectionAuthenticationToken(this.token, null, null, null))
+				.isInstanceOf(IllegalArgumentException.class)
+				.hasMessage("clientPrincipal cannot be null");
 	}
 
 	@Test
-	public void constructorWhenTokenAndClientPrincipalNullThenThrowIllegalArgumentException() {
-		assertThatThrownBy(() -> new OAuth2TokenIntrospectionAuthenticationToken(null, this.claims))
-				.isInstanceOf(IllegalArgumentException.class).hasMessage("clientPrincipal cannot be null");
+	public void constructorWhenAuthenticatedAndTokenNullThenThrowIllegalArgumentException() {
+		assertThatThrownBy(() -> new OAuth2TokenIntrospectionAuthenticationToken(null, this.clientPrincipal, this.tokenClaims))
+				.isInstanceOf(IllegalArgumentException.class)
+				.hasMessage("token cannot be empty");
 	}
 
 	@Test
-	public void constructorWhenTokenValueProvidedThenCreated() {
-		OAuth2TokenIntrospectionAuthenticationToken authentication = new OAuth2TokenIntrospectionAuthenticationToken(
-				this.tokenValue, this.clientPrincipal, this.tokenTypeHint);
-		assertThat(authentication.getTokenValue()).isEqualTo(this.tokenValue);
-		assertThat(authentication.getPrincipal()).isEqualTo(this.clientPrincipal);
-		assertThat(authentication.getTokenTypeHint()).isEqualTo(this.tokenTypeHint);
-		assertThat(authentication.getClaims()).isNull();
-		assertThat(authentication.isTokenActive()).isFalse();
-		assertThat(authentication.getCredentials().toString()).isEmpty();
-		assertThat(authentication.isAuthenticated()).isFalse();
+	public void constructorWhenAuthenticatedAndClientPrincipalNullThenThrowIllegalArgumentException() {
+		assertThatThrownBy(() -> new OAuth2TokenIntrospectionAuthenticationToken(this.token, null, this.tokenClaims))
+				.isInstanceOf(IllegalArgumentException.class)
+				.hasMessage("clientPrincipal cannot be null");
+	}
+
+	@Test
+	public void constructorWhenAuthenticatedAndTokenClaimsNullThenThrowIllegalArgumentException() {
+		assertThatThrownBy(() -> new OAuth2TokenIntrospectionAuthenticationToken(this.token, this.clientPrincipal, null))
+				.isInstanceOf(IllegalArgumentException.class)
+				.hasMessage("tokenClaims cannot be null");
 	}
 
 	@Test
 	public void constructorWhenTokenProvidedThenCreated() {
+		Map<String, Object> additionalParameters = Collections.singletonMap("custom-param", "custom-value");
 		OAuth2TokenIntrospectionAuthenticationToken authentication = new OAuth2TokenIntrospectionAuthenticationToken(
-				this.clientPrincipal, this.claims);
-		assertThat(authentication.getTokenValue()).isNull();
+				this.token, this.clientPrincipal, OAuth2TokenType.ACCESS_TOKEN.getValue(), additionalParameters);
+		assertThat(authentication.getToken()).isEqualTo(this.token);
 		assertThat(authentication.getPrincipal()).isEqualTo(this.clientPrincipal);
-		assertThat(authentication.getClaims()).isEqualTo(this.claims);
-		assertThat(authentication.isTokenActive()).isTrue();
-		assertThat(authentication.getTokenTypeHint()).isNull();
 		assertThat(authentication.getCredentials().toString()).isEmpty();
-		assertThat(authentication.isAuthenticated()).isTrue();
+		assertThat(authentication.getTokenTypeHint()).isEqualTo(OAuth2TokenType.ACCESS_TOKEN.getValue());
+		assertThat(authentication.getAdditionalParameters()).containsExactlyInAnyOrderEntriesOf(additionalParameters);
+		assertThat(authentication.getTokenClaims()).isNotNull();
+		assertThat(authentication.getTokenClaims().isActive()).isFalse();
+		assertThat(authentication.isAuthenticated()).isFalse();
 	}
 
 	@Test
-	public void constructorWhenNullTokenProvidedThenCreatedAsTokenNotActive() {
+	public void constructorWhenTokenClaimsProvidedThenCreated() {
 		OAuth2TokenIntrospectionAuthenticationToken authentication = new OAuth2TokenIntrospectionAuthenticationToken(
-				this.clientPrincipal, null);
-		assertThat(authentication.getTokenValue()).isNull();
+				this.token, this.clientPrincipal, this.tokenClaims);
+		assertThat(authentication.getToken()).isEqualTo(this.token);
 		assertThat(authentication.getPrincipal()).isEqualTo(this.clientPrincipal);
-		assertThat(authentication.getClaims()).isNull();
-		assertThat(authentication.isTokenActive()).isFalse();
-		assertThat(authentication.getTokenTypeHint()).isNull();
 		assertThat(authentication.getCredentials().toString()).isEmpty();
+		assertThat(authentication.getTokenTypeHint()).isNull();
+		assertThat(authentication.getAdditionalParameters()).isEmpty();
+		assertThat(authentication.getTokenClaims()).isEqualTo(this.tokenClaims);
 		assertThat(authentication.isAuthenticated()).isTrue();
 	}
+
 }

+ 13 - 1
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/ProviderSettingsTests.java

@@ -36,6 +36,7 @@ public class ProviderSettingsTests {
 		assertThat(providerSettings.tokenEndpoint()).isEqualTo("/oauth2/token");
 		assertThat(providerSettings.jwkSetEndpoint()).isEqualTo("/oauth2/jwks");
 		assertThat(providerSettings.tokenRevocationEndpoint()).isEqualTo("/oauth2/revoke");
+		assertThat(providerSettings.tokenIntrospectionEndpoint()).isEqualTo("/oauth2/introspect");
 	}
 
 	@Test
@@ -44,6 +45,7 @@ public class ProviderSettingsTests {
 		String tokenEndpoint = "/oauth2/v1/token";
 		String jwkSetEndpoint = "/oauth2/v1/jwks";
 		String tokenRevocationEndpoint = "/oauth2/v1/revoke";
+		String tokenIntrospectionEndpoint = "/oauth2/v1/introspect";
 		String issuer = "https://example.com:9000";
 
 		ProviderSettings providerSettings = new ProviderSettings()
@@ -51,13 +53,15 @@ public class ProviderSettingsTests {
 				.authorizationEndpoint(authorizationEndpoint)
 				.tokenEndpoint(tokenEndpoint)
 				.jwkSetEndpoint(jwkSetEndpoint)
-				.tokenRevocationEndpoint(tokenRevocationEndpoint);
+				.tokenRevocationEndpoint(tokenRevocationEndpoint)
+				.tokenIntrospectionEndpoint(tokenIntrospectionEndpoint);
 
 		assertThat(providerSettings.issuer()).isEqualTo(issuer);
 		assertThat(providerSettings.authorizationEndpoint()).isEqualTo(authorizationEndpoint);
 		assertThat(providerSettings.tokenEndpoint()).isEqualTo(tokenEndpoint);
 		assertThat(providerSettings.jwkSetEndpoint()).isEqualTo(jwkSetEndpoint);
 		assertThat(providerSettings.tokenRevocationEndpoint()).isEqualTo(tokenRevocationEndpoint);
+		assertThat(providerSettings.tokenIntrospectionEndpoint()).isEqualTo(tokenIntrospectionEndpoint);
 	}
 
 	@Test
@@ -103,6 +107,14 @@ public class ProviderSettingsTests {
 				.hasMessage("value cannot be null");
 	}
 
+	@Test
+	public void tokenIntrospectionEndpointWhenNullThenThrowIllegalArgumentException() {
+		ProviderSettings settings = new ProviderSettings();
+		assertThatThrownBy(() -> settings.tokenIntrospectionEndpoint(null))
+				.isInstanceOf(IllegalArgumentException.class)
+				.hasMessage("value cannot be null");
+	}
+
 	@Test
 	public void jwksEndpointWhenNullThenThrowIllegalArgumentException() {
 		ProviderSettings settings = new ProviderSettings();

+ 117 - 127
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2TokenIntrospectionEndpointFilterTests.java

@@ -15,31 +15,20 @@
  */
 package org.springframework.security.oauth2.server.authorization.web;
 
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.assertThatThrownBy;
-import static org.assertj.core.api.Assertions.entry;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyNoInteractions;
-import static org.mockito.Mockito.when;
-import static org.springframework.security.oauth2.core.OAuth2TokenIntrospectionClaimAccessor.ACTIVE;
-import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames.CLIENT_ID;
-import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames.SCOPE;
-import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames.TOKEN_TYPE;
-import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames2.TOKEN;
-import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames2.TOKEN_TYPE_HINT;
-import static org.springframework.security.oauth2.jwt.JwtClaimNames.EXP;
-import static org.springframework.security.oauth2.jwt.JwtClaimNames.IAT;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Arrays;
+import java.util.HashSet;
 
 import javax.servlet.FilterChain;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
-import org.assertj.core.api.Condition;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+
 import org.springframework.http.HttpStatus;
 import org.springframework.http.converter.HttpMessageConverter;
 import org.springframework.mock.http.client.MockClientHttpResponse;
@@ -52,34 +41,38 @@ import org.springframework.security.core.context.SecurityContextHolder;
 import org.springframework.security.oauth2.core.OAuth2AccessToken;
 import org.springframework.security.oauth2.core.OAuth2Error;
 import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
-import org.springframework.security.oauth2.core.OAuth2TokenIntrospectionClaims;
+import org.springframework.security.oauth2.core.OAuth2TokenIntrospection;
 import org.springframework.security.oauth2.core.OAuth2TokenType;
+import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames2;
 import org.springframework.security.oauth2.core.http.converter.OAuth2ErrorHttpMessageConverter;
-import org.springframework.security.oauth2.core.http.converter.OAuth2TokenIntrospectionClaimsHttpMessageConverter;
+import org.springframework.security.oauth2.core.http.converter.OAuth2TokenIntrospectionHttpMessageConverter;
 import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken;
 import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenIntrospectionAuthenticationToken;
 import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
 import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients;
 
-import java.time.Duration;
-import java.time.Instant;
-import java.util.HashMap;
-import java.util.Map;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.assertj.core.api.Assertions.entry;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.when;
 
 /**
  * Tests for {@link OAuth2TokenIntrospectionEndpointFilter}.
  *
  * @author Gerardo Roza
+ * @author Joe Grandja
  */
 public class OAuth2TokenIntrospectionEndpointFilterTests {
-
 	private AuthenticationManager authenticationManager;
 	private OAuth2TokenIntrospectionEndpointFilter filter;
-	private final HttpMessageConverter<OAuth2Error> errorHttpResponseConverter = new OAuth2ErrorHttpMessageConverter();
-	private final HttpMessageConverter<OAuth2TokenIntrospectionClaims> tokenIntrospectionHttpResponseConverter = new OAuth2TokenIntrospectionClaimsHttpMessageConverter();
-	private final Condition<Object> scopesMatchesInAnyOrder = new Condition<>(
-			scopes -> scopes.equals("scope1 Scope2") || scopes.equals("Scope2 scope1"), "scopes match");
-	private final String tokenValue = "token.123";
+	private final HttpMessageConverter<OAuth2TokenIntrospection> tokenIntrospectionHttpResponseConverter =
+			new OAuth2TokenIntrospectionHttpMessageConverter();
+	private final HttpMessageConverter<OAuth2Error> errorHttpResponseConverter =
+			new OAuth2ErrorHttpMessageConverter();
 
 	@Before
 	public void setUp() {
@@ -95,11 +88,19 @@ public class OAuth2TokenIntrospectionEndpointFilterTests {
 	@Test
 	public void constructorWhenAuthenticationManagerNullThenThrowIllegalArgumentException() {
 		assertThatThrownBy(() -> new OAuth2TokenIntrospectionEndpointFilter(null))
-				.isInstanceOf(IllegalArgumentException.class).hasMessage("authenticationManager cannot be null");
+				.isInstanceOf(IllegalArgumentException.class)
+				.hasMessage("authenticationManager cannot be null");
 	}
 
 	@Test
-	public void doFilterWhenNotIntrospectionRequestThenNotProcessed() throws Exception {
+	public void constructorWhenTokenIntrospectionEndpointUriNullThenThrowIllegalArgumentException() {
+		assertThatThrownBy(() -> new OAuth2TokenIntrospectionEndpointFilter(this.authenticationManager, null))
+				.isInstanceOf(IllegalArgumentException.class)
+				.hasMessage("tokenIntrospectionEndpointUri cannot be empty");
+	}
+
+	@Test
+	public void doFilterWhenNotTokenIntrospectionRequestThenNotProcessed() throws Exception {
 		String requestUri = "/path";
 		MockHttpServletRequest request = new MockHttpServletRequest("POST", requestUri);
 		request.setServletPath(requestUri);
@@ -112,7 +113,7 @@ public class OAuth2TokenIntrospectionEndpointFilterTests {
 	}
 
 	@Test
-	public void doFilterWhenIntrospectionRequestGetThenNotProcessed() throws Exception {
+	public void doFilterWhenTokenIntrospectionRequestGetThenNotProcessed() throws Exception {
 		String requestUri = OAuth2TokenIntrospectionEndpointFilter.DEFAULT_TOKEN_INTROSPECTION_ENDPOINT_URI;
 		MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
 		request.setServletPath(requestUri);
@@ -125,117 +126,106 @@ public class OAuth2TokenIntrospectionEndpointFilterTests {
 	}
 
 	@Test
-	public void doFilterWhenIntrospectionRequestMissingTokenParamThenInvalidRequestError() throws Exception {
+	public void doFilterWhenTokenIntrospectionRequestMissingTokenThenInvalidRequestError() throws Exception {
 		MockHttpServletRequest request = createTokenIntrospectionRequest(
-				this.tokenValue, OAuth2TokenType.ACCESS_TOKEN.getValue());
-		request.removeParameter(TOKEN);
+				"token", OAuth2TokenType.ACCESS_TOKEN.getValue());
+		request.removeParameter(OAuth2ParameterNames2.TOKEN);
 
 		doFilterWhenTokenIntrospectionRequestInvalidParameterThenError(
-				TOKEN, OAuth2ErrorCodes.INVALID_REQUEST, request);
+				OAuth2ParameterNames2.TOKEN, OAuth2ErrorCodes.INVALID_REQUEST, request);
 	}
 
 	@Test
-	public void doFilterWhenTokenRequestMultipleTokenParamThenInvalidRequestError() throws Exception {
+	public void doFilterWhenTokenIntrospectionRequestMultipleTokenThenInvalidRequestError() throws Exception {
 		MockHttpServletRequest request = createTokenIntrospectionRequest(
-				this.tokenValue, OAuth2TokenType.ACCESS_TOKEN.getValue());
-		request.addParameter(TOKEN, "token.456");
+				"token", OAuth2TokenType.ACCESS_TOKEN.getValue());
+		request.addParameter(OAuth2ParameterNames2.TOKEN, "other-token");
 
 		doFilterWhenTokenIntrospectionRequestInvalidParameterThenError(
-				TOKEN, OAuth2ErrorCodes.INVALID_REQUEST, request);
+				OAuth2ParameterNames2.TOKEN, OAuth2ErrorCodes.INVALID_REQUEST, request);
 	}
 
 	@Test
-	public void doFilterWhenTokenRequestMultipleTokenTypeHintParamThenInvalidRequestError() throws Exception {
+	public void doFilterWhenTokenIntrospectionRequestMultipleTokenTypeHintThenInvalidRequestError() throws Exception {
 		MockHttpServletRequest request = createTokenIntrospectionRequest(
-				this.tokenValue, OAuth2TokenType.ACCESS_TOKEN.getValue());
-		request.addParameter(TOKEN_TYPE_HINT, OAuth2TokenType.REFRESH_TOKEN.getValue());
+				"token", OAuth2TokenType.ACCESS_TOKEN.getValue());
+		request.addParameter(OAuth2ParameterNames2.TOKEN_TYPE_HINT, OAuth2TokenType.ACCESS_TOKEN.getValue());
 
 		doFilterWhenTokenIntrospectionRequestInvalidParameterThenError(
-				TOKEN_TYPE_HINT, OAuth2ErrorCodes.INVALID_REQUEST, request);
+				OAuth2ParameterNames2.TOKEN_TYPE_HINT, OAuth2ErrorCodes.INVALID_REQUEST, request);
 	}
 
 	@Test
-	public void doFilterWhenIntrospectWithNullClaimsThenNotActiveTokenOkReponse() throws Exception {
-		MockHttpServletRequest request = createTokenIntrospectionRequest(
-				this.tokenValue, OAuth2TokenType.ACCESS_TOKEN.getValue());
-		MockHttpServletResponse response = new MockHttpServletResponse();
-		FilterChain filterChain = mock(FilterChain.class);
-
-		Authentication clientPrincipal = setupSecurityContext();
-
-		OAuth2TokenIntrospectionAuthenticationToken tokenIntrospectionAuthentication = new OAuth2TokenIntrospectionAuthenticationToken(
-				clientPrincipal, null);
-
-		when(this.authenticationManager.authenticate(any())).thenReturn(tokenIntrospectionAuthentication);
-
-		this.filter.doFilter(request, response, filterChain);
-
-		verifyNoInteractions(filterChain);
+	public void doFilterWhenTokenIntrospectionRequestValidThenSuccessResponse() throws Exception {
+		RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
+		Authentication clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient);
+		OAuth2AccessToken accessToken = new OAuth2AccessToken(
+				OAuth2AccessToken.TokenType.BEARER, "token",
+				Instant.now(), Instant.now().plus(Duration.ofHours(1)),
+				new HashSet<>(Arrays.asList("scope1", "scope2")));
+		// @formatter:off
+		OAuth2TokenIntrospection tokenClaims = OAuth2TokenIntrospection.builder(true)
+				.clientId("authorized-client-id")
+				.username("authorizing-username")
+				.issuedAt(accessToken.getIssuedAt())
+				.expiresAt(accessToken.getExpiresAt())
+				.scopes(scopes -> scopes.addAll(accessToken.getScopes()))
+				.tokenType(accessToken.getTokenType().getValue())
+				.notBefore(accessToken.getIssuedAt())
+				.subject("authorizing-subject")
+				.audience("authorized-client-id")
+				.issuer("https://provider.com")
+				.id("jti")
+				.build();
+		// @formatter:on
+		OAuth2TokenIntrospectionAuthenticationToken tokenIntrospectionAuthenticationResult =
+				new OAuth2TokenIntrospectionAuthenticationToken(
+						accessToken.getTokenValue(), clientPrincipal, tokenClaims);
+
+		when(this.authenticationManager.authenticate(any())).thenReturn(tokenIntrospectionAuthenticationResult);
 
-		assertNotActiveTokenResponse(response);
-	}
+		SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
+		securityContext.setAuthentication(clientPrincipal);
+		SecurityContextHolder.setContext(securityContext);
 
-	@Test
-	public void doFilterWhenIntrospectWithClaimsThenActiveTokenReponse() throws Exception {
 		MockHttpServletRequest request = createTokenIntrospectionRequest(
-				this.tokenValue, OAuth2TokenType.ACCESS_TOKEN.getValue());
+				accessToken.getTokenValue(), OAuth2TokenType.ACCESS_TOKEN.getValue());
+		request.addParameter("custom-param-1", "custom-value-1");
+		request.addParameter("custom-param-2", "custom-value-2");
+
 		MockHttpServletResponse response = new MockHttpServletResponse();
 		FilterChain filterChain = mock(FilterChain.class);
 
-		Authentication clientPrincipal = setupSecurityContext();
-
-		Instant issuedAt = Instant.now();
-		Instant expiresAt = issuedAt.plus(Duration.ofHours(1));
-		String clientId = "clientId";
-		Map<String, Object> tokenIntrospectionClaims = new HashMap<>();
-		tokenIntrospectionClaims.put(ACTIVE, true);
-		tokenIntrospectionClaims.put(CLIENT_ID, clientId);
-		tokenIntrospectionClaims.put(IAT, issuedAt);
-		tokenIntrospectionClaims.put(EXP, expiresAt);
-		tokenIntrospectionClaims.put(TOKEN_TYPE, OAuth2AccessToken.TokenType.BEARER);
-		tokenIntrospectionClaims.put(SCOPE, "scope1 Scope2");
-
-		OAuth2TokenIntrospectionAuthenticationToken tokenIntrospectionAuthentication = new OAuth2TokenIntrospectionAuthenticationToken(
-				clientPrincipal, tokenIntrospectionClaims);
-
-		when(this.authenticationManager.authenticate(any())).thenReturn(tokenIntrospectionAuthentication);
-
 		this.filter.doFilter(request, response, filterChain);
 
-		verifyNoInteractions(filterChain);
-
-		assertThat(response.getStatus()).isEqualTo(HttpStatus.OK.value());
-		OAuth2TokenIntrospectionClaims tokenIntrospectionResponse = readTokenIntrospectionResponse(response);
+		ArgumentCaptor<OAuth2TokenIntrospectionAuthenticationToken> tokenIntrospectionAuthentication =
+				ArgumentCaptor.forClass(OAuth2TokenIntrospectionAuthenticationToken.class);
 
-		Map<String, Object> responseMap = tokenIntrospectionResponse.getClaims();
-		// @formatter:off
-		assertThat(responseMap).contains(
-				entry(ACTIVE, true),
-				entry(CLIENT_ID, clientId),
-				entry(TOKEN_TYPE, OAuth2AccessToken.TokenType.BEARER.getValue()),
-				entry(EXP, expiresAt.getEpochSecond()),
-				entry(IAT, issuedAt.getEpochSecond()))
-		.hasEntrySatisfying(SCOPE, scopesMatchesInAnyOrder)
-		.hasSize(6);
-		// @formatter: on
-	}
+		verifyNoInteractions(filterChain);
+		verify(this.authenticationManager).authenticate(tokenIntrospectionAuthentication.capture());
 
-	private void assertNotActiveTokenResponse(MockHttpServletResponse response) throws Exception {
 		assertThat(response.getStatus()).isEqualTo(HttpStatus.OK.value());
-		OAuth2TokenIntrospectionClaims tokenIntrospectionResponse = readTokenIntrospectionResponse(response);
-		assertThat(tokenIntrospectionResponse.getClaims()).containsEntry("active", false).hasSize(1);
-	}
-
-	private OAuth2Error readError(MockHttpServletResponse response) throws Exception {
-		MockClientHttpResponse httpResponse = new MockClientHttpResponse(response.getContentAsByteArray(),
-				HttpStatus.valueOf(response.getStatus()));
-		return this.errorHttpResponseConverter.read(OAuth2Error.class, httpResponse);
-	}
-
-	private OAuth2TokenIntrospectionClaims readTokenIntrospectionResponse(MockHttpServletResponse response) throws Exception {
-		MockClientHttpResponse httpResponse = new MockClientHttpResponse(response.getContentAsByteArray(),
-				HttpStatus.valueOf(response.getStatus()));
-		return this.tokenIntrospectionHttpResponseConverter.read(OAuth2TokenIntrospectionClaims.class, httpResponse);
+		assertThat(tokenIntrospectionAuthentication.getValue().getAdditionalParameters())
+				.contains(
+						entry("custom-param-1", "custom-value-1"),
+						entry("custom-param-2", "custom-value-2"));
+
+		OAuth2TokenIntrospection tokenIntrospectionResponse = readTokenIntrospectionResponse(response);
+		assertThat(tokenIntrospectionResponse.isActive()).isEqualTo(tokenClaims.isActive());
+		assertThat(tokenIntrospectionResponse.getClientId()).isEqualTo(tokenClaims.getClientId());
+		assertThat(tokenIntrospectionResponse.getUsername()).isEqualTo(tokenClaims.getUsername());
+		assertThat(tokenIntrospectionResponse.getIssuedAt()).isBetween(
+				tokenClaims.getIssuedAt().minusSeconds(1), tokenClaims.getIssuedAt().plusSeconds(1));
+		assertThat(tokenIntrospectionResponse.getExpiresAt()).isBetween(
+				tokenClaims.getExpiresAt().minusSeconds(1), tokenClaims.getExpiresAt().plusSeconds(1));
+		assertThat(tokenIntrospectionResponse.getScope()).containsExactlyInAnyOrderElementsOf(tokenClaims.getScope());
+		assertThat(tokenIntrospectionResponse.getTokenType()).isEqualTo(tokenClaims.getTokenType());
+		assertThat(tokenIntrospectionResponse.getNotBefore()).isBetween(
+				tokenClaims.getNotBefore().minusSeconds(1), tokenClaims.getNotBefore().plusSeconds(1));
+		assertThat(tokenIntrospectionResponse.getSubject()).isEqualTo(tokenClaims.getSubject());
+		assertThat(tokenIntrospectionResponse.getAudience()).containsExactlyInAnyOrderElementsOf(tokenClaims.getAudience());
+		assertThat(tokenIntrospectionResponse.getIssuer()).isEqualTo(tokenClaims.getIssuer());
+		assertThat(tokenIntrospectionResponse.getId()).isEqualTo(tokenClaims.getId());
 	}
 
 	private void doFilterWhenTokenIntrospectionRequestInvalidParameterThenError(String parameterName, String errorCode,
@@ -244,8 +234,6 @@ public class OAuth2TokenIntrospectionEndpointFilterTests {
 		MockHttpServletResponse response = new MockHttpServletResponse();
 		FilterChain filterChain = mock(FilterChain.class);
 
-		setupSecurityContext();
-
 		this.filter.doFilter(request, response, filterChain);
 
 		verifyNoInteractions(filterChain);
@@ -256,23 +244,25 @@ public class OAuth2TokenIntrospectionEndpointFilterTests {
 		assertThat(error.getDescription()).isEqualTo("OAuth 2.0 Token Introspection Parameter: " + parameterName);
 	}
 
-	private static Authentication setupSecurityContext() {
-		RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
-		Authentication clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient);
-		SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
-		securityContext.setAuthentication(clientPrincipal);
-		SecurityContextHolder.setContext(securityContext);
-		return clientPrincipal;
+	private OAuth2Error readError(MockHttpServletResponse response) throws Exception {
+		MockClientHttpResponse httpResponse = new MockClientHttpResponse(
+				response.getContentAsByteArray(), HttpStatus.valueOf(response.getStatus()));
+		return this.errorHttpResponseConverter.read(OAuth2Error.class, httpResponse);
+	}
+
+	private OAuth2TokenIntrospection readTokenIntrospectionResponse(MockHttpServletResponse response) throws Exception {
+		MockClientHttpResponse httpResponse = new MockClientHttpResponse(
+				response.getContentAsByteArray(), HttpStatus.valueOf(response.getStatus()));
+		return this.tokenIntrospectionHttpResponseConverter.read(OAuth2TokenIntrospection.class, httpResponse);
 	}
 
 	private static MockHttpServletRequest createTokenIntrospectionRequest(String token, String tokenTypeHint) {
 		String requestUri = OAuth2TokenIntrospectionEndpointFilter.DEFAULT_TOKEN_INTROSPECTION_ENDPOINT_URI;
 		MockHttpServletRequest request = new MockHttpServletRequest("POST", requestUri);
 		request.setServletPath(requestUri);
-
-		request.addParameter(TOKEN, token);
-		request.addParameter(TOKEN_TYPE_HINT, tokenTypeHint);
+		request.addParameter(OAuth2ParameterNames2.TOKEN, token);
+		request.addParameter(OAuth2ParameterNames2.TOKEN_TYPE_HINT, tokenTypeHint);
 		return request;
+	}
 
 }
-}