ソースを参照

Allow customizing Authorization Server Metadata Response

Closes gh-878
Joe Grandja 3 年 前
コミット
8043b8c949

+ 24 - 3
docs/src/docs/asciidoc/protocol-endpoints.adoc

@@ -179,10 +179,31 @@ public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity h
 [[oauth2-authorization-server-metadata-endpoint]]
 [[oauth2-authorization-server-metadata-endpoint]]
 == OAuth2 Authorization Server Metadata Endpoint
 == OAuth2 Authorization Server Metadata Endpoint
 
 
-`OAuth2AuthorizationServerConfigurer` provides support for the https://datatracker.ietf.org/doc/html/rfc8414#section-3[OAuth2 Authorization Server Metadata endpoint].
+`OAuth2AuthorizationServerMetadataEndpointConfigurer` provides the ability to customize the https://datatracker.ietf.org/doc/html/rfc8414#section-3[OAuth2 Authorization Server Metadata endpoint].
+It defines an extension point that lets you customize the https://datatracker.ietf.org/doc/html/rfc8414#section-3.2[OAuth2 Authorization Server Metadata response].
 
 
-`OAuth2AuthorizationServerConfigurer` configures the `OAuth2AuthorizationServerMetadataEndpointFilter` and registers it with the OAuth2 authorization server `SecurityFilterChain` `@Bean`.
-`OAuth2AuthorizationServerMetadataEndpointFilter` is the `Filter` that processes https://datatracker.ietf.org/doc/html/rfc8414#section-3.1[OAuth2 authorization server metadata requests] and returns the https://datatracker.ietf.org/doc/html/rfc8414#section-3.2[OAuth2AuthorizationServerMetadata response].
+`OAuth2AuthorizationServerMetadataEndpointConfigurer` provides the following configuration option:
+
+[source,java]
+----
+@Bean
+public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
+	OAuth2AuthorizationServerConfigurer authorizationServerConfigurer =
+		new OAuth2AuthorizationServerConfigurer();
+	http.apply(authorizationServerConfigurer);
+
+	authorizationServerConfigurer
+		.authorizationServerMetadataEndpoint(authorizationServerMetadataEndpoint ->
+			authorizationServerMetadataEndpoint
+				.authorizationServerMetadataCustomizer(authorizationServerMetadataCustomizer));   <1>
+
+	return http.build();
+}
+----
+<1> `authorizationServerMetadataCustomizer()`: The `Consumer` providing access to the `OAuth2AuthorizationServerMetadata.Builder` allowing the ability to customize the claims of the Authorization Server's configuration.
+
+`OAuth2AuthorizationServerMetadataEndpointConfigurer` configures the `OAuth2AuthorizationServerMetadataEndpointFilter` and registers it with the OAuth2 authorization server `SecurityFilterChain` `@Bean`.
+`OAuth2AuthorizationServerMetadataEndpointFilter` is the `Filter` that returns the https://datatracker.ietf.org/doc/html/rfc8414#section-3.2[OAuth2AuthorizationServerMetadata response].
 
 
 [[jwk-set-endpoint]]
 [[jwk-set-endpoint]]
 == JWK Set Endpoint
 == JWK Set Endpoint

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

@@ -34,7 +34,6 @@ import org.springframework.security.oauth2.server.authorization.client.Registere
 import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
 import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
 import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
 import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
 import org.springframework.security.oauth2.server.authorization.web.NimbusJwkSetEndpointFilter;
 import org.springframework.security.oauth2.server.authorization.web.NimbusJwkSetEndpointFilter;
-import org.springframework.security.oauth2.server.authorization.web.OAuth2AuthorizationServerMetadataEndpointFilter;
 import org.springframework.security.web.authentication.HttpStatusEntryPoint;
 import org.springframework.security.web.authentication.HttpStatusEntryPoint;
 import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;
 import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;
 import org.springframework.security.web.context.SecurityContextHolderFilter;
 import org.springframework.security.web.context.SecurityContextHolderFilter;
@@ -54,6 +53,7 @@ import org.springframework.util.Assert;
  * @since 0.0.1
  * @since 0.0.1
  * @see AbstractHttpConfigurer
  * @see AbstractHttpConfigurer
  * @see OAuth2ClientAuthenticationConfigurer
  * @see OAuth2ClientAuthenticationConfigurer
+ * @see OAuth2AuthorizationServerMetadataEndpointConfigurer
  * @see OAuth2AuthorizationEndpointConfigurer
  * @see OAuth2AuthorizationEndpointConfigurer
  * @see OAuth2TokenEndpointConfigurer
  * @see OAuth2TokenEndpointConfigurer
  * @see OAuth2TokenIntrospectionEndpointConfigurer
  * @see OAuth2TokenIntrospectionEndpointConfigurer
