瀏覽代碼

Add support for opaque access tokens

Closes gh-500
Joe Grandja 3 年之前
父節點
當前提交
32414451f5
共有 30 個文件被更改,包括 1591 次插入101 次删除
  1. 8 5
      oauth2-authorization-server/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2AuthorizationServerConfigurer.java
  2. 47 19
      oauth2-authorization-server/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2ConfigurerUtils.java
  3. 98 0
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/OAuth2TokenClaimAccessor.java
  4. 67 0
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/OAuth2TokenClaimNames.java
  5. 187 0
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/OAuth2TokenClaimsSet.java
  6. 81 0
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/OAuth2TokenFormat.java
  7. 5 0
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/JwtGenerator.java
  8. 148 0
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/OAuth2AccessTokenGenerator.java
  9. 109 0
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/OAuth2TokenClaimsContext.java
  10. 8 0
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/OAuth2TokenGenerator.java
  11. 6 4
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeAuthenticationProvider.java
  12. 13 5
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientCredentialsAuthenticationProvider.java
  13. 6 4
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2RefreshTokenAuthenticationProvider.java
  14. 9 12
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2TokenIntrospectionAuthenticationProvider.java
  15. 8 2
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/client/JdbcRegisteredClientRepository.java
  16. 8 1
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/ConfigurationSettingNames.java
  17. 26 1
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/TokenSettings.java
  18. 5 1
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/jackson2/OAuth2AuthorizationServerJackson2Module.java
  19. 41 0
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/jackson2/OAuth2TokenFormatMixin.java
  20. 3 3
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcClientRegistrationAuthenticationProvider.java
  21. 150 22
      oauth2-authorization-server/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2TokenIntrospectionTests.java
  22. 97 0
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/core/OAuth2TokenClaimsSetTests.java
  23. 20 0
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/JwtGeneratorTests.java
  24. 185 0
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/OAuth2AccessTokenGeneratorTests.java
  25. 112 0
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/OAuth2TokenClaimsContextTests.java
  26. 33 3
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeAuthenticationProviderTests.java
  27. 34 3
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientCredentialsAuthenticationProviderTests.java
  28. 33 1
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2RefreshTokenAuthenticationProviderTests.java
  29. 23 12
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2TokenIntrospectionAuthenticationProviderTests.java
  30. 21 3
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/TokenSettingsTests.java

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

@@ -26,6 +26,8 @@ import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletRequestWrapper;
 import javax.servlet.http.HttpServletResponse;
 
+import com.nimbusds.jose.jwk.source.JWKSource;
+
 import org.springframework.core.annotation.AnnotationUtils;
 import org.springframework.http.HttpMethod;
 import org.springframework.http.HttpStatus;
@@ -367,11 +369,12 @@ public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBui
 						providerSettings.getTokenIntrospectionEndpoint());
 		builder.addFilterAfter(postProcess(tokenIntrospectionEndpointFilter), FilterSecurityInterceptor.class);
 
-		NimbusJwkSetEndpointFilter jwkSetEndpointFilter =
-				new NimbusJwkSetEndpointFilter(
-						OAuth2ConfigurerUtils.getJwkSource(builder),
-						providerSettings.getJwkSetEndpoint());
-		builder.addFilterBefore(postProcess(jwkSetEndpointFilter), AbstractPreAuthenticatedProcessingFilter.class);
+		JWKSource<com.nimbusds.jose.proc.SecurityContext> jwkSource = OAuth2ConfigurerUtils.getJwkSource(builder);
+		if (jwkSource != null) {
+			NimbusJwkSetEndpointFilter jwkSetEndpointFilter = new NimbusJwkSetEndpointFilter(
+					jwkSource, providerSettings.getJwkSetEndpoint());
+			builder.addFilterBefore(postProcess(jwkSetEndpointFilter), AbstractPreAuthenticatedProcessingFilter.class);
+		}
 
 		OAuth2AuthorizationServerMetadataEndpointFilter authorizationServerMetadataEndpointFilter =
 				new OAuth2AuthorizationServerMetadataEndpointFilter(providerSettings);

+ 47 - 19
oauth2-authorization-server/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2ConfigurerUtils.java

@@ -34,9 +34,11 @@ import org.springframework.security.oauth2.server.authorization.InMemoryOAuth2Au
 import org.springframework.security.oauth2.server.authorization.InMemoryOAuth2AuthorizationService;
 import org.springframework.security.oauth2.server.authorization.JwtEncodingContext;
 import org.springframework.security.oauth2.server.authorization.JwtGenerator;
+import org.springframework.security.oauth2.server.authorization.OAuth2AccessTokenGenerator;
 import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
 import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
 import org.springframework.security.oauth2.server.authorization.OAuth2RefreshTokenGenerator;
+import org.springframework.security.oauth2.server.authorization.OAuth2TokenClaimsContext;
 import org.springframework.security.oauth2.server.authorization.OAuth2TokenCustomizer;
 import org.springframework.security.oauth2.server.authorization.OAuth2TokenGenerator;
 import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
