فهرست منبع

Polish gh-167

Joe Grandja 4 سال پیش
والد
کامیت
7dc9da3340
28فایلهای تغییر یافته به همراه1667 افزوده شده و 1599 حذف شده
  1. 9 8
      oauth2-authorization-server/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2AuthorizationServerConfigurer.java
  2. 0 370
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/AbstractOAuth2AuthorizationServerConfiguration.java
  3. 407 0
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/AbstractOAuth2AuthorizationServerMetadata.java
  4. 21 27
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/OAuth2AuthorizationServerMetadata.java
  5. 38 21
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/OAuth2AuthorizationServerMetadataClaimAccessor.java
  6. 21 14
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/OAuth2AuthorizationServerMetadataClaimNames.java
  7. 0 82
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/endpoint/PkceCodeChallengeMethod2.java
  8. 0 162
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/http/converter/OAuth2AuthorizationServerConfigurationHttpMessageConverter.java
  9. 164 0
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/http/converter/OAuth2AuthorizationServerMetadataHttpMessageConverter.java
  10. 42 50
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/oidc/OidcProviderConfiguration.java
  11. 4 5
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/oidc/OidcProviderMetadataClaimAccessor.java
  12. 2 1
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/oidc/OidcProviderMetadataClaimNames.java
  13. 2 3
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientAuthenticationProvider.java
  14. 2 4
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/ProviderSettings.java
  15. 7 8
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/web/OidcProviderConfigurationEndpointFilter.java
  16. 1 2
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationEndpointFilter.java
  17. 44 35
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationServerMetadataEndpointFilter.java
  18. 8 6
      oauth2-authorization-server/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2AuthorizationServerMetadataTests.java
  19. 585 0
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/core/OAuth2AuthorizationServerMetadataTests.java
  20. 0 451
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/core/endpoint/OAuth2AuthorizationServerConfigurationTests.java
  21. 0 41
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/core/endpoint/PkceCodeChallengeMethod2Test.java
  22. 0 218
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/core/http/converter/OAuth2AuthorizationServerConfigurationHttpMessageConverterTests.java
  23. 224 0
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/core/http/converter/OAuth2AuthorizationServerMetadataHttpMessageConverterTests.java
  24. 15 26
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/core/oidc/OidcProviderConfigurationTests.java
  25. 10 9
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/core/oidc/http/converter/OidcProviderConfigurationHttpMessageConverterTests.java
  26. 19 19
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/ProviderSettingsTests.java
  27. 1 1
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/web/OidcProviderConfigurationEndpointFilterTests.java
  28. 41 36
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationServerMetadataEndpointFilterTests.java

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

@@ -49,7 +49,7 @@ import org.springframework.security.oauth2.server.authorization.config.ProviderS
 import org.springframework.security.oauth2.server.authorization.oidc.web.OidcProviderConfigurationEndpointFilter;
 import org.springframework.security.oauth2.server.authorization.web.NimbusJwkSetEndpointFilter;
 import org.springframework.security.oauth2.server.authorization.web.OAuth2AuthorizationEndpointFilter;
-import org.springframework.security.oauth2.server.authorization.web.OAuth2AuthorizationServerConfigurationEndpointFilter;
+import org.springframework.security.oauth2.server.authorization.web.OAuth2AuthorizationServerMetadataEndpointFilter;
 import org.springframework.security.oauth2.server.authorization.web.OAuth2ClientAuthenticationFilter;
 import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenEndpointFilter;
 import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenIntrospectionEndpointFilter;
@@ -79,6 +79,7 @@ import org.springframework.util.StringUtils;
  * @see OAuth2TokenRevocationEndpointFilter
  * @see NimbusJwkSetEndpointFilter
  * @see OidcProviderConfigurationEndpointFilter
+ * @see OAuth2AuthorizationServerMetadataEndpointFilter
  * @see OAuth2ClientAuthenticationFilter
  */
 public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBuilder<B>>
@@ -90,7 +91,7 @@ public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBui
 	private RequestMatcher tokenRevocationEndpointMatcher;
 	private RequestMatcher jwkSetEndpointMatcher;
 	private RequestMatcher oidcProviderConfigurationEndpointMatcher;
-	private RequestMatcher oauth2ServerConfigurationEndpointMatcher;
+	private RequestMatcher authorizationServerMetadataEndpointMatcher;
 	private final RequestMatcher endpointsMatcher = (request) ->
 			this.authorizationEndpointMatcher.matches(request) ||
 			this.tokenEndpointMatcher.matches(request) ||
@@ -98,7 +99,7 @@ public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBui
 			this.tokenRevocationEndpointMatcher.matches(request) ||
 			this.jwkSetEndpointMatcher.matches(request) ||
 			this.oidcProviderConfigurationEndpointMatcher.matches(request) ||
-			this.oauth2ServerConfigurationEndpointMatcher.matches(request);
+			this.authorizationServerMetadataEndpointMatcher.matches(request);
 
 	/**
 	 * Sets the repository of registered clients.
@@ -218,9 +219,9 @@ public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBui
 					new OidcProviderConfigurationEndpointFilter(providerSettings);
 			builder.addFilterBefore(postProcess(oidcProviderConfigurationEndpointFilter), AbstractPreAuthenticatedProcessingFilter.class);
 
-			OAuth2AuthorizationServerConfigurationEndpointFilter authorizationServerConfigurationFilter
-					= new OAuth2AuthorizationServerConfigurationEndpointFilter(providerSettings);
-			builder.addFilterBefore(postProcess(authorizationServerConfigurationFilter), AbstractPreAuthenticatedProcessingFilter.class);
+			OAuth2AuthorizationServerMetadataEndpointFilter authorizationServerMetadataEndpointFilter =
+					new OAuth2AuthorizationServerMetadataEndpointFilter(providerSettings);
+			builder.addFilterBefore(postProcess(authorizationServerMetadataEndpointFilter), AbstractPreAuthenticatedProcessingFilter.class);
 		}
 
 		JWKSource<SecurityContext> jwkSource = getJwkSource(builder);
@@ -284,8 +285,8 @@ public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBui
 				providerSettings.jwkSetEndpoint(), HttpMethod.GET.name());
 		this.oidcProviderConfigurationEndpointMatcher = new AntPathRequestMatcher(
 				OidcProviderConfigurationEndpointFilter.DEFAULT_OIDC_PROVIDER_CONFIGURATION_ENDPOINT_URI, HttpMethod.GET.name());
-		this.oauth2ServerConfigurationEndpointMatcher = new AntPathRequestMatcher(
-				OAuth2AuthorizationServerConfigurationEndpointFilter.DEFAULT_OAUTH2_AUTHORIZATION_SERVER_CONFIGURATION_ENDPOINT_URI, HttpMethod.GET.name());
+		this.authorizationServerMetadataEndpointMatcher = new AntPathRequestMatcher(
+				OAuth2AuthorizationServerMetadataEndpointFilter.DEFAULT_OAUTH2_AUTHORIZATION_SERVER_METADATA_ENDPOINT_URI, HttpMethod.GET.name());
 	}
 
 	private static void validateProviderSettings(ProviderSettings providerSettings) {

+ 0 - 370
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/AbstractOAuth2AuthorizationServerConfiguration.java

@@ -1,370 +0,0 @@
-/*
- * Copyright 2020 the original author or authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * 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 org.springframework.util.Assert;
-
-import java.io.Serializable;
-import java.net.URI;
-import java.net.URL;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.LinkedHashMap;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.function.Consumer;
-import java.util.stream.Collectors;
-
-
-/**
- * A base representation of a Provider Configuration response, returned by an endpoint defined
- * either in OpenID Connect Discovery 1.0 or OAuth 2.0 Authorization Server Metadata.
- * It contains a set of claims about the Provider's configuration.
- *
- * @author Daniel Garnier-Moiroux
- * @see OAuth2AuthorizationServerMetadataClaimAccessor
- * @since 0.1.1
- * @see <a target="_blank" href="https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse">4.2. OpenID Provider Configuration Response</a>
- * @see <a target="_blank" href="https://tools.ietf.org/html/rfc8414#section-3.2">3.2. Authorization Server Metadata Response</a>
- */
-public abstract class AbstractOAuth2AuthorizationServerConfiguration implements OAuth2AuthorizationServerMetadataClaimAccessor, Serializable {
-	private static final long serialVersionUID = Version.SERIAL_VERSION_UID;
-
-	protected final Map<String, Object> claims;
-
-	protected AbstractOAuth2AuthorizationServerConfiguration(Map<String, Object> claims) {
-		Assert.notEmpty(claims, "claims cannot be empty");
-		this.claims = Collections.unmodifiableMap(new LinkedHashMap<>(claims));
-	}
-
-	/**
-	 * Returns the Authorization Server metadata.
-	 *
-	 * @return a {@code Map} of the metadata values
-	 */
-	@Override
-	public Map<String, Object> getClaims() {
-		return this.claims;
-	}
-
-	/**
-	 * An abstract builder for subclasses of {@link AbstractOAuth2AuthorizationServerConfiguration}.
-	 */
-	protected static abstract class AbstractBuilder<T extends AbstractOAuth2AuthorizationServerConfiguration, B extends AbstractBuilder<T, B>> {
-
-		protected final Map<String, Object> claims = new LinkedHashMap<>();
-
-		protected AbstractBuilder() {
-		}
-
-		@SuppressWarnings("unchecked")
-		protected B getThis() { return (B) this; };    // avoid unchecked casts in subclasses by using "getThis()" instead of "(B) this"
-
-		/**
-		 * Use this {@code issuer} in the resulting {@link AbstractOAuth2AuthorizationServerConfiguration}, REQUIRED.
-		 *
-		 * @param issuer the {@code URL} of the Authorization Server's Issuer Identifier
-		 * @return the {@link AbstractBuilder} for further configuration
-		 */
-		public B issuer(String issuer) {
-			return claim(OAuth2AuthorizationServerMetadataClaimNames.ISSUER, issuer);
-		}
-
-		/**
-		 * Use this {@code authorization_endpoint} in the resulting {@link AbstractOAuth2AuthorizationServerConfiguration}, REQUIRED.
-		 *
-		 * @param authorizationEndpoint the {@code URL} of the Authorization Server's OAuth 2.0 Authorization Endpoint
-		 * @return the {@link AbstractBuilder} for further configuration
-		 */
-		public B authorizationEndpoint(String authorizationEndpoint) {
-			return claim(OAuth2AuthorizationServerMetadataClaimNames.AUTHORIZATION_ENDPOINT, authorizationEndpoint);
-		}
-
-		/**
-		 * Use this {@code token_endpoint} in the resulting {@link AbstractOAuth2AuthorizationServerConfiguration}, REQUIRED.
-		 *
-		 * @param tokenEndpoint the {@code URL} of the Authorization Server's OAuth 2.0 Token Endpoint
-		 * @return the {@link AbstractBuilder} for further configuration
-		 */
-		public B tokenEndpoint(String tokenEndpoint) {
-			return claim(OAuth2AuthorizationServerMetadataClaimNames.TOKEN_ENDPOINT, tokenEndpoint);
-		}
-
-		/**
-		 * Add this Authentication Method to the collection of {@code token_endpoint_auth_methods_supported}
-		 * in the resulting {@link AbstractOAuth2AuthorizationServerConfiguration}, OPTIONAL.
-		 *
-		 * @param authenticationMethod the OAuth 2.0 Authentication Method supported by the Token Endpoint
-		 * @return the {@link AbstractBuilder} for further configuration
-		 */
-		public B tokenEndpointAuthenticationMethod(String authenticationMethod) {
-			addClaimToClaimList(OAuth2AuthorizationServerMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHODS_SUPPORTED, authenticationMethod);
-			return getThis();
-		}
-
-		/**
-		 * A {@code Consumer} of the Token Endpoint Authentication Method(s) allowing the ability to add, replace, or remove.
-		 *
-		 * @param authenticationMethodsConsumer a {@code Consumer} of the Token Endpoint Authentication Method(s)
-		 * @return the {@link AbstractBuilder} for further configuration
-		 */
-		public B tokenEndpointAuthenticationMethods(Consumer<List<String>> authenticationMethodsConsumer) {
-			acceptClaimValues(OAuth2AuthorizationServerMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHODS_SUPPORTED, authenticationMethodsConsumer);
-			return getThis();
-		}
-
-		/**
-		 * Use this {@code jwks_uri} in the resulting {@link AbstractOAuth2AuthorizationServerConfiguration}, REQUIRED.
-		 *
-		 * @param jwkSetUri the {@code URL} of the Authorization Server's JSON Web Key Set document
-		 * @return the {@link AbstractBuilder} for further configuration
-		 */
-		public B jwkSetUri(String jwkSetUri) {
-			return claim(OAuth2AuthorizationServerMetadataClaimNames.JWKS_URI, jwkSetUri);
-		}
-
-		/**
-		 * Add this Response Type to the collection of {@code response_types_supported} in the resulting
-		 * {@link AbstractOAuth2AuthorizationServerConfiguration}.
-		 *
-		 * @param responseType the OAuth 2.0 {@code response_type} value that the Authorization Server supports
-		 * @return the {@link AbstractBuilder} for further configuration
-		 */
-		public B responseType(String responseType) {
-			addClaimToClaimList(OAuth2AuthorizationServerMetadataClaimNames.RESPONSE_TYPES_SUPPORTED, responseType);
-			return getThis();
-		}
-
-		/**
-		 * A {@code Consumer} of the Response Type(s) allowing the ability to add, replace, or remove.
-		 *
-		 * @param responseTypesConsumer a {@code Consumer} of the Response Type(s)
-		 * @return the {@link AbstractBuilder} for further configuration
-		 */
-		public B responseTypes(Consumer<List<String>> responseTypesConsumer) {
-			acceptClaimValues(OAuth2AuthorizationServerMetadataClaimNames.RESPONSE_TYPES_SUPPORTED, responseTypesConsumer);
-			return getThis();
-		}
-
-		/**
-		 * Add this Grant Type to the collection of {@code grant_types_supported} in the resulting
-		 * {@link AbstractOAuth2AuthorizationServerConfiguration}, OPTIONAL.
-		 *
-		 * @param grantType the OAuth 2.0 {@code grant_type} value that the Authorization Server supports
-		 * @return the {@link AbstractBuilder} for further configuration
-		 */
-		public B grantType(String grantType) {
-			addClaimToClaimList(OAuth2AuthorizationServerMetadataClaimNames.GRANT_TYPES_SUPPORTED, grantType);
-			return getThis();
-		}
-
-		/**
-		 * A {@code Consumer} of the Grant Type(s) allowing the ability to add, replace, or remove.
-		 *
-		 * @param grantTypesConsumer a {@code Consumer} of the Grant Type(s)
-		 * @return the {@link AbstractBuilder} for further configuration
-		 */
-		public B grantTypes(Consumer<List<String>> grantTypesConsumer) {
-			acceptClaimValues(OAuth2AuthorizationServerMetadataClaimNames.GRANT_TYPES_SUPPORTED, grantTypesConsumer);
-			return getThis();
-		}
-
-		/**
-		 * Add this Scope to the collection of {@code scopes_supported} in the resulting
-		 * {@link AbstractOAuth2AuthorizationServerConfiguration}, RECOMMENDED.
-		 *
-		 * @param scope the OAuth 2.0 {@code scope} value that the Authorization Server supports
-		 * @return the {@link AbstractBuilder} for further configuration
-		 */
-		public B scope(String scope) {
-			addClaimToClaimList(OAuth2AuthorizationServerMetadataClaimNames.SCOPES_SUPPORTED, scope);
-			return getThis();
-		}
-
-		/**
-		 * A {@code Consumer} of the Scopes(s) allowing the ability to add, replace, or remove.
-		 *
-		 * @param scopesConsumer a {@code Consumer} of the Scopes(s)
-		 * @return the {@link AbstractBuilder} for further configuration
-		 */
-		public B scopes(Consumer<List<String>> scopesConsumer) {
-			acceptClaimValues(OAuth2AuthorizationServerMetadataClaimNames.SCOPES_SUPPORTED, scopesConsumer);
-			return getThis();
-		}
-
-		/**
-		 * Use this claim in the resulting {@link AbstractOAuth2AuthorizationServerConfiguration}
-		 *
-		 * @param name the claim name
-		 * @param value the claim value
-		 * @return the {@link AbstractBuilder} for further configuration
-		 */
-		public B 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 getThis();
-		}
-
-		/**
-		 * Provides access to every {@link #claim(String, Object)} declared so far with
-		 * the possibility to add, replace, or remove.
-		 *
-		 * @param claimsConsumer a {@code Consumer} of the claims
-		 * @return the {@link AbstractBuilder} for further configurations
-		 */
-		public B claims(Consumer<Map<String, Object>> claimsConsumer) {
-			claimsConsumer.accept(this.claims);
-			return getThis();
-		}
-
-		/**
-		 * Use this {@code revocation_endpoint} in the resulting {@link AbstractOAuth2AuthorizationServerConfiguration}, OPTIONAL.
-		 *
-		 * @param tokenRevocationEndpoint the {@code URL} of the OAuth 2.0 Authorization Server's Token Revocation Endpoint
-		 * @return the {@link AbstractBuilder} for further configuration
-		 */
-		public B tokenRevocationEndpoint(String tokenRevocationEndpoint) {
-			return claim(OAuth2AuthorizationServerMetadataClaimNames.REVOCATION_ENDPOINT, tokenRevocationEndpoint);
-		}
-
-		/**
-		 * Add this Authentication Method to the collection of {@code revocation_endpoint_auth_methods_supported}
-		 * in the resulting {@link AbstractOAuth2AuthorizationServerConfiguration}, OPTIONAL.
-		 *
-		 * @param authenticationMethod the OAuth 2.0 Authentication Method supported by the Revocation Endpoint
-		 * @return the {@link AbstractBuilder} for further configuration
-		 */
-		public B tokenRevocationEndpointAuthenticationMethod(String authenticationMethod) {
-			addClaimToClaimList(OAuth2AuthorizationServerMetadataClaimNames.REVOCATION_ENDPOINT_AUTH_METHODS_SUPPORTED, authenticationMethod);
-			return getThis();
-		}
-
-		/**
-		 * A {@code Consumer} of the Token Revocation Endpoint Authentication Method(s) allowing the ability to add,
-		 * replace, or remove.
-		 *
-		 * @param authenticationMethodsConsumer a {@code Consumer} of the OAuth 2.0 Token Revocation Endpoint Authentication Method(s)
-		 * @return the {@link AbstractBuilder} for further configuration
-		 */
-		public B tokenRevocationEndpointAuthenticationMethods(Consumer<List<String>> authenticationMethodsConsumer) {
-			acceptClaimValues(OAuth2AuthorizationServerMetadataClaimNames.REVOCATION_ENDPOINT_AUTH_METHODS_SUPPORTED, authenticationMethodsConsumer);
-			return getThis();
-		}
-
-		/**
-		 * Add this Proof Key for Code Exchange (PKCE) Code Challenge Method to the collection of
-		 * {@code code_challenge_methods_supported} in the resulting {@link AbstractOAuth2AuthorizationServerConfiguration}, OPTIONAL.
-		 *
-		 * @param codeChallengeMethod the Proof Key for Code Exchange (PKCE) Code Challenge Method
-		 * supported by the Authorization Server
-		 * @return the {@link AbstractBuilder} for further configuration
-		 */
-		public B codeChallengeMethod(String codeChallengeMethod) {
-			addClaimToClaimList(OAuth2AuthorizationServerMetadataClaimNames.CODE_CHALLENGE_METHODS_SUPPORTED, codeChallengeMethod);
-			return getThis();
-		}
-
-		/**
-		 * A {@code Consumer} of the Proof Key for Code Exchange (PKCE) Code Challenge Method(s) allowing
-		 * the ability to add, replace, or remove.
-		 *
-		 * @param codeChallengeMethodsConsumer a {@code Consumer} of the Proof Key for Code Exchange (PKCE)
-		 * Code Challenge Method(s)
-		 * @return the {@link AbstractBuilder} for further configuration
-		 */
-		public B codeChallengeMethods(Consumer<List<String>> codeChallengeMethodsConsumer) {
-			acceptClaimValues(OAuth2AuthorizationServerMetadataClaimNames.CODE_CHALLENGE_METHODS_SUPPORTED, codeChallengeMethodsConsumer);
-			return getThis();
-		}
-
-		/**
-		 * Creates the {@link AbstractOAuth2AuthorizationServerConfiguration}.
-		 *
-		 * @return the {@link AbstractOAuth2AuthorizationServerConfiguration}
-		 */
-		public abstract T build();
-
-		protected void validateCommonClaims() {
-			Assert.notNull(this.claims.get(OAuth2AuthorizationServerMetadataClaimNames.ISSUER), "issuer cannot be null");
-			validateURL(this.claims.get(OAuth2AuthorizationServerMetadataClaimNames.ISSUER), "issuer must be a valid URL");
-			Assert.notNull(this.claims.get(OAuth2AuthorizationServerMetadataClaimNames.AUTHORIZATION_ENDPOINT), "authorizationEndpoint cannot be null");
-			validateURL(this.claims.get(OAuth2AuthorizationServerMetadataClaimNames.AUTHORIZATION_ENDPOINT), "authorizationEndpoint must be a valid URL");
-			Assert.notNull(this.claims.get(OAuth2AuthorizationServerMetadataClaimNames.TOKEN_ENDPOINT), "tokenEndpoint cannot be null");
-			validateURL(this.claims.get(OAuth2AuthorizationServerMetadataClaimNames.TOKEN_ENDPOINT), "tokenEndpoint must be a valid URL");
-			Assert.notNull(this.claims.get(OAuth2AuthorizationServerMetadataClaimNames.JWKS_URI), "jwksUri cannot be null");
-			validateURL(this.claims.get(OAuth2AuthorizationServerMetadataClaimNames.JWKS_URI), "jwksUri must be a valid URL");
-			Assert.notNull(this.claims.get(OAuth2AuthorizationServerMetadataClaimNames.RESPONSE_TYPES_SUPPORTED), "responseTypes cannot be null");
-			Assert.isInstanceOf(List.class, this.claims.get(OAuth2AuthorizationServerMetadataClaimNames.RESPONSE_TYPES_SUPPORTED), "responseTypes must be of type List");
-			Assert.notEmpty((List<?>) this.claims.get(OAuth2AuthorizationServerMetadataClaimNames.RESPONSE_TYPES_SUPPORTED), "responseTypes cannot be empty");
-			if (this.claims.get(OAuth2AuthorizationServerMetadataClaimNames.REVOCATION_ENDPOINT) != null) {
-				validateURL(this.claims.get(OAuth2AuthorizationServerMetadataClaimNames.REVOCATION_ENDPOINT), "tokenRevocationEndpoint must be a valid URL");
-			}
-		}
-
-		/**
-		 * Remove claims of type Collection that have a size of zero.
-		 * <p>
-		 * Both <a target="_blank" href="https://tools.ietf.org/html/rfc8414#section-3.2">3.2. Authorization Server Metadata Response</a>
-		 * and <a target="_blank" href="https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse">4.2. OpenID Provider Configuration Response</a>
-		 * state "Claims with zero elements MUST be omitted from the response."
-		 */
-		protected void removeEmptyClaims() {
-			Set<String> claimsToRemove = this.claims.entrySet()
-					.stream()
-					.filter(entry -> entry.getValue() != null)
-					.filter(entry -> Collection.class.isAssignableFrom(entry.getValue().getClass()))
-					.filter(entry -> ((Collection<?>) entry.getValue()).size() == 0)
-					.map(Map.Entry::getKey)
-					.collect(Collectors.toSet());
-
-			for (String claimToRemove : claimsToRemove) {
-				this.claims.remove(claimToRemove);
-			}
-		}
-
-		protected static void validateURL(Object url, String errorMessage) {
-			if (URL.class.isAssignableFrom(url.getClass())) {
-				return;
-			}
-
-			try {
-				new URI(url.toString()).toURL();
-			} catch (Exception ex) {
-				throw new IllegalArgumentException(errorMessage, ex);
-			}
-		}
-
-		@SuppressWarnings("unchecked")
-		protected void addClaimToClaimList(String name, String value) {
-			Assert.hasText(name, "name cannot be empty");
-			Assert.notNull(value, "value cannot be null");
-			this.claims.computeIfAbsent(name, k -> new LinkedList<String>());
-			((List<String>) this.claims.get(name)).add(value);
-		}
-
-		@SuppressWarnings("unchecked")
-		protected void acceptClaimValues(String name, Consumer<List<String>> valuesConsumer) {
-			Assert.hasText(name, "name cannot be empty");
-			Assert.notNull(valuesConsumer, "valuesConsumer cannot be null");
-			this.claims.computeIfAbsent(name, k -> new LinkedList<String>());
-			List<String> values = (List<String>) this.claims.get(name);
-			valuesConsumer.accept(values);
-		}
-	}
-}

+ 407 - 0
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/AbstractOAuth2AuthorizationServerMetadata.java