@@ -63,22 +63,20 @@ import org.springframework.util.Assert;
  * @see OAuth2AuthorizationService
  * @see OAuth2AuthorizationService
  * @see OAuth2AuthorizationConsentService
  * @see OAuth2AuthorizationConsentService
  * @see NimbusJwkSetEndpointFilter
  * @see NimbusJwkSetEndpointFilter
- * @see OAuth2AuthorizationServerMetadataEndpointFilter
  */
  */
 public final class OAuth2AuthorizationServerConfigurer
 public final class OAuth2AuthorizationServerConfigurer
 		extends AbstractHttpConfigurer<OAuth2AuthorizationServerConfigurer, HttpSecurity> {
 		extends AbstractHttpConfigurer<OAuth2AuthorizationServerConfigurer, HttpSecurity> {
 
 
 	private final Map<Class<? extends AbstractOAuth2Configurer>, AbstractOAuth2Configurer> configurers = createConfigurers();
 	private final Map<Class<? extends AbstractOAuth2Configurer>, AbstractOAuth2Configurer> configurers = createConfigurers();
-	private RequestMatcher jwkSetEndpointMatcher;
-	private RequestMatcher authorizationServerMetadataEndpointMatcher;
 	private final RequestMatcher endpointsMatcher = (request) ->
 	private final RequestMatcher endpointsMatcher = (request) ->
-			getRequestMatcher(OAuth2AuthorizationEndpointConfigurer.class).matches(request) ||
-			getRequestMatcher(OAuth2TokenEndpointConfigurer.class).matches(request) ||
-			getRequestMatcher(OAuth2TokenIntrospectionEndpointConfigurer.class).matches(request) ||
-			getRequestMatcher(OAuth2TokenRevocationEndpointConfigurer.class).matches(request) ||
-			getRequestMatcher(OidcConfigurer.class).matches(request) ||
-			this.jwkSetEndpointMatcher.matches(request) ||
-			this.authorizationServerMetadataEndpointMatcher.matches(request);
+					getRequestMatcher(OAuth2AuthorizationServerMetadataEndpointConfigurer.class).matches(request) ||
+					getRequestMatcher(OAuth2AuthorizationEndpointConfigurer.class).matches(request) ||
+					getRequestMatcher(OAuth2TokenEndpointConfigurer.class).matches(request) ||
+					getRequestMatcher(OAuth2TokenIntrospectionEndpointConfigurer.class).matches(request) ||
+					getRequestMatcher(OAuth2TokenRevocationEndpointConfigurer.class).matches(request) ||
+					getRequestMatcher(OidcConfigurer.class).matches(request) ||
+					this.jwkSetEndpointMatcher.matches(request);
+	private RequestMatcher jwkSetEndpointMatcher;
 
 
 	/**
 	/**
 	 * Sets the repository of registered clients.
 	 * Sets the repository of registered clients.
@@ -152,6 +150,18 @@ public final class OAuth2AuthorizationServerConfigurer
 		return this;
 		return this;
 	}
 	}
 
 
+	/**
+	 * Configures the OAuth 2.0 Authorization Server Metadata Endpoint.
+	 *
+	 * @param authorizationServerMetadataEndpointCustomizer the {@link Customizer} providing access to the {@link OAuth2AuthorizationServerMetadataEndpointConfigurer}
+	 * @return the {@link OAuth2AuthorizationServerConfigurer} for further configuration
+	 * @since 0.4.0
+	 */
+	public OAuth2AuthorizationServerConfigurer authorizationServerMetadataEndpoint(Customizer<OAuth2AuthorizationServerMetadataEndpointConfigurer> authorizationServerMetadataEndpointCustomizer) {
+		authorizationServerMetadataEndpointCustomizer.customize(getConfigurer(OAuth2AuthorizationServerMetadataEndpointConfigurer.class));
+		return this;
+	}
+
 	/**
 	/**
 	 * Configures the OAuth 2.0 Authorization Endpoint.
 	 * Configures the OAuth 2.0 Authorization Endpoint.
 	 *
 	 *
@@ -222,7 +232,9 @@ public final class OAuth2AuthorizationServerConfigurer
 	public void init(HttpSecurity httpSecurity) {
 	public void init(HttpSecurity httpSecurity) {
 		AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils.getAuthorizationServerSettings(httpSecurity);
 		AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils.getAuthorizationServerSettings(httpSecurity);
 		validateAuthorizationServerSettings(authorizationServerSettings);
 		validateAuthorizationServerSettings(authorizationServerSettings);
-		initEndpointMatchers(authorizationServerSettings);
+
+		this.jwkSetEndpointMatcher = new AntPathRequestMatcher(
+				authorizationServerSettings.getJwkSetEndpoint(), HttpMethod.GET.name());
 
 
 		this.configurers.values().forEach(configurer -> configurer.init(httpSecurity));
 		this.configurers.values().forEach(configurer -> configurer.init(httpSecurity));
 
 
@@ -253,15 +265,12 @@ public final class OAuth2AuthorizationServerConfigurer
 					jwkSource, authorizationServerSettings.getJwkSetEndpoint());
 					jwkSource, authorizationServerSettings.getJwkSetEndpoint());
 			httpSecurity.addFilterBefore(postProcess(jwkSetEndpointFilter), AbstractPreAuthenticatedProcessingFilter.class);
 			httpSecurity.addFilterBefore(postProcess(jwkSetEndpointFilter), AbstractPreAuthenticatedProcessingFilter.class);
 		}
 		}
-
-		OAuth2AuthorizationServerMetadataEndpointFilter authorizationServerMetadataEndpointFilter =
-				new OAuth2AuthorizationServerMetadataEndpointFilter();
-		httpSecurity.addFilterBefore(postProcess(authorizationServerMetadataEndpointFilter), AbstractPreAuthenticatedProcessingFilter.class);
 	}
 	}
 
 
 	private Map<Class<? extends AbstractOAuth2Configurer>, AbstractOAuth2Configurer> createConfigurers() {
 	private Map<Class<? extends AbstractOAuth2Configurer>, AbstractOAuth2Configurer> createConfigurers() {
 		Map<Class<? extends AbstractOAuth2Configurer>, AbstractOAuth2Configurer> configurers = new LinkedHashMap<>();
 		Map<Class<? extends AbstractOAuth2Configurer>, AbstractOAuth2Configurer> configurers = new LinkedHashMap<>();
 		configurers.put(OAuth2ClientAuthenticationConfigurer.class, new OAuth2ClientAuthenticationConfigurer(this::postProcess));
 		configurers.put(OAuth2ClientAuthenticationConfigurer.class, new OAuth2ClientAuthenticationConfigurer(this::postProcess));
+		configurers.put(OAuth2AuthorizationServerMetadataEndpointConfigurer.class, new OAuth2AuthorizationServerMetadataEndpointConfigurer(this::postProcess));
 		configurers.put(OAuth2AuthorizationEndpointConfigurer.class, new OAuth2AuthorizationEndpointConfigurer(this::postProcess));
 		configurers.put(OAuth2AuthorizationEndpointConfigurer.class, new OAuth2AuthorizationEndpointConfigurer(this::postProcess));
 		configurers.put(OAuth2TokenEndpointConfigurer.class, new OAuth2TokenEndpointConfigurer(this::postProcess));
 		configurers.put(OAuth2TokenEndpointConfigurer.class, new OAuth2TokenEndpointConfigurer(this::postProcess));
 		configurers.put(OAuth2TokenIntrospectionEndpointConfigurer.class, new OAuth2TokenIntrospectionEndpointConfigurer(this::postProcess));
 		configurers.put(OAuth2TokenIntrospectionEndpointConfigurer.class, new OAuth2TokenIntrospectionEndpointConfigurer(this::postProcess));
@@ -279,13 +288,6 @@ public final class OAuth2AuthorizationServerConfigurer
 		return getConfigurer(configurerType).getRequestMatcher();
 		return getConfigurer(configurerType).getRequestMatcher();
 	}
 	}
 
 
-	private void initEndpointMatchers(AuthorizationServerSettings authorizationServerSettings) {
-		this.jwkSetEndpointMatcher = new AntPathRequestMatcher(
-				authorizationServerSettings.getJwkSetEndpoint(), HttpMethod.GET.name());
-		this.authorizationServerMetadataEndpointMatcher = new AntPathRequestMatcher(
-				"/.well-known/oauth-authorization-server", HttpMethod.GET.name());
-	}
-
 	private static void validateAuthorizationServerSettings(AuthorizationServerSettings authorizationServerSettings) {
 	private static void validateAuthorizationServerSettings(AuthorizationServerSettings authorizationServerSettings) {
 		if (authorizationServerSettings.getIssuer() != null) {
 		if (authorizationServerSettings.getIssuer() != null) {
 			URI issuerUri;
 			URI issuerUri;

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

@@ -0,0 +1,108 @@
+/*
+ * Copyright 2020-2022 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers;
+
+import java.util.function.Consumer;
+
+import org.springframework.http.HttpMethod;
+import org.springframework.security.config.annotation.ObjectPostProcessor;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationServerMetadata;
+import org.springframework.security.oauth2.server.authorization.web.OAuth2AuthorizationServerMetadataEndpointFilter;
+import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;
+import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
+import org.springframework.security.web.util.matcher.RequestMatcher;
+
+/**
+ * Configurer for the OAuth 2.0 Authorization Server Metadata Endpoint.
+ *
+ * @author Joe Grandja
+ * @since 0.4.0
+ * @see OAuth2AuthorizationServerConfigurer#authorizationServerMetadataEndpoint
+ * @see OAuth2AuthorizationServerMetadataEndpointFilter
+ */
+public final class OAuth2AuthorizationServerMetadataEndpointConfigurer extends AbstractOAuth2Configurer {
+	private RequestMatcher requestMatcher;
+	private Consumer<OAuth2AuthorizationServerMetadata.Builder> authorizationServerMetadataCustomizer;
+	private Consumer<OAuth2AuthorizationServerMetadata.Builder> defaultAuthorizationServerMetadataCustomizer;
+
+	/**
+	 * Restrict for internal use only.
+	 */
+	OAuth2AuthorizationServerMetadataEndpointConfigurer(ObjectPostProcessor<Object> objectPostProcessor) {
+		super(objectPostProcessor);
+	}
+
+	/**
+     * Sets the {@code Consumer} providing access to the {@link OAuth2AuthorizationServerMetadata.Builder}
+	 * allowing the ability to customize the claims of the Authorization Server's configuration.
+	 *
+	 * @param authorizationServerMetadataCustomizer the {@code Consumer} providing access to the {@link OAuth2AuthorizationServerMetadata.Builder}
+     * @return the {@link OAuth2AuthorizationServerMetadataEndpointConfigurer} for further configuration
+	 */
+	public OAuth2AuthorizationServerMetadataEndpointConfigurer authorizationServerMetadataCustomizer(
+			Consumer<OAuth2AuthorizationServerMetadata.Builder> authorizationServerMetadataCustomizer) {
+		this.authorizationServerMetadataCustomizer = authorizationServerMetadataCustomizer;
+		return this;
+	}
+
+	void addDefaultAuthorizationServerMetadataCustomizer(
+			Consumer<OAuth2AuthorizationServerMetadata.Builder> defaultAuthorizationServerMetadataCustomizer) {
+		this.defaultAuthorizationServerMetadataCustomizer =
+				this.defaultAuthorizationServerMetadataCustomizer == null ?
+						defaultAuthorizationServerMetadataCustomizer :
+						this.defaultAuthorizationServerMetadataCustomizer.andThen(defaultAuthorizationServerMetadataCustomizer);
+	}
+
+	@Override
+	void init(HttpSecurity httpSecurity) {
+		this.requestMatcher = new AntPathRequestMatcher(
+				"/.well-known/oauth-authorization-server", HttpMethod.GET.name());
+	}
+
+	@Override
+	void configure(HttpSecurity httpSecurity) {
+		OAuth2AuthorizationServerMetadataEndpointFilter authorizationServerMetadataEndpointFilter =
+				new OAuth2AuthorizationServerMetadataEndpointFilter();
+		Consumer<OAuth2AuthorizationServerMetadata.Builder> authorizationServerMetadataCustomizer = getAuthorizationServerMetadataCustomizer();
+		if (authorizationServerMetadataCustomizer != null) {
+			authorizationServerMetadataEndpointFilter.setAuthorizationServerMetadataCustomizer(authorizationServerMetadataCustomizer);
+		}
+		httpSecurity.addFilterBefore(postProcess(authorizationServerMetadataEndpointFilter), AbstractPreAuthenticatedProcessingFilter.class);
+	}
+
+	private Consumer<OAuth2AuthorizationServerMetadata.Builder> getAuthorizationServerMetadataCustomizer() {
+		Consumer<OAuth2AuthorizationServerMetadata.Builder> authorizationServerMetadataCustomizer = null;
+		if (this.defaultAuthorizationServerMetadataCustomizer != null || this.authorizationServerMetadataCustomizer != null) {
+			if (this.defaultAuthorizationServerMetadataCustomizer != null) {
+				authorizationServerMetadataCustomizer = this.defaultAuthorizationServerMetadataCustomizer;
+			}
+			if (this.authorizationServerMetadataCustomizer != null) {
+				authorizationServerMetadataCustomizer =
+						authorizationServerMetadataCustomizer == null ?
+								this.authorizationServerMetadataCustomizer :
+								authorizationServerMetadataCustomizer.andThen(this.authorizationServerMetadataCustomizer);
+			}
+		}
+		return authorizationServerMetadataCustomizer;
+	}
+
+	@Override
+	RequestMatcher getRequestMatcher() {
+		return this.requestMatcher;
+	}
+
+}

+ 20 - 4
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationServerMetadataEndpointFilter.java

@@ -37,6 +37,7 @@ import org.springframework.security.oauth2.server.authorization.http.converter.O
 import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
 import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
 import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
 import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
 import org.springframework.security.web.util.matcher.RequestMatcher;
 import org.springframework.security.web.util.matcher.RequestMatcher;
+import org.springframework.util.Assert;
 import org.springframework.web.filter.OncePerRequestFilter;
 import org.springframework.web.filter.OncePerRequestFilter;
 import org.springframework.web.util.UriComponentsBuilder;
 import org.springframework.web.util.UriComponentsBuilder;
 
 
@@ -44,6 +45,7 @@ import org.springframework.web.util.UriComponentsBuilder;
  * A {@code Filter} that processes OAuth 2.0 Authorization Server Metadata Requests.
  * A {@code Filter} that processes OAuth 2.0 Authorization Server Metadata Requests.
  *
  *
  * @author Daniel Garnier-Moiroux
  * @author Daniel Garnier-Moiroux
+ * @author Joe Grandja
  * @since 0.1.1
  * @since 0.1.1
  * @see OAuth2AuthorizationServerMetadata
  * @see OAuth2AuthorizationServerMetadata
  * @see AuthorizationServerSettings
  * @see AuthorizationServerSettings
@@ -60,6 +62,19 @@ public final class OAuth2AuthorizationServerMetadataEndpointFilter extends OnceP
 			HttpMethod.GET.name());
 			HttpMethod.GET.name());
 	private final OAuth2AuthorizationServerMetadataHttpMessageConverter authorizationServerMetadataHttpMessageConverter =
 	private final OAuth2AuthorizationServerMetadataHttpMessageConverter authorizationServerMetadataHttpMessageConverter =
 			new OAuth2AuthorizationServerMetadataHttpMessageConverter();
 			new OAuth2AuthorizationServerMetadataHttpMessageConverter();