@@ -93,28 +95,55 @@ final class OAuth2ConfigurerUtils {
 		if (tokenGenerator == null) {
 			tokenGenerator = getOptionalBean(builder, OAuth2TokenGenerator.class);
 			if (tokenGenerator == null) {
-				JwtGenerator jwtGenerator = new JwtGenerator(getJwtEncoder(builder));
-				OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer = getJwtCustomizer(builder);
-				if (jwtCustomizer != null) {
-					jwtGenerator.setJwtCustomizer(jwtCustomizer);
+				JwtGenerator jwtGenerator = getJwtGenerator(builder);
+				OAuth2AccessTokenGenerator accessTokenGenerator = new OAuth2AccessTokenGenerator();
+				OAuth2TokenCustomizer<OAuth2TokenClaimsContext> accessTokenCustomizer = getAccessTokenCustomizer(builder);
+				if (accessTokenCustomizer != null) {
+					accessTokenGenerator.setAccessTokenCustomizer(accessTokenCustomizer);
 				}
 				OAuth2RefreshTokenGenerator refreshTokenGenerator = new OAuth2RefreshTokenGenerator();
-				tokenGenerator = new DelegatingOAuth2TokenGenerator(jwtGenerator, refreshTokenGenerator);
+				if (jwtGenerator != null) {
+					tokenGenerator = new DelegatingOAuth2TokenGenerator(
+							jwtGenerator, accessTokenGenerator, refreshTokenGenerator);
+				} else {
+					tokenGenerator = new DelegatingOAuth2TokenGenerator(
+							accessTokenGenerator, refreshTokenGenerator);
+				}
 			}
 			builder.setSharedObject(OAuth2TokenGenerator.class, tokenGenerator);
 		}
 		return tokenGenerator;
 	}
 
+	private static <B extends HttpSecurityBuilder<B>> JwtGenerator getJwtGenerator(B builder) {
+		JwtGenerator jwtGenerator = builder.getSharedObject(JwtGenerator.class);
+		if (jwtGenerator == null) {
+			JwtEncoder jwtEncoder = getJwtEncoder(builder);
+			if (jwtEncoder != null) {
+				jwtGenerator = new JwtGenerator(jwtEncoder);
+				OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer = getJwtCustomizer(builder);
+				if (jwtCustomizer != null) {
+					jwtGenerator.setJwtCustomizer(jwtCustomizer);
+				}
+				builder.setSharedObject(JwtGenerator.class, jwtGenerator);
+			}
+		}
+		return jwtGenerator;
+	}
+
 	private static <B extends HttpSecurityBuilder<B>> JwtEncoder getJwtEncoder(B builder) {
 		JwtEncoder jwtEncoder = builder.getSharedObject(JwtEncoder.class);
 		if (jwtEncoder == null) {
 			jwtEncoder = getOptionalBean(builder, JwtEncoder.class);
 			if (jwtEncoder == null) {
 				JWKSource<SecurityContext> jwkSource = getJwkSource(builder);
-				jwtEncoder = new NimbusJwsEncoder(jwkSource);
+				if (jwkSource != null) {
+					jwtEncoder = new NimbusJwsEncoder(jwkSource);
+				}
+			}
+			if (jwtEncoder != null) {
+				builder.setSharedObject(JwtEncoder.class, jwtEncoder);
 			}
-			builder.setSharedObject(JwtEncoder.class, jwtEncoder);
 		}
 		return jwtEncoder;
 	}
@@ -124,23 +153,22 @@ final class OAuth2ConfigurerUtils {
 		JWKSource<SecurityContext> jwkSource = builder.getSharedObject(JWKSource.class);
 		if (jwkSource == null) {
 			ResolvableType type = ResolvableType.forClassWithGenerics(JWKSource.class, SecurityContext.class);
-			jwkSource = getBean(builder, type);
-			builder.setSharedObject(JWKSource.class, jwkSource);
+			jwkSource = getOptionalBean(builder, type);
+			if (jwkSource != null) {
+				builder.setSharedObject(JWKSource.class, jwkSource);
+			}
 		}
 		return jwkSource;
 	}
 
-	@SuppressWarnings("unchecked")
 	private static <B extends HttpSecurityBuilder<B>> OAuth2TokenCustomizer<JwtEncodingContext> getJwtCustomizer(B builder) {
-		OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer = builder.getSharedObject(OAuth2TokenCustomizer.class);
-		if (jwtCustomizer == null) {
-			ResolvableType type = ResolvableType.forClassWithGenerics(OAuth2TokenCustomizer.class, JwtEncodingContext.class);
-			jwtCustomizer = getOptionalBean(builder, type);
-			if (jwtCustomizer != null) {
-				builder.setSharedObject(OAuth2TokenCustomizer.class, jwtCustomizer);
-			}
-		}
-		return jwtCustomizer;
+		ResolvableType type = ResolvableType.forClassWithGenerics(OAuth2TokenCustomizer.class, JwtEncodingContext.class);
+		return getOptionalBean(builder, type);
+	}
+
+	private static <B extends HttpSecurityBuilder<B>> OAuth2TokenCustomizer<OAuth2TokenClaimsContext> getAccessTokenCustomizer(B builder) {
+		ResolvableType type = ResolvableType.forClassWithGenerics(OAuth2TokenCustomizer.class, OAuth2TokenClaimsContext.class);
+		return getOptionalBean(builder, type);
 	}
 
 	static <B extends HttpSecurityBuilder<B>> ProviderSettings getProviderSettings(B builder) {

+ 98 - 0
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/OAuth2TokenClaimAccessor.java

@@ -0,0 +1,98 @@
+/*
+ * Copyright 2020-2022 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.oauth2.core;
+
+import java.net.URL;
+import java.time.Instant;
+import java.util.List;
+
+/**
+ * A {@link ClaimAccessor} for the "claims" that may be contained in an {@link OAuth2TokenClaimsSet}.
+ *
+ * @author Joe Grandja
+ * @since 0.2.3
+ * @see ClaimAccessor
+ * @see OAuth2TokenClaimNames
+ * @see OAuth2TokenClaimsSet
+ */
+public interface OAuth2TokenClaimAccessor extends ClaimAccessor {
+
+	/**
+	 * Returns the Issuer {@code (iss)} claim which identifies the principal that issued the OAuth 2.0 Token.
+	 *
+	 * @return the Issuer identifier
+	 */
+	default URL getIssuer() {
+		return getClaimAsURL(OAuth2TokenClaimNames.ISS);
+	}
+
+	/**
+	 * Returns the Subject {@code (sub)} claim which identifies the principal that is the subject of the OAuth 2.0 Token.
+	 *
+	 * @return the Subject identifier
+	 */
+	default String getSubject() {
+		return getClaimAsString(OAuth2TokenClaimNames.SUB);
+	}
+
+	/**
+	 * Returns the Audience {@code (aud)} claim which identifies the recipient(s) that the OAuth 2.0 Token is intended for.
+	 *
+	 * @return the Audience(s) that this OAuth 2.0 Token is intended for
+	 */
+	default List<String> getAudience() {
+		return getClaimAsStringList(OAuth2TokenClaimNames.AUD);
+	}
+
+	/**
+	 * Returns the Expiration time {@code (exp)} claim which identifies the expiration time on or after
+	 * which the OAuth 2.0 Token MUST NOT be accepted for processing.
+	 *
+	 * @return the Expiration time on or after which the OAuth 2.0 Token MUST NOT be accepted for processing
+	 */
+	default Instant getExpiresAt() {
+		return getClaimAsInstant(OAuth2TokenClaimNames.EXP);
+	}
+
+	/**
+	 * Returns the Not Before {@code (nbf)} claim which identifies the time before
+	 * which the OAuth 2.0 Token MUST NOT be accepted for processing.
+	 *
+	 * @return the Not Before time before which the OAuth 2.0 Token MUST NOT be accepted for processing
+	 */
+	default Instant getNotBefore() {
+		return getClaimAsInstant(OAuth2TokenClaimNames.NBF);
+	}
+
+	/**
+	 * Returns the Issued at {@code (iat)} claim which identifies the time at which the OAuth 2.0 Token was issued.
+	 *
+	 * @return the Issued at claim which identifies the time at which the OAuth 2.0 Token was issued
+	 */
+	default Instant getIssuedAt() {
+		return getClaimAsInstant(OAuth2TokenClaimNames.IAT);
+	}
+
+	/**
+	 * Returns the ID {@code (jti)} claim which provides a unique identifier for the OAuth 2.0 Token.
+	 *
+	 * @return the ID claim which provides a unique identifier for the OAuth 2.0 Token
+	 */
+	default String getId() {
+		return getClaimAsString(OAuth2TokenClaimNames.JTI);
+	}
+
+}

+ 67 - 0
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/OAuth2TokenClaimNames.java

@@ -0,0 +1,67 @@
+/*
+ * Copyright 2020-2022 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.oauth2.core;
+
+/**
+ * The names of the "claims" that may be contained in an {@link OAuth2TokenClaimsSet}
+ * and are associated to an {@link OAuth2Token}.
+ *
+ * @author Joe Grandja
+ * @since 0.2.3
+ * @see OAuth2TokenClaimAccessor
+ * @see OAuth2TokenClaimsSet
+ * @see OAuth2Token
+ */
+public interface OAuth2TokenClaimNames {
+
+	/**
+	 * {@code iss} - the Issuer claim identifies the principal that issued the OAuth 2.0 Token
+	 */
+	String ISS = "iss";
+
+	/**
+	 * {@code sub} - the Subject claim identifies the principal that is the subject of the OAuth 2.0 Token
+	 */
+	String SUB = "sub";
+
+	/**
+	 * {@code aud} - the Audience claim identifies the recipient(s) that the OAuth 2.0 Token is intended for
+	 */
+	String AUD = "aud";
+
+	/**
+	 * {@code exp} - the Expiration time claim identifies the expiration time on or after
+	 * which the OAuth 2.0 Token MUST NOT be accepted for processing
+	 */
+	String EXP = "exp";
+
+	/**
+	 * {@code nbf} - the Not Before claim identifies the time before which the OAuth 2.0 Token
+	 * MUST NOT be accepted for processing
+	 */
+	String NBF = "nbf";
+
+	/**
+	 * {@code iat} - The Issued at claim identifies the time at which the OAuth 2.0 Token was issued
+	 */
+	String IAT = "iat";
+
+	/**
+	 * {@code jti} - The ID claim provides a unique identifier for the OAuth 2.0 Token
+	 */
+	String JTI = "jti";
+
+}

+ 187 - 0
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/OAuth2TokenClaimsSet.java

@@ -0,0 +1,187 @@
+/*
+ * Copyright 2020-2022 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.oauth2.core;
+
+import java.net.URL;
+import java.time.Instant;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Consumer;
+
+import org.springframework.security.oauth2.core.converter.ClaimConversionService;
+import org.springframework.util.Assert;
+
+/**
+ * A representation of a set of claims that are associated to an {@link OAuth2Token}.
+ *
+ * @author Joe Grandja
+ * @since 0.2.3
+ * @see OAuth2TokenClaimAccessor
+ * @see OAuth2TokenClaimNames
+ * @see OAuth2Token
+ */
+public final class OAuth2TokenClaimsSet implements OAuth2TokenClaimAccessor {
+	private final Map<String, Object> claims;
+
+	private OAuth2TokenClaimsSet(Map<String, Object> claims) {
+		this.claims = Collections.unmodifiableMap(new HashMap<>(claims));
+	}
+
+	@Override
+	public Map<String, Object> getClaims() {
+		return this.claims;
+	}
+
+	/**
+	 * Returns a new {@link Builder}.
+	 *
+	 * @return the {@link Builder}
+	 */
+	public static Builder builder() {
+		return new Builder();
+	}
+
+	/**
+	 * A builder for {@link OAuth2TokenClaimsSet}.
+	 */
+	public static final class Builder {
+		private final Map<String, Object> claims = new HashMap<>();
+
+		private Builder() {
+		}
+
+		/**
+		 * Sets the issuer {@code (iss)} claim, which identifies the principal that issued the OAuth 2.0 Token.
+		 *
+		 * @param issuer the issuer identifier
+		 * @return the {@link Builder}
+		 */
+		public Builder issuer(String issuer) {
+			return claim(OAuth2TokenClaimNames.ISS, issuer);
+		}
+
+		/**
+		 * Sets the subject {@code (sub)} claim, which identifies the principal that is the subject of the OAuth 2.0 Token.
+		 *
+		 * @param subject the subject identifier
+		 * @return the {@link Builder}
+		 */
+		public Builder subject(String subject) {
+			return claim(OAuth2TokenClaimNames.SUB, subject);
+		}
+
+		/**
+		 * Sets the audience {@code (aud)} claim, which identifies the recipient(s) that the OAuth 2.0 Token is intended for.
+		 *
+		 * @param audience the audience that this OAuth 2.0 Token is intended for
+		 * @return the {@link Builder}
+		 */
+		public Builder audience(List<String> audience) {
+			return claim(OAuth2TokenClaimNames.AUD, audience);
+		}
+
+		/**
+		 * Sets the expiration time {@code (exp)} claim, which identifies the time on or after
+		 * which the OAuth 2.0 Token MUST NOT be accepted for processing.
+		 *
+		 * @param expiresAt the time on or after which the OAuth 2.0 Token MUST NOT be accepted for processing
+		 * @return the {@link Builder}
+		 */
+		public Builder expiresAt(Instant expiresAt) {
+			return claim(OAuth2TokenClaimNames.EXP, expiresAt);
+		}
+
+		/**
+		 * Sets the not before {@code (nbf)} claim, which identifies the time before
+		 * which the OAuth 2.0 Token MUST NOT be accepted for processing.
+		 *
+		 * @param notBefore the time before which the OAuth 2.0 Token MUST NOT be accepted for processing
+		 * @return the {@link Builder}
+		 */
+		public Builder notBefore(Instant notBefore) {
+			return claim(OAuth2TokenClaimNames.NBF, notBefore);
+		}
+
+		/**
+		 * Sets the issued at {@code (iat)} claim, which identifies the time at which the OAuth 2.0 Token was issued.
+		 *
+		 * @param issuedAt the time at which the OAuth 2.0 Token was issued
+		 * @return the {@link Builder}
+		 */
+		public Builder issuedAt(Instant issuedAt) {
+			return claim(OAuth2TokenClaimNames.IAT, issuedAt);
+		}
+
+		/**
+		 * Sets the ID {@code (jti)} claim, which provides a unique identifier for the OAuth 2.0 Token.
+		 *
+		 * @param jti the unique identifier for the OAuth 2.0 Token
+		 * @return the {@link Builder}
+		 */
+		public Builder id(String jti) {
+			return claim(OAuth2TokenClaimNames.JTI, jti);
+		}
+
+		/**
+		 * Sets the claim.
+		 *
+		 * @param name the claim name
+		 * @param value the claim value
+		 * @return the {@link Builder}
+		 */
+		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;
+		}
+
+		/**
+		 * A {@code Consumer} to be provided access to the claims allowing the ability to add, replace, or remove.
+		 *
+		 * @param claimsConsumer a {@code Consumer} of the claims
+		 */
+		public Builder claims(Consumer<Map<String, Object>> claimsConsumer) {
+			claimsConsumer.accept(this.claims);
+			return this;
+		}
+
+		/**
+		 * Builds a new {@link OAuth2TokenClaimsSet}.
+		 *
+		 * @return a {@link OAuth2TokenClaimsSet}
+		 */
+		public OAuth2TokenClaimsSet build() {
+			Assert.notEmpty(this.claims, "claims cannot be empty");
+
+			// The value of the 'iss' claim is a String or URL (StringOrURI).
+			// Attempt to convert to URL.
+			Object issuer = this.claims.get(OAuth2TokenClaimNames.ISS);
+			if (issuer != null) {
+				URL convertedValue = ClaimConversionService.getSharedInstance().convert(issuer, URL.class);
+				if (convertedValue != null) {
+					this.claims.put(OAuth2TokenClaimNames.ISS, convertedValue);
+				}
+			}
+
+			return new OAuth2TokenClaimsSet(this.claims);
+		}
+
+	}
+
+}

+ 81 - 0
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/OAuth2TokenFormat.java

@@ -0,0 +1,81 @@
+/*
+ * Copyright 2020-2022 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.oauth2.core;
+
+import java.io.Serializable;
+
+import org.springframework.util.Assert;
+
+/**
+ * Standard data formats for OAuth 2.0 Tokens.
+ *
+ * @author Joe Grandja
+ * @since 0.2.3
+ */
+public final class OAuth2TokenFormat implements Serializable {
+	private static final long serialVersionUID = Version.SERIAL_VERSION_UID;
+
+	/**
+	 * Self-contained tokens use a protected, time-limited data structure that contains token metadata
+	 * and claims of the user and/or client. JSON Web Token (JWT) is a widely used format.
+	 */
+	public static final OAuth2TokenFormat SELF_CONTAINED = new OAuth2TokenFormat("self-contained");
+
+	/**
+	 * Reference (opaque) tokens are unique identifiers that serve as a reference
+	 * to the token metadata and claims of the user and/or client, stored at the provider.
+	 */
+	public static final OAuth2TokenFormat REFERENCE = new OAuth2TokenFormat("reference");
+
+	private final String value;
+
+	/**
+	 * Constructs an {@code OAuth2TokenFormat} using the provided value.
+	 *
+	 * @param value the value of the token format
+	 */
+	public OAuth2TokenFormat(String value) {
+		Assert.hasText(value, "value cannot be empty");
+		this.value = value;
+	}
+
+	/**
+	 * Returns the value of the token format.
+	 *
+	 * @return the value of the token format
+	 */
+	public String getValue() {
+		return this.value;
+	}
+
+	@Override
+	public boolean equals(Object obj) {
+		if (this == obj) {
+			return true;
+		}
+		if (obj == null || this.getClass() != obj.getClass()) {
+			return false;
+		}
+		OAuth2TokenFormat that = (OAuth2TokenFormat) obj;
+		return getValue().equals(that.getValue());
+	}
+
+	@Override
+	public int hashCode() {
+		return getValue().hashCode();
+	}
+
+}

+ 5 - 0
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/JwtGenerator.java

@@ -23,6 +23,7 @@ import java.util.function.Consumer;
 import org.springframework.lang.Nullable;
 import org.springframework.security.oauth2.core.AuthorizationGrantType;
 import org.springframework.security.oauth2.core.OAuth2AccessToken;
+import org.springframework.security.oauth2.core.OAuth2TokenFormat;
 import org.springframework.security.oauth2.core.OAuth2TokenType;
 import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
 import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
@@ -75,6 +76,10 @@ public final class JwtGenerator implements OAuth2TokenGenerator<Jwt> {
 						!OidcParameterNames.ID_TOKEN.equals(context.getTokenType().getValue()))) {
 			return null;
 		}