@@ -0,0 +1,407 @@
+/*
+ * Copyright 2020-2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.oauth2.core;
+
+import java.io.Serializable;
+import java.net.URI;
+import java.net.URL;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Consumer;
+
+import org.springframework.util.Assert;
+
+/**
+ * A base representation of OAuth 2.0 Authorization Server metadata,
+ * returned by an endpoint defined in OAuth 2.0 Authorization Server Metadata and OpenID Connect Discovery 1.0.
+ * The metadata endpoint returns a set of claims an Authorization Server describes about its configuration.
+ *
+ * @author Daniel Garnier-Moiroux
+ * @see OAuth2AuthorizationServerMetadataClaimAccessor
+ * @since 0.1.1
+ * @see <a target="_blank" href="https://tools.ietf.org/html/rfc8414#section-3.2">3.2. Authorization Server Metadata Response</a>
+ * @see <a target="_blank" href="https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse">4.2. OpenID Provider Configuration Response</a>
+ */
+public abstract class AbstractOAuth2AuthorizationServerMetadata implements OAuth2AuthorizationServerMetadataClaimAccessor, Serializable {
+	private static final long serialVersionUID = Version.SERIAL_VERSION_UID;
+	private final Map<String, Object> claims;
+
+	protected AbstractOAuth2AuthorizationServerMetadata(Map<String, Object> claims) {
+		Assert.notEmpty(claims, "claims cannot be empty");
+		this.claims = Collections.unmodifiableMap(new LinkedHashMap<>(claims));
+	}
+
+	/**
+	 * Returns the metadata as claims.
+	 *
+	 * @return a {@code Map} of the metadata as claims
+	 */
+	@Override
+	public Map<String, Object> getClaims() {
+		return this.claims;
+	}
+
+	/**
+	 * A builder for subclasses of {@link AbstractOAuth2AuthorizationServerMetadata}.
+	 */
+	protected static abstract class AbstractBuilder<T extends AbstractOAuth2AuthorizationServerMetadata, B extends AbstractBuilder<T, B>> {
+		private final Map<String, Object> claims = new LinkedHashMap<>();
+
+		protected AbstractBuilder() {
+		}
+
+		protected Map<String, Object> getClaims() {
+			return this.claims;
+		}
+
+		@SuppressWarnings("unchecked")
+		protected final B getThis() {
+			return (B) this;	// avoid unchecked casts in subclasses by using "getThis()" instead of "(B) this"
+		};
+
+		/**
+		 * Use this {@code issuer} in the resulting {@link AbstractOAuth2AuthorizationServerMetadata}, REQUIRED.
+		 *
+		 * @param issuer the {@code URL} of the Authorization Server's Issuer Identifier
+		 * @return the {@link AbstractBuilder} for further configuration
+		 */
+		public B issuer(String issuer) {
+			return claim(OAuth2AuthorizationServerMetadataClaimNames.ISSUER, issuer);
+		}
+
+		/**
+		 * Use this {@code authorization_endpoint} in the resulting {@link AbstractOAuth2AuthorizationServerMetadata}, REQUIRED.
+		 *
+		 * @param authorizationEndpoint the {@code URL} of the OAuth 2.0 Authorization Endpoint
+		 * @return the {@link AbstractBuilder} for further configuration
+		 */
+		public B authorizationEndpoint(String authorizationEndpoint) {
+			return claim(OAuth2AuthorizationServerMetadataClaimNames.AUTHORIZATION_ENDPOINT, authorizationEndpoint);
+		}
+
+		/**
+		 * Use this {@code token_endpoint} in the resulting {@link AbstractOAuth2AuthorizationServerMetadata}, REQUIRED.
+		 *
+		 * @param tokenEndpoint the {@code URL} of the OAuth 2.0 Token Endpoint
+		 * @return the {@link AbstractBuilder} for further configuration
+		 */
+		public B tokenEndpoint(String tokenEndpoint) {
+			return claim(OAuth2AuthorizationServerMetadataClaimNames.TOKEN_ENDPOINT, tokenEndpoint);
+		}
+
+		/**
+		 * Add this client authentication method to the collection of {@code token_endpoint_auth_methods_supported}
+		 * in the resulting {@link AbstractOAuth2AuthorizationServerMetadata}, OPTIONAL.
+		 *
+		 * @param authenticationMethod the client authentication method supported by the OAuth 2.0 Token Endpoint
+		 * @return the {@link AbstractBuilder} for further configuration
+		 */
+		public B tokenEndpointAuthenticationMethod(String authenticationMethod) {
+			addClaimToClaimList(OAuth2AuthorizationServerMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHODS_SUPPORTED, authenticationMethod);
+			return getThis();
+		}
+
+		/**
+		 * A {@code Consumer} of the client authentication method(s) allowing the ability to add, replace, or remove.
+		 *
+		 * @param authenticationMethodsConsumer a {@code Consumer} of the client authentication method(s) supported by the OAuth 2.0 Token Endpoint
+		 * @return the {@link AbstractBuilder} for further configuration
+		 */
+		public B tokenEndpointAuthenticationMethods(Consumer<List<String>> authenticationMethodsConsumer) {
+			acceptClaimValues(OAuth2AuthorizationServerMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHODS_SUPPORTED, authenticationMethodsConsumer);
+			return getThis();
+		}
+
+		/**
+		 * Use this {@code jwks_uri} in the resulting {@link AbstractOAuth2AuthorizationServerMetadata}, OPTIONAL.
+		 *
+		 * @param jwkSetUrl the {@code URL} of the JSON Web Key Set
+		 * @return the {@link AbstractBuilder} for further configuration
+		 */
+		public B jwkSetUrl(String jwkSetUrl) {
+			return claim(OAuth2AuthorizationServerMetadataClaimNames.JWKS_URI, jwkSetUrl);
+		}
+
+		/**
+		 * Add this OAuth 2.0 {@code scope} to the collection of {@code scopes_supported}
+		 * in the resulting {@link AbstractOAuth2AuthorizationServerMetadata}, RECOMMENDED.
+		 *
+		 * @param scope the OAuth 2.0 {@code scope} value supported
+		 * @return the {@link AbstractBuilder} for further configuration
+		 */
+		public B scope(String scope) {
+			addClaimToClaimList(OAuth2AuthorizationServerMetadataClaimNames.SCOPES_SUPPORTED, scope);
+			return getThis();
+		}
+
+		/**
+		 * A {@code Consumer} of the OAuth 2.0 {@code scope} values supported allowing the ability to add, replace, or remove.
+		 *
+		 * @param scopesConsumer a {@code Consumer} of the OAuth 2.0 {@code scope} values supported
+		 * @return the {@link AbstractBuilder} for further configuration
+		 */
+		public B scopes(Consumer<List<String>> scopesConsumer) {
+			acceptClaimValues(OAuth2AuthorizationServerMetadataClaimNames.SCOPES_SUPPORTED, scopesConsumer);
+			return getThis();
+		}
+
+		/**
+		 * Add this OAuth 2.0 {@code response_type} to the collection of {@code response_types_supported}
+		 * in the resulting {@link AbstractOAuth2AuthorizationServerMetadata}, REQUIRED.
+		 *
+		 * @param responseType the OAuth 2.0 {@code response_type} value supported
+		 * @return the {@link AbstractBuilder} for further configuration
+		 */
+		public B responseType(String responseType) {
+			addClaimToClaimList(OAuth2AuthorizationServerMetadataClaimNames.RESPONSE_TYPES_SUPPORTED, responseType);
+			return getThis();
+		}
+
+		/**
+		 * A {@code Consumer} of the OAuth 2.0 {@code response_type} values supported allowing the ability to add, replace, or remove.
+		 *
+		 * @param responseTypesConsumer a {@code Consumer} of the OAuth 2.0 {@code response_type} values supported
+		 * @return the {@link AbstractBuilder} for further configuration
+		 */
+		public B responseTypes(Consumer<List<String>> responseTypesConsumer) {
+			acceptClaimValues(OAuth2AuthorizationServerMetadataClaimNames.RESPONSE_TYPES_SUPPORTED, responseTypesConsumer);
+			return getThis();
+		}
+
+		/**
+		 * Add this OAuth 2.0 {@code grant_type} to the collection of {@code grant_types_supported}
+		 * in the resulting {@link AbstractOAuth2AuthorizationServerMetadata}, OPTIONAL.
+		 *
+		 * @param grantType the OAuth 2.0 {@code grant_type} value supported
+		 * @return the {@link AbstractBuilder} for further configuration
+		 */
+		public B grantType(String grantType) {
+			addClaimToClaimList(OAuth2AuthorizationServerMetadataClaimNames.GRANT_TYPES_SUPPORTED, grantType);
+			return getThis();
+		}
+
+		/**
+		 * A {@code Consumer} of the OAuth 2.0 {@code grant_type} values supported allowing the ability to add, replace, or remove.
+		 *
+		 * @param grantTypesConsumer a {@code Consumer} of the OAuth 2.0 {@code grant_type} values supported
+		 * @return the {@link AbstractBuilder} for further configuration
+		 */
+		public B grantTypes(Consumer<List<String>> grantTypesConsumer) {
+			acceptClaimValues(OAuth2AuthorizationServerMetadataClaimNames.GRANT_TYPES_SUPPORTED, grantTypesConsumer);
+			return getThis();
+		}
+
+		/**
+		 * Use this {@code revocation_endpoint} in the resulting {@link AbstractOAuth2AuthorizationServerMetadata}, OPTIONAL.
+		 *
+		 * @param tokenRevocationEndpoint the {@code URL} of the OAuth 2.0 Token Revocation Endpoint
+		 * @return the {@link AbstractBuilder} for further configuration
+		 */
+		public B tokenRevocationEndpoint(String tokenRevocationEndpoint) {
+			return claim(OAuth2AuthorizationServerMetadataClaimNames.REVOCATION_ENDPOINT, tokenRevocationEndpoint);
+		}
+
+		/**
+		 * Add this client authentication method to the collection of {@code revocation_endpoint_auth_methods_supported}
+		 * in the resulting {@link AbstractOAuth2AuthorizationServerMetadata}, OPTIONAL.
+		 *
+		 * @param authenticationMethod the client authentication method supported by the OAuth 2.0 Token Revocation Endpoint
+		 * @return the {@link AbstractBuilder} for further configuration
+		 */
+		public B tokenRevocationEndpointAuthenticationMethod(String authenticationMethod) {
+			addClaimToClaimList(OAuth2AuthorizationServerMetadataClaimNames.REVOCATION_ENDPOINT_AUTH_METHODS_SUPPORTED, authenticationMethod);
+			return getThis();
+		}
+
+		/**
+		 * A {@code Consumer} of the client authentication method(s) allowing the ability to add, replace, or remove.
+		 *
+		 * @param authenticationMethodsConsumer a {@code Consumer} of the client authentication method(s) supported by the OAuth 2.0 Token Revocation Endpoint
+		 * @return the {@link AbstractBuilder} for further configuration
+		 */
+		public B tokenRevocationEndpointAuthenticationMethods(Consumer<List<String>> authenticationMethodsConsumer) {
+			acceptClaimValues(OAuth2AuthorizationServerMetadataClaimNames.REVOCATION_ENDPOINT_AUTH_METHODS_SUPPORTED, authenticationMethodsConsumer);
+			return getThis();
+		}
+
+		/**
+		 * Use this {@code introspection_endpoint} in the resulting {@link AbstractOAuth2AuthorizationServerMetadata}, OPTIONAL.
+		 *
+		 * @param tokenIntrospectionEndpoint the {@code URL} of the OAuth 2.0 Token Introspection Endpoint
+		 * @return the {@link AbstractBuilder} for further configuration
+		 */
+		public B tokenIntrospectionEndpoint(String tokenIntrospectionEndpoint) {
+			return claim(OAuth2AuthorizationServerMetadataClaimNames.INTROSPECTION_ENDPOINT, tokenIntrospectionEndpoint);
+		}
+
+		/**
+		 * Add this client authentication method to the collection of {@code introspection_endpoint_auth_methods_supported}
+		 * in the resulting {@link AbstractOAuth2AuthorizationServerMetadata}, OPTIONAL.
+		 *
+		 * @param authenticationMethod the client authentication method supported by the OAuth 2.0 Token Introspection Endpoint
+		 * @return the {@link AbstractBuilder} for further configuration
+		 */
+		public B tokenIntrospectionEndpointAuthenticationMethod(String authenticationMethod) {
+			addClaimToClaimList(OAuth2AuthorizationServerMetadataClaimNames.INTROSPECTION_ENDPOINT_AUTH_METHODS_SUPPORTED, authenticationMethod);
+			return getThis();
+		}
+
+		/**
+		 * A {@code Consumer} of the client authentication method(s) allowing the ability to add, replace, or remove.
+		 *
+		 * @param authenticationMethodsConsumer a {@code Consumer} of the client authentication method(s) supported by the OAuth 2.0 Token Introspection Endpoint
+		 * @return the {@link AbstractBuilder} for further configuration
+		 */
+		public B tokenIntrospectionEndpointAuthenticationMethods(Consumer<List<String>> authenticationMethodsConsumer) {
+			acceptClaimValues(OAuth2AuthorizationServerMetadataClaimNames.INTROSPECTION_ENDPOINT_AUTH_METHODS_SUPPORTED, authenticationMethodsConsumer);
+			return getThis();
+		}
+
+		/**
+		 * Add this Proof Key for Code Exchange (PKCE) {@code code_challenge_method} to the collection of {@code code_challenge_methods_supported}
+		 * in the resulting {@link AbstractOAuth2AuthorizationServerMetadata}, OPTIONAL.
+		 *
+		 * @param codeChallengeMethod the {@code code_challenge_method} value supported
+		 * @return the {@link AbstractBuilder} for further configuration
+		 */
+		public B codeChallengeMethod(String codeChallengeMethod) {
+			addClaimToClaimList(OAuth2AuthorizationServerMetadataClaimNames.CODE_CHALLENGE_METHODS_SUPPORTED, codeChallengeMethod);
+			return getThis();
+		}
+
+		/**
+		 * A {@code Consumer} of the Proof Key for Code Exchange (PKCE) {@code code_challenge_method} values supported allowing the ability to add, replace, or remove.
+		 *
+		 * @param codeChallengeMethodsConsumer a {@code Consumer} of the {@code code_challenge_method} values supported
+		 * @return the {@link AbstractBuilder} for further configuration
+		 */
+		public B codeChallengeMethods(Consumer<List<String>> codeChallengeMethodsConsumer) {
+			acceptClaimValues(OAuth2AuthorizationServerMetadataClaimNames.CODE_CHALLENGE_METHODS_SUPPORTED, codeChallengeMethodsConsumer);
+			return getThis();
+		}
+
+		/**
+		 * Use this claim in the resulting {@link AbstractOAuth2AuthorizationServerMetadata}.
+		 *
+		 * @param name the claim name
+		 * @param value the claim value
+		 * @return the {@link AbstractBuilder} for further configuration
+		 */
+		public B 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 getThis();
+		}
+
+		/**
+		 * Provides access to every {@link #claim(String, Object)} declared so far with
+		 * the possibility to add, replace, or remove.
+		 *
+		 * @param claimsConsumer a {@code Consumer} of the claims
+		 * @return the {@link AbstractBuilder} for further configurations
+		 */
+		public B claims(Consumer<Map<String, Object>> claimsConsumer) {
+			claimsConsumer.accept(this.claims);
+			return getThis();
+		}
+
+		/**
+		 * Creates the {@link AbstractOAuth2AuthorizationServerMetadata}.
+		 *
+		 * @return the {@link AbstractOAuth2AuthorizationServerMetadata}
+		 */
+		public abstract T build();
+
+		protected void validate() {
+			Assert.notNull(getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.ISSUER), "issuer cannot be null");
+			validateURL(getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.ISSUER), "issuer must be a valid URL");
+			Assert.notNull(getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.AUTHORIZATION_ENDPOINT), "authorizationEndpoint cannot be null");
+			validateURL(getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.AUTHORIZATION_ENDPOINT), "authorizationEndpoint must be a valid URL");
+			Assert.notNull(getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.TOKEN_ENDPOINT), "tokenEndpoint cannot be null");
+			validateURL(getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.TOKEN_ENDPOINT), "tokenEndpoint must be a valid URL");
+			if (getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHODS_SUPPORTED) != null) {
+				Assert.isInstanceOf(List.class, getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHODS_SUPPORTED), "tokenEndpointAuthenticationMethods must be of type List");
+				Assert.notEmpty((List<?>) getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHODS_SUPPORTED), "tokenEndpointAuthenticationMethods cannot be empty");
+			}
+			if (getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.JWKS_URI) != null) {
+				validateURL(getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.JWKS_URI), "jwksUri must be a valid URL");
+			}
+			if (getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.SCOPES_SUPPORTED) != null) {
+				Assert.isInstanceOf(List.class, getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.SCOPES_SUPPORTED), "scopes must be of type List");
+				Assert.notEmpty((List<?>) getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.SCOPES_SUPPORTED), "scopes cannot be empty");
+			}
+			Assert.notNull(getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.RESPONSE_TYPES_SUPPORTED), "responseTypes cannot be null");
+			Assert.isInstanceOf(List.class, getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.RESPONSE_TYPES_SUPPORTED), "responseTypes must be of type List");
+			Assert.notEmpty((List<?>) getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.RESPONSE_TYPES_SUPPORTED), "responseTypes cannot be empty");
+			if (getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.GRANT_TYPES_SUPPORTED) != null) {
+				Assert.isInstanceOf(List.class, getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.GRANT_TYPES_SUPPORTED), "grantTypes must be of type List");
+				Assert.notEmpty((List<?>) getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.GRANT_TYPES_SUPPORTED), "grantTypes cannot be empty");
+			}
+			if (getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.REVOCATION_ENDPOINT) != null) {
+				validateURL(getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.REVOCATION_ENDPOINT), "tokenRevocationEndpoint must be a valid URL");
+			}
+			if (getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.REVOCATION_ENDPOINT_AUTH_METHODS_SUPPORTED) != null) {
+				Assert.isInstanceOf(List.class, getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.REVOCATION_ENDPOINT_AUTH_METHODS_SUPPORTED), "tokenRevocationEndpointAuthenticationMethods must be of type List");
+				Assert.notEmpty((List<?>) getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.REVOCATION_ENDPOINT_AUTH_METHODS_SUPPORTED), "tokenRevocationEndpointAuthenticationMethods cannot be empty");
+			}
+			if (getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.INTROSPECTION_ENDPOINT) != null) {
+				validateURL(getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.INTROSPECTION_ENDPOINT), "tokenIntrospectionEndpoint must be a valid URL");
+			}
+			if (getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.INTROSPECTION_ENDPOINT_AUTH_METHODS_SUPPORTED) != null) {
+				Assert.isInstanceOf(List.class, getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.INTROSPECTION_ENDPOINT_AUTH_METHODS_SUPPORTED), "tokenIntrospectionEndpointAuthenticationMethods must be of type List");
+				Assert.notEmpty((List<?>) getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.INTROSPECTION_ENDPOINT_AUTH_METHODS_SUPPORTED), "tokenIntrospectionEndpointAuthenticationMethods cannot be empty");
+			}
+			if (getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.CODE_CHALLENGE_METHODS_SUPPORTED) != null) {
+				Assert.isInstanceOf(List.class, getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.CODE_CHALLENGE_METHODS_SUPPORTED), "codeChallengeMethods must be of type List");
+				Assert.notEmpty((List<?>) getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.CODE_CHALLENGE_METHODS_SUPPORTED), "codeChallengeMethods cannot be empty");
+			}
+		}
+
+		@SuppressWarnings("unchecked")
+		private void addClaimToClaimList(String name, String value) {
+			Assert.hasText(name, "name cannot be empty");
+			Assert.notNull(value, "value cannot be null");
+			getClaims().computeIfAbsent(name, k -> new LinkedList<String>());
+			((List<String>) getClaims().get(name)).add(value);
+		}
+
+		@SuppressWarnings("unchecked")
+		private void acceptClaimValues(String name, Consumer<List<String>> valuesConsumer) {
+			Assert.hasText(name, "name cannot be empty");
+			Assert.notNull(valuesConsumer, "valuesConsumer cannot be null");
+			getClaims().computeIfAbsent(name, k -> new LinkedList<String>());
+			List<String> values = (List<String>) getClaims().get(name);
+			valuesConsumer.accept(values);
+		}
+
+		private static void validateURL(Object url, String errorMessage) {
+			if (URL.class.isAssignableFrom(url.getClass())) {
+				return;
+			}
+
+			try {
+				new URI(url.toString()).toURL();
+			} catch (Exception ex) {
+				throw new IllegalArgumentException(errorMessage, ex);
+			}
+		}
+
+	}
+}

+ 21 - 27
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2AuthorizationServerConfiguration.java → oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/OAuth2AuthorizationServerMetadata.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2020 the original author or authors.
+ * Copyright 2020-2021 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -13,34 +13,27 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.springframework.security.oauth2.core.endpoint;
+package org.springframework.security.oauth2.core;
 
-import org.springframework.security.oauth2.core.AbstractOAuth2AuthorizationServerConfiguration;
-import org.springframework.security.oauth2.core.OAuth2AuthorizationServerMetadataClaimAccessor;
-import org.springframework.security.oauth2.core.Version;
-import org.springframework.util.Assert;
-
-import java.io.Serializable;
 import java.util.Map;
 
+import org.springframework.util.Assert;
+
 /**
- * A representation of an OAuth 2.0 Authorization Server Configuration response,
- * which is returned form an OAuth 2.0 Authorization Server's Configuration Endpoint,
+ * A representation of an OAuth 2.0 Authorization Server Metadata response,
+ * which is returned from an OAuth 2.0 Authorization Server's Metadata Endpoint,
  * and contains a set of claims about the Authorization Server's configuration.
  * The claims are defined by the OAuth 2.0 Authorization Server Metadata
  * specification (RFC 8414).
  *
  * @author Daniel Garnier-Moiroux
  * @since 0.1.1
- * @see AbstractOAuth2AuthorizationServerConfiguration
- * @see OAuth2AuthorizationServerMetadataClaimAccessor
+ * @see AbstractOAuth2AuthorizationServerMetadata
  * @see <a target="_blank" href="https://tools.ietf.org/html/rfc8414#section-3.2">3.2. Authorization Server Metadata Response</a>
  */