+	private Consumer<OAuth2AuthorizationServerMetadata.Builder> authorizationServerMetadataCustomizer = (authorizationServerMetadata) -> {};
+
+	/**
+	 * Sets the {@code Consumer} providing access to the {@link OAuth2AuthorizationServerMetadata.Builder}
+	 * allowing the ability to customize the claims of the Authorization Server's configuration.
+	 *
+	 * @param authorizationServerMetadataCustomizer the {@code Consumer} providing access to the {@link OAuth2AuthorizationServerMetadata.Builder}
+	 * @since 0.4.0
+	 */
+	public void setAuthorizationServerMetadataCustomizer(Consumer<OAuth2AuthorizationServerMetadata.Builder> authorizationServerMetadataCustomizer) {
+		Assert.notNull(authorizationServerMetadataCustomizer, "authorizationServerMetadataCustomizer cannot be null");
+		this.authorizationServerMetadataCustomizer = authorizationServerMetadataCustomizer;
+	}
 
 
 	@Override
 	@Override
 	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
 	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
@@ -74,7 +89,7 @@ public final class OAuth2AuthorizationServerMetadataEndpointFilter extends OnceP
 		String issuer = authorizationServerContext.getIssuer();
 		String issuer = authorizationServerContext.getIssuer();
 		AuthorizationServerSettings authorizationServerSettings = authorizationServerContext.getAuthorizationServerSettings();
 		AuthorizationServerSettings authorizationServerSettings = authorizationServerContext.getAuthorizationServerSettings();
 
 