+		if (OAuth2TokenType.ACCESS_TOKEN.equals(context.getTokenType()) &&
+				!OAuth2TokenFormat.SELF_CONTAINED.equals(context.getRegisteredClient().getTokenSettings().getAccessTokenFormat())) {
+			return null;
+		}
 
 		String issuer = null;
 		if (context.getProviderContext() != null) {

+ 148 - 0
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/OAuth2AccessTokenGenerator.java

@@ -0,0 +1,148 @@
+/*
+ * Copyright 2020-2022 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.oauth2.server.authorization;
+
+import java.time.Instant;
+import java.util.Base64;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Consumer;
+
+import org.springframework.lang.Nullable;
+import org.springframework.security.crypto.keygen.Base64StringKeyGenerator;
+import org.springframework.security.crypto.keygen.StringKeyGenerator;
+import org.springframework.security.oauth2.core.ClaimAccessor;
+import org.springframework.security.oauth2.core.OAuth2AccessToken;
+import org.springframework.security.oauth2.core.OAuth2TokenClaimsSet;
+import org.springframework.security.oauth2.core.OAuth2TokenFormat;
+import org.springframework.security.oauth2.core.OAuth2TokenType;
+import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
+import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
+import org.springframework.util.Assert;
+import org.springframework.util.CollectionUtils;
+import org.springframework.util.StringUtils;
+
+/**
+ * An {@link OAuth2TokenGenerator} that generates a
+ * {@link OAuth2TokenFormat#REFERENCE "reference"} (opaque) {@link OAuth2AccessToken}.
+ *
+ * @author Joe Grandja
+ * @since 0.2.3
+ * @see OAuth2TokenGenerator
+ * @see OAuth2AccessToken
+ * @see OAuth2TokenCustomizer
+ * @see OAuth2TokenClaimsContext
+ * @see OAuth2TokenClaimsSet
+ */
+public final class OAuth2AccessTokenGenerator implements OAuth2TokenGenerator<OAuth2AccessToken> {
+	private final StringKeyGenerator accessTokenGenerator =
+			new Base64StringKeyGenerator(Base64.getUrlEncoder().withoutPadding(), 96);
+	private OAuth2TokenCustomizer<OAuth2TokenClaimsContext> accessTokenCustomizer;
+
+	@Nullable
+	@Override
+	public OAuth2AccessToken generate(OAuth2TokenContext context) {
+		if (!OAuth2TokenType.ACCESS_TOKEN.equals(context.getTokenType()) ||
+				!OAuth2TokenFormat.REFERENCE.equals(context.getRegisteredClient().getTokenSettings().getAccessTokenFormat())) {
+			return null;
+		}
+
+		String issuer = null;
+		if (context.getProviderContext() != null) {
+			issuer = context.getProviderContext().getIssuer();
+		}
+		RegisteredClient registeredClient = context.getRegisteredClient();
+
+		Instant issuedAt = Instant.now();
+		Instant expiresAt = issuedAt.plus(registeredClient.getTokenSettings().getAccessTokenTimeToLive());
+
+		// @formatter:off
+		OAuth2TokenClaimsSet.Builder claimsBuilder = OAuth2TokenClaimsSet.builder();
+		if (StringUtils.hasText(issuer)) {
+			claimsBuilder.issuer(issuer);
+		}
+		claimsBuilder
+				.subject(context.getPrincipal().getName())
+				.audience(Collections.singletonList(registeredClient.getClientId()))
+				.issuedAt(issuedAt)
+				.expiresAt(expiresAt)
+				.notBefore(issuedAt)
+				.id(UUID.randomUUID().toString());
+		if (!CollectionUtils.isEmpty(context.getAuthorizedScopes())) {
+			claimsBuilder.claim(OAuth2ParameterNames.SCOPE, context.getAuthorizedScopes());
+		}
+		// @formatter:on
+
+		if (this.accessTokenCustomizer != null) {
+			// @formatter:off
+			OAuth2TokenClaimsContext.Builder accessTokenContextBuilder = OAuth2TokenClaimsContext.with(claimsBuilder)
+					.registeredClient(context.getRegisteredClient())
+					.principal(context.getPrincipal())
+					.providerContext(context.getProviderContext())
+					.authorizedScopes(context.getAuthorizedScopes())
+					.tokenType(context.getTokenType())
+					.authorizationGrantType(context.getAuthorizationGrantType());
+			if (context.getAuthorization() != null) {
+				accessTokenContextBuilder.authorization(context.getAuthorization());
+			}
+			if (context.getAuthorizationGrant() != null) {
+				accessTokenContextBuilder.authorizationGrant(context.getAuthorizationGrant());
+			}
+			// @formatter:on
+
+			OAuth2TokenClaimsContext accessTokenContext = accessTokenContextBuilder.build();
+			this.accessTokenCustomizer.customize(accessTokenContext);
+		}
+
+		OAuth2TokenClaimsSet accessTokenClaimsSet = claimsBuilder.build();
+
+		OAuth2AccessToken accessToken = new OAuth2AccessTokenClaims(OAuth2AccessToken.TokenType.BEARER,
+				this.accessTokenGenerator.generateKey(), accessTokenClaimsSet.getIssuedAt(), accessTokenClaimsSet.getExpiresAt(),
+				context.getAuthorizedScopes(), accessTokenClaimsSet.getClaims());
+
+		return accessToken;
+	}
+
+	/**
+	 * Sets the {@link OAuth2TokenCustomizer} that customizes the
+	 * {@link OAuth2TokenClaimsContext.Builder#claims(Consumer) claims} for the {@link OAuth2AccessToken}.
+	 *
+	 * @param accessTokenCustomizer the {@link OAuth2TokenCustomizer} that customizes the claims for the {@code OAuth2AccessToken}
+	 */
+	public void setAccessTokenCustomizer(OAuth2TokenCustomizer<OAuth2TokenClaimsContext> accessTokenCustomizer) {
+		Assert.notNull(accessTokenCustomizer, "accessTokenCustomizer cannot be null");
+		this.accessTokenCustomizer = accessTokenCustomizer;
+	}
+
+	private static final class OAuth2AccessTokenClaims extends OAuth2AccessToken implements ClaimAccessor {
+		private final Map<String, Object> claims;
+
+		private OAuth2AccessTokenClaims(TokenType tokenType, String tokenValue,
+				Instant issuedAt, Instant expiresAt, Set<String> scopes, Map<String, Object> claims) {
+			super(tokenType, tokenValue, issuedAt, expiresAt, scopes);
+			this.claims = claims;
+		}
+
+		@Override
+		public Map<String, Object> getClaims() {
+			return this.claims;
+		}
+
+	}
+
+}

+ 109 - 0
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/OAuth2TokenClaimsContext.java

@@ -0,0 +1,109 @@
+/*
+ * Copyright 2020-2022 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.oauth2.server.authorization;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Consumer;
+
+import org.springframework.lang.Nullable;
+import org.springframework.security.oauth2.core.OAuth2TokenClaimsSet;
+import org.springframework.util.Assert;
+
+/**
+ * An {@link OAuth2TokenContext} implementation that provides access
+ * to the {@link #getClaims() claims} of an OAuth 2.0 Token, allowing the ability to customize.
+ *
+ * @author Joe Grandja
+ * @since 0.2.3
+ * @see OAuth2TokenContext
+ * @see OAuth2TokenClaimsSet.Builder
+ */
+public final class OAuth2TokenClaimsContext implements OAuth2TokenContext {
+	private final Map<Object, Object> context;
+
+	private OAuth2TokenClaimsContext(Map<Object, Object> context) {
+		this.context = Collections.unmodifiableMap(new HashMap<>(context));
+	}
+
+	@SuppressWarnings("unchecked")
+	@Nullable
+	@Override
+	public <V> V get(Object key) {
+		return hasKey(key) ? (V) this.context.get(key) : null;
+	}
+
+	@Override
+	public boolean hasKey(Object key) {
+		Assert.notNull(key, "key cannot be null");
+		return this.context.containsKey(key);
+	}
+
+	/**
+	 * Returns the {@link OAuth2TokenClaimsSet.Builder claims}
+	 * allowing the ability to add, replace, or remove.
+	 *
+	 * @return the {@link OAuth2TokenClaimsSet.Builder}
+	 */
+	public OAuth2TokenClaimsSet.Builder getClaims() {
+		return get(OAuth2TokenClaimsSet.Builder.class);
+	}
+
+	/**
+	 * Constructs a new {@link Builder} with the provided claims.
+	 *
+	 * @param claimsBuilder the claims to initialize the builder
+	 * @return the {@link Builder}
+	 */
+	public static Builder with(OAuth2TokenClaimsSet.Builder claimsBuilder) {
+		return new Builder(claimsBuilder);
+	}
+
+	/**
+	 * A builder for {@link OAuth2TokenClaimsContext}.
+	 */
+	public static final class Builder extends AbstractBuilder<OAuth2TokenClaimsContext, Builder> {
+
+		private Builder(OAuth2TokenClaimsSet.Builder claimsBuilder) {
+			Assert.notNull(claimsBuilder, "claimsBuilder cannot be null");
+			put(OAuth2TokenClaimsSet.Builder.class, claimsBuilder);
+		}
+
+		/**
+		 * A {@code Consumer} of the {@link OAuth2TokenClaimsSet.Builder claims}
+		 * allowing the ability to add, replace, or remove.
+		 *
+		 * @param claimsConsumer a {@code Consumer} of the {@link OAuth2TokenClaimsSet.Builder claims}
+		 * @return the {@link Builder} for further configuration
+		 */
+		public Builder claims(Consumer<OAuth2TokenClaimsSet.Builder> claimsConsumer) {
+			claimsConsumer.accept(get(OAuth2TokenClaimsSet.Builder.class));
+			return this;
+		}
+
+		/**
+		 * Builds a new {@link OAuth2TokenClaimsContext}.
+		 *
+		 * @return the {@link OAuth2TokenClaimsContext}
+		 */
+		public OAuth2TokenClaimsContext build() {
+			return new OAuth2TokenClaimsContext(getContext());
+		}
+
+	}
+
+}

+ 8 - 0
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/OAuth2TokenGenerator.java

@@ -16,7 +16,9 @@
 package org.springframework.security.oauth2.server.authorization;
 
 import org.springframework.lang.Nullable;
+import org.springframework.security.oauth2.core.ClaimAccessor;
 import org.springframework.security.oauth2.core.OAuth2Token;
+import org.springframework.security.oauth2.core.OAuth2TokenClaimsSet;
 
 /**
  * Implementations of this interface are responsible for generating an {@link OAuth2Token}
@@ -26,6 +28,8 @@ import org.springframework.security.oauth2.core.OAuth2Token;
  * @since 0.2.3
  * @see OAuth2Token
  * @see OAuth2TokenContext
+ * @see OAuth2TokenClaimsSet
+ * @see ClaimAccessor
  * @param <T> the type of the OAuth 2.0 Token
  */
 @FunctionalInterface
@@ -35,6 +39,10 @@ public interface OAuth2TokenGenerator<T extends OAuth2Token> {
 	 * Generate an OAuth 2.0 Token using the attributes contained in the {@link OAuth2TokenContext},
 	 * or return {@code null} if the {@link OAuth2TokenContext#getTokenType()} is not supported.
 	 *
+	 * <p>
+	 * If the returned {@link OAuth2Token} has a set of claims, it should implement {@link ClaimAccessor}
+	 * in order for it to be stored with the {@link OAuth2Authorization}.
+	 *
 	 * @param context the context containing the OAuth 2.0 Token attributes
 	 * @return an {@link OAuth2Token} or {@code null} if the {@link OAuth2TokenContext#getTokenType()} is not supported
 	 */

+ 6 - 4
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeAuthenticationProvider.java

@@ -27,6 +27,7 @@ import org.springframework.security.authentication.AuthenticationProvider;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.AuthenticationException;
 import org.springframework.security.oauth2.core.AuthorizationGrantType;
+import org.springframework.security.oauth2.core.ClaimAccessor;
 import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
 import org.springframework.security.oauth2.core.OAuth2AccessToken;
 import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
@@ -47,6 +48,7 @@ import org.springframework.security.oauth2.server.authorization.DefaultOAuth2Tok
 import org.springframework.security.oauth2.server.authorization.DelegatingOAuth2TokenGenerator;
 import org.springframework.security.oauth2.server.authorization.JwtEncodingContext;
 import org.springframework.security.oauth2.server.authorization.JwtGenerator;
+import org.springframework.security.oauth2.server.authorization.OAuth2AccessTokenGenerator;
 import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
 import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
 import org.springframework.security.oauth2.server.authorization.OAuth2RefreshTokenGenerator;
@@ -103,8 +105,8 @@ public final class OAuth2AuthorizationCodeAuthenticationProvider implements Auth
 		Assert.notNull(jwtEncoder, "jwtEncoder cannot be null");
 		this.authorizationService = authorizationService;
 		this.jwtGenerator = new JwtGenerator(jwtEncoder);
-		this.tokenGenerator = new DelegatingOAuth2TokenGenerator(
-				this.jwtGenerator, new OAuth2RefreshTokenGenerator());
+		this.tokenGenerator = new DelegatingOAuth2TokenGenerator(this.jwtGenerator,
+				new OAuth2AccessTokenGenerator(), new OAuth2RefreshTokenGenerator());
 	}
 
 	/**
@@ -216,9 +218,9 @@ public final class OAuth2AuthorizationCodeAuthenticationProvider implements Auth
 		OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
 				generatedAccessToken.getTokenValue(), generatedAccessToken.getIssuedAt(),
 				generatedAccessToken.getExpiresAt(), tokenContext.getAuthorizedScopes());
-		if (generatedAccessToken instanceof Jwt) {
+		if (generatedAccessToken instanceof ClaimAccessor) {
 			authorizationBuilder.token(accessToken, (metadata) ->
-					metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, ((Jwt) generatedAccessToken).getClaims()));
+					metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, ((ClaimAccessor) generatedAccessToken).getClaims()));
 		} else {
 			authorizationBuilder.accessToken(accessToken);
 		}

+ 13 - 5
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientCredentialsAuthenticationProvider.java

@@ -23,6 +23,7 @@ import org.springframework.security.authentication.AuthenticationProvider;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.AuthenticationException;
 import org.springframework.security.oauth2.core.AuthorizationGrantType;
+import org.springframework.security.oauth2.core.ClaimAccessor;
 import org.springframework.security.oauth2.core.OAuth2AccessToken;
 import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
 import org.springframework.security.oauth2.core.OAuth2Error;
@@ -32,8 +33,10 @@ import org.springframework.security.oauth2.core.OAuth2TokenType;
 import org.springframework.security.oauth2.jwt.Jwt;
 import org.springframework.security.oauth2.jwt.JwtEncoder;
 import org.springframework.security.oauth2.server.authorization.DefaultOAuth2TokenContext;
+import org.springframework.security.oauth2.server.authorization.DelegatingOAuth2TokenGenerator;
 import org.springframework.security.oauth2.server.authorization.JwtEncodingContext;
 import org.springframework.security.oauth2.server.authorization.JwtGenerator;
+import org.springframework.security.oauth2.server.authorization.OAuth2AccessTokenGenerator;
 import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
 import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
 import org.springframework.security.oauth2.server.authorization.OAuth2TokenContext;
@@ -65,6 +68,9 @@ public final class OAuth2ClientCredentialsAuthenticationProvider implements Auth
 	private final OAuth2AuthorizationService authorizationService;
 	private final OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator;
 
+	// TODO Remove after removing @Deprecated OAuth2ClientCredentialsAuthenticationProvider(OAuth2AuthorizationService, JwtEncoder)
+	private JwtGenerator jwtGenerator;
+
 	/**
 	 * Constructs an {@code OAuth2ClientCredentialsAuthenticationProvider} using the provided parameters.
 	 *
@@ -78,7 +84,9 @@ public final class OAuth2ClientCredentialsAuthenticationProvider implements Auth
 		Assert.notNull(authorizationService, "authorizationService cannot be null");
 		Assert.notNull(jwtEncoder, "jwtEncoder cannot be null");
 		this.authorizationService = authorizationService;
-		this.tokenGenerator = new JwtGenerator(jwtEncoder);
+		this.jwtGenerator = new JwtGenerator(jwtEncoder);
+		this.tokenGenerator = new DelegatingOAuth2TokenGenerator(
+				this.jwtGenerator, new OAuth2AccessTokenGenerator());
 	}
 
 	/**
@@ -107,8 +115,8 @@ public final class OAuth2ClientCredentialsAuthenticationProvider implements Auth
 	@Deprecated
 	public void setJwtCustomizer(OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer) {
 		Assert.notNull(jwtCustomizer, "jwtCustomizer cannot be null");
-		if (this.tokenGenerator instanceof JwtGenerator) {
-			((JwtGenerator) this.tokenGenerator).setJwtCustomizer(jwtCustomizer);
+		if (this.jwtGenerator != null) {
+			this.jwtGenerator.setJwtCustomizer(jwtCustomizer);
 		}
 	}
 
@@ -167,9 +175,9 @@ public final class OAuth2ClientCredentialsAuthenticationProvider implements Auth
 				.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
 				.attribute(OAuth2Authorization.AUTHORIZED_SCOPE_ATTRIBUTE_NAME, authorizedScopes);
 		// @formatter:on
-		if (generatedAccessToken instanceof Jwt) {
+		if (generatedAccessToken instanceof ClaimAccessor) {
 			authorizationBuilder.token(accessToken, (metadata) ->
-					metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, ((Jwt) generatedAccessToken).getClaims()));
+					metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, ((ClaimAccessor) generatedAccessToken).getClaims()));
 		} else {
 			authorizationBuilder.accessToken(accessToken);
 		}

+ 6 - 4
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2RefreshTokenAuthenticationProvider.java

@@ -28,6 +28,7 @@ import org.springframework.security.authentication.AuthenticationProvider;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.AuthenticationException;
 import org.springframework.security.oauth2.core.AuthorizationGrantType;
+import org.springframework.security.oauth2.core.ClaimAccessor;
 import org.springframework.security.oauth2.core.OAuth2AccessToken;
 import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
 import org.springframework.security.oauth2.core.OAuth2Error;
@@ -44,6 +45,7 @@ import org.springframework.security.oauth2.server.authorization.DefaultOAuth2Tok
 import org.springframework.security.oauth2.server.authorization.DelegatingOAuth2TokenGenerator;
 import org.springframework.security.oauth2.server.authorization.JwtEncodingContext;
 import org.springframework.security.oauth2.server.authorization.JwtGenerator;
+import org.springframework.security.oauth2.server.authorization.OAuth2AccessTokenGenerator;
 import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
 import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
 import org.springframework.security.oauth2.server.authorization.OAuth2RefreshTokenGenerator;
@@ -97,8 +99,8 @@ public final class OAuth2RefreshTokenAuthenticationProvider implements Authentic
 		Assert.notNull(jwtEncoder, "jwtEncoder cannot be null");
 		this.authorizationService = authorizationService;
 		this.jwtGenerator = new JwtGenerator(jwtEncoder);
-		this.tokenGenerator = new DelegatingOAuth2TokenGenerator(
-				this.jwtGenerator, new OAuth2RefreshTokenGenerator());
+		this.tokenGenerator = new DelegatingOAuth2TokenGenerator(this.jwtGenerator,
+				new OAuth2AccessTokenGenerator(), new OAuth2RefreshTokenGenerator());
 	}
 
 	/**
@@ -215,9 +217,9 @@ public final class OAuth2RefreshTokenAuthenticationProvider implements Authentic
 		OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
 				generatedAccessToken.getTokenValue(), generatedAccessToken.getIssuedAt(),
 				generatedAccessToken.getExpiresAt(), tokenContext.getAuthorizedScopes());
-		if (generatedAccessToken instanceof Jwt) {
+		if (generatedAccessToken instanceof ClaimAccessor) {
 			authorizationBuilder.token(accessToken, (metadata) -> {
-				metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, ((Jwt) generatedAccessToken).getClaims());
+				metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, ((ClaimAccessor) generatedAccessToken).getClaims());
 				metadata.put(OAuth2Authorization.Token.INVALIDATED_METADATA_NAME, false);
 			});
 		} else {

+ 9 - 12
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2TokenIntrospectionAuthenticationProvider.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2020-2021 the original author or authors.
+ * Copyright 2020-2022 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -18,15 +18,14 @@ package org.springframework.security.oauth2.server.authorization.authentication;
 import java.net.URL;
 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.core.OAuth2TokenClaimAccessor;
 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.OAuth2AuthorizationService;
 import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
@@ -121,25 +120,23 @@ public final class OAuth2TokenIntrospectionAuthenticationProvider implements Aut
 			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;
+			if (!CollectionUtils.isEmpty(authorizedToken.getClaims())) {
+				OAuth2TokenClaimAccessor accessTokenClaims = authorizedToken::getClaims;
 
-				Instant notBefore = jwtClaims.getNotBefore();
+				Instant notBefore = accessTokenClaims.getNotBefore();
 				if (notBefore != null) {
 					tokenClaims.notBefore(notBefore);
 				}
-				tokenClaims.subject(jwtClaims.getSubject());
-				List<String> audience = jwtClaims.getAudience();
+				tokenClaims.subject(accessTokenClaims.getSubject());
+				List<String> audience = accessTokenClaims.getAudience();
 				if (!CollectionUtils.isEmpty(audience)) {
 					tokenClaims.audiences(audiences -> audiences.addAll(audience));
 				}
-				URL issuer = jwtClaims.getIssuer();
+				URL issuer = accessTokenClaims.getIssuer();
 				if (issuer != null) {
 					tokenClaims.issuer(issuer.toExternalForm());
 				}
-				String jti = jwtClaims.getId();
+				String jti = accessTokenClaims.getId();
 				if (StringUtils.hasText(jti)) {
 					tokenClaims.id(jti);
 				}

+ 8 - 2
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/client/JdbcRegisteredClientRepository.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2020-2021 the original author or authors.
+ * Copyright 2020-2022 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -40,7 +40,9 @@ import org.springframework.security.crypto.password.PasswordEncoder;
 import org.springframework.security.jackson2.SecurityJackson2Modules;
 import org.springframework.security.oauth2.core.AuthorizationGrantType;
 import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
+import org.springframework.security.oauth2.core.OAuth2TokenFormat;
 import org.springframework.security.oauth2.server.authorization.config.ClientSettings;
+import org.springframework.security.oauth2.server.authorization.config.ConfigurationSettingNames;
 import org.springframework.security.oauth2.server.authorization.config.TokenSettings;
 import org.springframework.security.oauth2.server.authorization.jackson2.OAuth2AuthorizationServerJackson2Module;
 import org.springframework.util.Assert;
@@ -241,7 +243,11 @@ public class JdbcRegisteredClientRepository implements RegisteredClientRepositor
 			builder.clientSettings(ClientSettings.withSettings(clientSettingsMap).build());
 
 			Map<String, Object> tokenSettingsMap = parseMap(rs.getString("token_settings"));
-			builder.tokenSettings(TokenSettings.withSettings(tokenSettingsMap).build());
+			TokenSettings.Builder tokenSettingsBuilder = TokenSettings.withSettings(tokenSettingsMap);
+			if (!tokenSettingsMap.containsKey(ConfigurationSettingNames.Token.ACCESS_TOKEN_FORMAT)) {
+				tokenSettingsBuilder.accessTokenFormat(OAuth2TokenFormat.SELF_CONTAINED);
+			}
+			builder.tokenSettings(tokenSettingsBuilder.build());
 
 			return builder.build();
 		}

+ 8 - 1
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/ConfigurationSettingNames.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2020-2021 the original author or authors.
+ * Copyright 2020-2022 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -16,6 +16,7 @@
 package org.springframework.security.oauth2.server.authorization.config;
 
 import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
+import org.springframework.security.oauth2.core.OAuth2TokenFormat;
 import org.springframework.security.oauth2.core.oidc.OidcIdToken;
 import org.springframework.security.oauth2.jose.jws.JwsAlgorithm;
 import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
@@ -132,6 +133,12 @@ public final class ConfigurationSettingNames {
 		 */
 		public static final String ACCESS_TOKEN_TIME_TO_LIVE = TOKEN_SETTINGS_NAMESPACE.concat("access-token-time-to-live");
 
+		/**
+		 * Set the {@link OAuth2TokenFormat token format} for an access token.
+		 * @since 0.2.3
+		 */
+		public static final String ACCESS_TOKEN_FORMAT = TOKEN_SETTINGS_NAMESPACE.concat("access-token-format");
+
 		/**
 		 * Set to {@code true} if refresh tokens are reused when returning the access token response,
 		 * or {@code false} if a new refresh token is issued.

+ 26 - 1
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/TokenSettings.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2020-2021 the original author or authors.
+ * Copyright 2020-2022 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -18,6 +18,7 @@ package org.springframework.security.oauth2.server.authorization.config;
 import java.time.Duration;
 import java.util.Map;
 
+import org.springframework.security.oauth2.core.OAuth2TokenFormat;
 import org.springframework.security.oauth2.core.oidc.OidcIdToken;
 import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
 import org.springframework.util.Assert;
@@ -45,6 +46,17 @@ public final class TokenSettings extends AbstractSettings {
 		return getSetting(ConfigurationSettingNames.Token.ACCESS_TOKEN_TIME_TO_LIVE);
 	}
 
+	/**
+	 * Returns the token format for an access token.
+	 * The default is {@link OAuth2TokenFormat#SELF_CONTAINED}.
+	 *
+	 * @return the token format for an access token
+	 * @since 0.2.3
+	 */
+	public OAuth2TokenFormat getAccessTokenFormat() {
+		return getSetting(ConfigurationSettingNames.Token.ACCESS_TOKEN_FORMAT);
+	}
+
 	/**
 	 * Returns {@code true} if refresh tokens are reused when returning the access token response,
 	 * or {@code false} if a new refresh token is issued. The default is {@code true}.
@@ -80,6 +92,7 @@ public final class TokenSettings extends AbstractSettings {
 	public static Builder builder() {
 		return new Builder()
 				.accessTokenTimeToLive(Duration.ofMinutes(5))
+				.accessTokenFormat(OAuth2TokenFormat.SELF_CONTAINED)
 				.reuseRefreshTokens(true)
 				.refreshTokenTimeToLive(Duration.ofMinutes(60))
 				.idTokenSignatureAlgorithm(SignatureAlgorithm.RS256);
@@ -117,6 +130,18 @@ public final class TokenSettings extends AbstractSettings {
 			return setting(ConfigurationSettingNames.Token.ACCESS_TOKEN_TIME_TO_LIVE, accessTokenTimeToLive);
 		}
 
+		/**
+		 * Set the token format for an access token.
+		 *
+		 * @param accessTokenFormat the token format for an access token
+		 * @return the {@link Builder} for further configuration
+		 * @since 0.2.3
+		 */
+		public Builder accessTokenFormat(OAuth2TokenFormat accessTokenFormat) {
+			Assert.notNull(accessTokenFormat, "accessTokenFormat cannot be null");
+			return setting(ConfigurationSettingNames.Token.ACCESS_TOKEN_FORMAT, accessTokenFormat);
+		}
+
 		/**
 		 * Set to {@code true} if refresh tokens are reused when returning the access token response,
 		 * or {@code false} if a new refresh token is issued.

+ 5 - 1
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/jackson2/OAuth2AuthorizationServerJackson2Module.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2020-2021 the original author or authors.
+ * Copyright 2020-2022 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -24,6 +24,7 @@ import com.fasterxml.jackson.core.Version;
 import com.fasterxml.jackson.databind.module.SimpleModule;
 
 import org.springframework.security.jackson2.SecurityJackson2Modules;
+import org.springframework.security.oauth2.core.OAuth2TokenFormat;
 import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
 import org.springframework.security.oauth2.jose.jws.MacAlgorithm;
 import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
@@ -38,6 +39,7 @@ import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
  * <li>{@link OAuth2AuthorizationRequestMixin}</li>
  * <li>{@link DurationMixin}</li>
  * <li>{@link JwsAlgorithmMixin}</li>
+ * <li>{@link OAuth2TokenFormatMixin}</li>
  * </ul>
  *
  * If not already enabled, default typing will be automatically enabled as type info is
@@ -60,6 +62,7 @@ import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
  * @see OAuth2AuthorizationRequestMixin
  * @see DurationMixin
  * @see JwsAlgorithmMixin
+ * @see OAuth2TokenFormatMixin
  */
 public class OAuth2AuthorizationServerJackson2Module extends SimpleModule {
 
@@ -78,6 +81,7 @@ public class OAuth2AuthorizationServerJackson2Module extends SimpleModule {
 		context.setMixInAnnotations(Duration.class, DurationMixin.class);
 		context.setMixInAnnotations(SignatureAlgorithm.class, JwsAlgorithmMixin.class);
 		context.setMixInAnnotations(MacAlgorithm.class, JwsAlgorithmMixin.class);
+		context.setMixInAnnotations(OAuth2TokenFormat.class, OAuth2TokenFormatMixin.class);
 	}
 
 }

+ 41 - 0
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/jackson2/OAuth2TokenFormatMixin.java

@@ -0,0 +1,41 @@
+/*
+ * Copyright 2020-2022 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.oauth2.server.authorization.jackson2;
+
+import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+
+import org.springframework.security.oauth2.core.OAuth2TokenFormat;
+
+/**
+ * This mixin class is used to serialize/deserialize {@link OAuth2TokenFormat}.
+ *
+ * @author Joe Grandja
+ * @since 0.2.3
+ * @see OAuth2TokenFormat
+ */
+@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
+@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE,
+		isGetterVisibility = JsonAutoDetect.Visibility.NONE)
+abstract class OAuth2TokenFormatMixin {
+
+	@JsonCreator
+	OAuth2TokenFormatMixin(@JsonProperty("value") String value) {
+	}
+
+}

+ 3 - 3
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcClientRegistrationAuthenticationProvider.java

@@ -33,6 +33,7 @@ import org.springframework.security.core.AuthenticationException;
 import org.springframework.security.crypto.keygen.Base64StringKeyGenerator;
 import org.springframework.security.crypto.keygen.StringKeyGenerator;
 import org.springframework.security.oauth2.core.AuthorizationGrantType;
+import org.springframework.security.oauth2.core.ClaimAccessor;
 import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
 import org.springframework.security.oauth2.core.OAuth2AccessToken;
 import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
@@ -46,7 +47,6 @@ import org.springframework.security.oauth2.core.oidc.OidcClientMetadataClaimName
 import org.springframework.security.oauth2.core.oidc.OidcClientRegistration;
 import org.springframework.security.oauth2.jose.jws.MacAlgorithm;
 import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
-import org.springframework.security.oauth2.jwt.Jwt;
 import org.springframework.security.oauth2.jwt.JwtEncoder;
 import org.springframework.security.oauth2.server.authorization.DefaultOAuth2TokenContext;
 import org.springframework.security.oauth2.server.authorization.JwtGenerator;
@@ -286,9 +286,9 @@ public final class OidcClientRegistrationAuthenticationProvider implements Authe
 				.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
 				.attribute(OAuth2Authorization.AUTHORIZED_SCOPE_ATTRIBUTE_NAME, authorizedScopes);
 		// @formatter:on
-		if (registrationAccessToken instanceof Jwt) {
+		if (registrationAccessToken instanceof ClaimAccessor) {
 			authorizationBuilder.token(accessToken, (metadata) ->
-					metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, ((Jwt) registrationAccessToken).getClaims()));
+					metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, ((ClaimAccessor) registrationAccessToken).getClaims()));
 		} else {
 			authorizationBuilder.accessToken(accessToken);
 		}

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