-public final class OAuth2AuthorizationServerConfiguration extends AbstractOAuth2AuthorizationServerConfiguration
-		implements OAuth2AuthorizationServerMetadataClaimAccessor, Serializable {
-	private static final long serialVersionUID = Version.SERIAL_VERSION_UID;
+public final class OAuth2AuthorizationServerMetadata extends AbstractOAuth2AuthorizationServerMetadata {
 
-	private OAuth2AuthorizationServerConfiguration(Map<String, Object> claims) {
+	private OAuth2AuthorizationServerMetadata(Map<String, Object> claims) {
 		super(claims);
 	}
 
@@ -66,27 +59,28 @@ public final class OAuth2AuthorizationServerConfiguration extends AbstractOAuth2
 	}
 
 	/**
-	 * Helps configure an {@link OAuth2AuthorizationServerConfiguration}.
+	 * Helps configure an {@link OAuth2AuthorizationServerMetadata}.
 	 */
-	public static class Builder
-			extends AbstractOAuth2AuthorizationServerConfiguration.AbstractBuilder<OAuth2AuthorizationServerConfiguration, Builder> {
+	public static class Builder extends AbstractBuilder<OAuth2AuthorizationServerMetadata, Builder> {
+
 		private Builder() {
 		}
 
 		/**
-		 * Validate the claims and build the {@link OAuth2AuthorizationServerConfiguration}.
+		 * Validate the claims and build the {@link OAuth2AuthorizationServerMetadata}.
 		 * <p>
 		 * The following claims are REQUIRED:
-		 * {@code issuer}, {@code authorization_endpoint}, {@code token_endpoint},
-		 * {@code jwks_uri} and {@code response_types_supported}.
+		 * {@code issuer}, {@code authorization_endpoint}, {@code token_endpoint}
+		 * and {@code response_types_supported}.
 		 *
-		 * @return the {@link OAuth2AuthorizationServerConfiguration}
+		 * @return the {@link OAuth2AuthorizationServerMetadata}
 		 */
-		public OAuth2AuthorizationServerConfiguration build() {
-			validateCommonClaims();
-			removeEmptyClaims();
-			return new OAuth2AuthorizationServerConfiguration(this.claims);
+		@Override
+		public OAuth2AuthorizationServerMetadata build() {
+			validate();
+			return new OAuth2AuthorizationServerMetadata(getClaims());
 		}
 
 	}
+
 }

+ 38 - 21
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/OAuth2AuthorizationServerMetadataClaimAccessor.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2020 the original author or authors.
+ * Copyright 2020-2021 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -15,18 +15,17 @@
  */
 package org.springframework.security.oauth2.core;
 
-
 import java.net.URL;
 import java.util.List;
 
 /**
- * A base {@link ClaimAccessor} for the "claims" the Authorization Server can make about
- * its configuration, used either in OpenID Connect Discovery 1.0 or OAuth 2.0 Authorization
- * Server Metadata.
+ * A {@link ClaimAccessor} for the "claims" an Authorization Server describes about its configuration,
+ * used in OAuth 2.0 Authorization Server Metadata and OpenID Connect Discovery 1.0.
  *
  * @author Daniel Garnier-Moiroux
  * @since 0.1.1
  * @see ClaimAccessor
+ * @see OAuth2AuthorizationServerMetadataClaimNames
  * @see <a target="_blank" href="https://tools.ietf.org/html/rfc8414#section-2">2. Authorization Server Metadata</a>
  * @see <a target="_blank" href="https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata">3. OpenID Provider Metadata</a>
  */
@@ -73,10 +72,19 @@ public interface OAuth2AuthorizationServerMetadataClaimAccessor extends ClaimAcc
 	 *
 	 * @return the {@code URL} of the JSON Web Key Set
 	 */
-	default URL getJwkSetUri() {
+	default URL getJwkSetUrl() {
 		return getClaimAsURL(OAuth2AuthorizationServerMetadataClaimNames.JWKS_URI);
 	}
 
+	/**
+	 * Returns the OAuth 2.0 {@code scope} values supported {@code (scopes_supported)}.
+	 *
+	 * @return the OAuth 2.0 {@code scope} values supported
+	 */
+	default List<String> getScopes() {
+		return getClaimAsStringList(OAuth2AuthorizationServerMetadataClaimNames.SCOPES_SUPPORTED);
+	}
+
 	/**
 	 * Returns the OAuth 2.0 {@code response_type} values supported {@code (response_types_supported)}.
 	 *
@@ -95,22 +103,13 @@ public interface OAuth2AuthorizationServerMetadataClaimAccessor extends ClaimAcc
 		return getClaimAsStringList(OAuth2AuthorizationServerMetadataClaimNames.GRANT_TYPES_SUPPORTED);
 	}
 
-	/**
-	 * Returns the OAuth 2.0 {@code scope} values supported {@code (scopes_supported)}.
-	 *
-	 * @return the OAuth 2.0 {@code scope} values supported
-	 */
-	default List<String> getScopes() {
-		return this.getClaimAsStringList(OAuth2AuthorizationServerMetadataClaimNames.SCOPES_SUPPORTED);
-	}
-
 	/**
 	 * Returns the {@code URL} of the OAuth 2.0 Token Revocation Endpoint {@code (revocation_endpoint)}.
 	 *
 	 * @return the {@code URL} of the OAuth 2.0 Token Revocation Endpoint
 	 */
 	default URL getTokenRevocationEndpoint() {
-		return this.getClaimAsURL(OAuth2AuthorizationServerMetadataClaimNames.REVOCATION_ENDPOINT);
+		return getClaimAsURL(OAuth2AuthorizationServerMetadataClaimNames.REVOCATION_ENDPOINT);
 	}
 
 	/**
@@ -119,16 +118,34 @@ public interface OAuth2AuthorizationServerMetadataClaimAccessor extends ClaimAcc
 	 * @return the client authentication methods supported by the OAuth 2.0 Token Revocation Endpoint
 	 */
 	default List<String> getTokenRevocationEndpointAuthenticationMethods() {
-		return this.getClaimAsStringList(OAuth2AuthorizationServerMetadataClaimNames.REVOCATION_ENDPOINT_AUTH_METHODS_SUPPORTED);
+		return getClaimAsStringList(OAuth2AuthorizationServerMetadataClaimNames.REVOCATION_ENDPOINT_AUTH_METHODS_SUPPORTED);
+	}
+
+	/**
+	 * Returns the {@code URL} of the OAuth 2.0 Token Introspection Endpoint {@code (introspection_endpoint)}.
+	 *
+	 * @return the {@code URL} of the OAuth 2.0 Token Introspection Endpoint
+	 */
+	default URL getTokenIntrospectionEndpoint() {
+		return getClaimAsURL(OAuth2AuthorizationServerMetadataClaimNames.INTROSPECTION_ENDPOINT);
 	}
 
 	/**
-	 * Returns the Proof Key for Code Exchange (PKCE) code challenge methods supported by the
-	 * OAuth 2.0 Authorization Server {@code (code_challenge_methods_supported)}.
+	 * Returns the client authentication methods supported by the OAuth 2.0 Token Introspection Endpoint {@code (introspection_endpoint_auth_methods_supported)}.
 	 *
-	 * @return the code challenge methods supported by the OAuth 2.0 Authorization Server
+	 * @return the client authentication methods supported by the OAuth 2.0 Token Introspection Endpoint
+	 */
+	default List<String> getTokenIntrospectionEndpointAuthenticationMethods() {
+		return getClaimAsStringList(OAuth2AuthorizationServerMetadataClaimNames.INTROSPECTION_ENDPOINT_AUTH_METHODS_SUPPORTED);
+	}
+
+	/**
+	 * Returns the Proof Key for Code Exchange (PKCE) {@code code_challenge_method} values supported {@code (code_challenge_methods_supported)}.
+	 *
+	 * @return the {@code code_challenge_method} values supported
 	 */
 	default List<String> getCodeChallengeMethods() {
-		return this.getClaimAsStringList(OAuth2AuthorizationServerMetadataClaimNames.CODE_CHALLENGE_METHODS_SUPPORTED);
+		return getClaimAsStringList(OAuth2AuthorizationServerMetadataClaimNames.CODE_CHALLENGE_METHODS_SUPPORTED);
 	}
+
 }

+ 21 - 14
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/OAuth2AuthorizationServerMetadataClaimNames.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2020 the original author or authors.
+ * Copyright 2020-2021 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -15,15 +15,12 @@
  */
 package org.springframework.security.oauth2.core;
 
-import org.springframework.security.oauth2.core.oidc.OidcProviderMetadataClaimNames;
-
 /**
- * The names of the "claims" an Authorization Server can make about its configuration,
- * used either in OpenID Connect Discovery 1.0 or OAuth 2.0 Authorization Server Metadata.
+ * The names of the "claims" an Authorization Server describes about its configuration,
+ * used in OAuth 2.0 Authorization Server Metadata and OpenID Connect Discovery 1.0.
  *
  * @author Daniel Garnier-Moiroux
  * @since 0.1.1
- * @see OidcProviderMetadataClaimNames
  * @see <a target="_blank" href="https://tools.ietf.org/html/rfc8414#section-2">2. Authorization Server Metadata</a>
  * @see <a target="_blank" href="https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata">3. OpenID Provider Metadata</a>
  */
@@ -54,6 +51,11 @@ public interface OAuth2AuthorizationServerMetadataClaimNames {
 	 */
 	String JWKS_URI = "jwks_uri";
 
+	/**
+	 * {@code scopes_supported} - the OAuth 2.0 {@code scope} values supported
+	 */
+	String SCOPES_SUPPORTED = "scopes_supported";
+
 	/**
 	 * {@code response_types_supported} - the OAuth 2.0 {@code response_type} values supported
 	 */
@@ -64,24 +66,29 @@ public interface OAuth2AuthorizationServerMetadataClaimNames {
 	 */
 	String GRANT_TYPES_SUPPORTED = "grant_types_supported";
 
-	/**
-	 * {@code scopes_supported} - the OAuth 2.0 {@code scope} values supported
-	 */
-	String SCOPES_SUPPORTED = "scopes_supported";
-
 	/**
 	 * {@code revocation_endpoint} - the {@code URL} of the OAuth 2.0 Token Revocation Endpoint
 	 */
 	String REVOCATION_ENDPOINT = "revocation_endpoint";
 
 	/**
-	 * {@code token_endpoint_auth_methods_supported} - the client authentication methods supported by the OAuth 2.0 Token Revocation Endpoint
+	 * {@code revocation_endpoint_auth_methods_supported} - the client authentication methods supported by the OAuth 2.0 Token Revocation Endpoint
 	 */
 	String REVOCATION_ENDPOINT_AUTH_METHODS_SUPPORTED = "revocation_endpoint_auth_methods_supported";
 
 	/**
-	 * {@code code_challenge_methods_supported} - the Proof Key for Code Exchange (PKCE) code challenge methods
-	 * supported by the OAuth 2.0 Authorization Server
+	 * {@code introspection_endpoint} - the {@code URL} of the OAuth 2.0 Token Introspection Endpoint
+	 */
+	String INTROSPECTION_ENDPOINT = "introspection_endpoint";
+
+	/**
+	 * {@code introspection_endpoint_auth_methods_supported} - the client authentication methods supported by the OAuth 2.0 Token Introspection Endpoint
+	 */
+	String INTROSPECTION_ENDPOINT_AUTH_METHODS_SUPPORTED = "introspection_endpoint_auth_methods_supported";
+
+	/**
+	 * {@code code_challenge_methods_supported} - the Proof Key for Code Exchange (PKCE) {@code code_challenge_method} values supported
 	 */
 	String CODE_CHALLENGE_METHODS_SUPPORTED = "code_challenge_methods_supported";
+
 }

+ 0 - 82
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/endpoint/PkceCodeChallengeMethod2.java

@@ -1,82 +0,0 @@
-/*
- * Copyright 2020 the original author or authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * 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.endpoint;
-
-import org.springframework.security.oauth2.core.Version;
-import org.springframework.util.Assert;
-
-import java.io.Serializable;
-
-
-/**
- * TODO
- * This class is temporary and will be removed after upgrading to Spring Security 5.5.0 GA.
- *
- * The {@code code_challenge_method} is consumed by the authorization endpoint. The client sets
- * the {@code code_challenge_method} parameter and a {@code code_challenge} derived from the
- * {@code code_verifier} using the {@code code_challenge_method}.
- *
- * <p>
- * The {@code code_challenge_method} parameter value may be one of &quot;S256&quot; or
- * &quot;plain&quot;. If the client is capable of using &quot;S256&quot;, it MUST use
- * &quot;S256&quot;, as it is mandatory to implement on the server.
- *
- * @author Daniel Garnier-Moiroux
- * @since 0.1.1
- * @see <a target="_blank" href="https://tools.ietf.org/html/rfc7636#section-6.2">6.2. PKCE Code Challenge Method Registry</a>
- */
-public final class PkceCodeChallengeMethod2 implements Serializable {
-
-	private static final long serialVersionUID = Version.SERIAL_VERSION_UID;
-
-	public static final PkceCodeChallengeMethod2 S256 = new PkceCodeChallengeMethod2("S256");
-
-	public static final PkceCodeChallengeMethod2 PLAIN = new PkceCodeChallengeMethod2("plain");
-
-	private final String value;
-
-	private PkceCodeChallengeMethod2(String value) {
-		Assert.hasText(value, "value cannot be empty");
-		this.value = value;
-	}
-
-	/**
-	 * Returns the value of the authorization response type.
-	 *
-	 * @return the value of the authorization response type
-	 */
-	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;
-		}
-		PkceCodeChallengeMethod2 that = (PkceCodeChallengeMethod2) obj;
-		return this.getValue().equals(that.getValue());
-	}
-
-	@Override
-	public int hashCode() {
-		return this.getValue().hashCode();
-	}
-}

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

@@ -1,162 +0,0 @@
-/*
- * Copyright 2020 the original author or authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.springframework.security.oauth2.core.http.converter;
-
-import org.springframework.core.ParameterizedTypeReference;
-import org.springframework.core.convert.TypeDescriptor;
-import org.springframework.core.convert.converter.Converter;
-import org.springframework.http.HttpInputMessage;
-import org.springframework.http.HttpOutputMessage;
-import org.springframework.http.MediaType;
-import org.springframework.http.converter.AbstractHttpMessageConverter;
-import org.springframework.http.converter.GenericHttpMessageConverter;
-import org.springframework.http.converter.HttpMessageConverter;
-import org.springframework.http.converter.HttpMessageNotReadableException;
-import org.springframework.http.converter.HttpMessageNotWritableException;
-import org.springframework.security.oauth2.core.OAuth2AuthorizationServerMetadataClaimNames;
-import org.springframework.security.oauth2.core.converter.ClaimConversionService;
-import org.springframework.security.oauth2.core.converter.ClaimTypeConverter;
-import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationServerConfiguration;
-import org.springframework.util.Assert;
-
-import java.net.URL;
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.Map;
-
-
-/**
- * A {@link HttpMessageConverter} for an {@link OAuth2AuthorizationServerConfiguration OAuth 2.0 Authorization Server Configuration Response}.
- *
- * @author Daniel Garnier-Moiroux
- * @since 0.1.1
- * @see AbstractHttpMessageConverter
- * @see OAuth2AuthorizationServerConfiguration
- */
-public class OAuth2AuthorizationServerConfigurationHttpMessageConverter
-		extends AbstractHttpMessageConverter<OAuth2AuthorizationServerConfiguration> {
-
-	private static final ParameterizedTypeReference<Map<String, Object>> STRING_OBJECT_MAP =
-			new ParameterizedTypeReference<Map<String, Object>>() {};
-
-	private final GenericHttpMessageConverter<Object> jsonMessageConverter = HttpMessageConverters.getJsonMessageConverter();
-
-	private Converter<Map<String, Object>, OAuth2AuthorizationServerConfiguration> authorizationServerConfigurationConverter = new OAuth2AuthorizationServerConfigurationConverter();
-	private Converter<OAuth2AuthorizationServerConfiguration, Map<String, Object>> authorizationServerConfigurationParametersConverter = OAuth2AuthorizationServerConfiguration::getClaims;
-
-	public OAuth2AuthorizationServerConfigurationHttpMessageConverter() {
-		super(MediaType.APPLICATION_JSON, new MediaType("application", "*+json"));
-	}
-
-	@Override
-	protected boolean supports(Class<?> clazz) {
-		return OAuth2AuthorizationServerConfiguration.class.isAssignableFrom(clazz);
-	}
-
-	@Override
-	@SuppressWarnings("unchecked")
-	protected OAuth2AuthorizationServerConfiguration readInternal(Class<? extends OAuth2AuthorizationServerConfiguration> clazz, HttpInputMessage inputMessage)
-			throws HttpMessageNotReadableException {
-		try {
-			Map<String, Object> serverConfigurationParameters =
-					(Map<String, Object>) this.jsonMessageConverter.read(STRING_OBJECT_MAP.getType(), null, inputMessage);
-			return this.authorizationServerConfigurationConverter.convert(serverConfigurationParameters);
-		} catch (Exception ex) {
-			throw new HttpMessageNotReadableException(
-					"An error occurred reading the OAuth 2.0 Authorization Server Configuration: " + ex.getMessage(), ex, inputMessage);
-		}
-	}
-
-	@Override
-	protected void writeInternal(OAuth2AuthorizationServerConfiguration providerConfiguration, HttpOutputMessage outputMessage)
-			throws HttpMessageNotWritableException {
-		try {
-			Map<String, Object> providerConfigurationResponseParameters =
-					this.authorizationServerConfigurationParametersConverter.convert(providerConfiguration);
-			this.jsonMessageConverter.write(
-					providerConfigurationResponseParameters,
-					STRING_OBJECT_MAP.getType(),
-					MediaType.APPLICATION_JSON,
-					outputMessage
-			);
-		} catch (Exception ex) {
-			throw new HttpMessageNotWritableException(
-					"An error occurred writing the OAuth 2.0 Authorization Server Configuration: " + ex.getMessage(), ex);
-		}
-	}
-
-	/**
-	 * Sets the {@link Converter} used for converting the OAuth 2.0 Authorization Server Configuration
-	 * parameters to an {@link OAuth2AuthorizationServerConfiguration}.
-	 *
-	 * @param authorizationServerConfigurationConverter the {@link Converter} used for converting to
-	 * an {@link OAuth2AuthorizationServerConfiguration}.
-	 */
-	public void setAuthorizationServerConfigurationConverter(Converter<Map<String, Object>, OAuth2AuthorizationServerConfiguration> authorizationServerConfigurationConverter) {
-		Assert.notNull(authorizationServerConfigurationConverter, "authorizationServerConfigurationConverter cannot be null");
-		this.authorizationServerConfigurationConverter = authorizationServerConfigurationConverter;
-	}
-
-	/**
-	 * Sets the {@link Converter} used for converting the {@link OAuth2AuthorizationServerConfiguration} to a
-	 * {@code Map} representation of the OAuth 2.0 Authorization Server Configuration.
-	 *
-	 * @param authorizationServerConfigurationParametersConverter the {@link Converter} used for converting to a
-	 * {@code Map} representation of the OAuth 2.0 Authorization Server Configuration.
-	 */
-	public void setAuthorizationServerConfigurationParametersConverter(Converter<OAuth2AuthorizationServerConfiguration, Map<String, Object>> authorizationServerConfigurationParametersConverter) {
-		Assert.notNull(authorizationServerConfigurationParametersConverter, "authorizationServerConfigurationParametersConverter cannot be null");
-		this.authorizationServerConfigurationParametersConverter = authorizationServerConfigurationParametersConverter;
-	}
-
-	private static final class OAuth2AuthorizationServerConfigurationConverter implements Converter<Map<String, Object>, OAuth2AuthorizationServerConfiguration> {
-		private static final ClaimConversionService CLAIM_CONVERSION_SERVICE = ClaimConversionService.getSharedInstance();
-		private static final TypeDescriptor OBJECT_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(Object.class);
-		private static final TypeDescriptor STRING_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(String.class);
-		private static final TypeDescriptor URL_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(URL.class);
-		private final ClaimTypeConverter claimTypeConverter;
-
-		private OAuth2AuthorizationServerConfigurationConverter() {
-			Map<String, Converter<Object, ?>> claimNameToConverter = new HashMap<>();
-			Converter<Object, ?> collectionStringConverter = getConverter(
-					TypeDescriptor.collection(Collection.class, STRING_TYPE_DESCRIPTOR));
-			Converter<Object, ?> urlConverter = getConverter(URL_TYPE_DESCRIPTOR);
-
-			claimNameToConverter.put(OAuth2AuthorizationServerMetadataClaimNames.ISSUER, urlConverter);
-			claimNameToConverter.put(OAuth2AuthorizationServerMetadataClaimNames.AUTHORIZATION_ENDPOINT, urlConverter);
-			claimNameToConverter.put(OAuth2AuthorizationServerMetadataClaimNames.TOKEN_ENDPOINT, urlConverter);
-			claimNameToConverter.put(OAuth2AuthorizationServerMetadataClaimNames.REVOCATION_ENDPOINT, urlConverter);
-			claimNameToConverter.put(OAuth2AuthorizationServerMetadataClaimNames.JWKS_URI, urlConverter);
-			claimNameToConverter.put(OAuth2AuthorizationServerMetadataClaimNames.GRANT_TYPES_SUPPORTED, collectionStringConverter);
-			claimNameToConverter.put(OAuth2AuthorizationServerMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHODS_SUPPORTED, collectionStringConverter);
-			claimNameToConverter.put(OAuth2AuthorizationServerMetadataClaimNames.REVOCATION_ENDPOINT_AUTH_METHODS_SUPPORTED, collectionStringConverter);
-			claimNameToConverter.put(OAuth2AuthorizationServerMetadataClaimNames.SCOPES_SUPPORTED, collectionStringConverter);
-			claimNameToConverter.put(OAuth2AuthorizationServerMetadataClaimNames.RESPONSE_TYPES_SUPPORTED, collectionStringConverter);
-			claimNameToConverter.put(OAuth2AuthorizationServerMetadataClaimNames.CODE_CHALLENGE_METHODS_SUPPORTED, collectionStringConverter);
-			this.claimTypeConverter = new ClaimTypeConverter(claimNameToConverter);
-		}
-
-		@Override
-		public OAuth2AuthorizationServerConfiguration convert(Map<String, Object> source) {
-			Map<String, Object> parsedClaims = this.claimTypeConverter.convert(source);
-			return OAuth2AuthorizationServerConfiguration.withClaims(parsedClaims).build();
-		}
-
-		private static Converter<Object, ?> getConverter(TypeDescriptor targetDescriptor) {
-			return (source) -> CLAIM_CONVERSION_SERVICE.convert(source, OBJECT_TYPE_DESCRIPTOR, targetDescriptor);
-		}
-	}
-}

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

@@ -0,0 +1,164 @@
+/*
+ * Copyright 2020-2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.oauth2.core.http.converter;
+
+import java.net.URL;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.springframework.core.ParameterizedTypeReference;
+import org.springframework.core.convert.TypeDescriptor;
+import org.springframework.core.convert.converter.Converter;
+import org.springframework.http.HttpInputMessage;
+import org.springframework.http.HttpOutputMessage;
+import org.springframework.http.MediaType;
+import org.springframework.http.converter.AbstractHttpMessageConverter;
+import org.springframework.http.converter.GenericHttpMessageConverter;
+import org.springframework.http.converter.HttpMessageConverter;
+import org.springframework.http.converter.HttpMessageNotReadableException;
+import org.springframework.http.converter.HttpMessageNotWritableException;
+import org.springframework.security.oauth2.core.OAuth2AuthorizationServerMetadata;
+import org.springframework.security.oauth2.core.OAuth2AuthorizationServerMetadataClaimNames;
+import org.springframework.security.oauth2.core.converter.ClaimConversionService;
+import org.springframework.security.oauth2.core.converter.ClaimTypeConverter;
+import org.springframework.util.Assert;
+
+/**
+ * A {@link HttpMessageConverter} for an {@link OAuth2AuthorizationServerMetadata OAuth 2.0 Authorization Server Metadata Response}.
+ *
+ * @author Daniel Garnier-Moiroux
+ * @since 0.1.1
+ * @see AbstractHttpMessageConverter
+ * @see OAuth2AuthorizationServerMetadata
+ */
+public class OAuth2AuthorizationServerMetadataHttpMessageConverter
+		extends AbstractHttpMessageConverter<OAuth2AuthorizationServerMetadata> {
+
+	private static final ParameterizedTypeReference<Map<String, Object>> STRING_OBJECT_MAP =
+			new ParameterizedTypeReference<Map<String, Object>>() {};
+
+	private final GenericHttpMessageConverter<Object> jsonMessageConverter = HttpMessageConverters.getJsonMessageConverter();
+
+	private Converter<Map<String, Object>, OAuth2AuthorizationServerMetadata> authorizationServerMetadataConverter = new OAuth2AuthorizationServerMetadataConverter();
+	private Converter<OAuth2AuthorizationServerMetadata, Map<String, Object>> authorizationServerMetadataParametersConverter = OAuth2AuthorizationServerMetadata::getClaims;
+
+	public OAuth2AuthorizationServerMetadataHttpMessageConverter() {
+		super(MediaType.APPLICATION_JSON, new MediaType("application", "*+json"));
+	}
+
+	@Override
+	protected boolean supports(Class<?> clazz) {
+		return OAuth2AuthorizationServerMetadata.class.isAssignableFrom(clazz);
+	}
+
+	@Override
+	@SuppressWarnings("unchecked")
+	protected OAuth2AuthorizationServerMetadata readInternal(Class<? extends OAuth2AuthorizationServerMetadata> clazz, HttpInputMessage inputMessage)
+			throws HttpMessageNotReadableException {
+		try {
+			Map<String, Object> authorizationServerMetadataParameters =
+					(Map<String, Object>) this.jsonMessageConverter.read(STRING_OBJECT_MAP.getType(), null, inputMessage);
+			return this.authorizationServerMetadataConverter.convert(authorizationServerMetadataParameters);
+		} catch (Exception ex) {
+			throw new HttpMessageNotReadableException(
+					"An error occurred reading the OAuth 2.0 Authorization Server Metadata: " + ex.getMessage(), ex, inputMessage);
+		}
+	}
+
+	@Override
+	protected void writeInternal(OAuth2AuthorizationServerMetadata authorizationServerMetadata, HttpOutputMessage outputMessage)
+			throws HttpMessageNotWritableException {
+		try {
+			Map<String, Object> authorizationServerMetadataResponseParameters =
+					this.authorizationServerMetadataParametersConverter.convert(authorizationServerMetadata);
+			this.jsonMessageConverter.write(
+					authorizationServerMetadataResponseParameters,
+					STRING_OBJECT_MAP.getType(),
+					MediaType.APPLICATION_JSON,
+					outputMessage
+			);
+		} catch (Exception ex) {
+			throw new HttpMessageNotWritableException(
+					"An error occurred writing the OAuth 2.0 Authorization Server Metadata: " + ex.getMessage(), ex);
+		}
+	}
+
+	/**
+	 * Sets the {@link Converter} used for converting the OAuth 2.0 Authorization Server Metadata
+	 * parameters to an {@link OAuth2AuthorizationServerMetadata}.
+	 *
+	 * @param authorizationServerMetadataConverter the {@link Converter} used for converting to
+	 * an {@link OAuth2AuthorizationServerMetadata}.
+	 */
+	public final void setAuthorizationServerMetadataConverter(Converter<Map<String, Object>, OAuth2AuthorizationServerMetadata> authorizationServerMetadataConverter) {
+		Assert.notNull(authorizationServerMetadataConverter, "authorizationServerMetadataConverter cannot be null");
+		this.authorizationServerMetadataConverter = authorizationServerMetadataConverter;
+	}
+
+	/**
+	 * Sets the {@link Converter} used for converting the {@link OAuth2AuthorizationServerMetadata} to a
+	 * {@code Map} representation of the OAuth 2.0 Authorization Server Metadata.
+	 *
+	 * @param authorizationServerMetadataParametersConverter the {@link Converter} used for converting to a
+	 * {@code Map} representation of the OAuth 2.0 Authorization Server Metadata.
+	 */
+	public final void setAuthorizationServerMetadataParametersConverter(Converter<OAuth2AuthorizationServerMetadata, Map<String, Object>> authorizationServerMetadataParametersConverter) {
+		Assert.notNull(authorizationServerMetadataParametersConverter, "authorizationServerMetadataParametersConverter cannot be null");
+		this.authorizationServerMetadataParametersConverter = authorizationServerMetadataParametersConverter;
+	}
+
+	private static final class OAuth2AuthorizationServerMetadataConverter implements Converter<Map<String, Object>, OAuth2AuthorizationServerMetadata> {
+		private static final ClaimConversionService CLAIM_CONVERSION_SERVICE = ClaimConversionService.getSharedInstance();
+		private static final TypeDescriptor OBJECT_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(Object.class);
+		private static final TypeDescriptor STRING_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(String.class);
+		private static final TypeDescriptor URL_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(URL.class);
+		private final ClaimTypeConverter claimTypeConverter;
+
+		private OAuth2AuthorizationServerMetadataConverter() {
+			Converter<Object, ?> collectionStringConverter = getConverter(
+					TypeDescriptor.collection(Collection.class, STRING_TYPE_DESCRIPTOR));
+			Converter<Object, ?> urlConverter = getConverter(URL_TYPE_DESCRIPTOR);
+
+			Map<String, Converter<Object, ?>> claimConverters = new HashMap<>();
+			claimConverters.put(OAuth2AuthorizationServerMetadataClaimNames.ISSUER, urlConverter);
+			claimConverters.put(OAuth2AuthorizationServerMetadataClaimNames.AUTHORIZATION_ENDPOINT, urlConverter);
+			claimConverters.put(OAuth2AuthorizationServerMetadataClaimNames.TOKEN_ENDPOINT, urlConverter);
+			claimConverters.put(OAuth2AuthorizationServerMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHODS_SUPPORTED, collectionStringConverter);
+			claimConverters.put(OAuth2AuthorizationServerMetadataClaimNames.JWKS_URI, urlConverter);
+			claimConverters.put(OAuth2AuthorizationServerMetadataClaimNames.SCOPES_SUPPORTED, collectionStringConverter);
+			claimConverters.put(OAuth2AuthorizationServerMetadataClaimNames.RESPONSE_TYPES_SUPPORTED, collectionStringConverter);
+			claimConverters.put(OAuth2AuthorizationServerMetadataClaimNames.GRANT_TYPES_SUPPORTED, collectionStringConverter);
+			claimConverters.put(OAuth2AuthorizationServerMetadataClaimNames.REVOCATION_ENDPOINT, urlConverter);
+			claimConverters.put(OAuth2AuthorizationServerMetadataClaimNames.REVOCATION_ENDPOINT_AUTH_METHODS_SUPPORTED, collectionStringConverter);
+			claimConverters.put(OAuth2AuthorizationServerMetadataClaimNames.INTROSPECTION_ENDPOINT, urlConverter);
+			claimConverters.put(OAuth2AuthorizationServerMetadataClaimNames.INTROSPECTION_ENDPOINT_AUTH_METHODS_SUPPORTED, collectionStringConverter);
+			claimConverters.put(OAuth2AuthorizationServerMetadataClaimNames.CODE_CHALLENGE_METHODS_SUPPORTED, collectionStringConverter);
+			this.claimTypeConverter = new ClaimTypeConverter(claimConverters);
+		}
+
+		@Override
+		public OAuth2AuthorizationServerMetadata convert(Map<String, Object> source) {
+			Map<String, Object> parsedClaims = this.claimTypeConverter.convert(source);
+			return OAuth2AuthorizationServerMetadata.withClaims(parsedClaims).build();
+		}
+
+		private static Converter<Object, ?> getConverter(TypeDescriptor targetDescriptor) {
+			return (source) -> CLAIM_CONVERSION_SERVICE.convert(source, OBJECT_TYPE_DESCRIPTOR, targetDescriptor);
+		}
+	}
+
+}

+ 42 - 50
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/oidc/OidcProviderConfiguration.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2020 the original author or authors.
+ * Copyright 2020-2021 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -15,16 +15,15 @@
  */
 package org.springframework.security.oauth2.core.oidc;
 
-import org.springframework.security.oauth2.core.AbstractOAuth2AuthorizationServerConfiguration;
-import org.springframework.security.oauth2.core.Version;
-import org.springframework.security.oauth2.jose.jws.JwsAlgorithm;
-import org.springframework.util.Assert;
-
-import java.io.Serializable;
+import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
 import java.util.function.Consumer;
 
+import org.springframework.security.oauth2.core.AbstractOAuth2AuthorizationServerMetadata;
+import org.springframework.security.oauth2.jose.jws.JwsAlgorithm;
+import org.springframework.util.Assert;
+
 /**
  * A representation of an OpenID Provider Configuration Response,
  * which is returned from an Issuer's Discovery Endpoint,
@@ -33,13 +32,12 @@ import java.util.function.Consumer;
  *
  * @author Daniel Garnier-Moiroux
  * @since 0.1.0
+ * @see AbstractOAuth2AuthorizationServerMetadata
  * @see OidcProviderMetadataClaimAccessor
- * @see AbstractOAuth2AuthorizationServerConfiguration
  * @see <a target="_blank" href="https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse">4.2. OpenID Provider Configuration Response</a>
  */
-public final class OidcProviderConfiguration extends AbstractOAuth2AuthorizationServerConfiguration
-		implements OidcProviderMetadataClaimAccessor, Serializable {
-	private static final long serialVersionUID = Version.SERIAL_VERSION_UID;
+public final class OidcProviderConfiguration extends AbstractOAuth2AuthorizationServerMetadata
+		implements OidcProviderMetadataClaimAccessor {
 
 	private OidcProviderConfiguration(Map<String, Object> claims) {
 		super(claims);
@@ -66,9 +64,10 @@ public final class OidcProviderConfiguration extends AbstractOAuth2Authorization
 	}
 
 	/**
-	 * Helps configure an {@link OidcProviderConfiguration}
+	 * Helps configure an {@link OidcProviderConfiguration}.
 	 */
-	public static class Builder extends AbstractOAuth2AuthorizationServerConfiguration.AbstractBuilder<OidcProviderConfiguration, Builder> {
+	public static class Builder extends AbstractBuilder<OidcProviderConfiguration, Builder> {
+
 		private Builder() {
 		}
 
@@ -119,32 +118,6 @@ public final class OidcProviderConfiguration extends AbstractOAuth2Authorization
 			return this;
 		}
 
-		/**
-		 * Use this claim in the resulting {@link OidcProviderConfiguration}.
-		 *
-		 * @param name the claim name
-		 * @param value the claim value
-		 * @return the {@link Builder} for further configuration
-		 */
-		public Builder claim(String name, Object value) {
-			Assert.hasText(name, "name cannot be empty");
-			Assert.notNull(value, "value cannot be null");
-			this.claims.put(name, value);
-			return this;
-		}
-
-		/**
-		 * Provides access to every {@link #claim(String, Object)} declared so far with
-		 * the possibility to add, replace, or remove.
-		 *
-		 * @param claimsConsumer a {@code Consumer} of the claims
-		 * @return the {@link Builder} for further configurations
-		 */
-		public Builder claims(Consumer<Map<String, Object>> claimsConsumer) {
-			claimsConsumer.accept(this.claims);
-			return this;
-		}
-
 		/**
 		 * Validate the claims and build the {@link OidcProviderConfiguration}.
 		 * <p>
@@ -157,19 +130,38 @@ public final class OidcProviderConfiguration extends AbstractOAuth2Authorization
 		 */
 		@Override
 		public OidcProviderConfiguration build() {
-			validateCommonClaims();
-			validateOidcSpecificClaims();
-			removeEmptyClaims();
-			return new OidcProviderConfiguration(this.claims);
+			validate();
+			return new OidcProviderConfiguration(getClaims());
 		}
 
-		private void validateOidcSpecificClaims() {
-			Assert.notNull(this.claims.get(OidcProviderMetadataClaimNames.SUBJECT_TYPES_SUPPORTED), "subjectTypes cannot be null");
-			Assert.isInstanceOf(List.class, this.claims.get(OidcProviderMetadataClaimNames.SUBJECT_TYPES_SUPPORTED), "subjectTypes must be of type List");
-			Assert.notEmpty((List<?>) this.claims.get(OidcProviderMetadataClaimNames.SUBJECT_TYPES_SUPPORTED), "subjectTypes cannot be empty");
-			Assert.notNull(this.claims.get(OidcProviderMetadataClaimNames.ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED), "idTokenSigningAlgorithms cannot be null");
-			Assert.isInstanceOf(List.class, this.claims.get(OidcProviderMetadataClaimNames.ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED), "idTokenSigningAlgorithms must be of type List");
-			Assert.notEmpty((List<?>) this.claims.get(OidcProviderMetadataClaimNames.ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED), "idTokenSigningAlgorithms cannot be empty");
+		@Override
+		protected void validate() {
+			super.validate();
+			Assert.notNull(getClaims().get(OidcProviderMetadataClaimNames.JWKS_URI), "jwksUri cannot be null");
+			Assert.notNull(getClaims().get(OidcProviderMetadataClaimNames.SUBJECT_TYPES_SUPPORTED), "subjectTypes cannot be null");
+			Assert.isInstanceOf(List.class, getClaims().get(OidcProviderMetadataClaimNames.SUBJECT_TYPES_SUPPORTED), "subjectTypes must be of type List");
+			Assert.notEmpty((List<?>) getClaims().get(OidcProviderMetadataClaimNames.SUBJECT_TYPES_SUPPORTED), "subjectTypes cannot be empty");
+			Assert.notNull(getClaims().get(OidcProviderMetadataClaimNames.ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED), "idTokenSigningAlgorithms cannot be null");
+			Assert.isInstanceOf(List.class, getClaims().get(OidcProviderMetadataClaimNames.ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED), "idTokenSigningAlgorithms must be of type List");
+			Assert.notEmpty((List<?>) getClaims().get(OidcProviderMetadataClaimNames.ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED), "idTokenSigningAlgorithms cannot be empty");
 		}
+
+		@SuppressWarnings("unchecked")
+		private void addClaimToClaimList(String name, String value) {
+			Assert.hasText(name, "name cannot be empty");
+			Assert.notNull(value, "value cannot be null");
+			getClaims().computeIfAbsent(name, k -> new LinkedList<String>());
+			((List<String>) getClaims().get(name)).add(value);
+		}
+
+		@SuppressWarnings("unchecked")
+		private void acceptClaimValues(String name, Consumer<List<String>> valuesConsumer) {
+			Assert.hasText(name, "name cannot be empty");
+			Assert.notNull(valuesConsumer, "valuesConsumer cannot be null");
+			getClaims().computeIfAbsent(name, k -> new LinkedList<String>());
+			List<String> values = (List<String>) getClaims().get(name);
+			valuesConsumer.accept(values);
+		}
+
 	}
 }

+ 4 - 5
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/oidc/OidcProviderMetadataClaimAccessor.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2020 the original author or authors.
+ * Copyright 2020-2021 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -16,14 +16,13 @@
 package org.springframework.security.oauth2.core.oidc;
 
 
-import org.springframework.security.oauth2.core.OAuth2AuthorizationServerMetadataClaimAccessor;
+import java.util.List;
+
 import org.springframework.security.oauth2.core.ClaimAccessor;
+import org.springframework.security.oauth2.core.OAuth2AuthorizationServerMetadataClaimAccessor;
 import org.springframework.security.oauth2.jose.jws.JwsAlgorithm;
 import org.springframework.security.oauth2.jwt.Jwt;
 
-
-import java.util.List;
-
 /**
  * A {@link ClaimAccessor} for the "claims" that can be returned
  * in the OpenID Provider Configuration Response.

+ 2 - 1
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/oidc/OidcProviderMetadataClaimNames.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2020 the original author or authors.
+ * Copyright 2020-2021 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -24,6 +24,7 @@ import org.springframework.security.oauth2.jose.jws.JwsAlgorithm;
  *
  * @author Daniel Garnier-Moiroux
  * @since 0.1.0
+ * @see OAuth2AuthorizationServerMetadataClaimNames
  * @see <a target="_blank" href="https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata">3. OpenID Provider Metadata</a>
  */
 public interface OidcProviderMetadataClaimNames extends OAuth2AuthorizationServerMetadataClaimNames {

+ 2 - 3
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientAuthenticationProvider.java

@@ -31,7 +31,6 @@ import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
 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.core.endpoint.PkceCodeChallengeMethod2;
 import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
 import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
 import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
@@ -157,9 +156,9 @@ public class OAuth2ClientAuthenticationProvider implements AuthenticationProvide
 	private static boolean codeVerifierValid(String codeVerifier, String codeChallenge, String codeChallengeMethod) {
 		if (!StringUtils.hasText(codeVerifier)) {
 			return false;
-		} else if (!StringUtils.hasText(codeChallengeMethod) || PkceCodeChallengeMethod2.PLAIN.getValue().equals(codeChallengeMethod)) {
+		} else if (!StringUtils.hasText(codeChallengeMethod) || "plain".equals(codeChallengeMethod)) {
 			return  codeVerifier.equals(codeChallenge);
-		} else if (PkceCodeChallengeMethod2.S256.getValue().equals(codeChallengeMethod)) {
+		} else if ("S256".equals(codeChallengeMethod)) {
 			try {
 				MessageDigest md = MessageDigest.getInstance("SHA-256");
 				byte[] digest = md.digest(codeVerifier.getBytes(StandardCharsets.US_ASCII));

+ 2 - 4
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/ProviderSettings.java

@@ -70,8 +70,7 @@ public class ProviderSettings extends Settings {
 	}
 
 	/**
-	 * Returns the Provider's OAuth 2.0 Authorization endpoint.
-	 * The default is {@code /oauth2/authorize}.
+	 * Returns the Provider's OAuth 2.0 Authorization endpoint. The default is {@code /oauth2/authorize}.
 	 *
 	 * @return the Authorization endpoint
 	 */
@@ -90,8 +89,7 @@ public class ProviderSettings extends Settings {
 	}
 
 	/**
-	 * Returns the Provider's OAuth 2.0 Token endpoint.
-	 * The default is {@code /oauth2/token}.
+	 * Returns the Provider's OAuth 2.0 Token endpoint. The default is {@code /oauth2/token}.
 	 *
 	 * @return the Token endpoint
 	 */

+ 7 - 8
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/web/OidcProviderConfigurationEndpointFilter.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2020 the original author or authors.
+ * Copyright 2020-2021 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -75,21 +75,20 @@ public class OidcProviderConfigurationEndpointFilter extends OncePerRequestFilte
 			return;
 		}
 
-		OidcProviderConfiguration providerConfiguration = OidcProviderConfiguration
-				.builder()
+		OidcProviderConfiguration providerConfiguration = OidcProviderConfiguration.builder()
 				.issuer(this.providerSettings.issuer())
 				.authorizationEndpoint(asUrl(this.providerSettings.issuer(), this.providerSettings.authorizationEndpoint()))
 				.tokenEndpoint(asUrl(this.providerSettings.issuer(), this.providerSettings.tokenEndpoint()))
-				.tokenEndpointAuthenticationMethod("client_secret_basic") // TODO: Use ClientAuthenticationMethod.CLIENT_SECRET_BASIC in Spring Security 5.5.0
-				.tokenEndpointAuthenticationMethod("client_secret_post") // TODO: Use ClientAuthenticationMethod.CLIENT_SECRET_POST in Spring Security 5.5.0
-				.jwkSetUri(asUrl(this.providerSettings.issuer(), this.providerSettings.jwkSetEndpoint()))
+				.tokenEndpointAuthenticationMethod("client_secret_basic")	// TODO: Use ClientAuthenticationMethod.CLIENT_SECRET_BASIC in Spring Security 5.5.0
+				.tokenEndpointAuthenticationMethod("client_secret_post")	// TODO: Use ClientAuthenticationMethod.CLIENT_SECRET_POST in Spring Security 5.5.0
+				.jwkSetUrl(asUrl(this.providerSettings.issuer(), this.providerSettings.jwkSetEndpoint()))
+				.responseType(OAuth2AuthorizationResponseType.CODE.getValue())
 				.grantType(AuthorizationGrantType.AUTHORIZATION_CODE.getValue())
 				.grantType(AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())
 				.grantType(AuthorizationGrantType.REFRESH_TOKEN.getValue())
-				.responseType(OAuth2AuthorizationResponseType.CODE.getValue())
-				.scope(OidcScopes.OPENID)
 				.subjectType("public")
 				.idTokenSigningAlgorithm(SignatureAlgorithm.RS256.getName())
+				.scope(OidcScopes.OPENID)
 				.build();
 
 		ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);

+ 1 - 2
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationEndpointFilter.java

@@ -48,7 +48,6 @@ import org.springframework.security.oauth2.core.OAuth2TokenType;
 import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
 import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponseType;
 import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
-import org.springframework.security.oauth2.core.endpoint.PkceCodeChallengeMethod2;
 import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
 import org.springframework.security.oauth2.core.oidc.OidcScopes;
 import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
@@ -371,7 +370,7 @@ public class OAuth2AuthorizationEndpointFilter extends OncePerRequestFilter {
 			String codeChallengeMethod = authorizationRequestContext.getParameters().getFirst(PkceParameterNames.CODE_CHALLENGE_METHOD);
 			if (StringUtils.hasText(codeChallengeMethod)) {
 				if (authorizationRequestContext.getParameters().get(PkceParameterNames.CODE_CHALLENGE_METHOD).size() != 1 ||
-						(!PkceCodeChallengeMethod2.S256.getValue().equals(codeChallengeMethod) && !PkceCodeChallengeMethod2.PLAIN.getValue().equals(codeChallengeMethod))) {
+						(!"S256".equals(codeChallengeMethod) && !"plain".equals(codeChallengeMethod))) {
 					authorizationRequestContext.setError(
 							createError(OAuth2ErrorCodes.INVALID_REQUEST, PkceParameterNames.CODE_CHALLENGE_METHOD, PKCE_ERROR_URI));
 					return;

+ 44 - 35
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationServerConfigurationEndpointFilter.java → oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationServerMetadataEndpointFilter.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2020 the original author or authors.
+ * Copyright 2020-2021 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -15,15 +15,22 @@
  */
 package org.springframework.security.oauth2.server.authorization.web;
 
+import java.io.IOException;
+import java.util.List;
+import java.util.function.Consumer;
+
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
 import org.springframework.http.HttpMethod;
 import org.springframework.http.MediaType;
 import org.springframework.http.server.ServletServerHttpResponse;
 import org.springframework.security.oauth2.core.AuthorizationGrantType;
+import org.springframework.security.oauth2.core.OAuth2AuthorizationServerMetadata;
 import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponseType;
-import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationServerConfiguration;
-import org.springframework.security.oauth2.core.endpoint.PkceCodeChallengeMethod2;
-import org.springframework.security.oauth2.core.http.converter.OAuth2AuthorizationServerConfigurationHttpMessageConverter;
-import org.springframework.security.oauth2.core.oidc.OidcScopes;
+import org.springframework.security.oauth2.core.http.converter.OAuth2AuthorizationServerMetadataHttpMessageConverter;
 import org.springframework.security.oauth2.server.authorization.config.ProviderSettings;
 import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
 import org.springframework.security.web.util.matcher.RequestMatcher;
@@ -31,36 +38,31 @@ import org.springframework.util.Assert;
 import org.springframework.web.filter.OncePerRequestFilter;
 import org.springframework.web.util.UriComponentsBuilder;
 
-import javax.servlet.FilterChain;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import java.io.IOException;
-
 /**
- * A {@code Filter} that processes OAuth 2.0 Authorization Server Configuration Requests.
+ * A {@code Filter} that processes OAuth 2.0 Authorization Server Metadata Requests.
  *
  * @author Daniel Garnier-Moiroux
  * @since 0.1.1
+ * @see OAuth2AuthorizationServerMetadata
  * @see ProviderSettings
  * @see <a target="_blank" href="https://tools.ietf.org/html/rfc8414#section-3">3. Obtaining Authorization Server Metadata</a>
  */
-public class OAuth2AuthorizationServerConfigurationEndpointFilter extends OncePerRequestFilter {
+public class OAuth2AuthorizationServerMetadataEndpointFilter extends OncePerRequestFilter {
 	/**
-	 * The default endpoint {@code URI} for OAuth 2.0 Authorization Server Configuration requests.
+	 * The default endpoint {@code URI} for OAuth 2.0 Authorization Server Metadata requests.
 	 */
-	public static final String DEFAULT_OAUTH2_AUTHORIZATION_SERVER_CONFIGURATION_ENDPOINT_URI = "/.well-known/oauth-authorization-server";
+	public static final String DEFAULT_OAUTH2_AUTHORIZATION_SERVER_METADATA_ENDPOINT_URI = "/.well-known/oauth-authorization-server";
 
-	private final RequestMatcher requestMatcher;
 	private final ProviderSettings providerSettings;
-	private final OAuth2AuthorizationServerConfigurationHttpMessageConverter authorizationServerConfigurationHttpMessageConverter
-			= new OAuth2AuthorizationServerConfigurationHttpMessageConverter();
+	private final RequestMatcher requestMatcher;
+	private final OAuth2AuthorizationServerMetadataHttpMessageConverter authorizationServerMetadataHttpMessageConverter =
+			new OAuth2AuthorizationServerMetadataHttpMessageConverter();
 
-	public OAuth2AuthorizationServerConfigurationEndpointFilter(ProviderSettings providerSettings) {
+	public OAuth2AuthorizationServerMetadataEndpointFilter(ProviderSettings providerSettings) {
 		Assert.notNull(providerSettings, "providerSettings cannot be null");
 		this.providerSettings = providerSettings;
 		this.requestMatcher = new AntPathRequestMatcher(
-				DEFAULT_OAUTH2_AUTHORIZATION_SERVER_CONFIGURATION_ENDPOINT_URI,
+				DEFAULT_OAUTH2_AUTHORIZATION_SERVER_METADATA_ENDPOINT_URI,
 				HttpMethod.GET.name()
 		);
 	}
@@ -68,37 +70,44 @@ public class OAuth2AuthorizationServerConfigurationEndpointFilter extends OncePe
 	@Override
 	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
 			throws ServletException, IOException {
+
 		if (!this.requestMatcher.matches(request)) {
 			filterChain.doFilter(request, response);
 			return;
 		}
 
-		OAuth2AuthorizationServerConfiguration authorizationServerConfiguration = OAuth2AuthorizationServerConfiguration
-				.builder()
+		OAuth2AuthorizationServerMetadata authorizationServerMetadata = OAuth2AuthorizationServerMetadata.builder()
 				.issuer(this.providerSettings.issuer())
 				.authorizationEndpoint(asUrl(this.providerSettings.issuer(), this.providerSettings.authorizationEndpoint()))
 				.tokenEndpoint(asUrl(this.providerSettings.issuer(), this.providerSettings.tokenEndpoint()))
-				.tokenEndpointAuthenticationMethod("client_secret_basic") // TODO: Use ClientAuthenticationMethod.CLIENT_SECRET_BASIC in Spring Security 5.5.0
-				.tokenEndpointAuthenticationMethod("client_secret_post") // TODO: Use ClientAuthenticationMethod.CLIENT_SECRET_POST in Spring Security 5.5.0
-				.tokenRevocationEndpoint(asUrl(this.providerSettings.issuer(), this.providerSettings.tokenRevocationEndpoint()))
-				.tokenRevocationEndpointAuthenticationMethod("client_secret_basic") // TODO: Use ClientAuthenticationMethod.CLIENT_SECRET_BASIC in Spring Security 5.5.0
-				.tokenRevocationEndpointAuthenticationMethod("client_secret_post") // TODO: Use ClientAuthenticationMethod.CLIENT_SECRET_POST in Spring Security 5.5.0
-				.jwkSetUri(asUrl(this.providerSettings.issuer(), this.providerSettings.jwkSetEndpoint()))
+				.tokenEndpointAuthenticationMethods(clientAuthenticationMethods())
+				.jwkSetUrl(asUrl(this.providerSettings.issuer(), this.providerSettings.jwkSetEndpoint()))
+				.responseType(OAuth2AuthorizationResponseType.CODE.getValue())
 				.grantType(AuthorizationGrantType.AUTHORIZATION_CODE.getValue())
 				.grantType(AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())
 				.grantType(AuthorizationGrantType.REFRESH_TOKEN.getValue())
-				.responseType(OAuth2AuthorizationResponseType.CODE.getValue())
-				.scope(OidcScopes.OPENID)
-				.codeChallengeMethod(PkceCodeChallengeMethod2.PLAIN.getValue())
-				.codeChallengeMethod(PkceCodeChallengeMethod2.S256.getValue())
+				.tokenRevocationEndpoint(asUrl(this.providerSettings.issuer(), this.providerSettings.tokenRevocationEndpoint()))
+				.tokenRevocationEndpointAuthenticationMethods(clientAuthenticationMethods())
+				.tokenIntrospectionEndpoint(asUrl(this.providerSettings.issuer(), this.providerSettings.tokenIntrospectionEndpoint()))
+				.tokenIntrospectionEndpointAuthenticationMethods(clientAuthenticationMethods())
+				.codeChallengeMethod("plain")
+				.codeChallengeMethod("S256")
 				.build();
 
-		ServletServerHttpResponse resp = new ServletServerHttpResponse(response);
-		this.authorizationServerConfigurationHttpMessageConverter.write(
-				authorizationServerConfiguration, MediaType.APPLICATION_JSON, resp);
+		ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
+		this.authorizationServerMetadataHttpMessageConverter.write(
+				authorizationServerMetadata, MediaType.APPLICATION_JSON, httpResponse);
+	}
+
+	private static Consumer<List<String>> clientAuthenticationMethods() {
+		return (authenticationMethods) -> {
+			authenticationMethods.add("client_secret_basic");	// TODO: Use ClientAuthenticationMethod.CLIENT_SECRET_BASIC in Spring Security 5.5.0
+			authenticationMethods.add("client_secret_post");	// TODO: Use ClientAuthenticationMethod.CLIENT_SECRET_POST in Spring Security 5.5.0
+		};
 	}
 
 	private static String asUrl(String issuer, String endpoint) {
 		return UriComponentsBuilder.fromUriString(issuer).path(endpoint).toUriString();
 	}
+
 }

+ 8 - 6
oauth2-authorization-server/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2AuthorizationServerConfigurationTests.java → oauth2-authorization-server/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2AuthorizationServerMetadataTests.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2020 the original author or authors.
+ * Copyright 2020-2021 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -21,6 +21,7 @@ import com.nimbusds.jose.proc.SecurityContext;
 import org.junit.BeforeClass;
 import org.junit.Rule;
 import org.junit.Test;
+
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Import;
@@ -30,7 +31,7 @@ import org.springframework.security.config.test.SpringTestRule;
 import org.springframework.security.oauth2.jose.TestJwks;
 import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
 import org.springframework.security.oauth2.server.authorization.config.ProviderSettings;
-import org.springframework.security.oauth2.server.authorization.web.OAuth2AuthorizationServerConfigurationEndpointFilter;
+import org.springframework.security.oauth2.server.authorization.web.OAuth2AuthorizationServerMetadataEndpointFilter;
 import org.springframework.test.web.servlet.MockMvc;
 
 import static org.mockito.Mockito.mock;
@@ -39,11 +40,11 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
 import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
 
 /**
- * Integration tests for OAuth 2.0 Authorization Server Configuration.
+ * Integration tests for the OAuth 2.0 Authorization Server Metadata endpoint.
  *
  * @author Daniel Garnier-Moiroux
  */
-public class OAuth2AuthorizationServerConfigurationTests {
+public class OAuth2AuthorizationServerMetadataTests {
 	private static final String issuerUrl = "https://example.com/issuer1";
 	private static JWKSource<SecurityContext> jwkSource;
 
@@ -60,10 +61,10 @@ public class OAuth2AuthorizationServerConfigurationTests {
 	}
 
 	@Test
-	public void requestWhenServerConfigurationRequestAndIssuerSetThenReturnServerConfigurationResponse() throws Exception {
+	public void requestWhenAuthorizationServerMetadataRequestAndIssuerSetThenReturnMetadataResponse() throws Exception {
 		this.spring.register(AuthorizationServerConfiguration.class).autowire();
 
-		this.mvc.perform(get(OAuth2AuthorizationServerConfigurationEndpointFilter.DEFAULT_OAUTH2_AUTHORIZATION_SERVER_CONFIGURATION_ENDPOINT_URI))
+		this.mvc.perform(get(OAuth2AuthorizationServerMetadataEndpointFilter.DEFAULT_OAUTH2_AUTHORIZATION_SERVER_METADATA_ENDPOINT_URI))
 				.andExpect(status().is2xxSuccessful())
 				.andExpect(jsonPath("issuer").value(issuerUrl))
 				.andReturn();
@@ -88,4 +89,5 @@ public class OAuth2AuthorizationServerConfigurationTests {
 			return new ProviderSettings().issuer(issuerUrl);
 		}
 	}
+
 }

+ 585 - 0
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/core/OAuth2AuthorizationServerMetadataTests.java

@@ -0,0 +1,585 @@
+/*
+ * Copyright 2020-2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.oauth2.core;
+
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+
+import org.junit.Test;
+
+import org.springframework.security.oauth2.core.OAuth2AuthorizationServerMetadata.Builder;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
+
+/**
+ * Tests for {@link OAuth2AuthorizationServerMetadata}.
+ *
+ * @author Daniel Garnier-Moiroux
+ */
+public class OAuth2AuthorizationServerMetadataTests {
+	// @formatter:off
+	private final Builder minimalBuilder =
+			OAuth2AuthorizationServerMetadata.builder()
+					.issuer("https://example.com/issuer1")
+					.authorizationEndpoint("https://example.com/issuer1/oauth2/authorize")
+					.tokenEndpoint("https://example.com/issuer1/oauth2/token")
+					.responseType("code");
+	// @formatter:on
+
+	@Test
+	public void buildWhenAllClaimsProvidedThenCreated() {
+		OAuth2AuthorizationServerMetadata authorizationServerMetadata = OAuth2AuthorizationServerMetadata.builder()
+				.issuer("https://example.com/issuer1")
+				.authorizationEndpoint("https://example.com/issuer1/oauth2/authorize")
+				.tokenEndpoint("https://example.com/issuer1/oauth2/token")
+				.tokenEndpointAuthenticationMethod("client_secret_basic")
+				.jwkSetUrl("https://example.com/issuer1/oauth2/jwks")
+				.scope("openid")
+				.responseType("code")
+				.grantType("authorization_code")
+				.grantType("client_credentials")
+				.tokenRevocationEndpoint("https://example.com/issuer1/oauth2/revoke")
+				.tokenRevocationEndpointAuthenticationMethod("client_secret_basic")
+				.tokenIntrospectionEndpoint("https://example.com/issuer1/oauth2/introspect")
+				.tokenIntrospectionEndpointAuthenticationMethod("client_secret_basic")
+				.codeChallengeMethod("plain")
+				.codeChallengeMethod("S256")
+				.claim("a-claim", "a-value")
+				.build();
+
+		assertThat(authorizationServerMetadata.getIssuer()).isEqualTo(url("https://example.com/issuer1"));
+		assertThat(authorizationServerMetadata.getAuthorizationEndpoint()).isEqualTo(url("https://example.com/issuer1/oauth2/authorize"));
+		assertThat(authorizationServerMetadata.getTokenEndpoint()).isEqualTo(url("https://example.com/issuer1/oauth2/token"));
+		assertThat(authorizationServerMetadata.getTokenEndpointAuthenticationMethods()).containsExactly("client_secret_basic");
+		assertThat(authorizationServerMetadata.getJwkSetUrl()).isEqualTo(url("https://example.com/issuer1/oauth2/jwks"));
+		assertThat(authorizationServerMetadata.getScopes()).containsExactly("openid");
+		assertThat(authorizationServerMetadata.getResponseTypes()).containsExactly("code");
+		assertThat(authorizationServerMetadata.getGrantTypes()).containsExactlyInAnyOrder("authorization_code", "client_credentials");
+		assertThat(authorizationServerMetadata.getTokenRevocationEndpoint()).isEqualTo(url("https://example.com/issuer1/oauth2/revoke"));
+		assertThat(authorizationServerMetadata.getTokenRevocationEndpointAuthenticationMethods()).containsExactly("client_secret_basic");
+		assertThat(authorizationServerMetadata.getTokenIntrospectionEndpoint()).isEqualTo(url("https://example.com/issuer1/oauth2/introspect"));
+		assertThat(authorizationServerMetadata.getTokenIntrospectionEndpointAuthenticationMethods()).containsExactly("client_secret_basic");
+		assertThat(authorizationServerMetadata.getCodeChallengeMethods()).containsExactlyInAnyOrder("plain", "S256");
+		assertThat(authorizationServerMetadata.getClaimAsString("a-claim")).isEqualTo("a-value");
+	}
+
+	@Test
+	public void buildWhenOnlyRequiredClaimsProvidedThenCreated() {
+		OAuth2AuthorizationServerMetadata authorizationServerMetadata = OAuth2AuthorizationServerMetadata.builder()
+				.issuer("https://example.com/issuer1")
+				.authorizationEndpoint("https://example.com/issuer1/oauth2/authorize")
+				.tokenEndpoint("https://example.com/issuer1/oauth2/token")
+				.responseType("code")
+				.build();
+
+		assertThat(authorizationServerMetadata.getIssuer()).isEqualTo(url("https://example.com/issuer1"));
+		assertThat(authorizationServerMetadata.getAuthorizationEndpoint()).isEqualTo(url("https://example.com/issuer1/oauth2/authorize"));
+		assertThat(authorizationServerMetadata.getTokenEndpoint()).isEqualTo(url("https://example.com/issuer1/oauth2/token"));
+		assertThat(authorizationServerMetadata.getTokenEndpointAuthenticationMethods()).isNull();
+		assertThat(authorizationServerMetadata.getJwkSetUrl()).isNull();
+		assertThat(authorizationServerMetadata.getScopes()).isNull();
+		assertThat(authorizationServerMetadata.getResponseTypes()).containsExactly("code");
+		assertThat(authorizationServerMetadata.getGrantTypes()).isNull();
+		assertThat(authorizationServerMetadata.getTokenRevocationEndpoint()).isNull();
+		assertThat(authorizationServerMetadata.getTokenRevocationEndpointAuthenticationMethods()).isNull();
+		assertThat(authorizationServerMetadata.getTokenIntrospectionEndpoint()).isNull();
+		assertThat(authorizationServerMetadata.getTokenIntrospectionEndpointAuthenticationMethods()).isNull();
+		assertThat(authorizationServerMetadata.getCodeChallengeMethods()).isNull();
+	}
+
+	@Test
+	public void withClaimsWhenClaimsProvidedThenCreated() {
+		HashMap<String, Object> claims = new HashMap<>();
+		claims.put(OAuth2AuthorizationServerMetadataClaimNames.ISSUER, "https://example.com/issuer1");
+		claims.put(OAuth2AuthorizationServerMetadataClaimNames.AUTHORIZATION_ENDPOINT, "https://example.com/issuer1/oauth2/authorize");
+		claims.put(OAuth2AuthorizationServerMetadataClaimNames.TOKEN_ENDPOINT, "https://example.com/issuer1/oauth2/token");
+		claims.put(OAuth2AuthorizationServerMetadataClaimNames.JWKS_URI, "https://example.com/issuer1/oauth2/jwks");
+		claims.put(OAuth2AuthorizationServerMetadataClaimNames.SCOPES_SUPPORTED, Collections.singletonList("openid"));
+		claims.put(OAuth2AuthorizationServerMetadataClaimNames.RESPONSE_TYPES_SUPPORTED, Collections.singletonList("code"));
+		claims.put(OAuth2AuthorizationServerMetadataClaimNames.REVOCATION_ENDPOINT, "https://example.com/issuer1/oauth2/revoke");
+		claims.put(OAuth2AuthorizationServerMetadataClaimNames.INTROSPECTION_ENDPOINT, "https://example.com/issuer1/oauth2/introspect");
+		claims.put("some-claim", "some-value");
+
+		OAuth2AuthorizationServerMetadata authorizationServerMetadata = OAuth2AuthorizationServerMetadata.withClaims(claims).build();
+
+		assertThat(authorizationServerMetadata.getIssuer()).isEqualTo(url("https://example.com/issuer1"));
+		assertThat(authorizationServerMetadata.getAuthorizationEndpoint()).isEqualTo(url("https://example.com/issuer1/oauth2/authorize"));
+		assertThat(authorizationServerMetadata.getTokenEndpoint()).isEqualTo(url("https://example.com/issuer1/oauth2/token"));
+		assertThat(authorizationServerMetadata.getTokenEndpointAuthenticationMethods()).isNull();
+		assertThat(authorizationServerMetadata.getJwkSetUrl()).isEqualTo(url("https://example.com/issuer1/oauth2/jwks"));
+		assertThat(authorizationServerMetadata.getScopes()).containsExactly("openid");
+		assertThat(authorizationServerMetadata.getResponseTypes()).containsExactly("code");
+		assertThat(authorizationServerMetadata.getGrantTypes()).isNull();
+		assertThat(authorizationServerMetadata.getTokenRevocationEndpoint()).isEqualTo(url("https://example.com/issuer1/oauth2/revoke"));
+		assertThat(authorizationServerMetadata.getTokenRevocationEndpointAuthenticationMethods()).isNull();
+		assertThat(authorizationServerMetadata.getTokenIntrospectionEndpoint()).isEqualTo(url("https://example.com/issuer1/oauth2/introspect"));
+		assertThat(authorizationServerMetadata.getTokenIntrospectionEndpointAuthenticationMethods()).isNull();
+		assertThat(authorizationServerMetadata.getCodeChallengeMethods()).isNull();
+		assertThat(authorizationServerMetadata.getClaimAsString("some-claim")).isEqualTo("some-value");
+	}
+
+	@Test
+	public void withClaimsWhenClaimsWithUrlsProvidedThenCreated() {
+		HashMap<String, Object> claims = new HashMap<>();
+		claims.put(OAuth2AuthorizationServerMetadataClaimNames.ISSUER, url("https://example.com/issuer1"));
+		claims.put(OAuth2AuthorizationServerMetadataClaimNames.AUTHORIZATION_ENDPOINT, url("https://example.com/issuer1/oauth2/authorize"));
+		claims.put(OAuth2AuthorizationServerMetadataClaimNames.TOKEN_ENDPOINT, url("https://example.com/issuer1/oauth2/token"));
+		claims.put(OAuth2AuthorizationServerMetadataClaimNames.JWKS_URI, url("https://example.com/issuer1/oauth2/jwks"));
+		claims.put(OAuth2AuthorizationServerMetadataClaimNames.RESPONSE_TYPES_SUPPORTED, Collections.singletonList("code"));
+		claims.put(OAuth2AuthorizationServerMetadataClaimNames.REVOCATION_ENDPOINT, url("https://example.com/issuer1/oauth2/revoke"));
+		claims.put(OAuth2AuthorizationServerMetadataClaimNames.INTROSPECTION_ENDPOINT, url("https://example.com/issuer1/oauth2/introspect"));
+		claims.put("some-claim", "some-value");
+
+		OAuth2AuthorizationServerMetadata authorizationServerMetadata = OAuth2AuthorizationServerMetadata.withClaims(claims).build();
+
+		assertThat(authorizationServerMetadata.getIssuer()).isEqualTo(url("https://example.com/issuer1"));
+		assertThat(authorizationServerMetadata.getAuthorizationEndpoint()).isEqualTo(url("https://example.com/issuer1/oauth2/authorize"));
+		assertThat(authorizationServerMetadata.getTokenEndpoint()).isEqualTo(url("https://example.com/issuer1/oauth2/token"));
+		assertThat(authorizationServerMetadata.getTokenEndpointAuthenticationMethods()).isNull();
+		assertThat(authorizationServerMetadata.getJwkSetUrl()).isEqualTo(url("https://example.com/issuer1/oauth2/jwks"));
+		assertThat(authorizationServerMetadata.getScopes()).isNull();
+		assertThat(authorizationServerMetadata.getResponseTypes()).containsExactly("code");
+		assertThat(authorizationServerMetadata.getGrantTypes()).isNull();
+		assertThat(authorizationServerMetadata.getTokenRevocationEndpoint()).isEqualTo(url("https://example.com/issuer1/oauth2/revoke"));
+		assertThat(authorizationServerMetadata.getTokenRevocationEndpointAuthenticationMethods()).isNull();
+		assertThat(authorizationServerMetadata.getTokenIntrospectionEndpoint()).isEqualTo(url("https://example.com/issuer1/oauth2/introspect"));
+		assertThat(authorizationServerMetadata.getTokenIntrospectionEndpointAuthenticationMethods()).isNull();
+		assertThat(authorizationServerMetadata.getCodeChallengeMethods()).isNull();
+		assertThat(authorizationServerMetadata.getClaimAsString("some-claim")).isEqualTo("some-value");
+	}
+
+	@Test
+	public void withClaimsWhenNullThenThrowIllegalArgumentException() {
+		assertThatIllegalArgumentException()
+				.isThrownBy(() -> OAuth2AuthorizationServerMetadata.withClaims(null))
+				.withMessage("claims cannot be empty");
+	}
+
+	@Test
+	public void withClaimsWhenMissingRequiredClaimsThenThrowIllegalArgumentException() {
+		assertThatIllegalArgumentException()
+				.isThrownBy(() -> OAuth2AuthorizationServerMetadata.withClaims(Collections.emptyMap()))
+				.withMessage("claims cannot be empty");
+	}
+
+	@Test
+	public void buildWhenCalledTwiceThenGeneratesTwoConfigurations() {
+		OAuth2AuthorizationServerMetadata first = this.minimalBuilder
+				.grantType("client_credentials")
+				.build();
+
+		OAuth2AuthorizationServerMetadata second = this.minimalBuilder
+				.claims((claims) ->
+						{
+							List<String> newGrantTypes = new ArrayList<>();
+							newGrantTypes.add("authorization_code");
+							newGrantTypes.add("custom_grant");
+							claims.put(OAuth2AuthorizationServerMetadataClaimNames.GRANT_TYPES_SUPPORTED, newGrantTypes);
+						}
+				)
+				.build();
+
+		assertThat(first.getGrantTypes()).containsExactly("client_credentials");
+		assertThat(second.getGrantTypes()).containsExactlyInAnyOrder("authorization_code", "custom_grant");
+	}
+
+	@Test
+	public void buildWhenMissingIssuerThenThrowIllegalArgumentException() {
+		Builder builder = this.minimalBuilder
+				.claims((claims) -> claims.remove(OAuth2AuthorizationServerMetadataClaimNames.ISSUER));
+
+		assertThatIllegalArgumentException()
+				.isThrownBy(builder::build)
+				.withMessage("issuer cannot be null");
+	}
+
+	@Test
+	public void buildWhenIssuerNotUrlThenThrowIllegalArgumentException() {
+		Builder builder = this.minimalBuilder
+				.claims((claims) -> claims.put(OAuth2AuthorizationServerMetadataClaimNames.ISSUER, "not an url"));
+
+		assertThatIllegalArgumentException()
+				.isThrownBy(builder::build)
+				.withMessage("issuer must be a valid URL");
+	}
+
+	@Test
+	public void buildWhenMissingAuthorizationEndpointThenThrowIllegalArgumentException() {
+		Builder builder = this.minimalBuilder
+				.claims((claims) -> claims.remove(OAuth2AuthorizationServerMetadataClaimNames.AUTHORIZATION_ENDPOINT));
+
+		assertThatIllegalArgumentException()
+				.isThrownBy(builder::build)
+				.withMessage("authorizationEndpoint cannot be null");
+	}
+
+	@Test
+	public void buildWhenAuthorizationEndpointNotUrlThenThrowIllegalArgumentException() {
+		Builder builder = this.minimalBuilder
+				.claims((claims) -> claims.put(OAuth2AuthorizationServerMetadataClaimNames.AUTHORIZATION_ENDPOINT, "not an url"));
+
+		assertThatIllegalArgumentException()
+				.isThrownBy(builder::build)
+				.withMessage("authorizationEndpoint must be a valid URL");
+	}
+
+	@Test
+	public void buildWhenMissingTokenEndpointThenThrowsIllegalArgumentException() {
+		Builder builder = this.minimalBuilder
+				.claims((claims) -> claims.remove(OAuth2AuthorizationServerMetadataClaimNames.TOKEN_ENDPOINT));
+
+		assertThatIllegalArgumentException()
+				.isThrownBy(builder::build)
+				.withMessage("tokenEndpoint cannot be null");
+	}
+
+	@Test
+	public void buildWhenTokenEndpointNotUrlThenThrowIllegalArgumentException() {
+		Builder builder = this.minimalBuilder
+				.claims((claims) -> claims.put(OAuth2AuthorizationServerMetadataClaimNames.TOKEN_ENDPOINT, "not an url"));
+
+		assertThatIllegalArgumentException()
+				.isThrownBy(builder::build)
+				.withMessage("tokenEndpoint must be a valid URL");
+	}
+
+	@Test
+	public void buildWhenTokenEndpointAuthenticationMethodsNotListThenThrowIllegalArgumentException() {
+		Builder builder = this.minimalBuilder
+				.claims((claims) -> claims.put(OAuth2AuthorizationServerMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHODS_SUPPORTED, "not-a-list"));
+
+		assertThatIllegalArgumentException()
+				.isThrownBy(builder::build)
+				.withMessageStartingWith("tokenEndpointAuthenticationMethods must be of type List");
+	}
+
+	@Test
+	public void buildWhenTokenEndpointAuthenticationMethodsEmptyListThenThrowIllegalArgumentException() {
+		Builder builder = this.minimalBuilder
+				.claims((claims) -> claims.put(OAuth2AuthorizationServerMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHODS_SUPPORTED, Collections.emptyList()));
+
+		assertThatIllegalArgumentException()
+				.isThrownBy(builder::build)
+				.withMessage("tokenEndpointAuthenticationMethods cannot be empty");
+	}
+
+	@Test
+	public void buildWhenTokenEndpointAuthenticationMethodsAddingOrRemovingThenCorrectValues() {
+		OAuth2AuthorizationServerMetadata authorizationServerMetadata = this.minimalBuilder
+				.tokenEndpointAuthenticationMethod("should-be-removed")
+				.tokenEndpointAuthenticationMethods(authMethods -> {
+					authMethods.clear();
+					authMethods.add("some-authentication-method");
+				})
+				.build();
+
+		assertThat(authorizationServerMetadata.getTokenEndpointAuthenticationMethods()).containsExactly("some-authentication-method");
+	}
+
+	@Test
+	public void buildWhenJwksUriNotUrlThenThrowIllegalArgumentException() {
+		Builder builder = this.minimalBuilder
+				.claims((claims) -> claims.put(OAuth2AuthorizationServerMetadataClaimNames.JWKS_URI, "not an url"));
+
+		assertThatIllegalArgumentException()
+				.isThrownBy(builder::build)
+				.withMessage("jwksUri must be a valid URL");
+	}
+
+	@Test
+	public void buildWhenScopesNotListThenThrowIllegalArgumentException() {
+		Builder builder = this.minimalBuilder
+				.claims((claims) -> claims.put(OAuth2AuthorizationServerMetadataClaimNames.SCOPES_SUPPORTED, "not-a-list"));
+
+		assertThatIllegalArgumentException()
+				.isThrownBy(builder::build)
+				.withMessageStartingWith("scopes must be of type List");
+	}
+
+	@Test
+	public void buildWhenScopesEmptyListThenThrowIllegalArgumentException() {
+		Builder builder = this.minimalBuilder
+				.claims((claims) -> claims.put(OAuth2AuthorizationServerMetadataClaimNames.SCOPES_SUPPORTED, Collections.emptyList()));
+
+		assertThatIllegalArgumentException()
+				.isThrownBy(builder::build)
+				.withMessage("scopes cannot be empty");
+	}
+
+	@Test
+	public void buildWhenScopesAddingOrRemovingThenCorrectValues() {
+		OAuth2AuthorizationServerMetadata authorizationServerMetadata = this.minimalBuilder
+				.scope("should-be-removed")
+				.scopes(scopes -> {
+					scopes.clear();
+					scopes.add("some-scope");
+				})
+				.build();
+
+		assertThat(authorizationServerMetadata.getScopes()).containsExactly("some-scope");
+	}
+
+	@Test
+	public void buildWhenMissingResponseTypesThenThrowIllegalArgumentException() {
+		Builder builder = this.minimalBuilder
+				.claims((claims) -> claims.remove(OAuth2AuthorizationServerMetadataClaimNames.RESPONSE_TYPES_SUPPORTED));
+
+		assertThatIllegalArgumentException()
+				.isThrownBy(builder::build)
+				.withMessage("responseTypes cannot be null");
+	}
+
+	@Test
+	public void buildWhenResponseTypesNotListThenThrowIllegalArgumentException() {
+		Builder builder = this.minimalBuilder
+				.claims((claims) -> claims.put(OAuth2AuthorizationServerMetadataClaimNames.RESPONSE_TYPES_SUPPORTED, "not-a-list"));
+
+		assertThatIllegalArgumentException()
+				.isThrownBy(builder::build)
+				.withMessageStartingWith("responseTypes must be of type List");
+	}
+
+	@Test
+	public void buildWhenResponseTypesEmptyListThenThrowIllegalArgumentException() {
+		Builder builder = this.minimalBuilder
+				.claims((claims) -> claims.put(OAuth2AuthorizationServerMetadataClaimNames.RESPONSE_TYPES_SUPPORTED, Collections.emptyList()));
+
+		assertThatIllegalArgumentException()
+				.isThrownBy(builder::build)
+				.withMessage("responseTypes cannot be empty");
+	}
+
+	@Test
+	public void buildWhenResponseTypesAddingOrRemovingThenCorrectValues() {
+		OAuth2AuthorizationServerMetadata authorizationServerMetadata = this.minimalBuilder
+				.responseType("should-be-removed")
+				.responseTypes(responseTypes -> {
+					responseTypes.clear();
+					responseTypes.add("some-response-type");
+				})
+				.build();
+
+		assertThat(authorizationServerMetadata.getResponseTypes()).containsExactly("some-response-type");
+	}
+
+	@Test
+	public void buildWhenResponseTypesNotPresentAndAddingThenCorrectValues() {
+		OAuth2AuthorizationServerMetadata authorizationServerMetadata = this.minimalBuilder
+				.claims(claims -> claims.remove(OAuth2AuthorizationServerMetadataClaimNames.RESPONSE_TYPES_SUPPORTED))
+				.responseTypes(responseTypes -> responseTypes.add("some-response-type"))
+				.build();
+
+		assertThat(authorizationServerMetadata.getResponseTypes()).containsExactly("some-response-type");
+	}
+
+	@Test
+	public void buildWhenGrantTypesNotListThenThrowIllegalArgumentException() {
+		Builder builder = this.minimalBuilder
+				.claims((claims) -> claims.put(OAuth2AuthorizationServerMetadataClaimNames.GRANT_TYPES_SUPPORTED, "not-a-list"));
+
+		assertThatIllegalArgumentException()
+				.isThrownBy(builder::build)
+				.withMessageStartingWith("grantTypes must be of type List");
+	}
+
+	@Test
+	public void buildWhenGrantTypesEmptyListThenThrowIllegalArgumentException() {
+		Builder builder = this.minimalBuilder
+				.claims((claims) -> claims.put(OAuth2AuthorizationServerMetadataClaimNames.GRANT_TYPES_SUPPORTED, Collections.emptyList()));
+
+		assertThatIllegalArgumentException()
+				.isThrownBy(builder::build)
+				.withMessage("grantTypes cannot be empty");
+	}
+
+	@Test
+	public void buildWhenGrantTypesAddingOrRemovingThenCorrectValues() {
+		OAuth2AuthorizationServerMetadata authorizationServerMetadata = this.minimalBuilder
+				.grantType("should-be-removed")
+				.grantTypes(grantTypes -> {
+					grantTypes.clear();
+					grantTypes.add("some-grant-type");
+				})
+				.build();
+
+		assertThat(authorizationServerMetadata.getGrantTypes()).containsExactly("some-grant-type");
+	}
+
+	@Test
+	public void buildWhenTokenRevocationEndpointNotUrlThenThrowIllegalArgumentException() {
+		Builder builder = this.minimalBuilder
+				.tokenRevocationEndpoint("not a valid URL");
+
+		assertThatIllegalArgumentException()
+				.isThrownBy(builder::build)
+				.withMessage("tokenRevocationEndpoint must be a valid URL");
+	}
+
+	@Test
+	public void buildWhenTokenRevocationEndpointAuthenticationMethodsNotListThenThrowIllegalArgumentException() {
+		Builder builder = this.minimalBuilder
+				.claims((claims) -> claims.put(OAuth2AuthorizationServerMetadataClaimNames.REVOCATION_ENDPOINT_AUTH_METHODS_SUPPORTED, "not-a-list"));
+
+		assertThatIllegalArgumentException()
+				.isThrownBy(builder::build)
+				.withMessageStartingWith("tokenRevocationEndpointAuthenticationMethods must be of type List");
+	}
+
+	@Test
+	public void buildWhenTokenRevocationEndpointAuthenticationMethodsEmptyListThenThrowIllegalArgumentException() {
+		Builder builder = this.minimalBuilder
+				.claims((claims) -> claims.put(OAuth2AuthorizationServerMetadataClaimNames.REVOCATION_ENDPOINT_AUTH_METHODS_SUPPORTED, Collections.emptyList()));
+
+		assertThatIllegalArgumentException()
+				.isThrownBy(builder::build)
+				.withMessage("tokenRevocationEndpointAuthenticationMethods cannot be empty");
+	}
+
+	@Test
+	public void buildWhenTokenRevocationEndpointAuthenticationMethodsAddingOrRemovingThenCorrectValues() {
+		OAuth2AuthorizationServerMetadata authorizationServerMetadata = this.minimalBuilder
+				.tokenRevocationEndpointAuthenticationMethod("should-be-removed")
+				.tokenRevocationEndpointAuthenticationMethods(authMethods -> {
+					authMethods.clear();
+					authMethods.add("some-authentication-method");
+				})
+				.build();
+
+		assertThat(authorizationServerMetadata.getTokenRevocationEndpointAuthenticationMethods()).containsExactly("some-authentication-method");
+	}
+
+	@Test
+	public void buildWhenTokenIntrospectionEndpointNotUrlThenThrowIllegalArgumentException() {
+		Builder builder = this.minimalBuilder
+				.tokenIntrospectionEndpoint("not a valid URL");
+
+		assertThatIllegalArgumentException()
+				.isThrownBy(builder::build)
+				.withMessage("tokenIntrospectionEndpoint must be a valid URL");
+	}
+
+	@Test
+	public void buildWhenTokenIntrospectionEndpointAuthenticationMethodsNotListThenThrowIllegalArgumentException() {
+		Builder builder = this.minimalBuilder
+				.claims((claims) -> claims.put(OAuth2AuthorizationServerMetadataClaimNames.INTROSPECTION_ENDPOINT_AUTH_METHODS_SUPPORTED, "not-a-list"));
+
+		assertThatIllegalArgumentException()
+				.isThrownBy(builder::build)
+				.withMessageStartingWith("tokenIntrospectionEndpointAuthenticationMethods must be of type List");
+	}
+
+	@Test
+	public void buildWhenTokenIntrospectionEndpointAuthenticationMethodsEmptyListThenThrowIllegalArgumentException() {
+		Builder builder = this.minimalBuilder
+				.claims((claims) -> claims.put(OAuth2AuthorizationServerMetadataClaimNames.INTROSPECTION_ENDPOINT_AUTH_METHODS_SUPPORTED, Collections.emptyList()));
+
+		assertThatIllegalArgumentException()
+				.isThrownBy(builder::build)
+				.withMessage("tokenIntrospectionEndpointAuthenticationMethods cannot be empty");
+	}
+
+	@Test
+	public void buildWhenTokenIntrospectionEndpointAuthenticationMethodsAddingOrRemovingThenCorrectValues() {
+		OAuth2AuthorizationServerMetadata authorizationServerMetadata = this.minimalBuilder
+				.tokenIntrospectionEndpointAuthenticationMethod("should-be-removed")
+				.tokenIntrospectionEndpointAuthenticationMethods(authMethods -> {
+					authMethods.clear();
+					authMethods.add("some-authentication-method");
+				})
+				.build();
+
+		assertThat(authorizationServerMetadata.getTokenIntrospectionEndpointAuthenticationMethods()).containsExactly("some-authentication-method");
+	}
+
+	@Test
+	public void buildWhenCodeChallengeMethodsNotListThenThrowIllegalArgumentException() {
+		Builder builder = this.minimalBuilder
+				.claims((claims) -> claims.put(OAuth2AuthorizationServerMetadataClaimNames.CODE_CHALLENGE_METHODS_SUPPORTED, "not-a-list"));
+
+		assertThatIllegalArgumentException()
+				.isThrownBy(builder::build)
+				.withMessageStartingWith("codeChallengeMethods must be of type List");
+	}
+
+	@Test
+	public void buildWhenCodeChallengeMethodsEmptyListThenThrowIllegalArgumentException() {
+		Builder builder = this.minimalBuilder
+				.claims((claims) -> claims.put(OAuth2AuthorizationServerMetadataClaimNames.CODE_CHALLENGE_METHODS_SUPPORTED, Collections.emptyList()));
+
+		assertThatIllegalArgumentException()
+				.isThrownBy(builder::build)
+				.withMessage("codeChallengeMethods cannot be empty");
+	}
+
+	@Test
+	public void buildWhenCodeChallengeMethodsAddingOrRemovingThenCorrectValues() {
+		OAuth2AuthorizationServerMetadata authorizationServerMetadata = this.minimalBuilder
+				.codeChallengeMethod("should-be-removed")
+				.codeChallengeMethods(codeChallengeMethods -> {
+					codeChallengeMethods.clear();
+					codeChallengeMethods.add("some-authentication-method");
+				})
+				.build();
+
+		assertThat(authorizationServerMetadata.getCodeChallengeMethods()).containsExactly("some-authentication-method");
+	}
+
+	@Test
+	public void claimWhenNameNullThenThrowIllegalArgumentException() {
+		assertThatIllegalArgumentException()
+				.isThrownBy(() -> OAuth2AuthorizationServerMetadata.builder().claim(null, "claim-value"))
+				.withMessage("name cannot be empty");
+	}
+
+	@Test
+	public void claimWhenValueNullThenThrowIllegalArgumentException() {
+		assertThatIllegalArgumentException()
+				.isThrownBy(() -> OAuth2AuthorizationServerMetadata.builder().claim("claim-name", null))
+				.withMessage("value cannot be null");
+	}
+
+	@Test
+	public void claimsWhenRemovingClaimThenNotPresent() {
+		OAuth2AuthorizationServerMetadata authorizationServerMetadata =
+				this.minimalBuilder
+						.claim("claim-name", "claim-value")
+						.claims((claims) -> claims.remove("claim-name"))
+						.build();
+		assertThat(authorizationServerMetadata.containsClaim("claim-name")).isFalse();
+	}
+
+	@Test
+	public void claimsWhenAddingClaimThenPresent() {
+		OAuth2AuthorizationServerMetadata authorizationServerMetadata =
+				this.minimalBuilder
+						.claim("claim-name", "claim-value")
+						.build();
+		assertThat(authorizationServerMetadata.containsClaim("claim-name")).isTrue();
+	}
+
+	private static URL url(String urlString) {
+		try {
+			return new URL(urlString);
+		} catch (Exception ex) {
+			throw new IllegalArgumentException("urlString must be a valid URL and valid URI");
+		}
+	}
+
+}

+ 0 - 451
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/core/endpoint/OAuth2AuthorizationServerConfigurationTests.java

@@ -1,451 +0,0 @@
-/*
- * Copyright 2020 the original author or authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * 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.endpoint;
-
-import org.junit.Test;
-import org.springframework.security.oauth2.core.OAuth2AuthorizationServerMetadataClaimNames;
-import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationServerConfiguration.Builder;
-
-import java.net.URL;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.LinkedHashSet;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
-
-/**
- * Tests for {@link OAuth2AuthorizationServerConfiguration}.
- *
- * @author Daniel Garnier-Moiroux
- */
-public class OAuth2AuthorizationServerConfigurationTests {
-	private final Builder minimalConfigurationBuilder =
-			OAuth2AuthorizationServerConfiguration.builder()
-					.issuer("https://example.com/issuer1")
-					.authorizationEndpoint("https://example.com/issuer1/oauth2/authorize")
-					.tokenEndpoint("https://example.com/issuer1/oauth2/token")
-					.jwkSetUri("https://example.com/issuer1/oauth2/jwks")
-					.scope("openid")
-					.responseType("code");
-
-	@Test
-	public void buildWhenAllRequiredClaimsAndAdditionalClaimsThenCreated() {
-		OAuth2AuthorizationServerConfiguration authorizationServerConfiguration = OAuth2AuthorizationServerConfiguration.builder()
-				.issuer("https://example.com/issuer1")
-				.authorizationEndpoint("https://example.com/issuer1/oauth2/authorize")
-				.tokenEndpoint("https://example.com/issuer1/oauth2/token")
-				.tokenRevocationEndpoint("https://example.com/issuer1/oauth2/revoke")
-				.jwkSetUri("https://example.com/issuer1/oauth2/jwks")
-				.scope("openid")
-				.responseType("code")
-				.grantType("authorization_code")
-				.grantType("client_credentials")
-				.tokenEndpointAuthenticationMethod("client_secret_basic")
-				.tokenRevocationEndpointAuthenticationMethod("client_secret_basic")
-				.codeChallengeMethod("plain")
-				.codeChallengeMethod("S256")
-				.claim("a-claim", "a-value")
-				.build();
-
-		assertThat(authorizationServerConfiguration.getIssuer()).isEqualTo(url("https://example.com/issuer1"));
-		assertThat(authorizationServerConfiguration.getAuthorizationEndpoint()).isEqualTo(url("https://example.com/issuer1/oauth2/authorize"));
-		assertThat(authorizationServerConfiguration.getTokenEndpoint()).isEqualTo(url("https://example.com/issuer1/oauth2/token"));
-		assertThat(authorizationServerConfiguration.getTokenRevocationEndpoint()).isEqualTo(url("https://example.com/issuer1/oauth2/revoke"));
-		assertThat(authorizationServerConfiguration.getJwkSetUri()).isEqualTo(url("https://example.com/issuer1/oauth2/jwks"));
-		assertThat(authorizationServerConfiguration.getScopes()).containsExactly("openid");
-		assertThat(authorizationServerConfiguration.getResponseTypes()).containsExactly("code");
-		assertThat(authorizationServerConfiguration.getGrantTypes()).containsExactlyInAnyOrder("authorization_code", "client_credentials");
-		assertThat(authorizationServerConfiguration.getTokenEndpointAuthenticationMethods()).containsExactly("client_secret_basic");
-		assertThat(authorizationServerConfiguration.getTokenRevocationEndpointAuthenticationMethods()).containsExactly("client_secret_basic");
-		assertThat(authorizationServerConfiguration.getCodeChallengeMethods()).containsExactlyInAnyOrder("plain", "S256");
-		assertThat(authorizationServerConfiguration.getClaimAsString("a-claim")).isEqualTo("a-value");
-	}
-
-	@Test
-	public void buildWhenOnlyRequiredClaimsThenCreated() {
-		OAuth2AuthorizationServerConfiguration authorizationServerConfiguration = OAuth2AuthorizationServerConfiguration.builder()
-				.issuer("https://example.com/issuer1")
-				.authorizationEndpoint("https://example.com/issuer1/oauth2/authorize")
-				.tokenEndpoint("https://example.com/issuer1/oauth2/token")
-				.jwkSetUri("https://example.com/issuer1/oauth2/jwks")
-				.scope("openid")
-				.responseType("code")
-				.build();
-
-		assertThat(authorizationServerConfiguration.getIssuer()).isEqualTo(url("https://example.com/issuer1"));
-		assertThat(authorizationServerConfiguration.getAuthorizationEndpoint()).isEqualTo(url("https://example.com/issuer1/oauth2/authorize"));
-		assertThat(authorizationServerConfiguration.getTokenEndpoint()).isEqualTo(url("https://example.com/issuer1/oauth2/token"));
-		assertThat(authorizationServerConfiguration.getJwkSetUri()).isEqualTo(url("https://example.com/issuer1/oauth2/jwks"));
-		assertThat(authorizationServerConfiguration.getScopes()).containsExactly("openid");
-		assertThat(authorizationServerConfiguration.getResponseTypes()).containsExactly("code");
-		assertThat(authorizationServerConfiguration.getGrantTypes()).isNull();
-		assertThat(authorizationServerConfiguration.getTokenEndpointAuthenticationMethods()).isNull();
-		assertThat(authorizationServerConfiguration.getTokenRevocationEndpoint()).isNull();
-		assertThat(authorizationServerConfiguration.getTokenRevocationEndpointAuthenticationMethods()).isNull();
-		assertThat(authorizationServerConfiguration.getCodeChallengeMethods()).isNull();
-	}
-
-	@Test
-	public void buildFromClaimsThenCreated() {
-		HashMap<String, Object> claims = new HashMap<>();
-		claims.put(OAuth2AuthorizationServerMetadataClaimNames.ISSUER, "https://example.com/issuer1");
-		claims.put(OAuth2AuthorizationServerMetadataClaimNames.AUTHORIZATION_ENDPOINT, "https://example.com/issuer1/oauth2/authorize");
-		claims.put(OAuth2AuthorizationServerMetadataClaimNames.TOKEN_ENDPOINT, "https://example.com/issuer1/oauth2/token");
-		claims.put(OAuth2AuthorizationServerMetadataClaimNames.JWKS_URI, "https://example.com/issuer1/oauth2/jwks");
-		claims.put(OAuth2AuthorizationServerMetadataClaimNames.SCOPES_SUPPORTED, Collections.singletonList("openid"));
-		claims.put(OAuth2AuthorizationServerMetadataClaimNames.RESPONSE_TYPES_SUPPORTED, Collections.singletonList("code"));
-		claims.put("some-claim", "some-value");
-
-		OAuth2AuthorizationServerConfiguration authorizationServerConfiguration = OAuth2AuthorizationServerConfiguration.withClaims(claims).build();
-
-		assertThat(authorizationServerConfiguration.getIssuer()).isEqualTo(url("https://example.com/issuer1"));
-		assertThat(authorizationServerConfiguration.getAuthorizationEndpoint()).isEqualTo(url("https://example.com/issuer1/oauth2/authorize"));
-		assertThat(authorizationServerConfiguration.getTokenEndpoint()).isEqualTo(url("https://example.com/issuer1/oauth2/token"));
-		assertThat(authorizationServerConfiguration.getJwkSetUri()).isEqualTo(url("https://example.com/issuer1/oauth2/jwks"));
-		assertThat(authorizationServerConfiguration.getScopes()).containsExactly("openid");
-		assertThat(authorizationServerConfiguration.getResponseTypes()).containsExactly("code");
-		assertThat(authorizationServerConfiguration.getGrantTypes()).isNull();
-		assertThat(authorizationServerConfiguration.getTokenEndpointAuthenticationMethods()).isNull();
-		assertThat(authorizationServerConfiguration.getTokenRevocationEndpoint()).isNull();
-		assertThat(authorizationServerConfiguration.getTokenRevocationEndpointAuthenticationMethods()).isNull();
-		assertThat(authorizationServerConfiguration.getCodeChallengeMethods()).isNull();
-		assertThat(authorizationServerConfiguration.getClaimAsString("some-claim")).isEqualTo("some-value");
-	}
-
-	@Test
-	public void buildFromClaimsWhenUsingUrlsThenCreated() {
-		HashMap<String, Object> claims = new HashMap<>();
-		claims.put(OAuth2AuthorizationServerMetadataClaimNames.ISSUER, url("https://example.com/issuer1"));
-		claims.put(OAuth2AuthorizationServerMetadataClaimNames.AUTHORIZATION_ENDPOINT, url("https://example.com/issuer1/oauth2/authorize"));
-		claims.put(OAuth2AuthorizationServerMetadataClaimNames.TOKEN_ENDPOINT, url("https://example.com/issuer1/oauth2/token"));
-		claims.put(OAuth2AuthorizationServerMetadataClaimNames.REVOCATION_ENDPOINT, url("https://example.com/issuer1/oauth2/revoke"));
-		claims.put(OAuth2AuthorizationServerMetadataClaimNames.JWKS_URI, url("https://example.com/issuer1/oauth2/jwks"));
-		claims.put(OAuth2AuthorizationServerMetadataClaimNames.SCOPES_SUPPORTED, Collections.singletonList("openid"));
-		claims.put(OAuth2AuthorizationServerMetadataClaimNames.RESPONSE_TYPES_SUPPORTED, Collections.singletonList("code"));
-		claims.put("some-claim", "some-value");
-
-		OAuth2AuthorizationServerConfiguration authorizationServerConfiguration = OAuth2AuthorizationServerConfiguration.withClaims(claims).build();
-
-		assertThat(authorizationServerConfiguration.getIssuer()).isEqualTo(url("https://example.com/issuer1"));
-		assertThat(authorizationServerConfiguration.getAuthorizationEndpoint()).isEqualTo(url("https://example.com/issuer1/oauth2/authorize"));
-		assertThat(authorizationServerConfiguration.getTokenEndpoint()).isEqualTo(url("https://example.com/issuer1/oauth2/token"));
-		assertThat(authorizationServerConfiguration.getTokenRevocationEndpoint()).isEqualTo(url("https://example.com/issuer1/oauth2/revoke"));
-		assertThat(authorizationServerConfiguration.getJwkSetUri()).isEqualTo(url("https://example.com/issuer1/oauth2/jwks"));
-		assertThat(authorizationServerConfiguration.getScopes()).containsExactly("openid");
-		assertThat(authorizationServerConfiguration.getResponseTypes()).containsExactly("code");
-		assertThat(authorizationServerConfiguration.getGrantTypes()).isNull();
-		assertThat(authorizationServerConfiguration.getTokenEndpointAuthenticationMethods()).isNull();
-		assertThat(authorizationServerConfiguration.getTokenRevocationEndpointAuthenticationMethods()).isNull();
-		assertThat(authorizationServerConfiguration.getCodeChallengeMethods()).isNull();
-		assertThat(authorizationServerConfiguration.getClaimAsString("some-claim")).isEqualTo("some-value");
-	}
-
-	@Test
-	public void withClaimsWhenNullThenThrowsIllegalArgumentException() {
-		assertThatIllegalArgumentException()
-				.isThrownBy(() -> OAuth2AuthorizationServerConfiguration.withClaims(null))
-				.withMessage("claims cannot be empty");
-	}
-
-	@Test
-	public void withClaimsWhenMissingRequiredClaimsThenThrowsIllegalArgumentException() {
-		assertThatIllegalArgumentException()
-				.isThrownBy(() -> OAuth2AuthorizationServerConfiguration.withClaims(Collections.emptyMap()))
-				.withMessage("claims cannot be empty");
-	}
-
-	@Test
-	public void buildWhenCalledTwiceThenGeneratesTwoConfigurations() {
-		OAuth2AuthorizationServerConfiguration first = this.minimalConfigurationBuilder
-				.grantType("client_credentials")
-				.build();
-
-		OAuth2AuthorizationServerConfiguration second = this.minimalConfigurationBuilder
-				.claims((claims) ->
-						{
-							LinkedHashSet<String> newGrantTypes = new LinkedHashSet<>();
-							newGrantTypes.add("authorization_code");
-							newGrantTypes.add("custom_grant");
-							claims.put(OAuth2AuthorizationServerMetadataClaimNames.GRANT_TYPES_SUPPORTED, newGrantTypes);
-						}
-				)
-				.build();
-
-		assertThat(first.getGrantTypes()).containsExactly("client_credentials");
-		assertThat(second.getGrantTypes()).containsExactlyInAnyOrder("authorization_code", "custom_grant");
-	}
-
-	@Test
-	public void buildWhenEmptyClaimsThenOmitted() {
-		OAuth2AuthorizationServerConfiguration authorizationServerConfiguration = this.minimalConfigurationBuilder
-				.claim("some-claim", Collections.emptyList())
-				.claims(claims -> claims.put(OAuth2AuthorizationServerMetadataClaimNames.GRANT_TYPES_SUPPORTED, Collections.emptyList()))
-				.build();
-
-		assertThat(authorizationServerConfiguration.getClaimAsStringList("some-claim")).isNull();
-		assertThat(authorizationServerConfiguration.getClaimAsStringList(OAuth2AuthorizationServerMetadataClaimNames.GRANT_TYPES_SUPPORTED)).isNull();
-	}
-
-	@Test
-	public void buildWhenMissingIssuerThenThrowsIllegalArgumentException() {
-		Builder builder = this.minimalConfigurationBuilder
-				.claims((claims) -> claims.remove(OAuth2AuthorizationServerMetadataClaimNames.ISSUER));
-
-		assertThatIllegalArgumentException()
-				.isThrownBy(builder::build)
-				.withMessage("issuer cannot be null");
-	}
-
-	@Test
-	public void buildWhenIssuerIsNotAnUrlThenThrowsIllegalArgumentException() {
-		Builder builder = this.minimalConfigurationBuilder
-				.claims((claims) -> claims.put(OAuth2AuthorizationServerMetadataClaimNames.ISSUER, "not an url"));
-
-		assertThatIllegalArgumentException()
-				.isThrownBy(builder::build)
-				.withMessageStartingWith("issuer must be a valid URL");
-	}
-
-	@Test
-	public void buildWhenMissingAuthorizationEndpointThenThrowsIllegalArgumentException() {
-		Builder builder = this.minimalConfigurationBuilder
-				.claims((claims) -> claims.remove(OAuth2AuthorizationServerMetadataClaimNames.AUTHORIZATION_ENDPOINT));
-
-		assertThatIllegalArgumentException()
-				.isThrownBy(builder::build)
-				.withMessage("authorizationEndpoint cannot be null");
-	}
-
-	@Test
-	public void buildWhenAuthorizationEndpointIsNotAnUrlThenThrowsIllegalArgumentException() {
-		Builder builder = this.minimalConfigurationBuilder
-				.claims((claims) -> claims.put(OAuth2AuthorizationServerMetadataClaimNames.AUTHORIZATION_ENDPOINT, "not an url"));
-
-		assertThatIllegalArgumentException()
-				.isThrownBy(builder::build)
-				.withMessageStartingWith("authorizationEndpoint must be a valid URL");
-	}
-
-	@Test
-	public void buildWhenMissingTokenEndpointThenThrowsIllegalArgumentException() {
-		Builder builder = this.minimalConfigurationBuilder
-				.claims((claims) -> claims.remove(OAuth2AuthorizationServerMetadataClaimNames.TOKEN_ENDPOINT));
-
-		assertThatIllegalArgumentException()
-				.isThrownBy(builder::build)
-				.withMessage("tokenEndpoint cannot be null");
-	}
-
-	@Test
-	public void buildWhenTokenEndpointIsNotAnUrlThenThrowsIllegalArgumentException() {
-		Builder builder = this.minimalConfigurationBuilder
-				.claims((claims) -> claims.put(OAuth2AuthorizationServerMetadataClaimNames.TOKEN_ENDPOINT, "not an url"));
-
-		assertThatIllegalArgumentException()
-				.isThrownBy(builder::build)
-				.withMessageStartingWith("tokenEndpoint must be a valid URL");
-	}
-
-	@Test
-	public void buildWhenMissingJwksUriThenThrowsIllegalArgumentException() {
-		Builder builder = this.minimalConfigurationBuilder
-				.claims((claims) -> claims.remove(OAuth2AuthorizationServerMetadataClaimNames.JWKS_URI));
-
-		assertThatIllegalArgumentException()
-				.isThrownBy(builder::build)
-				.withMessage("jwksUri cannot be null");
-	}
-
-	@Test
-	public void buildWhenJwksUriIsNotAnUrlThenThrowsIllegalArgumentException() {
-		Builder builder = this.minimalConfigurationBuilder
-				.claims((claims) -> claims.put(OAuth2AuthorizationServerMetadataClaimNames.JWKS_URI, "not an url"));
-
-		assertThatIllegalArgumentException()
-				.isThrownBy(builder::build)
-				.withMessageStartingWith("jwksUri must be a valid URL");
-	}
-
-	@Test
-	public void buildWhenMissingResponseTypesThenThrowsIllegalArgumentException() {
-		Builder builder = this.minimalConfigurationBuilder
-				.claims((claims) -> claims.remove(OAuth2AuthorizationServerMetadataClaimNames.RESPONSE_TYPES_SUPPORTED));
-
-		assertThatIllegalArgumentException()
-				.isThrownBy(builder::build)
-				.withMessage("responseTypes cannot be null");
-	}
-
-	@Test
-	public void buildWhenResponseTypesNotListThenThrowIllegalArgumentException() {
-		Builder builder = this.minimalConfigurationBuilder
-				.claims((claims) -> claims.put(OAuth2AuthorizationServerMetadataClaimNames.RESPONSE_TYPES_SUPPORTED, "not-a-list"));
-
-		assertThatIllegalArgumentException()
-				.isThrownBy(builder::build)
-				.withMessageStartingWith("responseTypes must be of type List");
-	}
-
-	@Test
-	public void buildWhenResponseTypesEmptyListThenThrowIllegalArgumentException() {
-		Builder builder = this.minimalConfigurationBuilder
-				.claims((claims) -> claims.put(OAuth2AuthorizationServerMetadataClaimNames.RESPONSE_TYPES_SUPPORTED, Collections.emptyList()));
-
-		assertThatIllegalArgumentException()
-				.isThrownBy(builder::build)
-				.withMessage("responseTypes cannot be empty");
-	}
-
-	@Test
-	public void buildWhenInvalidTokenRevocationEndpointThenThrowsIllegalArgumentException() {
-		Builder builder = this.minimalConfigurationBuilder
-				.tokenRevocationEndpoint("not a valid URL");
-
-		assertThatIllegalArgumentException()
-				.isThrownBy(builder::build)
-				.withMessage("tokenRevocationEndpoint must be a valid URL");
-	}
-
-	@Test
-	public void responseTypesWhenAddingOrRemovingThenCorrectValues() {
-		OAuth2AuthorizationServerConfiguration configuration = this.minimalConfigurationBuilder
-				.responseType("should-be-removed")
-				.responseTypes(responseTypes -> {
-					responseTypes.clear();
-					responseTypes.add("some-response-type");
-				})
-				.build();
-
-		assertThat(configuration.getResponseTypes()).containsExactly("some-response-type");
-	}
-
-	@Test
-	public void responseTypesWhenNotPresentAndAddingThenCorrectValues() {
-		OAuth2AuthorizationServerConfiguration configuration = this.minimalConfigurationBuilder
-				.claims(claims -> claims.remove(OAuth2AuthorizationServerMetadataClaimNames.RESPONSE_TYPES_SUPPORTED))
-				.responseTypes(responseTypes -> responseTypes.add("some-response-type"))
-				.build();
-
-		assertThat(configuration.getResponseTypes()).containsExactly("some-response-type");
-	}
-
-	@Test
-	public void scopesWhenAddingOrRemovingThenCorrectValues() {
-		OAuth2AuthorizationServerConfiguration configuration = this.minimalConfigurationBuilder
-				.scope("should-be-removed")
-				.scopes(scopes -> {
-					scopes.clear();
-					scopes.add("some-scope");
-				})
-				.build();
-
-		assertThat(configuration.getScopes()).containsExactly("some-scope");
-	}
-
-	@Test
-	public void grantTypesWhenAddingOrRemovingThenCorrectValues() {
-		OAuth2AuthorizationServerConfiguration configuration = this.minimalConfigurationBuilder
-				.grantType("should-be-removed")
-				.grantTypes(grantTypes -> {
-					grantTypes.clear();
-					grantTypes.add("some-grant-type");
-				})
-				.build();
-
-		assertThat(configuration.getGrantTypes()).containsExactly("some-grant-type");
-	}
-
-	@Test
-	public void tokenEndpointAuthenticationMethodsWhenAddingOrRemovingThenCorrectValues() {
-		OAuth2AuthorizationServerConfiguration configuration = this.minimalConfigurationBuilder
-				.tokenEndpointAuthenticationMethod("should-be-removed")
-				.tokenEndpointAuthenticationMethods(authMethods -> {
-					authMethods.clear();
-					authMethods.add("some-authentication-method");
-				})
-				.build();
-
-		assertThat(configuration.getTokenEndpointAuthenticationMethods()).containsExactly("some-authentication-method");
-	}
-
-	@Test
-	public void tokenRevocationEndpointAuthenticationMethodsWhenAddingOrRemovingThenCorrectValues() {
-		OAuth2AuthorizationServerConfiguration configuration = this.minimalConfigurationBuilder
-				.tokenRevocationEndpointAuthenticationMethod("should-be-removed")
-				.tokenRevocationEndpointAuthenticationMethods(authMethods -> {
-					authMethods.clear();
-					authMethods.add("some-authentication-method");
-				})
-				.build();
-
-		assertThat(configuration.getTokenRevocationEndpointAuthenticationMethods()).containsExactly("some-authentication-method");
-	}
-
-	@Test
-	public void codeChallengeMethodsMethodsWhenAddingOrRemovingThenCorrectValues() {
-		OAuth2AuthorizationServerConfiguration configuration = this.minimalConfigurationBuilder
-				.codeChallengeMethod("should-be-removed")
-				.codeChallengeMethods(codeChallengeMethods -> {
-					codeChallengeMethods.clear();
-					codeChallengeMethods.add("some-authentication-method");
-				})
-				.build();
-
-		assertThat(configuration.getCodeChallengeMethods()).containsExactly("some-authentication-method");
-	}
-
-	@Test
-	public void claimWhenNameIsNullThenThrowIllegalArgumentException() {
-		assertThatIllegalArgumentException()
-				.isThrownBy(() -> OAuth2AuthorizationServerConfiguration.builder().claim(null, "value"))
-				.withMessage("name cannot be empty");
-	}
-
-	@Test
-	public void claimWhenValueIsNullThenThrowIllegalArgumentException() {
-		assertThatIllegalArgumentException()
-				.isThrownBy(() -> OAuth2AuthorizationServerConfiguration.builder().claim("claim-name", null))
-				.withMessage("value cannot be null");
-	}
-
-	@Test
-	public void claimsWhenRemovingClaimThenNotPresent() {
-		OAuth2AuthorizationServerConfiguration configuration =
-				this.minimalConfigurationBuilder
-						.grantType("some-grant-type")
-						.claims((claims) -> claims.remove(OAuth2AuthorizationServerMetadataClaimNames.GRANT_TYPES_SUPPORTED))
-						.build();
-		assertThat(configuration.getGrantTypes()).isNull();
-	}
-
-	@Test
-	public void claimsWhenAddingClaimThenPresent() {
-		OAuth2AuthorizationServerConfiguration configuration =
-				this.minimalConfigurationBuilder
-						.claims((claims) -> claims.put(OAuth2AuthorizationServerMetadataClaimNames.GRANT_TYPES_SUPPORTED, "authorization_code"))
-						.build();
-		assertThat(configuration.getGrantTypes()).containsExactly("authorization_code");
-	}
-
-	private static URL url(String urlString) {
-		try {
-			return new URL(urlString);
-		} catch (Exception ex) {
-			throw new IllegalArgumentException("urlString must be a valid URL and valid URI");
-		}
-	}
-}

+ 0 - 41
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/core/endpoint/PkceCodeChallengeMethod2Test.java

@@ -1,41 +0,0 @@
-/*
- * Copyright 2020 the original author or authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * 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.endpoint;
-
-import org.junit.Test;
-
-import static org.assertj.core.api.Assertions.assertThat;
-
-/**
- * TODO
- * This class is temporary and will be removed after upgrading to Spring Security 5.5.0 GA.
- *
- * Tests for {@link PkceCodeChallengeMethod2}.
- *
- * @author Daniel Garnier-Moiroux
- */
-public class PkceCodeChallengeMethod2Test {
-
-	@Test
-	public void getValueWhenCodeChallengeMethodPlainThenReturnPlain() {
-		assertThat(PkceCodeChallengeMethod2.PLAIN.getValue()).isEqualTo("plain");
-	}
-
-	@Test
-	public void getValueWhenCodeChallengeMethodS256ThenReturnS256() {
-		assertThat(PkceCodeChallengeMethod2.S256.getValue()).isEqualTo("S256");
-	}
-}

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

@@ -1,218 +0,0 @@
-/*
- * Copyright 2020 the original author or authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.springframework.security.oauth2.core.http.converter;
-
-
-import org.junit.Test;
-import org.springframework.core.convert.converter.Converter;
-import org.springframework.http.HttpStatus;
-import org.springframework.http.converter.HttpMessageNotReadableException;
-import org.springframework.http.converter.HttpMessageNotWritableException;
-import org.springframework.mock.http.MockHttpOutputMessage;
-import org.springframework.mock.http.client.MockClientHttpResponse;
-import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationServerConfiguration;
-
-import java.net.URL;
-import java.util.Arrays;
-import java.util.Map;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
-import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
-
-/**
- * Tests for {@link OAuth2AuthorizationServerConfigurationHttpMessageConverter}
- *
- * @author Daniel Garnier-Moiroux
- */
-public class OAuth2AuthorizationServerConfigurationHttpMessageConverterTests {
-	private final OAuth2AuthorizationServerConfigurationHttpMessageConverter messageConverter = new OAuth2AuthorizationServerConfigurationHttpMessageConverter();
-
-	@Test
-	public void supportsWhenOAuth2AuthorizationServerConfigurationThenTrue() {
-		assertThat(this.messageConverter.supports(OAuth2AuthorizationServerConfiguration.class)).isTrue();
-	}
-
-	@Test
-	public void setAuthorizationServerConfigurationParametersConverterWhenConverterIsNullThenThrowIllegalArgumentException() {
-		assertThatIllegalArgumentException().isThrownBy(() -> this.messageConverter.setAuthorizationServerConfigurationParametersConverter(null));
-	}
-
-	@Test
-	public void setAuthorizationServerConfigurationConverterWhenConverterIsNullThenThrowIllegalArgumentException() {
-		assertThatIllegalArgumentException().isThrownBy(() -> this.messageConverter.setAuthorizationServerConfigurationConverter(null));
-	}
-
-	@Test
-	public void readInternalWhenRequiredParametersThenSuccess() throws Exception {
-		// @formatter:off
-		String serverConfigurationResponse = "{\n"
-				+ "		\"issuer\": \"https://example.com/issuer1\",\n"
-				+ "		\"authorization_endpoint\": \"https://example.com/issuer1/oauth2/authorize\",\n"
-				+ "		\"token_endpoint\": \"https://example.com/issuer1/oauth2/token\",\n"
-				+ "		\"jwks_uri\": \"https://example.com/issuer1/oauth2/jwks\",\n"
-				+ "		\"response_types_supported\": [\"code\"]\n"
-				+ "}\n";
-		// @formatter:on
-		MockClientHttpResponse response = new MockClientHttpResponse(serverConfigurationResponse.getBytes(), HttpStatus.OK);
-		OAuth2AuthorizationServerConfiguration serverConfiguration = this.messageConverter
-				.readInternal(OAuth2AuthorizationServerConfiguration.class, response);
-
-		assertThat(serverConfiguration.getIssuer()).isEqualTo(new URL("https://example.com/issuer1"));
-		assertThat(serverConfiguration.getAuthorizationEndpoint()).isEqualTo(new URL("https://example.com/issuer1/oauth2/authorize"));
-		assertThat(serverConfiguration.getTokenEndpoint()).isEqualTo(new URL("https://example.com/issuer1/oauth2/token"));
-		assertThat(serverConfiguration.getJwkSetUri()).isEqualTo(new URL("https://example.com/issuer1/oauth2/jwks"));
-		assertThat(serverConfiguration.getResponseTypes()).containsExactly("code");
-		assertThat(serverConfiguration.getScopes()).isNull();
-		assertThat(serverConfiguration.getGrantTypes()).isNull();
-		assertThat(serverConfiguration.getTokenEndpointAuthenticationMethods()).isNull();
-		assertThat(serverConfiguration.getCodeChallengeMethods()).isNull();
-		assertThat(serverConfiguration.getTokenRevocationEndpoint()).isNull();
-		assertThat(serverConfiguration.getTokenRevocationEndpointAuthenticationMethods()).isNull();
-	}
-
-	@Test
-	public void readInternalWhenValidParametersThenSuccess() throws Exception {
-		// @formatter:off
-		String serverConfigurationResponse = "{\n"
-				+ "		\"issuer\": \"https://example.com/issuer1\",\n"
-				+ "		\"authorization_endpoint\": \"https://example.com/issuer1/oauth2/authorize\",\n"
-				+ "		\"token_endpoint\": \"https://example.com/issuer1/oauth2/token\",\n"
-				+ "		\"revocation_endpoint\": \"https://example.com/issuer1/oauth2/revoke\",\n"
-				+ "		\"jwks_uri\": \"https://example.com/issuer1/oauth2/jwks\",\n"
-				+ "		\"response_types_supported\": [\"code\"],\n"
-				+ "		\"grant_types_supported\": [\"authorization_code\", \"client_credentials\"],\n"
-				+ "		\"scopes_supported\": [\"openid\"],\n"
-				+ "		\"token_endpoint_auth_methods_supported\": [\"client_secret_basic\"],\n"
-				+ "		\"revocation_endpoint_auth_methods_supported\": [\"client_secret_basic\"],\n"
-				+ "		\"code_challenge_methods_supported\": [\"plain\",\"S256\"],\n"
-				+ "		\"custom_claim\": \"value\",\n"
-				+ "		\"custom_collection_claim\": [\"value1\", \"value2\"]\n"
-				+ "}\n";
-		// @formatter:on
-		MockClientHttpResponse response = new MockClientHttpResponse(serverConfigurationResponse.getBytes(), HttpStatus.OK);
-		OAuth2AuthorizationServerConfiguration serverConfiguration = this.messageConverter
-				.readInternal(OAuth2AuthorizationServerConfiguration.class, response);
-
-		assertThat(serverConfiguration.getClaims()).hasSize(13);
-		assertThat(serverConfiguration.getIssuer()).isEqualTo(new URL("https://example.com/issuer1"));
-		assertThat(serverConfiguration.getAuthorizationEndpoint()).isEqualTo(new URL("https://example.com/issuer1/oauth2/authorize"));
-		assertThat(serverConfiguration.getTokenEndpoint()).isEqualTo(new URL("https://example.com/issuer1/oauth2/token"));
-		assertThat(serverConfiguration.getTokenRevocationEndpoint()).isEqualTo(new URL("https://example.com/issuer1/oauth2/revoke"));
-		assertThat(serverConfiguration.getJwkSetUri()).isEqualTo(new URL("https://example.com/issuer1/oauth2/jwks"));
-		assertThat(serverConfiguration.getResponseTypes()).containsExactly("code");
-		assertThat(serverConfiguration.getGrantTypes()).containsExactlyInAnyOrder("authorization_code", "client_credentials");
-		assertThat(serverConfiguration.getScopes()).containsExactly("openid");
-		assertThat(serverConfiguration.getTokenEndpointAuthenticationMethods()).containsExactly("client_secret_basic");
-		assertThat(serverConfiguration.getTokenRevocationEndpointAuthenticationMethods()).containsExactly("client_secret_basic");
-		assertThat(serverConfiguration.getCodeChallengeMethods()).containsExactlyInAnyOrder("plain", "S256");
-		assertThat(serverConfiguration.getClaimAsString("custom_claim")).isEqualTo("value");
-		assertThat(serverConfiguration.getClaimAsStringList("custom_collection_claim")).containsExactlyInAnyOrder("value1", "value2");
-	}
-
-	@Test
-	public void readInternalWhenFailingConverterThenThrowException() {
-		String errorMessage = "this is not a valid converter";
-		this.messageConverter.setAuthorizationServerConfigurationConverter(source -> {
-			throw new RuntimeException(errorMessage);
-		});
-		MockClientHttpResponse response = new MockClientHttpResponse("{}".getBytes(), HttpStatus.OK);
-
-		assertThatExceptionOfType(HttpMessageNotReadableException.class)
-				.isThrownBy(() -> this.messageConverter.readInternal(OAuth2AuthorizationServerConfiguration.class, response))
-				.withMessageContaining("An error occurred reading the OAuth 2.0 Authorization Server Configuration")
-				.withMessageContaining(errorMessage);
-	}
-
-	@Test
-	public void readInternalWhenInvalidOAuth2AuthorizationServerConfigurationThenThrowException() {
-		String providerConfigurationResponse = "{ \"issuer\": null }";
-		MockClientHttpResponse response = new MockClientHttpResponse(providerConfigurationResponse.getBytes(), HttpStatus.OK);
-
-		assertThatExceptionOfType(HttpMessageNotReadableException.class)
-				.isThrownBy(() -> this.messageConverter.readInternal(OAuth2AuthorizationServerConfiguration.class, response))
-				.withMessageContaining("An error occurred reading the OAuth 2.0 Authorization Server Configuration")
-				.withMessageContaining("issuer cannot be null");
-	}
-
-	@Test
-	public void writeInternalWhenOAuth2AuthorizationServerConfigurationThenSuccess() {
-		OAuth2AuthorizationServerConfiguration serverConfiguration =
-				OAuth2AuthorizationServerConfiguration
-						.builder()
-						.issuer("https://example.com/issuer1")
-						.authorizationEndpoint("https://example.com/issuer1/oauth2/authorize")
-						.tokenEndpoint("https://example.com/issuer1/oauth2/token")
-						.tokenRevocationEndpoint("https://example.com/issuer1/oauth2/revoke")
-						.jwkSetUri("https://example.com/issuer1/oauth2/jwks")
-						.scope("openid")
-						.responseType("code")
-						.grantType("authorization_code")
-						.grantType("client_credentials")
-						.tokenEndpointAuthenticationMethod("client_secret_basic")
-						.tokenRevocationEndpointAuthenticationMethod("client_secret_basic")
-						.codeChallengeMethod("plain")
-						.codeChallengeMethod("S256")
-						.claim("custom_claim", "value")
-						.claim("custom_collection_claim", Arrays.asList("value1", "value2"))
-						.build();
-		MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
-
-		this.messageConverter.writeInternal(serverConfiguration, outputMessage);
-
-		String serverConfigurationResponse = outputMessage.getBodyAsString();
-		assertThat(serverConfigurationResponse).contains("\"issuer\":\"https://example.com/issuer1\"");
-		assertThat(serverConfigurationResponse).contains("\"authorization_endpoint\":\"https://example.com/issuer1/oauth2/authorize\"");
-		assertThat(serverConfigurationResponse).contains("\"token_endpoint\":\"https://example.com/issuer1/oauth2/token\"");
-		assertThat(serverConfigurationResponse).contains("\"revocation_endpoint\":\"https://example.com/issuer1/oauth2/revoke\"");
-		assertThat(serverConfigurationResponse).contains("\"jwks_uri\":\"https://example.com/issuer1/oauth2/jwks\"");
-		assertThat(serverConfigurationResponse).contains("\"scopes_supported\":[\"openid\"]");
-		assertThat(serverConfigurationResponse).contains("\"response_types_supported\":[\"code\"]");
-		assertThat(serverConfigurationResponse).contains("\"grant_types_supported\":[\"authorization_code\",\"client_credentials\"]");
-		assertThat(serverConfigurationResponse).contains("\"token_endpoint_auth_methods_supported\":[\"client_secret_basic\"]");
-		assertThat(serverConfigurationResponse).contains("\"revocation_endpoint_auth_methods_supported\":[\"client_secret_basic\"]");
-		assertThat(serverConfigurationResponse).contains("\"code_challenge_methods_supported\":[\"plain\",\"S256\"]");
-		assertThat(serverConfigurationResponse).contains("\"custom_claim\":\"value\"");
-		assertThat(serverConfigurationResponse).contains("\"custom_collection_claim\":[\"value1\",\"value2\"]");
-
-	}
-
-	@Test
-	public void writeInternalWhenWriteFailsThenThrowsException() {
-		String errorMessage = "this is not a valid converter";
-		Converter<OAuth2AuthorizationServerConfiguration, Map<String, Object>> failingConverter =
-				source -> {
-					throw new RuntimeException(errorMessage);
-				};
-		this.messageConverter.setAuthorizationServerConfigurationParametersConverter(failingConverter);
-
-		MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
-		OAuth2AuthorizationServerConfiguration serverConfiguration =
-				OAuth2AuthorizationServerConfiguration
-						.builder()
-						.issuer("https://example.com/issuer1")
-						.authorizationEndpoint("https://example.com/issuer1/oauth2/authorize")
-						.tokenEndpoint("https://example.com/issuer1/oauth2/token")
-						.jwkSetUri("https://example.com/issuer1/oauth2/jwks")
-						.responseType("code")
-						.build();
-
-		assertThatExceptionOfType(HttpMessageNotWritableException.class)
-				.isThrownBy(() -> this.messageConverter.writeInternal(serverConfiguration, outputMessage))
-				.withMessageContaining("An error occurred writing the OAuth 2.0 Authorization Server Configuration")
-				.withMessageContaining(errorMessage);
-	}
-}

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

@@ -0,0 +1,224 @@
+/*
+ * Copyright 2020-2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.oauth2.core.http.converter;
+
+
+import java.net.URL;
+import java.util.Arrays;
+import java.util.Map;
+
+import org.junit.Test;
+
+import org.springframework.core.convert.converter.Converter;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.converter.HttpMessageNotReadableException;
+import org.springframework.http.converter.HttpMessageNotWritableException;
+import org.springframework.mock.http.MockHttpOutputMessage;
+import org.springframework.mock.http.client.MockClientHttpResponse;
+import org.springframework.security.oauth2.core.OAuth2AuthorizationServerMetadata;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
+
+/**
+ * Tests for {@link OAuth2AuthorizationServerMetadataHttpMessageConverter}
+ *
+ * @author Daniel Garnier-Moiroux
+ */
+public class OAuth2AuthorizationServerMetadataHttpMessageConverterTests {
+	private final OAuth2AuthorizationServerMetadataHttpMessageConverter messageConverter = new OAuth2AuthorizationServerMetadataHttpMessageConverter();
+
+	@Test
+	public void supportsWhenOAuth2AuthorizationServerMetadataThenTrue() {
+		assertThat(this.messageConverter.supports(OAuth2AuthorizationServerMetadata.class)).isTrue();
+	}
+
+	@Test
+	public void setAuthorizationServerMetadataParametersConverterWhenConverterIsNullThenThrowIllegalArgumentException() {
+		assertThatIllegalArgumentException().isThrownBy(() -> this.messageConverter.setAuthorizationServerMetadataParametersConverter(null));
+	}
+
+	@Test
+	public void setAuthorizationServerMetadataConverterWhenConverterIsNullThenThrowIllegalArgumentException() {
+		assertThatIllegalArgumentException().isThrownBy(() -> this.messageConverter.setAuthorizationServerMetadataConverter(null));
+	}
+
+	@Test
+	public void readInternalWhenRequiredParametersThenSuccess() throws Exception {
+		// @formatter:off
+		String authorizationServerMetadataResponse = "{\n"
+				+ "		\"issuer\": \"https://example.com/issuer1\",\n"
+				+ "		\"authorization_endpoint\": \"https://example.com/issuer1/oauth2/authorize\",\n"
+				+ "		\"token_endpoint\": \"https://example.com/issuer1/oauth2/token\",\n"
+				+ "		\"response_types_supported\": [\"code\"]\n"
+				+ "}\n";
+		// @formatter:on
+		MockClientHttpResponse response = new MockClientHttpResponse(authorizationServerMetadataResponse.getBytes(), HttpStatus.OK);
+		OAuth2AuthorizationServerMetadata authorizationServerMetadata = this.messageConverter
+				.readInternal(OAuth2AuthorizationServerMetadata.class, response);
+
+		assertThat(authorizationServerMetadata.getIssuer()).isEqualTo(new URL("https://example.com/issuer1"));
+		assertThat(authorizationServerMetadata.getAuthorizationEndpoint()).isEqualTo(new URL("https://example.com/issuer1/oauth2/authorize"));
+		assertThat(authorizationServerMetadata.getTokenEndpoint()).isEqualTo(new URL("https://example.com/issuer1/oauth2/token"));
+		assertThat(authorizationServerMetadata.getTokenEndpointAuthenticationMethods()).isNull();
+		assertThat(authorizationServerMetadata.getJwkSetUrl()).isNull();
+		assertThat(authorizationServerMetadata.getResponseTypes()).containsExactly("code");
+		assertThat(authorizationServerMetadata.getScopes()).isNull();
+		assertThat(authorizationServerMetadata.getGrantTypes()).isNull();
+		assertThat(authorizationServerMetadata.getTokenRevocationEndpoint()).isNull();
+		assertThat(authorizationServerMetadata.getTokenRevocationEndpointAuthenticationMethods()).isNull();
+		assertThat(authorizationServerMetadata.getTokenIntrospectionEndpoint()).isNull();
+		assertThat(authorizationServerMetadata.getTokenIntrospectionEndpointAuthenticationMethods()).isNull();
+		assertThat(authorizationServerMetadata.getCodeChallengeMethods()).isNull();
+	}
+
+	@Test
+	public void readInternalWhenValidParametersThenSuccess() throws Exception {
+		// @formatter:off
+		String authorizationServerMetadataResponse = "{\n"
+				+ "		\"issuer\": \"https://example.com/issuer1\",\n"
+				+ "		\"authorization_endpoint\": \"https://example.com/issuer1/oauth2/authorize\",\n"
+				+ "		\"token_endpoint\": \"https://example.com/issuer1/oauth2/token\",\n"
+				+ "		\"token_endpoint_auth_methods_supported\": [\"client_secret_basic\"],\n"
+				+ "		\"jwks_uri\": \"https://example.com/issuer1/oauth2/jwks\",\n"
+				+ "		\"scopes_supported\": [\"openid\"],\n"
+				+ "		\"response_types_supported\": [\"code\"],\n"
+				+ "		\"grant_types_supported\": [\"authorization_code\", \"client_credentials\"],\n"
+				+ "		\"revocation_endpoint\": \"https://example.com/issuer1/oauth2/revoke\",\n"
+				+ "		\"revocation_endpoint_auth_methods_supported\": [\"client_secret_basic\"],\n"
+				+ "		\"introspection_endpoint\": \"https://example.com/issuer1/oauth2/introspect\",\n"
+				+ "		\"introspection_endpoint_auth_methods_supported\": [\"client_secret_basic\"],\n"
+				+ "		\"code_challenge_methods_supported\": [\"plain\",\"S256\"],\n"
+				+ "		\"custom_claim\": \"value\",\n"
+				+ "		\"custom_collection_claim\": [\"value1\", \"value2\"]\n"
+				+ "}\n";
+		// @formatter:on
+		MockClientHttpResponse response = new MockClientHttpResponse(authorizationServerMetadataResponse.getBytes(), HttpStatus.OK);
+		OAuth2AuthorizationServerMetadata authorizationServerMetadata = this.messageConverter
+				.readInternal(OAuth2AuthorizationServerMetadata.class, response);
+
+		assertThat(authorizationServerMetadata.getClaims()).hasSize(15);
+		assertThat(authorizationServerMetadata.getIssuer()).isEqualTo(new URL("https://example.com/issuer1"));
+		assertThat(authorizationServerMetadata.getAuthorizationEndpoint()).isEqualTo(new URL("https://example.com/issuer1/oauth2/authorize"));
+		assertThat(authorizationServerMetadata.getTokenEndpoint()).isEqualTo(new URL("https://example.com/issuer1/oauth2/token"));
+		assertThat(authorizationServerMetadata.getTokenEndpointAuthenticationMethods()).containsExactly("client_secret_basic");
+		assertThat(authorizationServerMetadata.getJwkSetUrl()).isEqualTo(new URL("https://example.com/issuer1/oauth2/jwks"));
+		assertThat(authorizationServerMetadata.getScopes()).containsExactly("openid");
+		assertThat(authorizationServerMetadata.getResponseTypes()).containsExactly("code");
+		assertThat(authorizationServerMetadata.getGrantTypes()).containsExactlyInAnyOrder("authorization_code", "client_credentials");
+		assertThat(authorizationServerMetadata.getTokenRevocationEndpoint()).isEqualTo(new URL("https://example.com/issuer1/oauth2/revoke"));
+		assertThat(authorizationServerMetadata.getTokenRevocationEndpointAuthenticationMethods()).containsExactly("client_secret_basic");
+		assertThat(authorizationServerMetadata.getTokenIntrospectionEndpoint()).isEqualTo(new URL("https://example.com/issuer1/oauth2/introspect"));
+		assertThat(authorizationServerMetadata.getTokenIntrospectionEndpointAuthenticationMethods()).containsExactly("client_secret_basic");
+		assertThat(authorizationServerMetadata.getCodeChallengeMethods()).containsExactlyInAnyOrder("plain", "S256");
+		assertThat(authorizationServerMetadata.getClaimAsString("custom_claim")).isEqualTo("value");
+		assertThat(authorizationServerMetadata.getClaimAsStringList("custom_collection_claim")).containsExactlyInAnyOrder("value1", "value2");
+	}
+
+	@Test
+	public void readInternalWhenFailingConverterThenThrowException() {
+		String errorMessage = "this is not a valid converter";
+		this.messageConverter.setAuthorizationServerMetadataConverter(source -> {
+			throw new RuntimeException(errorMessage);
+		});
+		MockClientHttpResponse response = new MockClientHttpResponse("{}".getBytes(), HttpStatus.OK);
+
+		assertThatExceptionOfType(HttpMessageNotReadableException.class)
+				.isThrownBy(() -> this.messageConverter.readInternal(OAuth2AuthorizationServerMetadata.class, response))
+				.withMessageContaining("An error occurred reading the OAuth 2.0 Authorization Server Metadata")
+				.withMessageContaining(errorMessage);
+	}
+
+	@Test
+	public void readInternalWhenInvalidOAuth2AuthorizationServerMetadataThenThrowException() {
+		String authorizationServerMetadataResponse = "{ \"issuer\": null }";
+		MockClientHttpResponse response = new MockClientHttpResponse(authorizationServerMetadataResponse.getBytes(), HttpStatus.OK);
+
+		assertThatExceptionOfType(HttpMessageNotReadableException.class)
+				.isThrownBy(() -> this.messageConverter.readInternal(OAuth2AuthorizationServerMetadata.class, response))
+				.withMessageContaining("An error occurred reading the OAuth 2.0 Authorization Server Metadata")
+				.withMessageContaining("issuer cannot be null");
+	}
+
+	@Test
+	public void writeInternalWhenOAuth2AuthorizationServerMetadataThenSuccess() {
+		OAuth2AuthorizationServerMetadata authorizationServerMetadata =
+				OAuth2AuthorizationServerMetadata.builder()
+						.issuer("https://example.com/issuer1")
+						.authorizationEndpoint("https://example.com/issuer1/oauth2/authorize")
+						.tokenEndpoint("https://example.com/issuer1/oauth2/token")
+						.tokenEndpointAuthenticationMethod("client_secret_basic")
+						.jwkSetUrl("https://example.com/issuer1/oauth2/jwks")
+						.scope("openid")
+						.responseType("code")
+						.grantType("authorization_code")
+						.grantType("client_credentials")
+						.tokenRevocationEndpoint("https://example.com/issuer1/oauth2/revoke")
+						.tokenRevocationEndpointAuthenticationMethod("client_secret_basic")
+						.tokenIntrospectionEndpoint("https://example.com/issuer1/oauth2/introspect")
+						.tokenIntrospectionEndpointAuthenticationMethod("client_secret_basic")
+						.codeChallengeMethod("plain")
+						.codeChallengeMethod("S256")
+						.claim("custom_claim", "value")
+						.claim("custom_collection_claim", Arrays.asList("value1", "value2"))
+						.build();
+		MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
+
+		this.messageConverter.writeInternal(authorizationServerMetadata, outputMessage);
+
+		String authorizationServerMetadataResponse = outputMessage.getBodyAsString();
+		assertThat(authorizationServerMetadataResponse).contains("\"issuer\":\"https://example.com/issuer1\"");
+		assertThat(authorizationServerMetadataResponse).contains("\"authorization_endpoint\":\"https://example.com/issuer1/oauth2/authorize\"");
+		assertThat(authorizationServerMetadataResponse).contains("\"token_endpoint\":\"https://example.com/issuer1/oauth2/token\"");
+		assertThat(authorizationServerMetadataResponse).contains("\"token_endpoint_auth_methods_supported\":[\"client_secret_basic\"]");
+		assertThat(authorizationServerMetadataResponse).contains("\"jwks_uri\":\"https://example.com/issuer1/oauth2/jwks\"");
+		assertThat(authorizationServerMetadataResponse).contains("\"scopes_supported\":[\"openid\"]");
+		assertThat(authorizationServerMetadataResponse).contains("\"response_types_supported\":[\"code\"]");
+		assertThat(authorizationServerMetadataResponse).contains("\"grant_types_supported\":[\"authorization_code\",\"client_credentials\"]");
+		assertThat(authorizationServerMetadataResponse).contains("\"revocation_endpoint\":\"https://example.com/issuer1/oauth2/revoke\"");
+		assertThat(authorizationServerMetadataResponse).contains("\"revocation_endpoint_auth_methods_supported\":[\"client_secret_basic\"]");
+		assertThat(authorizationServerMetadataResponse).contains("\"introspection_endpoint\":\"https://example.com/issuer1/oauth2/introspect\"");
+		assertThat(authorizationServerMetadataResponse).contains("\"introspection_endpoint_auth_methods_supported\":[\"client_secret_basic\"]");
+		assertThat(authorizationServerMetadataResponse).contains("\"code_challenge_methods_supported\":[\"plain\",\"S256\"]");
+		assertThat(authorizationServerMetadataResponse).contains("\"custom_claim\":\"value\"");
+		assertThat(authorizationServerMetadataResponse).contains("\"custom_collection_claim\":[\"value1\",\"value2\"]");
+	}
+
+	@Test
+	public void writeInternalWhenWriteFailsThenThrowException() {
+		String errorMessage = "this is not a valid converter";
+		Converter<OAuth2AuthorizationServerMetadata, Map<String, Object>> failingConverter =
+				source -> {
+					throw new RuntimeException(errorMessage);
+				};
+		this.messageConverter.setAuthorizationServerMetadataParametersConverter(failingConverter);
+
+		MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
+		OAuth2AuthorizationServerMetadata authorizationServerMetadata =
+				OAuth2AuthorizationServerMetadata.builder()
+						.issuer("https://example.com/issuer1")
+						.authorizationEndpoint("https://example.com/issuer1/oauth2/authorize")
+						.tokenEndpoint("https://example.com/issuer1/oauth2/token")
+						.responseType("code")
+						.build();
+
+		assertThatExceptionOfType(HttpMessageNotWritableException.class)
+				.isThrownBy(() -> this.messageConverter.writeInternal(authorizationServerMetadata, outputMessage))
+				.withMessageContaining("An error occurred writing the OAuth 2.0 Authorization Server Metadata")
+				.withMessageContaining(errorMessage);
+	}
+}

+ 15 - 26
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/core/oidc/OidcProviderConfigurationTests.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2020 the original author or authors.
+ * Copyright 2020-2021 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -15,14 +15,14 @@
  */
 package org.springframework.security.oauth2.core.oidc;
 
-import org.junit.Test;
-
 import java.net.URL;
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
-import java.util.LinkedHashSet;
+import java.util.List;
 import java.util.Map;
-import java.util.Set;
+
+import org.junit.Test;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
@@ -38,7 +38,7 @@ public class OidcProviderConfigurationTests {
 					.issuer("https://example.com/issuer1")
 					.authorizationEndpoint("https://example.com/issuer1/oauth2/authorize")
 					.tokenEndpoint("https://example.com/issuer1/oauth2/token")
-					.jwkSetUri("https://example.com/issuer1/oauth2/jwks")
+					.jwkSetUrl("https://example.com/issuer1/oauth2/jwks")
 					.scope("openid")
 					.responseType("code")
 					.subjectType("public")
@@ -50,7 +50,7 @@ public class OidcProviderConfigurationTests {
 				.issuer("https://example.com/issuer1")
 				.authorizationEndpoint("https://example.com/issuer1/oauth2/authorize")
 				.tokenEndpoint("https://example.com/issuer1/oauth2/token")
-				.jwkSetUri("https://example.com/issuer1/oauth2/jwks")
+				.jwkSetUrl("https://example.com/issuer1/oauth2/jwks")
 				.scope("openid")
 				.responseType("code")
 				.grantType("authorization_code")
@@ -64,7 +64,7 @@ public class OidcProviderConfigurationTests {
 		assertThat(providerConfiguration.getIssuer()).isEqualTo(url("https://example.com/issuer1"));
 		assertThat(providerConfiguration.getAuthorizationEndpoint()).isEqualTo(url("https://example.com/issuer1/oauth2/authorize"));
 		assertThat(providerConfiguration.getTokenEndpoint()).isEqualTo(url("https://example.com/issuer1/oauth2/token"));
-		assertThat(providerConfiguration.getJwkSetUri()).isEqualTo(url("https://example.com/issuer1/oauth2/jwks"));
+		assertThat(providerConfiguration.getJwkSetUrl()).isEqualTo(url("https://example.com/issuer1/oauth2/jwks"));
 		assertThat(providerConfiguration.getScopes()).containsExactly("openid");
 		assertThat(providerConfiguration.getResponseTypes()).containsExactly("code");
 		assertThat(providerConfiguration.getGrantTypes()).containsExactlyInAnyOrder("authorization_code", "client_credentials");
@@ -80,7 +80,7 @@ public class OidcProviderConfigurationTests {
 				.issuer("https://example.com/issuer1")
 				.authorizationEndpoint("https://example.com/issuer1/oauth2/authorize")
 				.tokenEndpoint("https://example.com/issuer1/oauth2/token")
-				.jwkSetUri("https://example.com/issuer1/oauth2/jwks")
+				.jwkSetUrl("https://example.com/issuer1/oauth2/jwks")
 				.scope("openid")
 				.responseType("code")
 				.subjectType("public")
@@ -90,7 +90,7 @@ public class OidcProviderConfigurationTests {
 		assertThat(providerConfiguration.getIssuer()).isEqualTo(url("https://example.com/issuer1"));
 		assertThat(providerConfiguration.getAuthorizationEndpoint()).isEqualTo(url("https://example.com/issuer1/oauth2/authorize"));
 		assertThat(providerConfiguration.getTokenEndpoint()).isEqualTo(url("https://example.com/issuer1/oauth2/token"));
-		assertThat(providerConfiguration.getJwkSetUri()).isEqualTo(url("https://example.com/issuer1/oauth2/jwks"));
+		assertThat(providerConfiguration.getJwkSetUrl()).isEqualTo(url("https://example.com/issuer1/oauth2/jwks"));
 		assertThat(providerConfiguration.getScopes()).containsExactly("openid");
 		assertThat(providerConfiguration.getResponseTypes()).containsExactly("code");
 		assertThat(providerConfiguration.getGrantTypes()).isNull();
@@ -117,7 +117,7 @@ public class OidcProviderConfigurationTests {
 		assertThat(providerConfiguration.getIssuer()).isEqualTo(url("https://example.com/issuer1"));
 		assertThat(providerConfiguration.getAuthorizationEndpoint()).isEqualTo(url("https://example.com/issuer1/oauth2/authorize"));
 		assertThat(providerConfiguration.getTokenEndpoint()).isEqualTo(url("https://example.com/issuer1/oauth2/token"));
-		assertThat(providerConfiguration.getJwkSetUri()).isEqualTo(url("https://example.com/issuer1/oauth2/jwks"));
+		assertThat(providerConfiguration.getJwkSetUrl()).isEqualTo(url("https://example.com/issuer1/oauth2/jwks"));
 		assertThat(providerConfiguration.getScopes()).containsExactly("openid");
 		assertThat(providerConfiguration.getResponseTypes()).containsExactly("code");
 		assertThat(providerConfiguration.getGrantTypes()).isNull();
@@ -145,7 +145,7 @@ public class OidcProviderConfigurationTests {
 		assertThat(providerConfiguration.getIssuer()).isEqualTo(url("https://example.com/issuer1"));
 		assertThat(providerConfiguration.getAuthorizationEndpoint()).isEqualTo(url("https://example.com/issuer1/oauth2/authorize"));
 		assertThat(providerConfiguration.getTokenEndpoint()).isEqualTo(url("https://example.com/issuer1/oauth2/token"));
-		assertThat(providerConfiguration.getJwkSetUri()).isEqualTo(url("https://example.com/issuer1/oauth2/jwks"));
+		assertThat(providerConfiguration.getJwkSetUrl()).isEqualTo(url("https://example.com/issuer1/oauth2/jwks"));
 		assertThat(providerConfiguration.getScopes()).containsExactly("openid");
 		assertThat(providerConfiguration.getResponseTypes()).containsExactly("code");
 		assertThat(providerConfiguration.getGrantTypes()).isNull();
@@ -178,7 +178,7 @@ public class OidcProviderConfigurationTests {
 		OidcProviderConfiguration second = this.minimalConfigurationBuilder
 				.claims((claims) ->
 						{
-							Set<String> newGrantTypes = new LinkedHashSet<>();
+							List<String> newGrantTypes = new ArrayList<>();
 							newGrantTypes.add("authorization_code");
 							newGrantTypes.add("custom_grant");
 							claims.put(OidcProviderMetadataClaimNames.GRANT_TYPES_SUPPORTED, newGrantTypes);
@@ -190,17 +190,6 @@ public class OidcProviderConfigurationTests {
 		assertThat(second.getGrantTypes()).containsExactlyInAnyOrder("authorization_code", "custom_grant");
 	}
 
-	@Test
-	public void buildWhenEmptyClaimsThenOmitted() {
-		OidcProviderConfiguration providerConfiguration = this.minimalConfigurationBuilder
-				.claim("some-claim", Collections.emptyList())
-				.claims(claims -> claims.put(OidcProviderMetadataClaimNames.GRANT_TYPES_SUPPORTED, Collections.emptyList()))
-				.build();
-
-		assertThat(providerConfiguration.getClaimAsStringList("some-claim")).isNull();
-		assertThat(providerConfiguration.getClaimAsStringList(OidcProviderMetadataClaimNames.GRANT_TYPES_SUPPORTED)).isNull();
-	}
-
 	@Test
 	public void buildWhenMissingIssuerThenThrowIllegalArgumentException() {
 		OidcProviderConfiguration.Builder builder = this.minimalConfigurationBuilder
@@ -505,9 +494,9 @@ public class OidcProviderConfigurationTests {
 	public void claimsWhenAddingClaimThenPresent() {
 		OidcProviderConfiguration configuration =
 				this.minimalConfigurationBuilder
-						.claims((claims) -> claims.put(OidcProviderMetadataClaimNames.GRANT_TYPES_SUPPORTED, "authorization_code"))
+						.claim("claim-name", "claim-value")
 						.build();
-		assertThat(configuration.getGrantTypes()).containsExactly("authorization_code");
+		assertThat(configuration.containsClaim("claim-name")).isTrue();
 	}
 
 	private static URL url(String urlString) {

+ 10 - 9
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/core/oidc/http/converter/OidcProviderConfigurationHttpMessageConverterTests.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2020 the original author or authors.
+ * Copyright 2020-2021 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -15,7 +15,12 @@
  */
 package org.springframework.security.oauth2.core.oidc.http.converter;
 
+import java.net.URL;
+import java.util.Arrays;
+import java.util.Map;
+
 import org.junit.Test;
+
 import org.springframework.core.convert.converter.Converter;
 import org.springframework.http.HttpStatus;
 import org.springframework.http.converter.HttpMessageNotReadableException;
@@ -24,10 +29,6 @@ import org.springframework.mock.http.MockHttpOutputMessage;
 import org.springframework.mock.http.client.MockClientHttpResponse;
 import org.springframework.security.oauth2.core.oidc.OidcProviderConfiguration;
 
-import java.net.URL;
-import java.util.Arrays;
-import java.util.Map;
-
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
 import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
@@ -75,7 +76,7 @@ public class OidcProviderConfigurationHttpMessageConverterTests {
 		assertThat(providerConfiguration.getIssuer()).isEqualTo(new URL("https://example.com/issuer1"));
 		assertThat(providerConfiguration.getAuthorizationEndpoint()).isEqualTo(new URL("https://example.com/issuer1/oauth2/authorize"));
 		assertThat(providerConfiguration.getTokenEndpoint()).isEqualTo(new URL("https://example.com/issuer1/oauth2/token"));
-		assertThat(providerConfiguration.getJwkSetUri()).isEqualTo(new URL("https://example.com/issuer1/oauth2/jwks"));
+		assertThat(providerConfiguration.getJwkSetUrl()).isEqualTo(new URL("https://example.com/issuer1/oauth2/jwks"));
 		assertThat(providerConfiguration.getResponseTypes()).containsExactly("code");
 		assertThat(providerConfiguration.getSubjectTypes()).containsExactly("public");
 		assertThat(providerConfiguration.getIdTokenSigningAlgorithms()).containsExactly("RS256");
@@ -109,7 +110,7 @@ public class OidcProviderConfigurationHttpMessageConverterTests {
 		assertThat(providerConfiguration.getIssuer()).isEqualTo(new URL("https://example.com/issuer1"));
 		assertThat(providerConfiguration.getAuthorizationEndpoint()).isEqualTo(new URL("https://example.com/issuer1/oauth2/authorize"));
 		assertThat(providerConfiguration.getTokenEndpoint()).isEqualTo(new URL("https://example.com/issuer1/oauth2/token"));
-		assertThat(providerConfiguration.getJwkSetUri()).isEqualTo(new URL("https://example.com/issuer1/oauth2/jwks"));
+		assertThat(providerConfiguration.getJwkSetUrl()).isEqualTo(new URL("https://example.com/issuer1/oauth2/jwks"));
 		assertThat(providerConfiguration.getScopes()).containsExactly("openid");
 		assertThat(providerConfiguration.getResponseTypes()).containsExactly("code");
 		assertThat(providerConfiguration.getGrantTypes()).containsExactlyInAnyOrder("authorization_code", "client_credentials");
@@ -152,7 +153,7 @@ public class OidcProviderConfigurationHttpMessageConverterTests {
 						.issuer("https://example.com/issuer1")
 						.authorizationEndpoint("https://example.com/issuer1/oauth2/authorize")
 						.tokenEndpoint("https://example.com/issuer1/oauth2/token")
-						.jwkSetUri("https://example.com/issuer1/oauth2/jwks")
+						.jwkSetUrl("https://example.com/issuer1/oauth2/jwks")
 						.scope("openid")
 						.responseType("code")
 						.grantType("authorization_code")
@@ -196,7 +197,7 @@ public class OidcProviderConfigurationHttpMessageConverterTests {
 						.issuer("https://example.com/issuer1")
 						.authorizationEndpoint("https://example.com/issuer1/oauth2/authorize")
 						.tokenEndpoint("https://example.com/issuer1/oauth2/token")
-						.jwkSetUri("https://example.com/issuer1/oauth2/jwks")
+						.jwkSetUrl("https://example.com/issuer1/oauth2/jwks")
 						.responseType("code")
 						.subjectType("public")
 						.idTokenSigningAlgorithm("RS256")

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

@@ -18,7 +18,7 @@ package org.springframework.security.oauth2.server.authorization.config;
 import org.junit.Test;
 
 import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
 /**
  * Tests for {@link ProviderSettings}.
@@ -78,48 +78,48 @@ public class ProviderSettingsTests {
 	@Test
 	public void issuerWhenNullThenThrowIllegalArgumentException() {
 		ProviderSettings settings = new ProviderSettings();
-		assertThatIllegalArgumentException()
-				.isThrownBy(() -> settings.issuer(null))
-				.withMessage("value cannot be null");
+		assertThatThrownBy(() -> settings.issuer(null))
+				.isInstanceOf(IllegalArgumentException.class)
+				.hasMessage("value cannot be null");
 	}
 
 	@Test
 	public void authorizationEndpointWhenNullThenThrowIllegalArgumentException() {
 		ProviderSettings settings = new ProviderSettings();
-		assertThatIllegalArgumentException()
-				.isThrownBy(() -> settings.authorizationEndpoint(null))
-				.withMessage("value cannot be null");
+		assertThatThrownBy(() -> settings.authorizationEndpoint(null))
+				.isInstanceOf(IllegalArgumentException.class)
+				.hasMessage("value cannot be null");
 	}
 
 	@Test
 	public void tokenEndpointWhenNullThenThrowIllegalArgumentException() {
 		ProviderSettings settings = new ProviderSettings();
-		assertThatIllegalArgumentException()
-				.isThrownBy(() -> settings.tokenEndpoint(null))
-				.withMessage("value cannot be null");
+		assertThatThrownBy(() -> settings.tokenEndpoint(null))
+				.isInstanceOf(IllegalArgumentException.class)
+				.hasMessage("value cannot be null");
 	}
 
 	@Test
 	public void tokenRevocationEndpointWhenNullThenThrowIllegalArgumentException() {
 		ProviderSettings settings = new ProviderSettings();
-		assertThatIllegalArgumentException()
-				.isThrownBy(() -> settings.tokenRevocationEndpoint(null))
-				.withMessage("value cannot be null");
+		assertThatThrownBy(() -> settings.tokenRevocationEndpoint(null))
+				.isInstanceOf(IllegalArgumentException.class)
+				.hasMessage("value cannot be null");
 	}
 
 	@Test
 	public void tokenIntrospectionEndpointWhenNullThenThrowIllegalArgumentException() {
 		ProviderSettings settings = new ProviderSettings();
-		assertThatIllegalArgumentException()
-				.isThrownBy(() -> settings.tokenIntrospectionEndpoint(null))
-				.withMessage("value cannot be null");
+		assertThatThrownBy(() -> settings.tokenIntrospectionEndpoint(null))
+				.isInstanceOf(IllegalArgumentException.class)
+				.hasMessage("value cannot be null");
 	}
 
 	@Test
 	public void jwksEndpointWhenNullThenThrowIllegalArgumentException() {
 		ProviderSettings settings = new ProviderSettings();
-		assertThatIllegalArgumentException()
-				.isThrownBy(() -> settings.jwkSetEndpoint(null))
-				.withMessage("value cannot be null");
+		assertThatThrownBy(() -> settings.jwkSetEndpoint(null))
+				.isInstanceOf(IllegalArgumentException.class)
+				.hasMessage("value cannot be null");
 	}
 }

+ 1 - 1
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/web/OidcProviderConfigurationEndpointFilterTests.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2020 the original author or authors.
+ * Copyright 2020-2021 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.

+ 41 - 36
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationServerConfigurationEndpointFilterTests.java → oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationServerMetadataEndpointFilterTests.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2020 the original author or authors.
+ * Copyright 2020-2021 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -16,16 +16,17 @@
 package org.springframework.security.oauth2.server.authorization.web;
 
 
+import javax.servlet.FilterChain;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
 import org.junit.Test;
+
 import org.springframework.http.MediaType;
 import org.springframework.mock.web.MockHttpServletRequest;
 import org.springframework.mock.web.MockHttpServletResponse;
 import org.springframework.security.oauth2.server.authorization.config.ProviderSettings;
 
-import javax.servlet.FilterChain;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
 import static org.mockito.ArgumentMatchers.any;
@@ -34,23 +35,23 @@ import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyNoInteractions;
 
 /**
- * Tests for {@link OAuth2AuthorizationServerConfigurationEndpointFilter}.
+ * Tests for {@link OAuth2AuthorizationServerMetadataEndpointFilter}.
  *
  * @author Daniel Garnier-Moiroux
  */
-public class OAuth2AuthorizationServerConfigurationEndpointFilterTests {
+public class OAuth2AuthorizationServerMetadataEndpointFilterTests {
 
 	@Test
 	public void constructorWhenProviderSettingsNullThenThrowIllegalArgumentException() {
 		assertThatIllegalArgumentException()
-				.isThrownBy(() -> new OAuth2AuthorizationServerConfigurationEndpointFilter(null))
+				.isThrownBy(() -> new OAuth2AuthorizationServerMetadataEndpointFilter(null))
 				.withMessage("providerSettings cannot be null");
 	}
 
 	@Test
-	public void doFilterWhenNotAuthorizationServerConfigurationRequestThenNotProcessed() throws Exception {
-		OAuth2AuthorizationServerConfigurationEndpointFilter filter =
-				new OAuth2AuthorizationServerConfigurationEndpointFilter(new ProviderSettings().issuer("https://example.com"));
+	public void doFilterWhenNotAuthorizationServerMetadataRequestThenNotProcessed() throws Exception {
+		OAuth2AuthorizationServerMetadataEndpointFilter filter =
+				new OAuth2AuthorizationServerMetadataEndpointFilter(new ProviderSettings().issuer("https://example.com"));
 
 		String requestUri = "/path";
 		MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
@@ -64,11 +65,11 @@ public class OAuth2AuthorizationServerConfigurationEndpointFilterTests {
 	}
 
 	@Test
-	public void doFilterWhenAuthorizationServerConfigurationRequestPostThenNotProcessed() throws Exception {
-		OAuth2AuthorizationServerConfigurationEndpointFilter filter =
-				new OAuth2AuthorizationServerConfigurationEndpointFilter(new ProviderSettings().issuer("https://example.com"));
+	public void doFilterWhenAuthorizationServerMetadataRequestPostThenNotProcessed() throws Exception {
+		OAuth2AuthorizationServerMetadataEndpointFilter filter =
+				new OAuth2AuthorizationServerMetadataEndpointFilter(new ProviderSettings().issuer("https://example.com"));
 
-		String requestUri = OAuth2AuthorizationServerConfigurationEndpointFilter.DEFAULT_OAUTH2_AUTHORIZATION_SERVER_CONFIGURATION_ENDPOINT_URI;
+		String requestUri = OAuth2AuthorizationServerMetadataEndpointFilter.DEFAULT_OAUTH2_AUTHORIZATION_SERVER_METADATA_ENDPOINT_URI;
 		MockHttpServletRequest request = new MockHttpServletRequest("POST", requestUri);
 		request.setServletPath(requestUri);
 		MockHttpServletResponse response = new MockHttpServletResponse();
@@ -80,22 +81,24 @@ public class OAuth2AuthorizationServerConfigurationEndpointFilterTests {
 	}
 
 	@Test
-	public void doFilterWhenAuthorizationServerConfigurationRequestThenAuthorizationServerConfigurationResponse() throws Exception {
+	public void doFilterWhenAuthorizationServerMetadataRequestThenMetadataResponse() throws Exception {
 		String authorizationEndpoint = "/oauth2/v1/authorize";
 		String tokenEndpoint = "/oauth2/v1/token";
-		String tokenRevocationEndpoint = "/oauth2/v1/revoke";
 		String jwkSetEndpoint = "/oauth2/v1/jwks";
+		String tokenRevocationEndpoint = "/oauth2/v1/revoke";
+		String tokenIntrospectionEndpoint = "/oauth2/v1/introspect";
 
 		ProviderSettings providerSettings = new ProviderSettings()
 				.issuer("https://example.com/issuer1")
 				.authorizationEndpoint(authorizationEndpoint)
 				.tokenEndpoint(tokenEndpoint)
+				.jwkSetEndpoint(jwkSetEndpoint)
 				.tokenRevocationEndpoint(tokenRevocationEndpoint)
-				.jwkSetEndpoint(jwkSetEndpoint);
-		OAuth2AuthorizationServerConfigurationEndpointFilter filter =
-				new OAuth2AuthorizationServerConfigurationEndpointFilter(providerSettings);
+				.tokenIntrospectionEndpoint(tokenIntrospectionEndpoint);
+		OAuth2AuthorizationServerMetadataEndpointFilter filter =
+				new OAuth2AuthorizationServerMetadataEndpointFilter(providerSettings);
 
-		String requestUri = OAuth2AuthorizationServerConfigurationEndpointFilter.DEFAULT_OAUTH2_AUTHORIZATION_SERVER_CONFIGURATION_ENDPOINT_URI;
+		String requestUri = OAuth2AuthorizationServerMetadataEndpointFilter.DEFAULT_OAUTH2_AUTHORIZATION_SERVER_METADATA_ENDPOINT_URI;
 		MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
 		request.setServletPath(requestUri);
 		MockHttpServletResponse response = new MockHttpServletResponse();
@@ -106,28 +109,29 @@ public class OAuth2AuthorizationServerConfigurationEndpointFilterTests {
 		verifyNoInteractions(filterChain);
 
 		assertThat(response.getContentType()).isEqualTo(MediaType.APPLICATION_JSON_VALUE);
-		String serverConfigurationResponse = response.getContentAsString();
-		assertThat(serverConfigurationResponse).contains("\"issuer\":\"https://example.com/issuer1\"");
-		assertThat(serverConfigurationResponse).contains("\"authorization_endpoint\":\"https://example.com/issuer1/oauth2/v1/authorize\"");
-		assertThat(serverConfigurationResponse).contains("\"token_endpoint\":\"https://example.com/issuer1/oauth2/v1/token\"");
-		assertThat(serverConfigurationResponse).contains("\"revocation_endpoint\":\"https://example.com/issuer1/oauth2/v1/revoke\"");
-		assertThat(serverConfigurationResponse).contains("\"jwks_uri\":\"https://example.com/issuer1/oauth2/v1/jwks\"");
-		assertThat(serverConfigurationResponse).contains("\"scopes_supported\":[\"openid\"]");
-		assertThat(serverConfigurationResponse).contains("\"response_types_supported\":[\"code\"]");
-		assertThat(serverConfigurationResponse).contains("\"grant_types_supported\":[\"authorization_code\",\"client_credentials\",\"refresh_token\"]");
-		assertThat(serverConfigurationResponse).contains("\"token_endpoint_auth_methods_supported\":[\"client_secret_basic\",\"client_secret_post\"]");
-		assertThat(serverConfigurationResponse).contains("\"revocation_endpoint_auth_methods_supported\":[\"client_secret_basic\",\"client_secret_post\"]");
-		assertThat(serverConfigurationResponse).contains("\"code_challenge_methods_supported\":[\"plain\",\"S256\"]");
+		String authorizationServerMetadataResponse = response.getContentAsString();
+		assertThat(authorizationServerMetadataResponse).contains("\"issuer\":\"https://example.com/issuer1\"");
+		assertThat(authorizationServerMetadataResponse).contains("\"authorization_endpoint\":\"https://example.com/issuer1/oauth2/v1/authorize\"");
+		assertThat(authorizationServerMetadataResponse).contains("\"token_endpoint\":\"https://example.com/issuer1/oauth2/v1/token\"");
+		assertThat(authorizationServerMetadataResponse).contains("\"token_endpoint_auth_methods_supported\":[\"client_secret_basic\",\"client_secret_post\"]");
+		assertThat(authorizationServerMetadataResponse).contains("\"jwks_uri\":\"https://example.com/issuer1/oauth2/v1/jwks\"");
+		assertThat(authorizationServerMetadataResponse).contains("\"response_types_supported\":[\"code\"]");
+		assertThat(authorizationServerMetadataResponse).contains("\"grant_types_supported\":[\"authorization_code\",\"client_credentials\",\"refresh_token\"]");
+		assertThat(authorizationServerMetadataResponse).contains("\"revocation_endpoint\":\"https://example.com/issuer1/oauth2/v1/revoke\"");
+		assertThat(authorizationServerMetadataResponse).contains("\"revocation_endpoint_auth_methods_supported\":[\"client_secret_basic\",\"client_secret_post\"]");
+		assertThat(authorizationServerMetadataResponse).contains("\"introspection_endpoint\":\"https://example.com/issuer1/oauth2/v1/introspect\"");
+		assertThat(authorizationServerMetadataResponse).contains("\"introspection_endpoint_auth_methods_supported\":[\"client_secret_basic\",\"client_secret_post\"]");
+		assertThat(authorizationServerMetadataResponse).contains("\"code_challenge_methods_supported\":[\"plain\",\"S256\"]");
 	}
 
 	@Test
 	public void doFilterWhenProviderSettingsWithInvalidIssuerThenThrowIllegalArgumentException() {
 		ProviderSettings providerSettings = new ProviderSettings()
 				.issuer("https://this is an invalid URL");
-		OAuth2AuthorizationServerConfigurationEndpointFilter filter =
-				new OAuth2AuthorizationServerConfigurationEndpointFilter(providerSettings);
+		OAuth2AuthorizationServerMetadataEndpointFilter filter =
+				new OAuth2AuthorizationServerMetadataEndpointFilter(providerSettings);
 
-		String requestUri = OAuth2AuthorizationServerConfigurationEndpointFilter.DEFAULT_OAUTH2_AUTHORIZATION_SERVER_CONFIGURATION_ENDPOINT_URI;
+		String requestUri = OAuth2AuthorizationServerMetadataEndpointFilter.DEFAULT_OAUTH2_AUTHORIZATION_SERVER_METADATA_ENDPOINT_URI;
 		MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
 		request.setServletPath(requestUri);
 		MockHttpServletResponse response = new MockHttpServletResponse();
@@ -138,4 +142,5 @@ public class OAuth2AuthorizationServerConfigurationEndpointFilterTests {
 				.isThrownBy(() -> filter.doFilter(request, response, filterChain))
 				.withMessage("issuer must be a valid URL");
 	}
+
 }