-		OAuth2AuthorizationServerMetadata authorizationServerMetadata = OAuth2AuthorizationServerMetadata.builder()
+		OAuth2AuthorizationServerMetadata.Builder authorizationServerMetadata = OAuth2AuthorizationServerMetadata.builder()
 				.issuer(issuer)
 				.issuer(issuer)
 				.authorizationEndpoint(asUrl(issuer, authorizationServerSettings.getAuthorizationEndpoint()))
 				.authorizationEndpoint(asUrl(issuer, authorizationServerSettings.getAuthorizationEndpoint()))
 				.tokenEndpoint(asUrl(issuer, authorizationServerSettings.getTokenEndpoint()))
 				.tokenEndpoint(asUrl(issuer, authorizationServerSettings.getTokenEndpoint()))
@@ -88,12 +103,13 @@ public final class OAuth2AuthorizationServerMetadataEndpointFilter extends OnceP
 				.tokenRevocationEndpointAuthenticationMethods(clientAuthenticationMethods())
 				.tokenRevocationEndpointAuthenticationMethods(clientAuthenticationMethods())
 				.tokenIntrospectionEndpoint(asUrl(issuer, authorizationServerSettings.getTokenIntrospectionEndpoint()))
 				.tokenIntrospectionEndpoint(asUrl(issuer, authorizationServerSettings.getTokenIntrospectionEndpoint()))
 				.tokenIntrospectionEndpointAuthenticationMethods(clientAuthenticationMethods())
 				.tokenIntrospectionEndpointAuthenticationMethods(clientAuthenticationMethods())