@@ -1,5 +1,5 @@
 /*
- * Copyright 2020-2021 the original author or authors.
+ * Copyright 2020-2022 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -15,10 +15,16 @@
  */
 package org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization;
 
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
 import java.time.Duration;
 import java.time.Instant;
+import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Base64;
+import java.util.Collections;
 import java.util.HashSet;
+import java.util.List;
 
 import com.nimbusds.jose.jwk.JWKSet;
 import com.nimbusds.jose.jwk.source.JWKSource;
@@ -28,10 +34,12 @@ import org.junit.AfterClass;
 import org.junit.BeforeClass;
 import org.junit.Rule;
 import org.junit.Test;
+import org.mockito.ArgumentCaptor;
 
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Import;
+import org.springframework.http.HttpHeaders;
 import org.springframework.http.HttpStatus;
 import org.springframework.http.converter.HttpMessageConverter;
 import org.springframework.jdbc.core.JdbcOperations;
@@ -48,18 +56,26 @@ import org.springframework.security.config.test.SpringTestRule;
 import org.springframework.security.crypto.password.NoOpPasswordEncoder;
 import org.springframework.security.crypto.password.PasswordEncoder;
 import org.springframework.security.oauth2.core.AbstractOAuth2Token;
+import org.springframework.security.oauth2.core.AuthorizationGrantType;
 import org.springframework.security.oauth2.core.OAuth2AccessToken;
+import org.springframework.security.oauth2.core.OAuth2AuthorizationCode;
 import org.springframework.security.oauth2.core.OAuth2RefreshToken;
+import org.springframework.security.oauth2.core.OAuth2TokenClaimsSet;
+import org.springframework.security.oauth2.core.OAuth2TokenFormat;
 import org.springframework.security.oauth2.core.OAuth2TokenIntrospection;
 import org.springframework.security.oauth2.core.OAuth2TokenType;
+import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
 import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
+import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter;
 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.JdbcOAuth2AuthorizationConsentService;
 import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationService;
 import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
+import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
 import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
+import org.springframework.security.oauth2.server.authorization.OAuth2TokenClaimsContext;
+import org.springframework.security.oauth2.server.authorization.OAuth2TokenCustomizer;
 import org.springframework.security.oauth2.server.authorization.TestOAuth2Authorizations;
 import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository;
 import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository.RegisteredClientParametersMapper;
@@ -67,6 +83,7 @@ import org.springframework.security.oauth2.server.authorization.client.Registere
 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.config.ProviderSettings;
+import org.springframework.security.oauth2.server.authorization.config.TokenSettings;
 import org.springframework.security.oauth2.server.authorization.jackson2.TestingAuthenticationTokenMixin;
 import org.springframework.test.web.servlet.MockMvc;
 import org.springframework.test.web.servlet.MvcResult;
@@ -74,7 +91,8 @@ import org.springframework.util.LinkedMultiValueMap;
 import org.springframework.util.MultiValueMap;
 
 import static org.assertj.core.api.Assertions.assertThat;
-import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
 import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
 
@@ -88,8 +106,11 @@ public class OAuth2TokenIntrospectionTests {
 	private static EmbeddedDatabase db;
 	private static JWKSource<SecurityContext> jwkSource;
 	private static ProviderSettings providerSettings;
-	private final HttpMessageConverter<OAuth2TokenIntrospection> tokenIntrospectionHttpResponseConverter =
+	private static OAuth2TokenCustomizer<OAuth2TokenClaimsContext> accessTokenCustomizer;
+	private static final HttpMessageConverter<OAuth2TokenIntrospection> tokenIntrospectionHttpResponseConverter =
 			new OAuth2TokenIntrospectionHttpMessageConverter();
+	private static final HttpMessageConverter<OAuth2AccessTokenResponse> accessTokenHttpResponseConverter =
+			new OAuth2AccessTokenResponseHttpMessageConverter();
 
 	@Rule
 	public final SpringTestRule spring = new SpringTestRule();
@@ -111,6 +132,7 @@ public class OAuth2TokenIntrospectionTests {
 		JWKSet jwkSet = new JWKSet(TestJwks.DEFAULT_RSA_JWK);
 		jwkSource = (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
 		providerSettings = ProviderSettings.builder().tokenIntrospectionEndpoint("/test/introspect").build();
+		accessTokenCustomizer = mock(OAuth2TokenCustomizer.class);
 		db = new EmbeddedDatabaseBuilder()
 				.generateUniqueName(true)
 				.setType(EmbeddedDatabaseType.HSQL)
@@ -145,9 +167,19 @@ public class OAuth2TokenIntrospectionTests {
 		OAuth2AccessToken accessToken = new OAuth2AccessToken(
 				OAuth2AccessToken.TokenType.BEARER, "access-token", issuedAt, expiresAt,
 				new HashSet<>(Arrays.asList("scope1", "scope2")));
-		JwtClaimsSet tokenClaims = TestJwtClaimsSets.jwtClaimsSet().build();
+		// @formatter:off
+		OAuth2TokenClaimsSet accessTokenClaims = OAuth2TokenClaimsSet.builder()
+				.issuer("https://provider.com")
+				.subject("subject")
+				.audience(Collections.singletonList(authorizedRegisteredClient.getClientId()))
+				.issuedAt(issuedAt)
+				.notBefore(issuedAt)
+				.expiresAt(expiresAt)
+				.id("id")
+				.build();
+		// @formatter:on
 		OAuth2Authorization authorization = TestOAuth2Authorizations
-				.authorization(authorizedRegisteredClient, accessToken, tokenClaims.getClaims())
+				.authorization(authorizedRegisteredClient, accessToken, accessTokenClaims.getClaims())
 				.build();
 		this.registeredClientRepository.save(authorizedRegisteredClient);
 		this.authorizationService.save(authorization);
@@ -155,7 +187,7 @@ public class OAuth2TokenIntrospectionTests {
 		// @formatter:off
 		MvcResult mvcResult = this.mvc.perform(post(providerSettings.getTokenIntrospectionEndpoint())
 				.params(getTokenIntrospectionRequestParameters(accessToken, OAuth2TokenType.ACCESS_TOKEN))
-				.with(httpBasic(introspectRegisteredClient.getClientId(), introspectRegisteredClient.getClientSecret())))
+				.header(HttpHeaders.AUTHORIZATION, getAuthorizationHeader(introspectRegisteredClient)))
 				.andExpect(status().isOk())
 				.andReturn();
 		// @formatter:on
@@ -165,17 +197,17 @@ public class OAuth2TokenIntrospectionTests {
 		assertThat(tokenIntrospectionResponse.getClientId()).isEqualTo(authorizedRegisteredClient.getClientId());
 		assertThat(tokenIntrospectionResponse.getUsername()).isNull();
 		assertThat(tokenIntrospectionResponse.getIssuedAt()).isBetween(
-				accessToken.getIssuedAt().minusSeconds(1), accessToken.getIssuedAt().plusSeconds(1));
+				accessTokenClaims.getIssuedAt().minusSeconds(1), accessTokenClaims.getIssuedAt().plusSeconds(1));
 		assertThat(tokenIntrospectionResponse.getExpiresAt()).isBetween(
-				accessToken.getExpiresAt().minusSeconds(1), accessToken.getExpiresAt().plusSeconds(1));
+				accessTokenClaims.getExpiresAt().minusSeconds(1), accessTokenClaims.getExpiresAt().plusSeconds(1));
 		assertThat(tokenIntrospectionResponse.getScopes()).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());
+				accessTokenClaims.getNotBefore().minusSeconds(1), accessTokenClaims.getNotBefore().plusSeconds(1));
+		assertThat(tokenIntrospectionResponse.getSubject()).isEqualTo(accessTokenClaims.getSubject());
+		assertThat(tokenIntrospectionResponse.getAudience()).containsExactlyInAnyOrderElementsOf(accessTokenClaims.getAudience());
+		assertThat(tokenIntrospectionResponse.getIssuer()).isEqualTo(accessTokenClaims.getIssuer());
+		assertThat(tokenIntrospectionResponse.getId()).isEqualTo(accessTokenClaims.getId());
 	}
 
 	@Test
@@ -195,7 +227,7 @@ public class OAuth2TokenIntrospectionTests {
 		// @formatter:off
 		MvcResult mvcResult = this.mvc.perform(post(providerSettings.getTokenIntrospectionEndpoint())
 				.params(getTokenIntrospectionRequestParameters(refreshToken, OAuth2TokenType.REFRESH_TOKEN))
-				.with(httpBasic(introspectRegisteredClient.getClientId(), introspectRegisteredClient.getClientSecret())))
+				.header(HttpHeaders.AUTHORIZATION, getAuthorizationHeader(introspectRegisteredClient)))
 				.andExpect(status().isOk())
 				.andReturn();
 		// @formatter:on
@@ -217,6 +249,71 @@ public class OAuth2TokenIntrospectionTests {
 		assertThat(tokenIntrospectionResponse.getId()).isNull();
 	}
 