-				.codeChallengeMethod("S256")
-				.build();
+				.codeChallengeMethod("S256");
+
+		this.authorizationServerMetadataCustomizer.accept(authorizationServerMetadata);
 
 
 		ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
 		ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
 		this.authorizationServerMetadataHttpMessageConverter.write(
 		this.authorizationServerMetadataHttpMessageConverter.write(
-				authorizationServerMetadata, MediaType.APPLICATION_JSON, httpResponse);
+				authorizationServerMetadata.build(), MediaType.APPLICATION_JSON, httpResponse);
 	}
 	}
 
 
 	private static Consumer<List<String>> clientAuthenticationMethods() {
 	private static Consumer<List<String>> clientAuthenticationMethods() {

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

@@ -15,6 +15,8 @@
  */
  */
 package org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers;
 package org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers;
 
 
+import java.util.function.Consumer;
+
 import com.nimbusds.jose.jwk.JWKSet;
 import com.nimbusds.jose.jwk.JWKSet;
 import com.nimbusds.jose.jwk.source.JWKSource;
 import com.nimbusds.jose.jwk.source.JWKSource;
 import com.nimbusds.jose.proc.SecurityContext;
 import com.nimbusds.jose.proc.SecurityContext;
@@ -26,14 +28,18 @@ import org.junit.Test;
 
 
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
 import org.springframework.context.annotation.Import;
 import org.springframework.context.annotation.Import;
 import org.springframework.jdbc.core.JdbcOperations;
 import org.springframework.jdbc.core.JdbcOperations;
 import org.springframework.jdbc.core.JdbcTemplate;
 import org.springframework.jdbc.core.JdbcTemplate;
 import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase;
 import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase;
 import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
 import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
 import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
 import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
 import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
 import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
 import org.springframework.security.oauth2.jose.TestJwks;
 import org.springframework.security.oauth2.jose.TestJwks;
+import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationServerMetadata;
+import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationServerMetadataClaimNames;
 import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository;
 import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository;
 import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
 import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
 import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
 import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
@@ -41,8 +47,11 @@ import org.springframework.security.oauth2.server.authorization.client.TestRegis
 import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
 import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
 import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
 import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
 import org.springframework.security.oauth2.server.authorization.test.SpringTestRule;
 import org.springframework.security.oauth2.server.authorization.test.SpringTestRule;
+import org.springframework.security.web.SecurityFilterChain;
+import org.springframework.security.web.util.matcher.RequestMatcher;
 import org.springframework.test.web.servlet.MockMvc;
 import org.springframework.test.web.servlet.MockMvc;
 
 
+import static org.hamcrest.CoreMatchers.hasItems;
 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
 import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
 import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
 import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
 import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@@ -111,6 +120,17 @@ public class OAuth2AuthorizationServerMetadataTests {
 				.andReturn();
 				.andReturn();
 	}
 	}
 
 
+	// gh-616
+	@Test
+	public void requestWhenAuthorizationServerMetadataRequestAndMetadataCustomizerSetThenReturnCustomMetadataResponse() throws Exception {
+		this.spring.register(AuthorizationServerConfigurationWithMetadataCustomizer.class).autowire();
+
+		this.mvc.perform(get(DEFAULT_OAUTH2_AUTHORIZATION_SERVER_METADATA_ENDPOINT_URI))
+				.andExpect(status().is2xxSuccessful())
+				.andExpect(jsonPath(OAuth2AuthorizationServerMetadataClaimNames.SCOPES_SUPPORTED,
+						hasItems("scope1", "scope2")));
+	}
+
 	@EnableWebSecurity
 	@EnableWebSecurity
 	@Import(OAuth2AuthorizationServerConfiguration.class)
 	@Import(OAuth2AuthorizationServerConfiguration.class)
 	static class AuthorizationServerConfiguration {
 	static class AuthorizationServerConfiguration {
@@ -139,6 +159,42 @@ public class OAuth2AuthorizationServerMetadataTests {
 		}
 		}
 	}
 	}
 
 
+	@Configuration
+	@EnableWebSecurity
+	static class AuthorizationServerConfigurationWithMetadataCustomizer extends AuthorizationServerConfiguration {
+
+		// @formatter:off
+		@Bean
+		public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
+			OAuth2AuthorizationServerConfigurer authorizationServerConfigurer =
+					new OAuth2AuthorizationServerConfigurer();
+			http.apply(authorizationServerConfigurer);
+
+			authorizationServerConfigurer
+					.authorizationServerMetadataEndpoint(authorizationServerMetadataEndpoint ->
+							authorizationServerMetadataEndpoint
+									.authorizationServerMetadataCustomizer(authorizationServerMetadataCustomizer()));
+
+			RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher();
+
+			http
+					.requestMatcher(endpointsMatcher)
+					.authorizeRequests(authorizeRequests ->
+							authorizeRequests.anyRequest().authenticated()
+					)
+					.csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher));
+
+			return http.build();
+		}
+		// @formatter:on
+
+		private Consumer<OAuth2AuthorizationServerMetadata.Builder> authorizationServerMetadataCustomizer() {
+			return (authorizationServerMetadata) ->
+					authorizationServerMetadata.scope("scope1").scope("scope2");
+		}
+
+	}
+
 	@EnableWebSecurity
 	@EnableWebSecurity
 	@Import(OAuth2AuthorizationServerConfiguration.class)
 	@Import(OAuth2AuthorizationServerConfiguration.class)
 	static class AuthorizationServerConfigurationWithIssuerNotSet extends AuthorizationServerConfiguration {
 	static class AuthorizationServerConfigurationWithIssuerNotSet extends AuthorizationServerConfiguration {

+ 14 - 18
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationServerMetadataEndpointFilterTests.java

@@ -31,6 +31,7 @@ import org.springframework.security.oauth2.server.authorization.settings.Authori
 
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
 import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verify;
@@ -40,9 +41,11 @@ import static org.mockito.Mockito.verifyNoInteractions;
  * Tests for {@link OAuth2AuthorizationServerMetadataEndpointFilter}.
  * Tests for {@link OAuth2AuthorizationServerMetadataEndpointFilter}.
  *
  *
  * @author Daniel Garnier-Moiroux
  * @author Daniel Garnier-Moiroux
+ * @author Joe Grandja
  */
  */
 public class OAuth2AuthorizationServerMetadataEndpointFilterTests {
 public class OAuth2AuthorizationServerMetadataEndpointFilterTests {
 	private static final String DEFAULT_OAUTH2_AUTHORIZATION_SERVER_METADATA_ENDPOINT_URI = "/.well-known/oauth-authorization-server";
 	private static final String DEFAULT_OAUTH2_AUTHORIZATION_SERVER_METADATA_ENDPOINT_URI = "/.well-known/oauth-authorization-server";
+	private final OAuth2AuthorizationServerMetadataEndpointFilter filter = new OAuth2AuthorizationServerMetadataEndpointFilter();
 
 
 	@After
 	@After
 	public void cleanup() {
 	public void cleanup() {
@@ -50,39 +53,34 @@ public class OAuth2AuthorizationServerMetadataEndpointFilterTests {
 	}
 	}
 
 
 	@Test
 	@Test
-	public void doFilterWhenNotAuthorizationServerMetadataRequestThenNotProcessed() throws Exception {
-		AuthorizationServerSettings authorizationServerSettings = AuthorizationServerSettings.builder()
-				.issuer("https://example.com")
-				.build();
-		AuthorizationServerContextHolder.setContext(new TestAuthorizationServerContext(authorizationServerSettings, null));
-		OAuth2AuthorizationServerMetadataEndpointFilter filter = new OAuth2AuthorizationServerMetadataEndpointFilter();
+	public void setAuthorizationServerMetadataCustomizerWhenNullThenThrowIllegalArgumentException() {
+		assertThatThrownBy(() -> this.filter.setAuthorizationServerMetadataCustomizer(null))
+				.isInstanceOf(IllegalArgumentException.class)
+				.hasMessage("authorizationServerMetadataCustomizer cannot be null");
+	}
 
 
+	@Test
+	public void doFilterWhenNotAuthorizationServerMetadataRequestThenNotProcessed() throws Exception {
 		String requestUri = "/path";
 		String requestUri = "/path";
 		MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
 		MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
 		request.setServletPath(requestUri);
 		request.setServletPath(requestUri);
 		MockHttpServletResponse response = new MockHttpServletResponse();
 		MockHttpServletResponse response = new MockHttpServletResponse();
 		FilterChain filterChain = mock(FilterChain.class);
 		FilterChain filterChain = mock(FilterChain.class);
 
 
-		filter.doFilter(request, response, filterChain);
+		this.filter.doFilter(request, response, filterChain);
 
 
 		verify(filterChain).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class));
 		verify(filterChain).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class));
 	}
 	}
 
 
 	@Test
 	@Test
 	public void doFilterWhenAuthorizationServerMetadataRequestPostThenNotProcessed() throws Exception {
 	public void doFilterWhenAuthorizationServerMetadataRequestPostThenNotProcessed() throws Exception {
-		AuthorizationServerSettings authorizationServerSettings = AuthorizationServerSettings.builder()
-				.issuer("https://example.com")
-				.build();
-		AuthorizationServerContextHolder.setContext(new TestAuthorizationServerContext(authorizationServerSettings, null));
-		OAuth2AuthorizationServerMetadataEndpointFilter filter = new OAuth2AuthorizationServerMetadataEndpointFilter();
-
 		String requestUri = DEFAULT_OAUTH2_AUTHORIZATION_SERVER_METADATA_ENDPOINT_URI;
 		String requestUri = DEFAULT_OAUTH2_AUTHORIZATION_SERVER_METADATA_ENDPOINT_URI;
 		MockHttpServletRequest request = new MockHttpServletRequest("POST", requestUri);
 		MockHttpServletRequest request = new MockHttpServletRequest("POST", requestUri);
 		request.setServletPath(requestUri);
 		request.setServletPath(requestUri);
 		MockHttpServletResponse response = new MockHttpServletResponse();
 		MockHttpServletResponse response = new MockHttpServletResponse();
 		FilterChain filterChain = mock(FilterChain.class);
 		FilterChain filterChain = mock(FilterChain.class);
 
 
-		filter.doFilter(request, response, filterChain);
+		this.filter.doFilter(request, response, filterChain);
 
 
 		verify(filterChain).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class));
 		verify(filterChain).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class));
 	}
 	}