+	@Test
+	public void requestWhenObtainReferenceAccessTokenAndIntrospectThenActive() throws Exception {
+		this.spring.register(AuthorizationServerConfiguration.class).autowire();
+
+		// @formatter:off
+		TokenSettings tokenSettings = TokenSettings.builder()
+				.accessTokenFormat(OAuth2TokenFormat.REFERENCE)
+				.build();
+		RegisteredClient authorizedRegisteredClient = TestRegisteredClients.registeredClient()
+				.tokenSettings(tokenSettings)
+				.build();
+		// @formatter:on
+		this.registeredClientRepository.save(authorizedRegisteredClient);
+
+		OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(authorizedRegisteredClient).build();
+		this.authorizationService.save(authorization);
+
+		// @formatter:off
+		MvcResult mvcResult = this.mvc.perform(post(providerSettings.getTokenEndpoint())
+				.params(getAuthorizationCodeTokenRequestParameters(authorizedRegisteredClient, authorization))
+				.header(HttpHeaders.AUTHORIZATION, getAuthorizationHeader(authorizedRegisteredClient)))
+				.andExpect(status().isOk())
+				.andReturn();
+		// @formatter:on
+
+		OAuth2AccessTokenResponse accessTokenResponse = readAccessTokenResponse(mvcResult);
+		OAuth2AccessToken accessToken = accessTokenResponse.getAccessToken();
+
+		RegisteredClient introspectRegisteredClient = TestRegisteredClients.registeredClient2().build();
+		this.registeredClientRepository.save(introspectRegisteredClient);
+
+		// @formatter:off
+		mvcResult = this.mvc.perform(post(providerSettings.getTokenIntrospectionEndpoint())
+				.params(getTokenIntrospectionRequestParameters(accessToken, OAuth2TokenType.ACCESS_TOKEN))
+				.header(HttpHeaders.AUTHORIZATION, getAuthorizationHeader(introspectRegisteredClient)))
+				.andExpect(status().isOk())
+				.andReturn();
+		// @formatter:on
+
+		OAuth2TokenIntrospection tokenIntrospectionResponse = readTokenIntrospectionResponse(mvcResult);
+
+		ArgumentCaptor<OAuth2TokenClaimsContext> accessTokenClaimsContextCaptor = ArgumentCaptor.forClass(OAuth2TokenClaimsContext.class);
+		verify(accessTokenCustomizer).customize(accessTokenClaimsContextCaptor.capture());
+
+		OAuth2TokenClaimsContext accessTokenClaimsContext = accessTokenClaimsContextCaptor.getValue();
+		OAuth2TokenClaimsSet accessTokenClaims = accessTokenClaimsContext.getClaims().build();
+
+		assertThat(tokenIntrospectionResponse.isActive()).isTrue();
+		assertThat(tokenIntrospectionResponse.getClientId()).isEqualTo(authorizedRegisteredClient.getClientId());
+		assertThat(tokenIntrospectionResponse.getUsername()).isNull();
+		assertThat(tokenIntrospectionResponse.getIssuedAt()).isBetween(
+				accessTokenClaims.getIssuedAt().minusSeconds(1), accessTokenClaims.getIssuedAt().plusSeconds(1));
+		assertThat(tokenIntrospectionResponse.getExpiresAt()).isBetween(
+				accessTokenClaims.getExpiresAt().minusSeconds(1), accessTokenClaims.getExpiresAt().plusSeconds(1));
+		List<String> scopes = new ArrayList<>(accessTokenClaims.getClaim(OAuth2ParameterNames.SCOPE));
+		assertThat(tokenIntrospectionResponse.getScopes()).containsExactlyInAnyOrderElementsOf(scopes);
+		assertThat(tokenIntrospectionResponse.getTokenType()).isEqualTo(accessToken.getTokenType().getValue());
+		assertThat(tokenIntrospectionResponse.getNotBefore()).isBetween(
+				accessTokenClaims.getNotBefore().minusSeconds(1), accessTokenClaims.getNotBefore().plusSeconds(1));
+		assertThat(tokenIntrospectionResponse.getSubject()).isEqualTo(accessTokenClaims.getSubject());
+		assertThat(tokenIntrospectionResponse.getAudience()).containsExactlyInAnyOrderElementsOf(accessTokenClaims.getAudience());
+		assertThat(tokenIntrospectionResponse.getIssuer()).isEqualTo(accessTokenClaims.getIssuer());
+		assertThat(tokenIntrospectionResponse.getId()).isEqualTo(accessTokenClaims.getId());
+	}
+
 	private static MultiValueMap<String, String> getTokenIntrospectionRequestParameters(AbstractOAuth2Token token,
 			OAuth2TokenType tokenType) {
 		MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
@@ -225,11 +322,37 @@ public class OAuth2TokenIntrospectionTests {
 		return parameters;
 	}
 
-	private OAuth2TokenIntrospection readTokenIntrospectionResponse(MvcResult mvcResult) throws Exception {
+	private static OAuth2TokenIntrospection readTokenIntrospectionResponse(MvcResult mvcResult) throws Exception {
+		MockHttpServletResponse servletResponse = mvcResult.getResponse();
+		MockClientHttpResponse httpResponse = new MockClientHttpResponse(
+				servletResponse.getContentAsByteArray(), HttpStatus.valueOf(servletResponse.getStatus()));
+		return tokenIntrospectionHttpResponseConverter.read(OAuth2TokenIntrospection.class, httpResponse);
+	}
+
+	private static MultiValueMap<String, String> getAuthorizationCodeTokenRequestParameters(RegisteredClient registeredClient,
+			OAuth2Authorization authorization) {
+		MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
+		parameters.set(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.AUTHORIZATION_CODE.getValue());
+		parameters.set(OAuth2ParameterNames.CODE, authorization.getToken(OAuth2AuthorizationCode.class).getToken().getTokenValue());
+		parameters.set(OAuth2ParameterNames.REDIRECT_URI, registeredClient.getRedirectUris().iterator().next());
+		return parameters;
+	}
+
+	private static OAuth2AccessTokenResponse readAccessTokenResponse(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);
+		return accessTokenHttpResponseConverter.read(OAuth2AccessTokenResponse.class, httpResponse);
+	}
+
+	private static String getAuthorizationHeader(RegisteredClient registeredClient) throws Exception {
+		String clientId = registeredClient.getClientId();
+		String clientSecret = registeredClient.getClientSecret();
+		clientId = URLEncoder.encode(clientId, StandardCharsets.UTF_8.name());
+		clientSecret = URLEncoder.encode(clientSecret, StandardCharsets.UTF_8.name());
+		String credentialsString = clientId + ":" + clientSecret;
+		byte[] encodedBytes = Base64.getEncoder().encode(credentialsString.getBytes(StandardCharsets.UTF_8));
+		return "Basic " + new String(encodedBytes, StandardCharsets.UTF_8);
 	}
 
 	@EnableWebSecurity
@@ -244,6 +367,11 @@ public class OAuth2TokenIntrospectionTests {
 			return authorizationService;
 		}
 
+		@Bean
+		OAuth2AuthorizationConsentService authorizationConsentService(JdbcOperations jdbcOperations, RegisteredClientRepository registeredClientRepository) {
+			return new JdbcOAuth2AuthorizationConsentService(jdbcOperations, registeredClientRepository);
+		}
+
 		@Bean
 		RegisteredClientRepository registeredClientRepository(JdbcOperations jdbcOperations, PasswordEncoder passwordEncoder) {
 			JdbcRegisteredClientRepository jdbcRegisteredClientRepository = new JdbcRegisteredClientRepository(jdbcOperations);
@@ -259,13 +387,13 @@ public class OAuth2TokenIntrospectionTests {
 		}
 
 		@Bean
-		JWKSource<SecurityContext> jwkSource() {
-			return jwkSource;
+		ProviderSettings providerSettings() {
+			return providerSettings;
 		}
 
 		@Bean
-		ProviderSettings providerSettings() {
-			return providerSettings;
+		OAuth2TokenCustomizer<OAuth2TokenClaimsContext> accessTokenCustomizer() {
+			return accessTokenCustomizer;
 		}
 
 		@Bean

+ 97 - 0
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/core/OAuth2TokenClaimsSetTests.java

@@ -0,0 +1,97 @@
+/*
+ * Copyright 2020-2022 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.oauth2.core;
+
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.Collections;
+
+import org.junit.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/**
+ * Tests for {@link OAuth2TokenClaimsSet}.
+ *
+ * @author Joe Grandja
+ */
+public class OAuth2TokenClaimsSetTests {
+
+	@Test
+	public void buildWhenClaimsEmptyThenThrowIllegalArgumentException() {
+		assertThatThrownBy(() -> OAuth2TokenClaimsSet.builder().build())
+				.isInstanceOf(IllegalArgumentException.class)
+				.hasMessage("claims cannot be empty");
+	}
+
+	@Test
+	public void buildWhenAllClaimsProvidedThenAllClaimsAreSet() {
+		Instant issuedAt = Instant.now();
+		Instant expiresAt = issuedAt.plus(1, ChronoUnit.HOURS);
+		String customClaimName = "custom-claim-name";
+		String customClaimValue = "custom-claim-value";
+
+		// @formatter:off
+		OAuth2TokenClaimsSet expectedClaimsSet = OAuth2TokenClaimsSet.builder()
+				.issuer("https://provider.com")
+				.subject("subject")
+				.audience(Collections.singletonList("client-1"))
+				.issuedAt(issuedAt)
+				.notBefore(issuedAt)
+				.expiresAt(expiresAt)
+				.id("id")
+				.claims(claims -> claims.put(customClaimName, customClaimValue))
+				.build();
+
+		OAuth2TokenClaimsSet claimsSet = OAuth2TokenClaimsSet.builder()
+				.issuer(expectedClaimsSet.getIssuer().toExternalForm())
+				.subject(expectedClaimsSet.getSubject())
+				.audience(expectedClaimsSet.getAudience())
+				.issuedAt(expectedClaimsSet.getIssuedAt())
+				.notBefore(expectedClaimsSet.getNotBefore())
+				.expiresAt(expectedClaimsSet.getExpiresAt())
+				.id(expectedClaimsSet.getId())
+				.claims(claims -> claims.put(customClaimName, expectedClaimsSet.getClaim(customClaimName)))
+				.build();
+		// @formatter:on
+
+		assertThat(claimsSet.getIssuer()).isEqualTo(expectedClaimsSet.getIssuer());
+		assertThat(claimsSet.getSubject()).isEqualTo(expectedClaimsSet.getSubject());
+		assertThat(claimsSet.getAudience()).isEqualTo(expectedClaimsSet.getAudience());
+		assertThat(claimsSet.getIssuedAt()).isEqualTo(expectedClaimsSet.getIssuedAt());
+		assertThat(claimsSet.getNotBefore()).isEqualTo(expectedClaimsSet.getNotBefore());
+		assertThat(claimsSet.getExpiresAt()).isEqualTo(expectedClaimsSet.getExpiresAt());
+		assertThat(claimsSet.getId()).isEqualTo(expectedClaimsSet.getId());
+		assertThat(claimsSet.<String>getClaim(customClaimName)).isEqualTo(expectedClaimsSet.getClaim(customClaimName));
+		assertThat(claimsSet.getClaims()).isEqualTo(expectedClaimsSet.getClaims());
+	}
+
+	@Test
+	public void claimWhenNameNullThenThrowIllegalArgumentException() {
+		assertThatThrownBy(() -> OAuth2TokenClaimsSet.builder().claim(null, "value"))
+				.isInstanceOf(IllegalArgumentException.class)
+				.hasMessage("name cannot be empty");
+	}
+
+	@Test
+	public void claimWhenValueNullThenThrowIllegalArgumentException() {
+		assertThatThrownBy(() -> OAuth2TokenClaimsSet.builder().claim("name", null))
+				.isInstanceOf(IllegalArgumentException.class)
+				.hasMessage("value cannot be null");
+	}
+
+}

+ 20 - 0
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/JwtGeneratorTests.java

@@ -29,6 +29,7 @@ import org.mockito.ArgumentCaptor;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.oauth2.core.AuthorizationGrantType;
 import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
+import org.springframework.security.oauth2.core.OAuth2TokenFormat;
 import org.springframework.security.oauth2.core.OAuth2TokenType;
 import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
 import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
@@ -45,6 +46,7 @@ import org.springframework.security.oauth2.server.authorization.authentication.O
 import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
 import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients;
 import org.springframework.security.oauth2.server.authorization.config.ProviderSettings;
+import org.springframework.security.oauth2.server.authorization.config.TokenSettings;
 import org.springframework.security.oauth2.server.authorization.context.ProviderContext;
 
 import static org.assertj.core.api.Assertions.assertThat;
@@ -99,6 +101,24 @@ public class JwtGeneratorTests {
 		assertThat(this.jwtGenerator.generate(tokenContext)).isNull();
 	}
 
+	@Test
+	public void generateWhenUnsupportedTokenFormatThenReturnNull() {
+		// @formatter:off
+		TokenSettings tokenSettings = TokenSettings.builder()
+				.accessTokenFormat(new OAuth2TokenFormat("unsupported_token_format"))
+				.build();
+		RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
+				.tokenSettings(tokenSettings)
+				.build();
+		OAuth2TokenContext tokenContext = DefaultOAuth2TokenContext.builder()
+				.registeredClient(registeredClient)
+				.tokenType(OAuth2TokenType.ACCESS_TOKEN)
+				.build();
+		// @formatter:on
+
+		assertThat(this.jwtGenerator.generate(tokenContext)).isNull();
+	}
+
 	@Test
 	public void generateWhenAccessTokenTypeThenReturnJwt() {
 		RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();

+ 185 - 0
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/OAuth2AccessTokenGeneratorTests.java

@@ -0,0 +1,185 @@
+/*
+ * Copyright 2020-2022 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.oauth2.server.authorization;
+
+import java.security.Principal;
+import java.time.Instant;
+import java.util.Collections;
+import java.util.Set;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+
+import org.springframework.security.core.Authentication;
+import org.springframework.security.oauth2.core.AuthorizationGrantType;
+import org.springframework.security.oauth2.core.ClaimAccessor;
+import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
+import org.springframework.security.oauth2.core.OAuth2AccessToken;
+import org.springframework.security.oauth2.core.OAuth2TokenClaimAccessor;
+import org.springframework.security.oauth2.core.OAuth2TokenFormat;
+import org.springframework.security.oauth2.core.OAuth2TokenType;
+import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
+import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
+import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeAuthenticationToken;
+import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken;
+import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
+import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients;
+import org.springframework.security.oauth2.server.authorization.config.ProviderSettings;
+import org.springframework.security.oauth2.server.authorization.config.TokenSettings;
+import org.springframework.security.oauth2.server.authorization.context.ProviderContext;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+/**
+ * Tests for {@link OAuth2AccessTokenGenerator}.
+ *
+ * @author Joe Grandja
+ */
+public class OAuth2AccessTokenGeneratorTests {
+	private OAuth2TokenCustomizer<OAuth2TokenClaimsContext> accessTokenCustomizer;
+	private OAuth2AccessTokenGenerator accessTokenGenerator;
+	private ProviderContext providerContext;
+
+	@Before
+	public void setUp() {
+		this.accessTokenCustomizer = mock(OAuth2TokenCustomizer.class);
+		this.accessTokenGenerator = new OAuth2AccessTokenGenerator();
+		this.accessTokenGenerator.setAccessTokenCustomizer(this.accessTokenCustomizer);
+		ProviderSettings providerSettings = ProviderSettings.builder().issuer("https://provider.com").build();
+		this.providerContext = new ProviderContext(providerSettings, null);
+	}
+
+	@Test
+	public void setAccessTokenCustomizerWhenNullThenThrowIllegalArgumentException() {
+		assertThatThrownBy(() -> this.accessTokenGenerator.setAccessTokenCustomizer(null))
+				.isInstanceOf(IllegalArgumentException.class)
+				.hasMessage("accessTokenCustomizer cannot be null");
+	}
+
+	@Test
+	public void generateWhenUnsupportedTokenTypeThenReturnNull() {
+		// @formatter:off
+		TokenSettings tokenSettings = TokenSettings.builder()
+				.accessTokenFormat(OAuth2TokenFormat.REFERENCE)
+				.build();
+		RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
+				.tokenSettings(tokenSettings)
+				.build();
+		OAuth2TokenContext tokenContext = DefaultOAuth2TokenContext.builder()
+				.registeredClient(registeredClient)
+				.tokenType(new OAuth2TokenType("unsupported_token_type"))
+				.build();
+		// @formatter:on
+
+		assertThat(this.accessTokenGenerator.generate(tokenContext)).isNull();
+	}
+
+	@Test
+	public void generateWhenUnsupportedTokenFormatThenReturnNull() {
+		// @formatter:off
+		TokenSettings tokenSettings = TokenSettings.builder()
+				.accessTokenFormat(new OAuth2TokenFormat("unsupported_token_format"))
+				.build();
+		RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
+				.tokenSettings(tokenSettings)
+				.build();
+		OAuth2TokenContext tokenContext = DefaultOAuth2TokenContext.builder()
+				.registeredClient(registeredClient)
+				.tokenType(OAuth2TokenType.ACCESS_TOKEN)
+				.build();
+		// @formatter:on
+
+		assertThat(this.accessTokenGenerator.generate(tokenContext)).isNull();
+	}
+
+	@Test
+	public void generateWhenReferenceAccessTokenTypeThenReturnAccessToken() {
+		// @formatter:off
+		TokenSettings tokenSettings = TokenSettings.builder()
+				.accessTokenFormat(OAuth2TokenFormat.REFERENCE)
+				.build();
+		RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
+				.tokenSettings(tokenSettings)
+				.build();
+		// @formatter:on
+		OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient).build();
+		Authentication principal = authorization.getAttribute(Principal.class.getName());
+
+		OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(
+				registeredClient, ClientAuthenticationMethod.CLIENT_SECRET_BASIC, registeredClient.getClientSecret());
+		OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute(
+				OAuth2AuthorizationRequest.class.getName());
+		OAuth2AuthorizationCodeAuthenticationToken authentication =
+				new OAuth2AuthorizationCodeAuthenticationToken("code", clientPrincipal, authorizationRequest.getRedirectUri(), null);
+
+		// @formatter:off
+		OAuth2TokenContext tokenContext = DefaultOAuth2TokenContext.builder()
+				.registeredClient(registeredClient)
+				.principal(principal)
+				.providerContext(this.providerContext)
+				.authorization(authorization)
+				.authorizedScopes(authorization.getAttribute(OAuth2Authorization.AUTHORIZED_SCOPE_ATTRIBUTE_NAME))
+				.tokenType(OAuth2TokenType.ACCESS_TOKEN)
+				.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
+				.authorizationGrant(authentication)
+				.build();
+		// @formatter:on
+
+		OAuth2AccessToken accessToken = this.accessTokenGenerator.generate(tokenContext);
+		assertThat(accessToken).isNotNull();
+
+		Instant issuedAt = Instant.now();
+		Instant expiresAt = issuedAt.plus(tokenContext.getRegisteredClient().getTokenSettings().getAccessTokenTimeToLive());
+		assertThat(accessToken.getIssuedAt()).isBetween(issuedAt.minusSeconds(1), issuedAt.plusSeconds(1));
+		assertThat(accessToken.getExpiresAt()).isBetween(expiresAt.minusSeconds(1), expiresAt.plusSeconds(1));
+		assertThat(accessToken.getScopes()).isEqualTo(tokenContext.getAuthorizedScopes());
+
+		assertThat(accessToken).isInstanceOf(ClaimAccessor.class);
+		OAuth2TokenClaimAccessor accessTokenClaims = ((ClaimAccessor) accessToken)::getClaims;
+		assertThat(accessTokenClaims.getClaims()).isNotEmpty();
+
+		assertThat(accessTokenClaims.getIssuer().toExternalForm()).isEqualTo(tokenContext.getProviderContext().getIssuer());
+		assertThat(accessTokenClaims.getSubject()).isEqualTo(tokenContext.getPrincipal().getName());
+		assertThat(accessTokenClaims.getAudience()).isEqualTo(
+				Collections.singletonList(tokenContext.getRegisteredClient().getClientId()));
+		assertThat(accessTokenClaims.getIssuedAt()).isBetween(issuedAt.minusSeconds(1), issuedAt.plusSeconds(1));
+		assertThat(accessTokenClaims.getExpiresAt()).isBetween(expiresAt.minusSeconds(1), expiresAt.plusSeconds(1));
+		assertThat(accessTokenClaims.getNotBefore()).isBetween(issuedAt.minusSeconds(1), issuedAt.plusSeconds(1));
+		assertThat(accessTokenClaims.getId()).isNotNull();
+
+		Set<String> scopes = accessTokenClaims.getClaim(OAuth2ParameterNames.SCOPE);
+		assertThat(scopes).isEqualTo(tokenContext.getAuthorizedScopes());
+
+		ArgumentCaptor<OAuth2TokenClaimsContext> tokenClaimsContextCaptor = ArgumentCaptor.forClass(OAuth2TokenClaimsContext.class);
+		verify(this.accessTokenCustomizer).customize(tokenClaimsContextCaptor.capture());
+
+		OAuth2TokenClaimsContext tokenClaimsContext = tokenClaimsContextCaptor.getValue();
+		assertThat(tokenClaimsContext.getClaims()).isNotNull();
+		assertThat(tokenClaimsContext.getRegisteredClient()).isEqualTo(tokenContext.getRegisteredClient());
+		assertThat(tokenClaimsContext.<Authentication>getPrincipal()).isEqualTo(tokenContext.getPrincipal());
+		assertThat(tokenClaimsContext.getProviderContext()).isEqualTo(tokenContext.getProviderContext());
+		assertThat(tokenClaimsContext.getAuthorization()).isEqualTo(tokenContext.getAuthorization());
+		assertThat(tokenClaimsContext.getAuthorizedScopes()).isEqualTo(tokenContext.getAuthorizedScopes());
+		assertThat(tokenClaimsContext.getTokenType()).isEqualTo(tokenContext.getTokenType());
+		assertThat(tokenClaimsContext.getAuthorizationGrantType()).isEqualTo(tokenContext.getAuthorizationGrantType());
+		assertThat(tokenClaimsContext.<Authentication>getAuthorizationGrant()).isEqualTo(tokenContext.getAuthorizationGrant());
+	}
+
+}

+ 112 - 0
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/OAuth2TokenClaimsContextTests.java

@@ -0,0 +1,112 @@
+/*
+ * Copyright 2020-2022 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.oauth2.server.authorization;
+
+import java.security.Principal;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.Collections;
+
+import org.junit.Test;
+
+import org.springframework.security.core.Authentication;
+import org.springframework.security.oauth2.core.AuthorizationGrantType;
+import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
+import org.springframework.security.oauth2.core.OAuth2TokenClaimsSet;
+import org.springframework.security.oauth2.core.OAuth2TokenType;
+import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
+import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeAuthenticationToken;
+import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationGrantAuthenticationToken;
+import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken;
+import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
+import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients;
+import org.springframework.security.oauth2.server.authorization.config.ProviderSettings;
+import org.springframework.security.oauth2.server.authorization.context.ProviderContext;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/**
+ * Tests for {@link OAuth2TokenClaimsContext}.
+ *
+ * @author Joe Grandja
+ */
+public class OAuth2TokenClaimsContextTests {
+
+	@Test
+	public void withWhenClaimsNullThenThrowIllegalArgumentException() {
+		assertThatThrownBy(() -> OAuth2TokenClaimsContext.with(null))
+				.isInstanceOf(IllegalArgumentException.class)
+				.hasMessage("claimsBuilder cannot be null");
+	}
+
+	@Test
+	public void buildWhenAllValuesProvidedThenAllValuesAreSet() {
+		String issuer = "https://provider.com";
+		Instant issuedAt = Instant.now();
+		Instant expiresAt = issuedAt.plus(1, ChronoUnit.HOURS);
+
+		// @formatter:off
+		OAuth2TokenClaimsSet.Builder claims = OAuth2TokenClaimsSet.builder()
+				.issuer(issuer)
+				.subject("subject")
+				.audience(Collections.singletonList("client-1"))
+				.issuedAt(issuedAt)
+				.notBefore(issuedAt)
+				.expiresAt(expiresAt)
+				.id("id");
+		// @formatter:on
+
+		RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
+		OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient).build();
+		Authentication principal = authorization.getAttribute(Principal.class.getName());
+		ProviderSettings providerSettings = ProviderSettings.builder().issuer(issuer).build();
+		ProviderContext providerContext = new ProviderContext(providerSettings, null);
+		OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(
+				registeredClient, ClientAuthenticationMethod.CLIENT_SECRET_BASIC, registeredClient.getClientSecret());
+		OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute(
+				OAuth2AuthorizationRequest.class.getName());
+		OAuth2AuthorizationCodeAuthenticationToken authorizationGrant =
+				new OAuth2AuthorizationCodeAuthenticationToken(
+						"code", clientPrincipal, authorizationRequest.getRedirectUri(), null);
+
+		// @formatter:off
+		OAuth2TokenClaimsContext context = OAuth2TokenClaimsContext.with(claims)
+				.registeredClient(registeredClient)
+				.principal(principal)
+				.providerContext(providerContext)
+				.authorization(authorization)
+				.tokenType(OAuth2TokenType.ACCESS_TOKEN)
+				.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
+				.authorizationGrant(authorizationGrant)
+				.put("custom-key-1", "custom-value-1")
+				.context(ctx -> ctx.put("custom-key-2", "custom-value-2"))
+				.build();
+		// @formatter:on
+
+		assertThat(context.getClaims()).isEqualTo(claims);
+		assertThat(context.getRegisteredClient()).isEqualTo(registeredClient);
+		assertThat(context.<Authentication>getPrincipal()).isEqualTo(principal);
+		assertThat(context.getProviderContext()).isEqualTo(providerContext);
+		assertThat(context.getAuthorization()).isEqualTo(authorization);
+		assertThat(context.getTokenType()).isEqualTo(OAuth2TokenType.ACCESS_TOKEN);
+		assertThat(context.getAuthorizationGrantType()).isEqualTo(AuthorizationGrantType.AUTHORIZATION_CODE);
+		assertThat(context.<OAuth2AuthorizationGrantAuthenticationToken>getAuthorizationGrant()).isEqualTo(authorizationGrant);
+		assertThat(context.<String>get("custom-key-1")).isEqualTo("custom-value-1");
+		assertThat(context.<String>get("custom-key-2")).isEqualTo("custom-value-2");
+	}
+
+}

+ 33 - 3
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeAuthenticationProviderTests.java

@@ -38,6 +38,7 @@ import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
 import org.springframework.security.oauth2.core.OAuth2AuthorizationCode;
 import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
 import org.springframework.security.oauth2.core.OAuth2Token;
+import org.springframework.security.oauth2.core.OAuth2TokenFormat;
 import org.springframework.security.oauth2.core.OAuth2TokenType;
 import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
 import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
@@ -52,9 +53,11 @@ import org.springframework.security.oauth2.jwt.JwtEncoder;
 import org.springframework.security.oauth2.server.authorization.DelegatingOAuth2TokenGenerator;
 import org.springframework.security.oauth2.server.authorization.JwtEncodingContext;
 import org.springframework.security.oauth2.server.authorization.JwtGenerator;
+import org.springframework.security.oauth2.server.authorization.OAuth2AccessTokenGenerator;
 import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
 import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
 import org.springframework.security.oauth2.server.authorization.OAuth2RefreshTokenGenerator;
+import org.springframework.security.oauth2.server.authorization.OAuth2TokenClaimsContext;
 import org.springframework.security.oauth2.server.authorization.OAuth2TokenContext;
 import org.springframework.security.oauth2.server.authorization.OAuth2TokenCustomizer;
 import org.springframework.security.oauth2.server.authorization.OAuth2TokenGenerator;
@@ -90,6 +93,7 @@ public class OAuth2AuthorizationCodeAuthenticationProviderTests {
 	private OAuth2AuthorizationService authorizationService;
 	private JwtEncoder jwtEncoder;
 	private OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer;
+	private OAuth2TokenCustomizer<OAuth2TokenClaimsContext> accessTokenCustomizer;
 	private OAuth2TokenGenerator<?> tokenGenerator;
 	private OAuth2AuthorizationCodeAuthenticationProvider authenticationProvider;
 
@@ -100,9 +104,12 @@ public class OAuth2AuthorizationCodeAuthenticationProviderTests {
 		this.jwtCustomizer = mock(OAuth2TokenCustomizer.class);
 		JwtGenerator jwtGenerator = new JwtGenerator(this.jwtEncoder);
 		jwtGenerator.setJwtCustomizer(this.jwtCustomizer);
+		this.accessTokenCustomizer = mock(OAuth2TokenCustomizer.class);
+		OAuth2AccessTokenGenerator accessTokenGenerator = new OAuth2AccessTokenGenerator();
+		accessTokenGenerator.setAccessTokenCustomizer(this.accessTokenCustomizer);
 		OAuth2RefreshTokenGenerator refreshTokenGenerator = new OAuth2RefreshTokenGenerator();
 		OAuth2TokenGenerator<OAuth2Token> delegatingTokenGenerator =
-				new DelegatingOAuth2TokenGenerator(jwtGenerator, refreshTokenGenerator);
+				new DelegatingOAuth2TokenGenerator(jwtGenerator, accessTokenGenerator, refreshTokenGenerator);
 		this.tokenGenerator = spy(new OAuth2TokenGenerator<OAuth2Token>() {
 			@Override
 			public OAuth2Token generate(OAuth2TokenContext context) {
@@ -312,8 +319,6 @@ public class OAuth2AuthorizationCodeAuthenticationProviderTests {
 		OAuth2AuthorizationCodeAuthenticationToken authentication =
 				new OAuth2AuthorizationCodeAuthenticationToken(AUTHORIZATION_CODE, clientPrincipal, authorizationRequest.getRedirectUri(), null);
 
-		when(this.jwtEncoder.encode(any(), any())).thenReturn(createJwt());
-
 		doAnswer(answer -> {
 			OAuth2TokenContext context = answer.getArgument(0);
 			if (OAuth2TokenType.ACCESS_TOKEN.equals(context.getTokenType())) {
@@ -686,6 +691,31 @@ public class OAuth2AuthorizationCodeAuthenticationProviderTests {
 		assertThat(accessTokenAuthentication.getRefreshToken().getTokenValue()).isEqualTo(refreshTokenGenerator.get());
 	}
 
+	@Test
+	public void authenticateWhenAccessTokenFormatReferenceThenAccessTokenGeneratorCalled() {
+		// @formatter:off
+		RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
+				.tokenSettings(TokenSettings.builder()
+						.accessTokenFormat(OAuth2TokenFormat.REFERENCE)
+						.build())
+				.build();
+		// @formatter:on
+		OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient).build();
+		when(this.authorizationService.findByToken(eq(AUTHORIZATION_CODE), eq(AUTHORIZATION_CODE_TOKEN_TYPE)))
+				.thenReturn(authorization);
+
+		OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(
+				registeredClient, ClientAuthenticationMethod.CLIENT_SECRET_BASIC, registeredClient.getClientSecret());
+		OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute(
+				OAuth2AuthorizationRequest.class.getName());
+		OAuth2AuthorizationCodeAuthenticationToken authentication =
+				new OAuth2AuthorizationCodeAuthenticationToken(AUTHORIZATION_CODE, clientPrincipal, authorizationRequest.getRedirectUri(), null);
+
+		this.authenticationProvider.authenticate(authentication);
+
+		verify(this.accessTokenCustomizer).customize(any());
+	}
+
 	private static Jwt createJwt() {
 		Instant issuedAt = Instant.now();
 		Instant expiresAt = issuedAt.plus(1, ChronoUnit.HOURS);

+ 34 - 3
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientCredentialsAuthenticationProviderTests.java

@@ -31,22 +31,28 @@ import org.springframework.security.oauth2.core.AuthorizationGrantType;
 import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
 import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
 import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
+import org.springframework.security.oauth2.core.OAuth2Token;
+import org.springframework.security.oauth2.core.OAuth2TokenFormat;
 import org.springframework.security.oauth2.core.OAuth2TokenType;
 import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
 import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
 import org.springframework.security.oauth2.jwt.JoseHeaderNames;
 import org.springframework.security.oauth2.jwt.Jwt;
 import org.springframework.security.oauth2.jwt.JwtEncoder;
+import org.springframework.security.oauth2.server.authorization.DelegatingOAuth2TokenGenerator;
 import org.springframework.security.oauth2.server.authorization.JwtEncodingContext;
 import org.springframework.security.oauth2.server.authorization.JwtGenerator;
+import org.springframework.security.oauth2.server.authorization.OAuth2AccessTokenGenerator;
 import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
 import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
+import org.springframework.security.oauth2.server.authorization.OAuth2TokenClaimsContext;
 import org.springframework.security.oauth2.server.authorization.OAuth2TokenContext;
 import org.springframework.security.oauth2.server.authorization.OAuth2TokenCustomizer;
 import org.springframework.security.oauth2.server.authorization.OAuth2TokenGenerator;
 import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
 import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients;
 import org.springframework.security.oauth2.server.authorization.config.ProviderSettings;
+import org.springframework.security.oauth2.server.authorization.config.TokenSettings;
 import org.springframework.security.oauth2.server.authorization.context.ProviderContext;
 import org.springframework.security.oauth2.server.authorization.context.ProviderContextHolder;
 
@@ -69,6 +75,7 @@ public class OAuth2ClientCredentialsAuthenticationProviderTests {
 	private OAuth2AuthorizationService authorizationService;
 	private JwtEncoder jwtEncoder;
 	private OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer;
+	private OAuth2TokenCustomizer<OAuth2TokenClaimsContext> accessTokenCustomizer;
 	private OAuth2TokenGenerator<?> tokenGenerator;
 	private OAuth2ClientCredentialsAuthenticationProvider authenticationProvider;
 
@@ -79,10 +86,15 @@ public class OAuth2ClientCredentialsAuthenticationProviderTests {
 		this.jwtCustomizer = mock(OAuth2TokenCustomizer.class);
 		JwtGenerator jwtGenerator = new JwtGenerator(this.jwtEncoder);
 		jwtGenerator.setJwtCustomizer(this.jwtCustomizer);
-		this.tokenGenerator = spy(new OAuth2TokenGenerator<Jwt>() {
+		this.accessTokenCustomizer = mock(OAuth2TokenCustomizer.class);
+		OAuth2AccessTokenGenerator accessTokenGenerator = new OAuth2AccessTokenGenerator();
+		accessTokenGenerator.setAccessTokenCustomizer(this.accessTokenCustomizer);
+		OAuth2TokenGenerator<OAuth2Token> delegatingTokenGenerator =
+				new DelegatingOAuth2TokenGenerator(jwtGenerator, accessTokenGenerator);
+		this.tokenGenerator = spy(new OAuth2TokenGenerator<OAuth2Token>() {
 			@Override
-			public Jwt generate(OAuth2TokenContext context) {
-				return jwtGenerator.generate(context);
+			public OAuth2Token generate(OAuth2TokenContext context) {
+				return delegatingTokenGenerator.generate(context);
 			}
 		});
 		this.authenticationProvider = new OAuth2ClientCredentialsAuthenticationProvider(
@@ -275,6 +287,25 @@ public class OAuth2ClientCredentialsAuthenticationProviderTests {
 		assertThat(accessTokenAuthentication.getAccessToken()).isEqualTo(authorization.getAccessToken().getToken());
 	}
 
+	@Test
+	public void authenticateWhenAccessTokenFormatReferenceThenAccessTokenGeneratorCalled() {
+		// @formatter:off
+		RegisteredClient registeredClient = TestRegisteredClients.registeredClient2()
+				.tokenSettings(TokenSettings.builder()
+						.accessTokenFormat(OAuth2TokenFormat.REFERENCE)
+						.build())
+				.build();
+		// @formatter:on
+		OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(
+				registeredClient, ClientAuthenticationMethod.CLIENT_SECRET_BASIC, registeredClient.getClientSecret());
+		OAuth2ClientCredentialsAuthenticationToken authentication =
+				new OAuth2ClientCredentialsAuthenticationToken(clientPrincipal, null, null);
+
+		this.authenticationProvider.authenticate(authentication);
+
+		verify(this.accessTokenCustomizer).customize(any());
+	}
+
 	private static Jwt createJwt(Set<String> scope) {
 		Instant issuedAt = Instant.now();
 		Instant expiresAt = issuedAt.plus(1, ChronoUnit.HOURS);

+ 33 - 1
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2RefreshTokenAuthenticationProviderTests.java

@@ -38,6 +38,7 @@ 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.OAuth2Token;
+import org.springframework.security.oauth2.core.OAuth2TokenFormat;
 import org.springframework.security.oauth2.core.OAuth2TokenType;
 import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
 import org.springframework.security.oauth2.core.oidc.OidcIdToken;
@@ -50,9 +51,11 @@ import org.springframework.security.oauth2.jwt.JwtEncoder;
 import org.springframework.security.oauth2.server.authorization.DelegatingOAuth2TokenGenerator;
 import org.springframework.security.oauth2.server.authorization.JwtEncodingContext;
 import org.springframework.security.oauth2.server.authorization.JwtGenerator;
+import org.springframework.security.oauth2.server.authorization.OAuth2AccessTokenGenerator;
 import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
 import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
 import org.springframework.security.oauth2.server.authorization.OAuth2RefreshTokenGenerator;
+import org.springframework.security.oauth2.server.authorization.OAuth2TokenClaimsContext;
 import org.springframework.security.oauth2.server.authorization.OAuth2TokenContext;
 import org.springframework.security.oauth2.server.authorization.OAuth2TokenCustomizer;
 import org.springframework.security.oauth2.server.authorization.OAuth2TokenGenerator;
@@ -88,6 +91,7 @@ public class OAuth2RefreshTokenAuthenticationProviderTests {
 	private OAuth2AuthorizationService authorizationService;
 	private JwtEncoder jwtEncoder;
 	private OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer;
+	private OAuth2TokenCustomizer<OAuth2TokenClaimsContext> accessTokenCustomizer;
 	private OAuth2TokenGenerator<?> tokenGenerator;
 	private OAuth2RefreshTokenAuthenticationProvider authenticationProvider;
 
@@ -99,9 +103,12 @@ public class OAuth2RefreshTokenAuthenticationProviderTests {
 		this.jwtCustomizer = mock(OAuth2TokenCustomizer.class);
 		JwtGenerator jwtGenerator = new JwtGenerator(this.jwtEncoder);
 		jwtGenerator.setJwtCustomizer(this.jwtCustomizer);
+		this.accessTokenCustomizer = mock(OAuth2TokenCustomizer.class);
+		OAuth2AccessTokenGenerator accessTokenGenerator = new OAuth2AccessTokenGenerator();
+		accessTokenGenerator.setAccessTokenCustomizer(this.accessTokenCustomizer);
 		OAuth2RefreshTokenGenerator refreshTokenGenerator = new OAuth2RefreshTokenGenerator();
 		OAuth2TokenGenerator<OAuth2Token> delegatingTokenGenerator =
-				new DelegatingOAuth2TokenGenerator(jwtGenerator, refreshTokenGenerator);
+				new DelegatingOAuth2TokenGenerator(jwtGenerator, accessTokenGenerator, refreshTokenGenerator);
 		this.tokenGenerator = spy(new OAuth2TokenGenerator<OAuth2Token>() {
 			@Override
 			public OAuth2Token generate(OAuth2TokenContext context) {
@@ -623,6 +630,31 @@ public class OAuth2RefreshTokenAuthenticationProviderTests {
 				});
 	}
 
+	@Test
+	public void authenticateWhenAccessTokenFormatReferenceThenAccessTokenGeneratorCalled() {
+		// @formatter:off
+		RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
+				.tokenSettings(TokenSettings.builder()
+						.accessTokenFormat(OAuth2TokenFormat.REFERENCE)
+						.build())
+				.build();
+		// @formatter:on
+		OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient).build();
+		when(this.authorizationService.findByToken(
+				eq(authorization.getRefreshToken().getToken().getTokenValue()),
+				eq(OAuth2TokenType.REFRESH_TOKEN)))
+				.thenReturn(authorization);
+
+		OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(
+				registeredClient, ClientAuthenticationMethod.CLIENT_SECRET_BASIC, registeredClient.getClientSecret());
+		OAuth2RefreshTokenAuthenticationToken authentication = new OAuth2RefreshTokenAuthenticationToken(
+				authorization.getRefreshToken().getToken().getTokenValue(), clientPrincipal, null, null);
+
+		this.authenticationProvider.authenticate(authentication);
+
+		verify(this.accessTokenCustomizer).customize(any());
+	}
+
 	private static Jwt createJwt(Set<String> scope) {
 		Instant issuedAt = Instant.now();
 		Instant expiresAt = issuedAt.plus(1, ChronoUnit.HOURS);

+ 23 - 12
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2TokenIntrospectionAuthenticationProviderTests.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2020-2021 the original author or authors.
+ * Copyright 2020-2022 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -31,10 +31,9 @@ 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.OAuth2TokenClaimNames;
+import org.springframework.security.oauth2.core.OAuth2TokenClaimsSet;
 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;
@@ -189,7 +188,7 @@ public class OAuth2TokenIntrospectionAuthenticationProviderTests {
 		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);
+		Map<String, Object> accessTokenClaims = Collections.singletonMap(OAuth2TokenClaimNames.NBF, notBefore);
 		OAuth2Authorization authorization = TestOAuth2Authorizations
 				.authorization(registeredClient, accessToken, accessTokenClaims)
 				.build();
@@ -217,9 +216,21 @@ public class OAuth2TokenIntrospectionAuthenticationProviderTests {
 		OAuth2AccessToken accessToken = new OAuth2AccessToken(
 				OAuth2AccessToken.TokenType.BEARER, "access-token", issuedAt, expiresAt,
 				new HashSet<>(Arrays.asList("scope1", "scope2")));
-		JwtClaimsSet jwtClaims = TestJwtClaimsSets.jwtClaimsSet().build();
+
+		// @formatter:off
+		OAuth2TokenClaimsSet claimsSet = OAuth2TokenClaimsSet.builder()
+				.issuer("https://provider.com")
+				.subject("subject")
+				.audience(Collections.singletonList(authorizedClient.getClientId()))
+				.issuedAt(issuedAt)
+				.notBefore(issuedAt)
+				.expiresAt(expiresAt)
+				.id("id")
+				.build();
+		// @formatter:on
+
 		OAuth2Authorization authorization = TestOAuth2Authorizations
-				.authorization(authorizedClient, accessToken, jwtClaims.getClaims())
+				.authorization(authorizedClient, accessToken, claimsSet.getClaims())
 				.build();
 		when(this.authorizationService.findByToken(eq(accessToken.getTokenValue()), isNull()))
 				.thenReturn(authorization);
@@ -243,11 +254,11 @@ public class OAuth2TokenIntrospectionAuthenticationProviderTests {
 		assertThat(tokenClaims.getExpiresAt()).isEqualTo(accessToken.getExpiresAt());
 		assertThat(tokenClaims.getScopes()).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());
+		assertThat(tokenClaims.getNotBefore()).isEqualTo(claimsSet.getNotBefore());
+		assertThat(tokenClaims.getSubject()).isEqualTo(claimsSet.getSubject());
+		assertThat(tokenClaims.getAudience()).containsExactlyInAnyOrderElementsOf(claimsSet.getAudience());
+		assertThat(tokenClaims.getIssuer()).isEqualTo(claimsSet.getIssuer());
+		assertThat(tokenClaims.getId()).isEqualTo(claimsSet.getId());
 	}
 
 	@Test

+ 21 - 3
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/TokenSettingsTests.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2020-2021 the original author or authors.
+ * Copyright 2020-2022 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -19,6 +19,7 @@ import java.time.Duration;
 
 import org.junit.Test;
 
+import org.springframework.security.oauth2.core.OAuth2TokenFormat;
 import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
 
 import static org.assertj.core.api.Assertions.assertThat;
@@ -34,8 +35,9 @@ public class TokenSettingsTests {
 	@Test
 	public void buildWhenDefaultThenDefaultsAreSet() {
 		TokenSettings tokenSettings = TokenSettings.builder().build();
-		assertThat(tokenSettings.getSettings()).hasSize(4);
+		assertThat(tokenSettings.getSettings()).hasSize(5);
 		assertThat(tokenSettings.getAccessTokenTimeToLive()).isEqualTo(Duration.ofMinutes(5));
+		assertThat(tokenSettings.getAccessTokenFormat()).isEqualTo(OAuth2TokenFormat.SELF_CONTAINED);
 		assertThat(tokenSettings.isReuseRefreshTokens()).isTrue();
 		assertThat(tokenSettings.getRefreshTokenTimeToLive()).isEqualTo(Duration.ofMinutes(60));
 		assertThat(tokenSettings.getIdTokenSignatureAlgorithm()).isEqualTo(SignatureAlgorithm.RS256);
@@ -68,6 +70,22 @@ public class TokenSettingsTests {
 				.isEqualTo("accessTokenTimeToLive must be greater than Duration.ZERO");
 	}
 
+	@Test
+	public void accessTokenFormatWhenProvidedThenSet() {
+		TokenSettings tokenSettings = TokenSettings.builder()
+				.accessTokenFormat(OAuth2TokenFormat.REFERENCE)
+				.build();
+		assertThat(tokenSettings.getAccessTokenFormat()).isEqualTo(OAuth2TokenFormat.REFERENCE);
+	}
+
+	@Test
+	public void accessTokenFormatWhenNullThenThrowIllegalArgumentException() {
+		assertThatThrownBy(() -> TokenSettings.builder().accessTokenFormat(null))
+				.isInstanceOf(IllegalArgumentException.class)
+				.extracting(Throwable::getMessage)
+				.isEqualTo("accessTokenFormat cannot be null");
+	}
+
 	@Test
 	public void reuseRefreshTokensWhenFalseThenSet() {
 		TokenSettings tokenSettings = TokenSettings.builder()
@@ -118,7 +136,7 @@ public class TokenSettingsTests {
 				.setting("name1", "value1")
 				.settings(settings -> settings.put("name2", "value2"))
 				.build();
-		assertThat(tokenSettings.getSettings()).hasSize(6);
+		assertThat(tokenSettings.getSettings()).hasSize(7);
 		assertThat(tokenSettings.<String>getSetting("name1")).isEqualTo("value1");
 		assertThat(tokenSettings.<String>getSetting("name2")).isEqualTo("value2");
 	}