@@ -105,7 +103,6 @@ public class OAuth2AuthorizationServerMetadataEndpointFilterTests {
 				.tokenIntrospectionEndpoint(tokenIntrospectionEndpoint)
 				.tokenIntrospectionEndpoint(tokenIntrospectionEndpoint)
 				.build();
 				.build();
 		AuthorizationServerContextHolder.setContext(new TestAuthorizationServerContext(authorizationServerSettings, null));
 		AuthorizationServerContextHolder.setContext(new TestAuthorizationServerContext(authorizationServerSettings, null));
-		OAuth2AuthorizationServerMetadataEndpointFilter filter = new OAuth2AuthorizationServerMetadataEndpointFilter();
 
 
 		String requestUri = DEFAULT_OAUTH2_AUTHORIZATION_SERVER_METADATA_ENDPOINT_URI;
 		String requestUri = DEFAULT_OAUTH2_AUTHORIZATION_SERVER_METADATA_ENDPOINT_URI;
 		MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
 		MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
@@ -113,7 +110,7 @@ public class OAuth2AuthorizationServerMetadataEndpointFilterTests {
 		MockHttpServletResponse response = new MockHttpServletResponse();
 		MockHttpServletResponse response = new MockHttpServletResponse();
 		FilterChain filterChain = mock(FilterChain.class);
 		FilterChain filterChain = mock(FilterChain.class);
 
 
-		filter.doFilter(request, response, filterChain);
+		this.filter.doFilter(request, response, filterChain);
 
 
 		verifyNoInteractions(filterChain);
 		verifyNoInteractions(filterChain);
 
 
@@ -139,7 +136,6 @@ public class OAuth2AuthorizationServerMetadataEndpointFilterTests {
 				.issuer("https://this is an invalid URL")
 				.issuer("https://this is an invalid URL")
 				.build();
 				.build();
 		AuthorizationServerContextHolder.setContext(new TestAuthorizationServerContext(authorizationServerSettings, null));
 		AuthorizationServerContextHolder.setContext(new TestAuthorizationServerContext(authorizationServerSettings, null));
-		OAuth2AuthorizationServerMetadataEndpointFilter filter = new OAuth2AuthorizationServerMetadataEndpointFilter();
 
 
 		String requestUri = DEFAULT_OAUTH2_AUTHORIZATION_SERVER_METADATA_ENDPOINT_URI;
 		String requestUri = DEFAULT_OAUTH2_AUTHORIZATION_SERVER_METADATA_ENDPOINT_URI;
 		MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
 		MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
@@ -149,7 +145,7 @@ public class OAuth2AuthorizationServerMetadataEndpointFilterTests {
 
 
 
 
 		assertThatIllegalArgumentException()
 		assertThatIllegalArgumentException()
-				.isThrownBy(() -> filter.doFilter(request, response, filterChain))
+				.isThrownBy(() -> this.filter.doFilter(request, response, filterChain))
 				.withMessage("issuer must be a valid URL");
 				.withMessage("issuer must be a valid URL");
 	}
 	}