Sfoglia il codice sorgente

Add support for multiple issuers per host using the path component

Closes gh-1342
Joe Grandja 1 anno fa
parent
commit
168077be24
33 ha cambiato i file con 583 aggiunte e 150 eliminazioni
  1. 62 23
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/AuthorizationServerContextFilter.java
  2. 7 8
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2AuthorizationEndpointConfigurer.java
  3. 5 3
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2AuthorizationServerConfigurer.java
  4. 2 2
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2AuthorizationServerMetadataEndpointConfigurer.java
  5. 7 5
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2ClientAuthenticationConfigurer.java
  6. 9 1
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2ConfigurerUtils.java
  7. 5 3
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2DeviceAuthorizationEndpointConfigurer.java
  8. 7 8
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2DeviceVerificationEndpointConfigurer.java
  9. 5 3
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2TokenEndpointConfigurer.java
  10. 5 3
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2TokenIntrospectionEndpointConfigurer.java
  11. 5 3
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2TokenRevocationEndpointConfigurer.java
  12. 5 3
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcClientRegistrationEndpointConfigurer.java
  13. 5 3
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcLogoutEndpointConfigurer.java
  14. 2 2
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcProviderConfigurationEndpointConfigurer.java
  15. 5 3
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcUserInfoEndpointConfigurer.java
  16. 16 3
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/context/AuthorizationServerContext.java
  17. 2 2
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/web/OidcProviderConfigurationEndpointFilter.java
  18. 2 2
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationServerMetadataEndpointFilter.java
  19. 4 5
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2DeviceAuthorizationEndpointFilter.java
  20. 133 0
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/AuthorizationServerContextFilterTests.java
  21. 9 1
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/JwkSetTests.java
  22. 41 1
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2AuthorizationCodeGrantTests.java
  23. 27 9
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2AuthorizationServerMetadataTests.java
  24. 25 1
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2ClientCredentialsGrantTests.java
  25. 9 5
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2DeviceCodeGrantTests.java
  26. 50 1
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2TokenIntrospectionTests.java
  27. 30 1
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2TokenRevocationTests.java
  28. 9 9
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcClientRegistrationTests.java
  29. 56 25
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcProviderConfigurationTests.java
  30. 6 4
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcTests.java
  31. 21 3
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcUserInfoTests.java
  32. 5 1
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2DeviceAuthorizationEndpointFilterTests.java
  33. 2 4
      samples/demo-authorizationserver/src/main/java/sample/config/AuthorizationServerConfig.java

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

@@ -1,5 +1,5 @@
 /*
 /*
- * Copyright 2020-2022 the original author or authors.
+ * Copyright 2020-2024 the original author or authors.
  *
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * you may not use this file except in compliance with the License.
@@ -16,7 +16,10 @@
 package org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers;
 package org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers;
 
 
 import java.io.IOException;
 import java.io.IOException;
-import java.util.function.Supplier;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
 
 
 import jakarta.servlet.FilterChain;
 import jakarta.servlet.FilterChain;
 import jakarta.servlet.ServletException;
 import jakarta.servlet.ServletException;
@@ -28,6 +31,7 @@ import org.springframework.security.oauth2.server.authorization.context.Authoriz
 import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
 import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
 import org.springframework.security.web.util.UrlUtils;
 import org.springframework.security.web.util.UrlUtils;
 import org.springframework.util.Assert;
 import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
 import org.springframework.web.filter.OncePerRequestFilter;
 import org.springframework.web.filter.OncePerRequestFilter;
 import org.springframework.web.util.UriComponentsBuilder;
 import org.springframework.web.util.UriComponentsBuilder;
 
 
@@ -42,10 +46,12 @@ import org.springframework.web.util.UriComponentsBuilder;
  */
  */
 final class AuthorizationServerContextFilter extends OncePerRequestFilter {
 final class AuthorizationServerContextFilter extends OncePerRequestFilter {
 	private final AuthorizationServerSettings authorizationServerSettings;
 	private final AuthorizationServerSettings authorizationServerSettings;
+	private final IssuerResolver issuerResolver;
 
 
 	AuthorizationServerContextFilter(AuthorizationServerSettings authorizationServerSettings) {
 	AuthorizationServerContextFilter(AuthorizationServerSettings authorizationServerSettings) {
 		Assert.notNull(authorizationServerSettings, "authorizationServerSettings cannot be null");
 		Assert.notNull(authorizationServerSettings, "authorizationServerSettings cannot be null");
 		this.authorizationServerSettings = authorizationServerSettings;
 		this.authorizationServerSettings = authorizationServerSettings;
+		this.issuerResolver = new IssuerResolver(authorizationServerSettings);
 	}
 	}
 
 
 	@Override
 	@Override
@@ -53,10 +59,9 @@ final class AuthorizationServerContextFilter extends OncePerRequestFilter {
 			throws ServletException, IOException {
 			throws ServletException, IOException {
 
 
 		try {
 		try {
+			String issuer = this.issuerResolver.resolve(request);
 			AuthorizationServerContext authorizationServerContext =
 			AuthorizationServerContext authorizationServerContext =
-					new DefaultAuthorizationServerContext(
-							() -> resolveIssuer(this.authorizationServerSettings, request),
-							this.authorizationServerSettings);
+					new DefaultAuthorizationServerContext(issuer, this.authorizationServerSettings);
 			AuthorizationServerContextHolder.setContext(authorizationServerContext);
 			AuthorizationServerContextHolder.setContext(authorizationServerContext);
 			filterChain.doFilter(request, response);
 			filterChain.doFilter(request, response);
 		} finally {
 		} finally {
@@ -64,35 +69,69 @@ final class AuthorizationServerContextFilter extends OncePerRequestFilter {
 		}
 		}
 	}
 	}
 
 
-	private static String resolveIssuer(AuthorizationServerSettings authorizationServerSettings, HttpServletRequest request) {
-		return authorizationServerSettings.getIssuer() != null ?
-				authorizationServerSettings.getIssuer() :
-				getContextPath(request);
-	}
+	private static final class IssuerResolver {
+		private final String issuer;
+		private final Set<String> endpointUris;
+
+		private IssuerResolver(AuthorizationServerSettings authorizationServerSettings) {
+			if (authorizationServerSettings.getIssuer() != null) {
+				this.issuer = authorizationServerSettings.getIssuer();
+				this.endpointUris = Collections.emptySet();
+			} else {
+				this.issuer = null;
+				this.endpointUris = new HashSet<>();
+				this.endpointUris.add("/.well-known/oauth-authorization-server");
+				this.endpointUris.add("/.well-known/openid-configuration");
+				for (Map.Entry<String, Object> setting : authorizationServerSettings.getSettings().entrySet()) {
+					if (setting.getKey().endsWith("-endpoint")) {
+						this.endpointUris.add((String) setting.getValue());
+					}
+				}
+			}
+		}
+
+		private String resolve(HttpServletRequest request) {
+			if (this.issuer != null) {
+				return this.issuer;
+			}
+
+			// Resolve Issuer Identifier dynamically from request
+			String path = request.getRequestURI();
+			if (!StringUtils.hasText(path)) {
+				path = "";
+			} else {
+				for (String endpointUri : this.endpointUris) {
+					if (path.contains(endpointUri)) {
+						path = path.replace(endpointUri, "");
+						break;
+					}
+				}
+			}
+
+			// @formatter:off
+			return UriComponentsBuilder.fromHttpUrl(UrlUtils.buildFullRequestUrl(request))
+					.replacePath(path)
+					.replaceQuery(null)
+					.fragment(null)
+					.build()
+					.toUriString();
+			// @formatter:on
+		}
 
 
-	private static String getContextPath(HttpServletRequest request) {
-		// @formatter:off
-		return UriComponentsBuilder.fromHttpUrl(UrlUtils.buildFullRequestUrl(request))
-				.replacePath(request.getContextPath())
-				.replaceQuery(null)
-				.fragment(null)
-				.build()
-				.toUriString();
-		// @formatter:on
 	}
 	}
 
 
 	private static final class DefaultAuthorizationServerContext implements AuthorizationServerContext {
 	private static final class DefaultAuthorizationServerContext implements AuthorizationServerContext {
-		private final Supplier<String> issuerSupplier;
+		private final String issuer;
 		private final AuthorizationServerSettings authorizationServerSettings;
 		private final AuthorizationServerSettings authorizationServerSettings;
 
 
-		private DefaultAuthorizationServerContext(Supplier<String> issuerSupplier, AuthorizationServerSettings authorizationServerSettings) {
-			this.issuerSupplier = issuerSupplier;
+		private DefaultAuthorizationServerContext(String issuer, AuthorizationServerSettings authorizationServerSettings) {
+			this.issuer = issuer;
 			this.authorizationServerSettings = authorizationServerSettings;
 			this.authorizationServerSettings = authorizationServerSettings;
 		}
 		}
 
 
 		@Override
 		@Override
 		public String getIssuer() {
 		public String getIssuer() {
-			return this.issuerSupplier.get();
+			return this.issuer;
 		}
 		}
 
 
 		@Override
 		@Override

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

@@ -1,5 +1,5 @@
 /*
 /*
- * Copyright 2020-2023 the original author or authors.
+ * Copyright 2020-2024 the original author or authors.
  *
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * you may not use this file except in compliance with the License.
@@ -51,6 +51,8 @@ import org.springframework.security.web.util.matcher.RequestMatcher;
 import org.springframework.util.Assert;
 import org.springframework.util.Assert;
 import org.springframework.util.StringUtils;
 import org.springframework.util.StringUtils;
 
 
+import static org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2ConfigurerUtils.withMultipleIssuerPattern;
+
 /**
 /**
  * Configurer for the OAuth 2.0 Authorization Endpoint.
  * Configurer for the OAuth 2.0 Authorization Endpoint.
  *
  *
@@ -209,13 +211,10 @@ public final class OAuth2AuthorizationEndpointConfigurer extends AbstractOAuth2C
 	@Override
 	@Override
 	void init(HttpSecurity httpSecurity) {
 	void init(HttpSecurity httpSecurity) {
 		AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils.getAuthorizationServerSettings(httpSecurity);
 		AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils.getAuthorizationServerSettings(httpSecurity);
+		String authorizationEndpointUri = withMultipleIssuerPattern(authorizationServerSettings.getAuthorizationEndpoint());
 		this.requestMatcher = new OrRequestMatcher(
 		this.requestMatcher = new OrRequestMatcher(
-				new AntPathRequestMatcher(
-						authorizationServerSettings.getAuthorizationEndpoint(),
-						HttpMethod.GET.name()),
-				new AntPathRequestMatcher(
-						authorizationServerSettings.getAuthorizationEndpoint(),
-						HttpMethod.POST.name()));
+				new AntPathRequestMatcher(authorizationEndpointUri, HttpMethod.GET.name()),
+				new AntPathRequestMatcher(authorizationEndpointUri, HttpMethod.POST.name()));
 
 
 		List<AuthenticationProvider> authenticationProviders = createDefaultAuthenticationProviders(httpSecurity);
 		List<AuthenticationProvider> authenticationProviders = createDefaultAuthenticationProviders(httpSecurity);
 		if (!this.authenticationProviders.isEmpty()) {
 		if (!this.authenticationProviders.isEmpty()) {
@@ -234,7 +233,7 @@ public final class OAuth2AuthorizationEndpointConfigurer extends AbstractOAuth2C
 		OAuth2AuthorizationEndpointFilter authorizationEndpointFilter =
 		OAuth2AuthorizationEndpointFilter authorizationEndpointFilter =
 				new OAuth2AuthorizationEndpointFilter(
 				new OAuth2AuthorizationEndpointFilter(
 						authenticationManager,
 						authenticationManager,
-						authorizationServerSettings.getAuthorizationEndpoint());
+						withMultipleIssuerPattern(authorizationServerSettings.getAuthorizationEndpoint()));
 		List<AuthenticationConverter> authenticationConverters = createDefaultAuthenticationConverters();
 		List<AuthenticationConverter> authenticationConverters = createDefaultAuthenticationConverters();
 		if (!this.authorizationRequestConverters.isEmpty()) {
 		if (!this.authorizationRequestConverters.isEmpty()) {
 			authenticationConverters.addAll(0, this.authorizationRequestConverters);
 			authenticationConverters.addAll(0, this.authorizationRequestConverters);

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

@@ -1,5 +1,5 @@
 /*
 /*
- * Copyright 2020-2022 the original author or authors.
+ * Copyright 2020-2024 the original author or authors.
  *
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * you may not use this file except in compliance with the License.
@@ -56,6 +56,8 @@ import org.springframework.security.web.util.matcher.OrRequestMatcher;
 import org.springframework.security.web.util.matcher.RequestMatcher;
 import org.springframework.security.web.util.matcher.RequestMatcher;
 import org.springframework.util.Assert;
 import org.springframework.util.Assert;
 
 
+import static org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2ConfigurerUtils.withMultipleIssuerPattern;
+
 /**
 /**
  * An {@link AbstractHttpConfigurer} for OAuth 2.0 Authorization Server support.
  * An {@link AbstractHttpConfigurer} for OAuth 2.0 Authorization Server support.
  *
  *
@@ -314,7 +316,7 @@ public final class OAuth2AuthorizationServerConfigurer
 			requestMatchers.add(configurer.getRequestMatcher());
 			requestMatchers.add(configurer.getRequestMatcher());
 		});
 		});
 		requestMatchers.add(new AntPathRequestMatcher(
 		requestMatchers.add(new AntPathRequestMatcher(
-				authorizationServerSettings.getJwkSetEndpoint(), HttpMethod.GET.name()));
+				withMultipleIssuerPattern(authorizationServerSettings.getJwkSetEndpoint()), HttpMethod.GET.name()));
 		this.endpointsMatcher = new OrRequestMatcher(requestMatchers);
 		this.endpointsMatcher = new OrRequestMatcher(requestMatchers);
 
 
 		ExceptionHandlingConfigurer<HttpSecurity> exceptionHandling = httpSecurity.getConfigurer(ExceptionHandlingConfigurer.class);
 		ExceptionHandlingConfigurer<HttpSecurity> exceptionHandling = httpSecurity.getConfigurer(ExceptionHandlingConfigurer.class);
@@ -342,7 +344,7 @@ public final class OAuth2AuthorizationServerConfigurer
 		JWKSource<com.nimbusds.jose.proc.SecurityContext> jwkSource = OAuth2ConfigurerUtils.getJwkSource(httpSecurity);
 		JWKSource<com.nimbusds.jose.proc.SecurityContext> jwkSource = OAuth2ConfigurerUtils.getJwkSource(httpSecurity);
 		if (jwkSource != null) {
 		if (jwkSource != null) {
 			NimbusJwkSetEndpointFilter jwkSetEndpointFilter = new NimbusJwkSetEndpointFilter(
 			NimbusJwkSetEndpointFilter jwkSetEndpointFilter = new NimbusJwkSetEndpointFilter(
-					jwkSource, authorizationServerSettings.getJwkSetEndpoint());
+					jwkSource, withMultipleIssuerPattern(authorizationServerSettings.getJwkSetEndpoint()));
 			httpSecurity.addFilterBefore(postProcess(jwkSetEndpointFilter), AbstractPreAuthenticatedProcessingFilter.class);
 			httpSecurity.addFilterBefore(postProcess(jwkSetEndpointFilter), AbstractPreAuthenticatedProcessingFilter.class);
 		}
 		}
 	}
 	}

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

@@ -1,5 +1,5 @@
 /*
 /*
- * Copyright 2020-2022 the original author or authors.
+ * Copyright 2020-2024 the original author or authors.
  *
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * you may not use this file except in compliance with the License.
@@ -70,7 +70,7 @@ public final class OAuth2AuthorizationServerMetadataEndpointConfigurer extends A
 	@Override
 	@Override
 	void init(HttpSecurity httpSecurity) {
 	void init(HttpSecurity httpSecurity) {
 		this.requestMatcher = new AntPathRequestMatcher(
 		this.requestMatcher = new AntPathRequestMatcher(
-				"/.well-known/oauth-authorization-server", HttpMethod.GET.name());
+				"/.well-known/oauth-authorization-server/**", HttpMethod.GET.name());
 	}
 	}
 
 
 	@Override
 	@Override

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

@@ -1,5 +1,5 @@
 /*
 /*
- * Copyright 2020-2023 the original author or authors.
+ * Copyright 2020-2024 the original author or authors.
  *
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * you may not use this file except in compliance with the License.
@@ -51,6 +51,8 @@ import org.springframework.security.web.util.matcher.OrRequestMatcher;
 import org.springframework.security.web.util.matcher.RequestMatcher;
 import org.springframework.security.web.util.matcher.RequestMatcher;
 import org.springframework.util.Assert;
 import org.springframework.util.Assert;
 
 
+import static org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2ConfigurerUtils.withMultipleIssuerPattern;
+
 /**
 /**
  * Configurer for OAuth 2.0 Client Authentication.
  * Configurer for OAuth 2.0 Client Authentication.
  *
  *
@@ -161,16 +163,16 @@ public final class OAuth2ClientAuthenticationConfigurer extends AbstractOAuth2Co
 		AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils.getAuthorizationServerSettings(httpSecurity);
 		AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils.getAuthorizationServerSettings(httpSecurity);
 		this.requestMatcher = new OrRequestMatcher(
 		this.requestMatcher = new OrRequestMatcher(
 				new AntPathRequestMatcher(
 				new AntPathRequestMatcher(
-						authorizationServerSettings.getTokenEndpoint(),
+						withMultipleIssuerPattern(authorizationServerSettings.getTokenEndpoint()),
 						HttpMethod.POST.name()),
 						HttpMethod.POST.name()),
 				new AntPathRequestMatcher(
 				new AntPathRequestMatcher(
-						authorizationServerSettings.getTokenIntrospectionEndpoint(),
+						withMultipleIssuerPattern(authorizationServerSettings.getTokenIntrospectionEndpoint()),
 						HttpMethod.POST.name()),
 						HttpMethod.POST.name()),
 				new AntPathRequestMatcher(
 				new AntPathRequestMatcher(
-						authorizationServerSettings.getTokenRevocationEndpoint(),
+						withMultipleIssuerPattern(authorizationServerSettings.getTokenRevocationEndpoint()),
 						HttpMethod.POST.name()),
 						HttpMethod.POST.name()),
 				new AntPathRequestMatcher(
 				new AntPathRequestMatcher(
-						authorizationServerSettings.getDeviceAuthorizationEndpoint(),
+						withMultipleIssuerPattern(authorizationServerSettings.getDeviceAuthorizationEndpoint()),
 						HttpMethod.POST.name()));
 						HttpMethod.POST.name()));
 
 
 		List<AuthenticationProvider> authenticationProviders = createDefaultAuthenticationProviders(httpSecurity);
 		List<AuthenticationProvider> authenticationProviders = createDefaultAuthenticationProviders(httpSecurity);

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

@@ -1,5 +1,5 @@
 /*
 /*
- * Copyright 2020-2023 the original author or authors.
+ * Copyright 2020-2024 the original author or authors.
  *
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * you may not use this file except in compliance with the License.
@@ -43,6 +43,7 @@ import org.springframework.security.oauth2.server.authorization.token.OAuth2Refr
 import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenClaimsContext;
 import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenClaimsContext;
 import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer;
 import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer;
 import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
 import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
+import org.springframework.util.Assert;
 import org.springframework.util.StringUtils;
 import org.springframework.util.StringUtils;
 
 
 /**
 /**
@@ -56,6 +57,13 @@ final class OAuth2ConfigurerUtils {
 	private OAuth2ConfigurerUtils() {
 	private OAuth2ConfigurerUtils() {
 	}
 	}
 
 
+	static String withMultipleIssuerPattern(String endpointUri) {
+		Assert.hasText(endpointUri, "endpointUri cannot be empty");
+		return endpointUri.startsWith("/") ?
+				"/**" + endpointUri :
+				"/**/" + endpointUri;
+	}
+
 	static RegisteredClientRepository getRegisteredClientRepository(HttpSecurity httpSecurity) {
 	static RegisteredClientRepository getRegisteredClientRepository(HttpSecurity httpSecurity) {
 		RegisteredClientRepository registeredClientRepository = httpSecurity.getSharedObject(RegisteredClientRepository.class);
 		RegisteredClientRepository registeredClientRepository = httpSecurity.getSharedObject(RegisteredClientRepository.class);
 		if (registeredClientRepository == null) {
 		if (registeredClientRepository == null) {

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

@@ -1,5 +1,5 @@
 /*
 /*
- * Copyright 2020-2023 the original author or authors.
+ * Copyright 2020-2024 the original author or authors.
  *
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * you may not use this file except in compliance with the License.
@@ -45,6 +45,8 @@ import org.springframework.security.web.util.matcher.RequestMatcher;
 import org.springframework.util.Assert;
 import org.springframework.util.Assert;
 import org.springframework.util.StringUtils;
 import org.springframework.util.StringUtils;
 
 
+import static org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2ConfigurerUtils.withMultipleIssuerPattern;
+
 /**
 /**
  * Configurer for the OAuth 2.0 Device Authorization Endpoint.
  * Configurer for the OAuth 2.0 Device Authorization Endpoint.
  *
  *
@@ -166,7 +168,7 @@ public final class OAuth2DeviceAuthorizationEndpointConfigurer extends AbstractO
 		AuthorizationServerSettings authorizationServerSettings =
 		AuthorizationServerSettings authorizationServerSettings =
 				OAuth2ConfigurerUtils.getAuthorizationServerSettings(builder);
 				OAuth2ConfigurerUtils.getAuthorizationServerSettings(builder);
 		this.requestMatcher = new AntPathRequestMatcher(
 		this.requestMatcher = new AntPathRequestMatcher(
-				authorizationServerSettings.getDeviceAuthorizationEndpoint(), HttpMethod.POST.name());
+				withMultipleIssuerPattern(authorizationServerSettings.getDeviceAuthorizationEndpoint()), HttpMethod.POST.name());
 
 
 		List<AuthenticationProvider> authenticationProviders = createDefaultAuthenticationProviders(builder);
 		List<AuthenticationProvider> authenticationProviders = createDefaultAuthenticationProviders(builder);
 		if (!this.authenticationProviders.isEmpty()) {
 		if (!this.authenticationProviders.isEmpty()) {
@@ -184,7 +186,7 @@ public final class OAuth2DeviceAuthorizationEndpointConfigurer extends AbstractO
 
 
 		OAuth2DeviceAuthorizationEndpointFilter deviceAuthorizationEndpointFilter =
 		OAuth2DeviceAuthorizationEndpointFilter deviceAuthorizationEndpointFilter =
 				new OAuth2DeviceAuthorizationEndpointFilter(
 				new OAuth2DeviceAuthorizationEndpointFilter(
-						authenticationManager, authorizationServerSettings.getDeviceAuthorizationEndpoint());
+						authenticationManager, withMultipleIssuerPattern(authorizationServerSettings.getDeviceAuthorizationEndpoint()));
 
 
 		List<AuthenticationConverter> authenticationConverters = createDefaultAuthenticationConverters();
 		List<AuthenticationConverter> authenticationConverters = createDefaultAuthenticationConverters();
 		if (!this.deviceAuthorizationRequestConverters.isEmpty()) {
 		if (!this.deviceAuthorizationRequestConverters.isEmpty()) {

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

@@ -1,5 +1,5 @@
 /*
 /*
- * Copyright 2020-2023 the original author or authors.
+ * Copyright 2020-2024 the original author or authors.
  *
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * you may not use this file except in compliance with the License.
@@ -50,6 +50,8 @@ import org.springframework.security.web.util.matcher.RequestMatcher;
 import org.springframework.util.Assert;
 import org.springframework.util.Assert;
 import org.springframework.util.StringUtils;
 import org.springframework.util.StringUtils;
 
 
+import static org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2ConfigurerUtils.withMultipleIssuerPattern;
+
 /**
 /**
  * Configurer for the OAuth 2.0 Device Verification Endpoint.
  * Configurer for the OAuth 2.0 Device Verification Endpoint.
  *
  *
@@ -195,13 +197,10 @@ public final class OAuth2DeviceVerificationEndpointConfigurer extends AbstractOA
 	public void init(HttpSecurity builder) {
 	public void init(HttpSecurity builder) {
 		AuthorizationServerSettings authorizationServerSettings =
 		AuthorizationServerSettings authorizationServerSettings =
 				OAuth2ConfigurerUtils.getAuthorizationServerSettings(builder);
 				OAuth2ConfigurerUtils.getAuthorizationServerSettings(builder);
+		String deviceVerificationEndpointUri = withMultipleIssuerPattern(authorizationServerSettings.getDeviceVerificationEndpoint());
 		this.requestMatcher = new OrRequestMatcher(
 		this.requestMatcher = new OrRequestMatcher(
-				new AntPathRequestMatcher(
-						authorizationServerSettings.getDeviceVerificationEndpoint(),
-						HttpMethod.GET.name()),
-				new AntPathRequestMatcher(
-						authorizationServerSettings.getDeviceVerificationEndpoint(),
-						HttpMethod.POST.name()));
+				new AntPathRequestMatcher(deviceVerificationEndpointUri, HttpMethod.GET.name()),
+				new AntPathRequestMatcher(deviceVerificationEndpointUri, HttpMethod.POST.name()));
 
 
 		List<AuthenticationProvider> authenticationProviders = createDefaultAuthenticationProviders(builder);
 		List<AuthenticationProvider> authenticationProviders = createDefaultAuthenticationProviders(builder);
 		if (!this.authenticationProviders.isEmpty()) {
 		if (!this.authenticationProviders.isEmpty()) {
@@ -221,7 +220,7 @@ public final class OAuth2DeviceVerificationEndpointConfigurer extends AbstractOA
 		OAuth2DeviceVerificationEndpointFilter deviceVerificationEndpointFilter =
 		OAuth2DeviceVerificationEndpointFilter deviceVerificationEndpointFilter =
 				new OAuth2DeviceVerificationEndpointFilter(
 				new OAuth2DeviceVerificationEndpointFilter(
 						authenticationManager,
 						authenticationManager,
-						authorizationServerSettings.getDeviceVerificationEndpoint());
+						withMultipleIssuerPattern(authorizationServerSettings.getDeviceVerificationEndpoint()));
 		List<AuthenticationConverter> authenticationConverters = createDefaultAuthenticationConverters();
 		List<AuthenticationConverter> authenticationConverters = createDefaultAuthenticationConverters();
 		if (!this.deviceVerificationRequestConverters.isEmpty()) {
 		if (!this.deviceVerificationRequestConverters.isEmpty()) {
 			authenticationConverters.addAll(0, this.deviceVerificationRequestConverters);
 			authenticationConverters.addAll(0, this.deviceVerificationRequestConverters);

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

@@ -1,5 +1,5 @@
 /*
 /*
- * Copyright 2020-2023 the original author or authors.
+ * Copyright 2020-2024 the original author or authors.
  *
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * you may not use this file except in compliance with the License.
@@ -54,6 +54,8 @@ 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.util.Assert;
 
 
+import static org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2ConfigurerUtils.withMultipleIssuerPattern;
+
 /**
 /**
  * Configurer for the OAuth 2.0 Token Endpoint.
  * Configurer for the OAuth 2.0 Token Endpoint.
  *
  *
@@ -163,7 +165,7 @@ public final class OAuth2TokenEndpointConfigurer extends AbstractOAuth2Configure
 	void init(HttpSecurity httpSecurity) {
 	void init(HttpSecurity httpSecurity) {
 		AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils.getAuthorizationServerSettings(httpSecurity);
 		AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils.getAuthorizationServerSettings(httpSecurity);
 		this.requestMatcher = new AntPathRequestMatcher(
 		this.requestMatcher = new AntPathRequestMatcher(
-				authorizationServerSettings.getTokenEndpoint(), HttpMethod.POST.name());
+				withMultipleIssuerPattern(authorizationServerSettings.getTokenEndpoint()), HttpMethod.POST.name());
 
 
 		List<AuthenticationProvider> authenticationProviders = createDefaultAuthenticationProviders(httpSecurity);
 		List<AuthenticationProvider> authenticationProviders = createDefaultAuthenticationProviders(httpSecurity);
 		if (!this.authenticationProviders.isEmpty()) {
 		if (!this.authenticationProviders.isEmpty()) {
@@ -182,7 +184,7 @@ public final class OAuth2TokenEndpointConfigurer extends AbstractOAuth2Configure
 		OAuth2TokenEndpointFilter tokenEndpointFilter =
 		OAuth2TokenEndpointFilter tokenEndpointFilter =
 				new OAuth2TokenEndpointFilter(
 				new OAuth2TokenEndpointFilter(
 						authenticationManager,
 						authenticationManager,
-						authorizationServerSettings.getTokenEndpoint());
+						withMultipleIssuerPattern(authorizationServerSettings.getTokenEndpoint()));
 		List<AuthenticationConverter> authenticationConverters = createDefaultAuthenticationConverters();
 		List<AuthenticationConverter> authenticationConverters = createDefaultAuthenticationConverters();
 		if (!this.accessTokenRequestConverters.isEmpty()) {
 		if (!this.accessTokenRequestConverters.isEmpty()) {
 			authenticationConverters.addAll(0, this.accessTokenRequestConverters);
 			authenticationConverters.addAll(0, this.accessTokenRequestConverters);

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

@@ -1,5 +1,5 @@
 /*
 /*
- * Copyright 2020-2022 the original author or authors.
+ * Copyright 2020-2024 the original author or authors.
  *
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * you may not use this file except in compliance with the License.
@@ -43,6 +43,8 @@ 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.util.Assert;
 
 
+import static org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2ConfigurerUtils.withMultipleIssuerPattern;
+
 /**
 /**
  * Configurer for the OAuth 2.0 Token Introspection Endpoint.
  * Configurer for the OAuth 2.0 Token Introspection Endpoint.
  *
  *
@@ -152,7 +154,7 @@ public final class OAuth2TokenIntrospectionEndpointConfigurer extends AbstractOA
 	void init(HttpSecurity httpSecurity) {
 	void init(HttpSecurity httpSecurity) {
 		AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils.getAuthorizationServerSettings(httpSecurity);
 		AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils.getAuthorizationServerSettings(httpSecurity);
 		this.requestMatcher = new AntPathRequestMatcher(
 		this.requestMatcher = new AntPathRequestMatcher(
-				authorizationServerSettings.getTokenIntrospectionEndpoint(), HttpMethod.POST.name());
+				withMultipleIssuerPattern(authorizationServerSettings.getTokenIntrospectionEndpoint()), HttpMethod.POST.name());
 
 
 		List<AuthenticationProvider> authenticationProviders = createDefaultAuthenticationProviders(httpSecurity);
 		List<AuthenticationProvider> authenticationProviders = createDefaultAuthenticationProviders(httpSecurity);
 		if (!this.authenticationProviders.isEmpty()) {
 		if (!this.authenticationProviders.isEmpty()) {
@@ -170,7 +172,7 @@ public final class OAuth2TokenIntrospectionEndpointConfigurer extends AbstractOA
 
 
 		OAuth2TokenIntrospectionEndpointFilter introspectionEndpointFilter =
 		OAuth2TokenIntrospectionEndpointFilter introspectionEndpointFilter =
 				new OAuth2TokenIntrospectionEndpointFilter(
 				new OAuth2TokenIntrospectionEndpointFilter(
-						authenticationManager, authorizationServerSettings.getTokenIntrospectionEndpoint());
+						authenticationManager, withMultipleIssuerPattern(authorizationServerSettings.getTokenIntrospectionEndpoint()));
 		List<AuthenticationConverter> authenticationConverters = createDefaultAuthenticationConverters();
 		List<AuthenticationConverter> authenticationConverters = createDefaultAuthenticationConverters();
 		if (!this.introspectionRequestConverters.isEmpty()) {
 		if (!this.introspectionRequestConverters.isEmpty()) {
 			authenticationConverters.addAll(0, this.introspectionRequestConverters);
 			authenticationConverters.addAll(0, this.introspectionRequestConverters);

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

@@ -1,5 +1,5 @@
 /*
 /*
- * Copyright 2020-2022 the original author or authors.
+ * Copyright 2020-2024 the original author or authors.
  *
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * you may not use this file except in compliance with the License.
@@ -42,6 +42,8 @@ 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.util.Assert;
 
 
+import static org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2ConfigurerUtils.withMultipleIssuerPattern;
+
 /**
 /**
  * Configurer for the OAuth 2.0 Token Revocation Endpoint.
  * Configurer for the OAuth 2.0 Token Revocation Endpoint.
  *
  *
@@ -151,7 +153,7 @@ public final class OAuth2TokenRevocationEndpointConfigurer extends AbstractOAuth
 	void init(HttpSecurity httpSecurity) {
 	void init(HttpSecurity httpSecurity) {
 		AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils.getAuthorizationServerSettings(httpSecurity);
 		AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils.getAuthorizationServerSettings(httpSecurity);
 		this.requestMatcher = new AntPathRequestMatcher(
 		this.requestMatcher = new AntPathRequestMatcher(
-				authorizationServerSettings.getTokenRevocationEndpoint(), HttpMethod.POST.name());
+				withMultipleIssuerPattern(authorizationServerSettings.getTokenRevocationEndpoint()), HttpMethod.POST.name());
 
 
 		List<AuthenticationProvider> authenticationProviders = createDefaultAuthenticationProviders(httpSecurity);
 		List<AuthenticationProvider> authenticationProviders = createDefaultAuthenticationProviders(httpSecurity);
 		if (!this.authenticationProviders.isEmpty()) {
 		if (!this.authenticationProviders.isEmpty()) {
@@ -169,7 +171,7 @@ public final class OAuth2TokenRevocationEndpointConfigurer extends AbstractOAuth
 
 
 		OAuth2TokenRevocationEndpointFilter revocationEndpointFilter =
 		OAuth2TokenRevocationEndpointFilter revocationEndpointFilter =
 				new OAuth2TokenRevocationEndpointFilter(
 				new OAuth2TokenRevocationEndpointFilter(
-						authenticationManager, authorizationServerSettings.getTokenRevocationEndpoint());
+						authenticationManager, withMultipleIssuerPattern(authorizationServerSettings.getTokenRevocationEndpoint()));
 		List<AuthenticationConverter> authenticationConverters = createDefaultAuthenticationConverters();
 		List<AuthenticationConverter> authenticationConverters = createDefaultAuthenticationConverters();
 		if (!this.revocationRequestConverters.isEmpty()) {
 		if (!this.revocationRequestConverters.isEmpty()) {
 			authenticationConverters.addAll(0, this.revocationRequestConverters);
 			authenticationConverters.addAll(0, this.revocationRequestConverters);

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

@@ -1,5 +1,5 @@
 /*
 /*
- * Copyright 2020-2023 the original author or authors.
+ * Copyright 2020-2024 the original author or authors.
  *
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * you may not use this file except in compliance with the License.
@@ -46,6 +46,8 @@ import org.springframework.security.web.util.matcher.OrRequestMatcher;
 import org.springframework.security.web.util.matcher.RequestMatcher;
 import org.springframework.security.web.util.matcher.RequestMatcher;
 import org.springframework.util.Assert;
 import org.springframework.util.Assert;
 
 
+import static org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2ConfigurerUtils.withMultipleIssuerPattern;
+
 /**
 /**
  * Configurer for OpenID Connect 1.0 Dynamic Client Registration Endpoint.
  * Configurer for OpenID Connect 1.0 Dynamic Client Registration Endpoint.
  *
  *
@@ -160,7 +162,7 @@ public final class OidcClientRegistrationEndpointConfigurer extends AbstractOAut
 	@Override
 	@Override
 	void init(HttpSecurity httpSecurity) {
 	void init(HttpSecurity httpSecurity) {
 		AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils.getAuthorizationServerSettings(httpSecurity);
 		AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils.getAuthorizationServerSettings(httpSecurity);
-		String clientRegistrationEndpointUri = authorizationServerSettings.getOidcClientRegistrationEndpoint();
+		String clientRegistrationEndpointUri = withMultipleIssuerPattern(authorizationServerSettings.getOidcClientRegistrationEndpoint());
 		this.requestMatcher = new OrRequestMatcher(
 		this.requestMatcher = new OrRequestMatcher(
 				new AntPathRequestMatcher(clientRegistrationEndpointUri, HttpMethod.POST.name()),
 				new AntPathRequestMatcher(clientRegistrationEndpointUri, HttpMethod.POST.name()),
 				new AntPathRequestMatcher(clientRegistrationEndpointUri, HttpMethod.GET.name())
 				new AntPathRequestMatcher(clientRegistrationEndpointUri, HttpMethod.GET.name())
@@ -183,7 +185,7 @@ public final class OidcClientRegistrationEndpointConfigurer extends AbstractOAut
 		OidcClientRegistrationEndpointFilter oidcClientRegistrationEndpointFilter =
 		OidcClientRegistrationEndpointFilter oidcClientRegistrationEndpointFilter =
 				new OidcClientRegistrationEndpointFilter(
 				new OidcClientRegistrationEndpointFilter(
 						authenticationManager,
 						authenticationManager,
-						authorizationServerSettings.getOidcClientRegistrationEndpoint());
+						withMultipleIssuerPattern(authorizationServerSettings.getOidcClientRegistrationEndpoint()));
 		List<AuthenticationConverter> authenticationConverters = createDefaultAuthenticationConverters();
 		List<AuthenticationConverter> authenticationConverters = createDefaultAuthenticationConverters();
 		if (!this.clientRegistrationRequestConverters.isEmpty()) {
 		if (!this.clientRegistrationRequestConverters.isEmpty()) {
 			authenticationConverters.addAll(0, this.clientRegistrationRequestConverters);
 			authenticationConverters.addAll(0, this.clientRegistrationRequestConverters);

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

@@ -1,5 +1,5 @@
 /*
 /*
- * Copyright 2020-2023 the original author or authors.
+ * Copyright 2020-2024 the original author or authors.
  *
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * you may not use this file except in compliance with the License.
@@ -44,6 +44,8 @@ import org.springframework.security.web.util.matcher.OrRequestMatcher;
 import org.springframework.security.web.util.matcher.RequestMatcher;
 import org.springframework.security.web.util.matcher.RequestMatcher;
 import org.springframework.util.Assert;
 import org.springframework.util.Assert;
 
 
+import static org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2ConfigurerUtils.withMultipleIssuerPattern;
+
 /**
 /**
  * Configurer for OpenID Connect 1.0 RP-Initiated Logout Endpoint.
  * Configurer for OpenID Connect 1.0 RP-Initiated Logout Endpoint.
  *
  *
@@ -151,7 +153,7 @@ public final class OidcLogoutEndpointConfigurer extends AbstractOAuth2Configurer
 	@Override
 	@Override
 	void init(HttpSecurity httpSecurity) {
 	void init(HttpSecurity httpSecurity) {
 		AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils.getAuthorizationServerSettings(httpSecurity);
 		AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils.getAuthorizationServerSettings(httpSecurity);
-		String logoutEndpointUri = authorizationServerSettings.getOidcLogoutEndpoint();
+		String logoutEndpointUri = withMultipleIssuerPattern(authorizationServerSettings.getOidcLogoutEndpoint());
 		this.requestMatcher = new OrRequestMatcher(
 		this.requestMatcher = new OrRequestMatcher(
 				new AntPathRequestMatcher(logoutEndpointUri, HttpMethod.GET.name()),
 				new AntPathRequestMatcher(logoutEndpointUri, HttpMethod.GET.name()),
 				new AntPathRequestMatcher(logoutEndpointUri, HttpMethod.POST.name())
 				new AntPathRequestMatcher(logoutEndpointUri, HttpMethod.POST.name())
@@ -174,7 +176,7 @@ public final class OidcLogoutEndpointConfigurer extends AbstractOAuth2Configurer
 		OidcLogoutEndpointFilter oidcLogoutEndpointFilter =
 		OidcLogoutEndpointFilter oidcLogoutEndpointFilter =
 				new OidcLogoutEndpointFilter(
 				new OidcLogoutEndpointFilter(
 						authenticationManager,
 						authenticationManager,
-						authorizationServerSettings.getOidcLogoutEndpoint());
+						withMultipleIssuerPattern(authorizationServerSettings.getOidcLogoutEndpoint()));
 		List<AuthenticationConverter> authenticationConverters = createDefaultAuthenticationConverters();
 		List<AuthenticationConverter> authenticationConverters = createDefaultAuthenticationConverters();
 		if (!this.logoutRequestConverters.isEmpty()) {
 		if (!this.logoutRequestConverters.isEmpty()) {
 			authenticationConverters.addAll(0, this.logoutRequestConverters);
 			authenticationConverters.addAll(0, this.logoutRequestConverters);

+ 2 - 2
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcProviderConfigurationEndpointConfigurer.java

@@ -1,5 +1,5 @@
 /*
 /*
- * Copyright 2020-2022 the original author or authors.
+ * Copyright 2020-2024 the original author or authors.
  *
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * you may not use this file except in compliance with the License.
@@ -70,7 +70,7 @@ public final class OidcProviderConfigurationEndpointConfigurer extends AbstractO
 	@Override
 	@Override
 	void init(HttpSecurity httpSecurity) {
 	void init(HttpSecurity httpSecurity) {
 		this.requestMatcher = new AntPathRequestMatcher(
 		this.requestMatcher = new AntPathRequestMatcher(
-				"/.well-known/openid-configuration", HttpMethod.GET.name());
+				"/**/.well-known/openid-configuration", HttpMethod.GET.name());
 	}
 	}
 
 
 	@Override
 	@Override

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

@@ -1,5 +1,5 @@
 /*
 /*
- * Copyright 2020-2022 the original author or authors.
+ * Copyright 2020-2024 the original author or authors.
  *
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * you may not use this file except in compliance with the License.
@@ -49,6 +49,8 @@ import org.springframework.security.web.util.matcher.OrRequestMatcher;
 import org.springframework.security.web.util.matcher.RequestMatcher;
 import org.springframework.security.web.util.matcher.RequestMatcher;
 import org.springframework.util.Assert;
 import org.springframework.util.Assert;
 
 
+import static org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2ConfigurerUtils.withMultipleIssuerPattern;
+
 /**
 /**
  * Configurer for OpenID Connect 1.0 UserInfo Endpoint.
  * Configurer for OpenID Connect 1.0 UserInfo Endpoint.
  *
  *
@@ -185,7 +187,7 @@ public final class OidcUserInfoEndpointConfigurer extends AbstractOAuth2Configur
 	@Override
 	@Override
 	void init(HttpSecurity httpSecurity) {
 	void init(HttpSecurity httpSecurity) {
 		AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils.getAuthorizationServerSettings(httpSecurity);
 		AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils.getAuthorizationServerSettings(httpSecurity);
-		String userInfoEndpointUri = authorizationServerSettings.getOidcUserInfoEndpoint();
+		String userInfoEndpointUri = withMultipleIssuerPattern(authorizationServerSettings.getOidcUserInfoEndpoint());
 		this.requestMatcher = new OrRequestMatcher(
 		this.requestMatcher = new OrRequestMatcher(
 				new AntPathRequestMatcher(userInfoEndpointUri, HttpMethod.GET.name()),
 				new AntPathRequestMatcher(userInfoEndpointUri, HttpMethod.GET.name()),
 				new AntPathRequestMatcher(userInfoEndpointUri, HttpMethod.POST.name()));
 				new AntPathRequestMatcher(userInfoEndpointUri, HttpMethod.POST.name()));
@@ -207,7 +209,7 @@ public final class OidcUserInfoEndpointConfigurer extends AbstractOAuth2Configur
 		OidcUserInfoEndpointFilter oidcUserInfoEndpointFilter =
 		OidcUserInfoEndpointFilter oidcUserInfoEndpointFilter =
 				new OidcUserInfoEndpointFilter(
 				new OidcUserInfoEndpointFilter(
 						authenticationManager,
 						authenticationManager,
-						authorizationServerSettings.getOidcUserInfoEndpoint());
+						withMultipleIssuerPattern(authorizationServerSettings.getOidcUserInfoEndpoint()));
 		List<AuthenticationConverter> authenticationConverters = createDefaultAuthenticationConverters();
 		List<AuthenticationConverter> authenticationConverters = createDefaultAuthenticationConverters();
 		if (!this.userInfoRequestConverters.isEmpty()) {
 		if (!this.userInfoRequestConverters.isEmpty()) {
 			authenticationConverters.addAll(0, this.userInfoRequestConverters);
 			authenticationConverters.addAll(0, this.userInfoRequestConverters);

+ 16 - 3
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/context/AuthorizationServerContext.java

@@ -1,5 +1,5 @@
 /*
 /*
- * Copyright 2020-2022 the original author or authors.
+ * Copyright 2020-2024 the original author or authors.
  *
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * you may not use this file except in compliance with the License.
@@ -28,9 +28,22 @@ import org.springframework.security.oauth2.server.authorization.settings.Authori
 public interface AuthorizationServerContext {
 public interface AuthorizationServerContext {
 
 
 	/**
 	/**
-	 * Returns the {@code URL} of the Authorization Server's issuer identifier.
+	 * Returns {@link AuthorizationServerSettings#getIssuer()} if available, otherwise,
+	 * resolves the issuer identifier from the <i>"current"</i> request.
 	 *
 	 *
-	 * @return the {@code URL} of the Authorization Server's issuer identifier
+	 * <p>
+	 * The issuer identifier may contain a path component to support multiple issuers per host in a multi-tenant hosting configuration.
+	 *
+	 * <p>
+	 * For example:
+	 * <ul>
+	 * <li>{@code https://example.com/issuer1/oauth2/token} &mdash; resolves the issuer to {@code https://example.com/issuer1}</li>
+	 * <li>{@code https://example.com/issuer2/oauth2/token} &mdash; resolves the issuer to {@code https://example.com/issuer2}</li>
+	 * <li>{@code https://example.com/authz/issuer1/oauth2/token} &mdash; resolves the issuer to {@code https://example.com/authz/issuer1}</li>
+	 * <li>{@code https://example.com/authz/issuer2/oauth2/token} &mdash; resolves the issuer to {@code https://example.com/authz/issuer2}</li>
+	 * </ul>
+	 *
+	 * @return {@link AuthorizationServerSettings#getIssuer()} if available, otherwise, resolves the issuer identifier from the <i>"current"</i> request
 	 */
 	 */
 	String getIssuer();
 	String getIssuer();
 
 

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

@@ -1,5 +1,5 @@
 /*
 /*
- * Copyright 2020-2023 the original author or authors.
+ * Copyright 2020-2024 the original author or authors.
  *
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * you may not use this file except in compliance with the License.
@@ -57,7 +57,7 @@ public final class OidcProviderConfigurationEndpointFilter extends OncePerReques
 	/**
 	/**
 	 * The default endpoint {@code URI} for OpenID Provider Configuration requests.
 	 * The default endpoint {@code URI} for OpenID Provider Configuration requests.
 	 */
 	 */
-	private static final String DEFAULT_OIDC_PROVIDER_CONFIGURATION_ENDPOINT_URI = "/.well-known/openid-configuration";
+	private static final String DEFAULT_OIDC_PROVIDER_CONFIGURATION_ENDPOINT_URI = "/**/.well-known/openid-configuration";
 
 
 	private final RequestMatcher requestMatcher = new AntPathRequestMatcher(
 	private final RequestMatcher requestMatcher = new AntPathRequestMatcher(
 			DEFAULT_OIDC_PROVIDER_CONFIGURATION_ENDPOINT_URI,
 			DEFAULT_OIDC_PROVIDER_CONFIGURATION_ENDPOINT_URI,

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

@@ -1,5 +1,5 @@
 /*
 /*
- * Copyright 2020-2023 the original author or authors.
+ * Copyright 2020-2024 the original author or authors.
  *
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * you may not use this file except in compliance with the License.
@@ -55,7 +55,7 @@ public final class OAuth2AuthorizationServerMetadataEndpointFilter extends OnceP
 	/**
 	/**
 	 * The default endpoint {@code URI} for OAuth 2.0 Authorization Server Metadata requests.
 	 * The default endpoint {@code URI} for OAuth 2.0 Authorization Server Metadata requests.
 	 */
 	 */
-	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 RequestMatcher requestMatcher = new AntPathRequestMatcher(
 	private final RequestMatcher requestMatcher = new AntPathRequestMatcher(
 			DEFAULT_OAUTH2_AUTHORIZATION_SERVER_METADATA_ENDPOINT_URI,
 			DEFAULT_OAUTH2_AUTHORIZATION_SERVER_METADATA_ENDPOINT_URI,

+ 4 - 5
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2DeviceAuthorizationEndpointFilter.java

@@ -1,5 +1,5 @@
 /*
 /*
- * Copyright 2020-2023 the original author or authors.
+ * Copyright 2020-2024 the original author or authors.
  *
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * you may not use this file except in compliance with the License.
@@ -40,13 +40,13 @@ import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
 import org.springframework.security.oauth2.core.http.converter.OAuth2DeviceAuthorizationResponseHttpMessageConverter;
 import org.springframework.security.oauth2.core.http.converter.OAuth2DeviceAuthorizationResponseHttpMessageConverter;
 import org.springframework.security.oauth2.server.authorization.authentication.OAuth2DeviceAuthorizationRequestAuthenticationProvider;
 import org.springframework.security.oauth2.server.authorization.authentication.OAuth2DeviceAuthorizationRequestAuthenticationProvider;
 import org.springframework.security.oauth2.server.authorization.authentication.OAuth2DeviceAuthorizationRequestAuthenticationToken;
 import org.springframework.security.oauth2.server.authorization.authentication.OAuth2DeviceAuthorizationRequestAuthenticationToken;
-import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContextHolder;
 import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2DeviceAuthorizationRequestAuthenticationConverter;
 import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2DeviceAuthorizationRequestAuthenticationConverter;
 import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2ErrorAuthenticationFailureHandler;
 import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2ErrorAuthenticationFailureHandler;
 import org.springframework.security.web.authentication.AuthenticationConverter;
 import org.springframework.security.web.authentication.AuthenticationConverter;
 import org.springframework.security.web.authentication.AuthenticationFailureHandler;
 import org.springframework.security.web.authentication.AuthenticationFailureHandler;
 import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
 import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
 import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
 import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
+import org.springframework.security.web.util.UrlUtils;
 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.util.Assert;
@@ -199,9 +199,8 @@ public final class OAuth2DeviceAuthorizationEndpointFilter extends OncePerReques
 		OAuth2UserCode userCode = deviceAuthorizationRequestAuthentication.getUserCode();
 		OAuth2UserCode userCode = deviceAuthorizationRequestAuthentication.getUserCode();
 
 
 		// Generate the fully-qualified verification URI
 		// Generate the fully-qualified verification URI
-		String issuerUri = AuthorizationServerContextHolder.getContext().getIssuer();
-		UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromHttpUrl(issuerUri)
-				.path(this.verificationUri);
+		UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromHttpUrl(UrlUtils.buildFullRequestUrl(request))
+				.replacePath(this.verificationUri);
 		String verificationUri = uriComponentsBuilder.build().toUriString();
 		String verificationUri = uriComponentsBuilder.build().toUriString();
 		// @formatter:off
 		// @formatter:off
 		String verificationUriComplete = uriComponentsBuilder
 		String verificationUriComplete = uriComponentsBuilder

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

@@ -0,0 +1,133 @@
+/*
+ * Copyright 2020-2024 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.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicReference;
+
+import jakarta.servlet.FilterChain;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.mock.web.MockHttpServletResponse;
+import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContextHolder;
+import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Tests for {@link AuthorizationServerContextFilter}.
+ *
+ * @author Joe Grandja
+ */
+class AuthorizationServerContextFilterTests {
+	private static final String SCHEME = "https";
+	private static final String HOST = "example.com";
+	private static final int PORT = 8443;
+	private static final String DEFAULT_ISSUER = SCHEME + "://" + HOST + ":" + PORT;
+	private AuthorizationServerContextFilter filter;
+
+	@Test
+	public void doFilterWhenDefaultEndpointsThenIssuerResolved() throws Exception {
+		AuthorizationServerSettings authorizationServerSettings = AuthorizationServerSettings.builder().build();
+		this.filter = new AuthorizationServerContextFilter(authorizationServerSettings);
+
+		String issuerPath = "/issuer1";
+		String issuerWithPath = DEFAULT_ISSUER.concat(issuerPath);
+		Set<String> endpointUris = getEndpointUris(authorizationServerSettings);
+
+		for (String endpointUri : endpointUris) {
+			assertResolvedIssuer(issuerPath.concat(endpointUri), issuerWithPath);
+		}
+	}
+
+	@Test
+	public void doFilterWhenCustomEndpointsThenIssuerResolved() throws Exception {
+		AuthorizationServerSettings authorizationServerSettings = AuthorizationServerSettings.builder()
+				.authorizationEndpoint("/oauth2/v1/authorize")
+				.deviceAuthorizationEndpoint("/oauth2/v1/device_authorization")
+				.deviceVerificationEndpoint("/oauth2/v1/device_verification")
+				.tokenEndpoint("/oauth2/v1/token")
+				.jwkSetEndpoint("/oauth2/v1/jwks")
+				.tokenRevocationEndpoint("/oauth2/v1/revoke")
+				.tokenIntrospectionEndpoint("/oauth2/v1/introspect")
+				.oidcClientRegistrationEndpoint("/connect/v1/register")
+				.oidcUserInfoEndpoint("/v1/userinfo")
+				.oidcLogoutEndpoint("/connect/v1/logout")
+				.build();
+		this.filter = new AuthorizationServerContextFilter(authorizationServerSettings);
+
+		String issuerPath = "/issuer2";
+		String issuerWithPath = DEFAULT_ISSUER.concat(issuerPath);
+		Set<String> endpointUris = getEndpointUris(authorizationServerSettings);
+
+		for (String endpointUri : endpointUris) {
+			assertResolvedIssuer(issuerPath.concat(endpointUri), issuerWithPath);
+		}
+	}
+
+	@Test
+	public void doFilterWhenIssuerHasMultiplePathsThenIssuerResolved() throws Exception {
+		AuthorizationServerSettings authorizationServerSettings = AuthorizationServerSettings.builder().build();
+		this.filter = new AuthorizationServerContextFilter(authorizationServerSettings);
+
+		String issuerPath = "/path1/path2/issuer3";
+		String issuerWithPath = DEFAULT_ISSUER.concat(issuerPath);
+		Set<String> endpointUris = getEndpointUris(authorizationServerSettings);
+
+		for (String endpointUri : endpointUris) {
+			assertResolvedIssuer(issuerPath.concat(endpointUri), issuerWithPath);
+		}
+	}
+
+	private void assertResolvedIssuer(String requestUri, String expectedIssuer) throws Exception {
+		MockHttpServletRequest request = createRequest(requestUri);
+		MockHttpServletResponse response = new MockHttpServletResponse();
+
+		AtomicReference<String> resolvedIssuer = new AtomicReference<>();
+		FilterChain filterChain = (req, resp) ->
+				resolvedIssuer.set(AuthorizationServerContextHolder.getContext().getIssuer());
+
+		this.filter.doFilter(request, response, filterChain);
+
+		assertThat(resolvedIssuer.get()).isEqualTo(expectedIssuer);
+	}
+
+	private static Set<String> getEndpointUris(AuthorizationServerSettings authorizationServerSettings) {
+		Set<String> endpointUris = new HashSet<>();
+		endpointUris.add("/.well-known/oauth-authorization-server");
+		endpointUris.add("/.well-known/openid-configuration");
+		for (Map.Entry<String, Object> setting : authorizationServerSettings.getSettings().entrySet()) {
+			if (setting.getKey().endsWith("-endpoint")) {
+				endpointUris.add((String) setting.getValue());
+			}
+		}
+		return endpointUris;
+	}
+
+	private static MockHttpServletRequest createRequest(String requestUri) {
+		MockHttpServletRequest request = new MockHttpServletRequest();
+		request.setRequestURI(requestUri);
+		request.setScheme(SCHEME);
+		request.setServerName(HOST);
+		request.setServerPort(PORT);
+		return request;
+	}
+
+}

+ 9 - 1
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/JwkSetTests.java

@@ -1,5 +1,5 @@
 /*
 /*
- * Copyright 2020-2022 the original author or authors.
+ * Copyright 2020-2024 the original author or authors.
  *
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * you may not use this file except in compliance with the License.
@@ -112,6 +112,14 @@ public class JwkSetTests {
 		assertJwkSetRequestThenReturnKeys(authorizationServerSettings.getJwkSetEndpoint());
 		assertJwkSetRequestThenReturnKeys(authorizationServerSettings.getJwkSetEndpoint());
 	}
 	}
 
 
+	@Test
+	public void requestWhenJwkSetRequestIncludesIssuerPathThenReturnKeys() throws Exception {
+		this.spring.register(AuthorizationServerConfigurationCustomEndpoints.class).autowire();
+
+		String issuer = "https://example.com:8443/issuer1";
+		assertJwkSetRequestThenReturnKeys(issuer.concat(authorizationServerSettings.getJwkSetEndpoint()));
+	}
+
 	private void assertJwkSetRequestThenReturnKeys(String jwkSetEndpointUri) throws Exception {
 	private void assertJwkSetRequestThenReturnKeys(String jwkSetEndpointUri) throws Exception {
 		this.mvc.perform(get(jwkSetEndpointUri))
 		this.mvc.perform(get(jwkSetEndpointUri))
 				.andExpect(status().isOk())
 				.andExpect(status().isOk())

+ 41 - 1
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2AuthorizationCodeGrantTests.java

@@ -1,5 +1,5 @@
 /*
 /*
- * Copyright 2020-2023 the original author or authors.
+ * Copyright 2020-2024 the original author or authors.
  *
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * you may not use this file except in compliance with the License.
@@ -826,6 +826,46 @@ public class OAuth2AuthorizationCodeGrantTests {
 		assertThat(securityContext.getAuthentication()).isNull();
 		assertThat(securityContext.getAuthentication()).isNull();
 	}
 	}
 
 
+	@Test
+	public void requestWhenAuthorizationAndTokenRequestIncludesIssuerPathThenIssuerResolvedWithPath() throws Exception {
+		this.spring.register(AuthorizationServerConfigurationWithTokenGenerator.class).autowire();
+
+		RegisteredClient registeredClient = TestRegisteredClients.registeredPublicClient().build();
+		this.registeredClientRepository.save(registeredClient);
+
+		String issuer = "https://example.com:8443/issuer1";
+
+		MvcResult mvcResult = this.mvc.perform(get(issuer.concat(DEFAULT_AUTHORIZATION_ENDPOINT_URI))
+						.queryParams(getAuthorizationRequestParameters(registeredClient))
+						.queryParam(PkceParameterNames.CODE_CHALLENGE, S256_CODE_CHALLENGE)
+						.queryParam(PkceParameterNames.CODE_CHALLENGE_METHOD, "S256")
+						.with(user("user")))
+				.andExpect(status().is3xxRedirection())
+				.andReturn();
+
+		String authorizationCode = extractParameterFromRedirectUri(mvcResult.getResponse().getRedirectedUrl(), "code");
+		OAuth2Authorization authorizationCodeAuthorization = this.authorizationService.findByToken(authorizationCode, AUTHORIZATION_CODE_TOKEN_TYPE);
+
+		this.mvc.perform(post(issuer.concat(DEFAULT_TOKEN_ENDPOINT_URI))
+				.params(getTokenRequestParameters(registeredClient, authorizationCodeAuthorization))
+				.param(OAuth2ParameterNames.CLIENT_ID, registeredClient.getClientId())
+				.param(PkceParameterNames.CODE_VERIFIER, S256_CODE_VERIFIER))
+				.andExpect(header().string(HttpHeaders.CACHE_CONTROL, containsString("no-store")))
+				.andExpect(header().string(HttpHeaders.PRAGMA, containsString("no-cache")))
+				.andExpect(status().isOk())
+				.andExpect(jsonPath("$.access_token").isNotEmpty())
+				.andExpect(jsonPath("$.token_type").isNotEmpty())
+				.andExpect(jsonPath("$.expires_in").isNotEmpty())
+				.andExpect(jsonPath("$.refresh_token").doesNotExist())
+				.andExpect(jsonPath("$.scope").isNotEmpty())
+				.andReturn();
+
+		ArgumentCaptor<OAuth2TokenContext> tokenContextCaptor = ArgumentCaptor.forClass(OAuth2TokenContext.class);
+		verify(tokenGenerator).generate(tokenContextCaptor.capture());
+		OAuth2TokenContext tokenContext = tokenContextCaptor.getValue();
+		assertThat(tokenContext.getAuthorizationServerContext().getIssuer()).isEqualTo(issuer);
+	}
+
 	private static MultiValueMap<String, String> getAuthorizationRequestParameters(RegisteredClient registeredClient) {
 	private static MultiValueMap<String, String> getAuthorizationRequestParameters(RegisteredClient registeredClient) {
 		MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
 		MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
 		parameters.set(OAuth2ParameterNames.RESPONSE_TYPE, OAuth2AuthorizationResponseType.CODE.getValue());
 		parameters.set(OAuth2ParameterNames.RESPONSE_TYPE, OAuth2AuthorizationResponseType.CODE.getValue());

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

@@ -1,5 +1,5 @@
 /*
 /*
- * Copyright 2020-2023 the original author or authors.
+ * Copyright 2020-2024 the original author or authors.
  *
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * you may not use this file except in compliance with the License.
@@ -65,7 +65,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
 @ExtendWith(SpringTestContextExtension.class)
 @ExtendWith(SpringTestContextExtension.class)
 public class OAuth2AuthorizationServerMetadataTests {
 public class OAuth2AuthorizationServerMetadataTests {
 	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 static final String ISSUER_URL = "https://example.com";
+	private static final String ISSUER = "https://example.com";
 	private static EmbeddedDatabase db;
 	private static EmbeddedDatabase db;
 	private static JWKSource<SecurityContext> jwkSource;
 	private static JWKSource<SecurityContext> jwkSource;
 
 
@@ -105,19 +105,37 @@ public class OAuth2AuthorizationServerMetadataTests {
 	public void requestWhenAuthorizationServerMetadataRequestAndIssuerSetThenUsed() throws Exception {
 	public void requestWhenAuthorizationServerMetadataRequestAndIssuerSetThenUsed() throws Exception {
 		this.spring.register(AuthorizationServerConfiguration.class).autowire();
 		this.spring.register(AuthorizationServerConfiguration.class).autowire();
 
 
-		this.mvc.perform(get(ISSUER_URL.concat(DEFAULT_OAUTH2_AUTHORIZATION_SERVER_METADATA_ENDPOINT_URI)))
+		this.mvc.perform(get(ISSUER.concat(DEFAULT_OAUTH2_AUTHORIZATION_SERVER_METADATA_ENDPOINT_URI)))
 				.andExpect(status().is2xxSuccessful())
 				.andExpect(status().is2xxSuccessful())
-				.andExpect(jsonPath("issuer").value(ISSUER_URL))
+				.andExpect(jsonPath("issuer").value(ISSUER))
 				.andReturn();
 				.andReturn();
 	}
 	}
 
 
 	@Test
 	@Test
-	public void requestWhenAuthorizationServerMetadataRequestAndIssuerNotSetThenResolveFromRequest() throws Exception {
+	public void requestWhenAuthorizationServerMetadataRequestIncludesIssuerPathThenMetadataResponseHasIssuerPath() throws Exception {
 		this.spring.register(AuthorizationServerConfigurationWithIssuerNotSet.class).autowire();
 		this.spring.register(AuthorizationServerConfigurationWithIssuerNotSet.class).autowire();
 
 
-		this.mvc.perform(get("http://localhost".concat(DEFAULT_OAUTH2_AUTHORIZATION_SERVER_METADATA_ENDPOINT_URI)))
+		String host = "https://example.com:8443";
+
+		String issuerPath = "/issuer1";
+		String issuer = host.concat(issuerPath);
+		this.mvc.perform(get(host.concat(DEFAULT_OAUTH2_AUTHORIZATION_SERVER_METADATA_ENDPOINT_URI).concat(issuerPath)))
+				.andExpect(status().is2xxSuccessful())
+				.andExpect(jsonPath("issuer").value(issuer))
+				.andReturn();
+
+		issuerPath = "/path1/issuer2";
+		issuer = host.concat(issuerPath);
+		this.mvc.perform(get(host.concat(DEFAULT_OAUTH2_AUTHORIZATION_SERVER_METADATA_ENDPOINT_URI).concat(issuerPath)))
+				.andExpect(status().is2xxSuccessful())
+				.andExpect(jsonPath("issuer").value(issuer))
+				.andReturn();
+
+		issuerPath = "/path1/path2/issuer3";
+		issuer = host.concat(issuerPath);
+		this.mvc.perform(get(host.concat(DEFAULT_OAUTH2_AUTHORIZATION_SERVER_METADATA_ENDPOINT_URI).concat(issuerPath)))
 				.andExpect(status().is2xxSuccessful())
 				.andExpect(status().is2xxSuccessful())
-				.andExpect(jsonPath("issuer").value("http://localhost"))
+				.andExpect(jsonPath("issuer").value(issuer))
 				.andReturn();
 				.andReturn();
 	}
 	}
 
 
@@ -126,7 +144,7 @@ public class OAuth2AuthorizationServerMetadataTests {
 	public void requestWhenAuthorizationServerMetadataRequestAndMetadataCustomizerSetThenReturnCustomMetadataResponse() throws Exception {
 	public void requestWhenAuthorizationServerMetadataRequestAndMetadataCustomizerSetThenReturnCustomMetadataResponse() throws Exception {
 		this.spring.register(AuthorizationServerConfigurationWithMetadataCustomizer.class).autowire();
 		this.spring.register(AuthorizationServerConfigurationWithMetadataCustomizer.class).autowire();
 
 
-		this.mvc.perform(get(ISSUER_URL.concat(DEFAULT_OAUTH2_AUTHORIZATION_SERVER_METADATA_ENDPOINT_URI)))
+		this.mvc.perform(get(ISSUER.concat(DEFAULT_OAUTH2_AUTHORIZATION_SERVER_METADATA_ENDPOINT_URI)))
 				.andExpect(status().is2xxSuccessful())
 				.andExpect(status().is2xxSuccessful())
 				.andExpect(jsonPath(OAuth2AuthorizationServerMetadataClaimNames.SCOPES_SUPPORTED,
 				.andExpect(jsonPath(OAuth2AuthorizationServerMetadataClaimNames.SCOPES_SUPPORTED,
 						hasItems("scope1", "scope2")));
 						hasItems("scope1", "scope2")));
@@ -156,7 +174,7 @@ public class OAuth2AuthorizationServerMetadataTests {
 
 
 		@Bean
 		@Bean
 		AuthorizationServerSettings authorizationServerSettings() {
 		AuthorizationServerSettings authorizationServerSettings() {
-			return AuthorizationServerSettings.builder().issuer(ISSUER_URL).build();
+			return AuthorizationServerSettings.builder().issuer(ISSUER).build();
 		}
 		}
 	}
 	}
 
 

+ 25 - 1
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2ClientCredentialsGrantTests.java

@@ -1,5 +1,5 @@
 /*
 /*
- * Copyright 2020-2023 the original author or authors.
+ * Copyright 2020-2024 the original author or authors.
  *
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * you may not use this file except in compliance with the License.
@@ -357,6 +357,30 @@ public class OAuth2ClientCredentialsGrantTests {
 		verify(authenticationSuccessHandler).onAuthenticationSuccess(any(), any(), eq(clientPrincipal));
 		verify(authenticationSuccessHandler).onAuthenticationSuccess(any(), any(), eq(clientPrincipal));
 	}
 	}
 
 
+	@Test
+	public void requestWhenTokenRequestIncludesIssuerPathThenIssuerResolvedWithPath() throws Exception {
+		this.spring.register(AuthorizationServerConfiguration.class).autowire();
+
+		RegisteredClient registeredClient = TestRegisteredClients.registeredClient2().build();
+		this.registeredClientRepository.save(registeredClient);
+
+		String issuer = "https://example.com:8443/issuer1";
+
+		this.mvc.perform(post(issuer.concat(DEFAULT_TOKEN_ENDPOINT_URI))
+						.param(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())
+						.param(OAuth2ParameterNames.SCOPE, "scope1 scope2")
+						.header(HttpHeaders.AUTHORIZATION, "Basic " + encodeBasicAuth(
+								registeredClient.getClientId(), registeredClient.getClientSecret())))
+				.andExpect(status().isOk())
+				.andExpect(jsonPath("$.access_token").isNotEmpty())
+				.andExpect(jsonPath("$.scope").value("scope1 scope2"));
+
+		ArgumentCaptor<JwtEncodingContext> jwtEncodingContextCaptor = ArgumentCaptor.forClass(JwtEncodingContext.class);
+		verify(jwtCustomizer).customize(jwtEncodingContextCaptor.capture());
+		JwtEncodingContext jwtEncodingContext = jwtEncodingContextCaptor.getValue();
+		assertThat(jwtEncodingContext.getAuthorizationServerContext().getIssuer()).isEqualTo(issuer);
+	}
+
 	private static String encodeBasicAuth(String clientId, String secret) throws Exception {
 	private static String encodeBasicAuth(String clientId, String secret) throws Exception {
 		clientId = URLEncoder.encode(clientId, StandardCharsets.UTF_8.name());
 		clientId = URLEncoder.encode(clientId, StandardCharsets.UTF_8.name());
 		secret = URLEncoder.encode(secret, StandardCharsets.UTF_8.name());
 		secret = URLEncoder.encode(secret, StandardCharsets.UTF_8.name());

+ 9 - 5
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2DeviceCodeGrantTests.java

@@ -1,5 +1,5 @@
 /*
 /*
- * Copyright 2020-2023 the original author or authors.
+ * Copyright 2020-2024 the original author or authors.
  *
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * you may not use this file except in compliance with the License.
@@ -218,8 +218,10 @@ public class OAuth2DeviceCodeGrantTests {
 		parameters.set(OAuth2ParameterNames.SCOPE,
 		parameters.set(OAuth2ParameterNames.SCOPE,
 				StringUtils.collectionToDelimitedString(registeredClient.getScopes(), " "));
 				StringUtils.collectionToDelimitedString(registeredClient.getScopes(), " "));
 
 
+		String issuer = "https://example.com:8443/issuer1";
+
 		// @formatter:off
 		// @formatter:off
-		MvcResult mvcResult = this.mvc.perform(post(DEFAULT_DEVICE_AUTHORIZATION_ENDPOINT_URI)
+		MvcResult mvcResult = this.mvc.perform(post(issuer.concat(DEFAULT_DEVICE_AUTHORIZATION_ENDPOINT_URI))
 				.params(parameters)
 				.params(parameters)
 				.headers(withClientAuth(registeredClient)))
 				.headers(withClientAuth(registeredClient)))
 				.andExpect(status().isOk())
 				.andExpect(status().isOk())
@@ -240,9 +242,9 @@ public class OAuth2DeviceCodeGrantTests {
 		String userCode = deviceAuthorizationResponse.getUserCode().getTokenValue();
 		String userCode = deviceAuthorizationResponse.getUserCode().getTokenValue();
 		assertThat(userCode).matches("[A-Z]{4}-[A-Z]{4}");
 		assertThat(userCode).matches("[A-Z]{4}-[A-Z]{4}");
 		assertThat(deviceAuthorizationResponse.getVerificationUri())
 		assertThat(deviceAuthorizationResponse.getVerificationUri())
-				.isEqualTo("http://localhost/oauth2/device_verification");
+				.isEqualTo("https://example.com:8443/oauth2/device_verification");
 		assertThat(deviceAuthorizationResponse.getVerificationUriComplete())
 		assertThat(deviceAuthorizationResponse.getVerificationUriComplete())
-				.isEqualTo("http://localhost/oauth2/device_verification?user_code=" + userCode);
+				.isEqualTo("https://example.com:8443/oauth2/device_verification?user_code=" + userCode);
 
 
 		String deviceCode = deviceAuthorizationResponse.getDeviceCode().getTokenValue();
 		String deviceCode = deviceAuthorizationResponse.getDeviceCode().getTokenValue();
 		OAuth2Authorization authorization = this.authorizationService.findByToken(deviceCode, DEVICE_CODE_TOKEN_TYPE);
 		OAuth2Authorization authorization = this.authorizationService.findByToken(deviceCode, DEVICE_CODE_TOKEN_TYPE);
@@ -311,8 +313,10 @@ public class OAuth2DeviceCodeGrantTests {
 		MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
 		MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
 		parameters.set(OAuth2ParameterNames.USER_CODE, USER_CODE);
 		parameters.set(OAuth2ParameterNames.USER_CODE, USER_CODE);
 
 
+		String issuer = "https://example.com:8443/issuer1";
+
 		// @formatter:off
 		// @formatter:off
-		MvcResult mvcResult = this.mvc.perform(get(DEFAULT_DEVICE_VERIFICATION_ENDPOINT_URI)
+		MvcResult mvcResult = this.mvc.perform(get(issuer.concat(DEFAULT_DEVICE_VERIFICATION_ENDPOINT_URI))
 				.queryParams(parameters)
 				.queryParams(parameters)
 				.with(user("user")))
 				.with(user("user")))
 				.andExpect(status().isOk())
 				.andExpect(status().isOk())

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

@@ -1,5 +1,5 @@
 /*
 /*
- * Copyright 2020-2022 the original author or authors.
+ * Copyright 2020-2024 the original author or authors.
  *
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * you may not use this file except in compliance with the License.
@@ -30,6 +30,7 @@ import java.util.function.Consumer;
 import org.junit.jupiter.api.AfterAll;
 import org.junit.jupiter.api.AfterAll;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
 import org.junit.jupiter.api.extension.ExtendWith;
 import org.mockito.ArgumentCaptor;
 import org.mockito.ArgumentCaptor;
@@ -107,6 +108,7 @@ import static org.assertj.core.api.Assertions.assertThat;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 import static org.mockito.Mockito.when;
 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
@@ -167,6 +169,18 @@ public class OAuth2TokenIntrospectionTests {
 				.build();
 				.build();
 	}
 	}
 
 
+	@SuppressWarnings("unchecked")
+	@BeforeEach
+	public void setup() {
+		reset(authenticationConverter);
+		reset(authenticationConvertersConsumer);
+		reset(authenticationProvider);
+		reset(authenticationProvidersConsumer);
+		reset(authenticationSuccessHandler);
+		reset(authenticationFailureHandler);
+		reset(accessTokenCustomizer);
+	}
+
 	@AfterEach
 	@AfterEach
 	public void tearDown() {
 	public void tearDown() {
 		jdbcOperations.update("truncate table oauth2_authorization");
 		jdbcOperations.update("truncate table oauth2_authorization");
@@ -395,6 +409,41 @@ public class OAuth2TokenIntrospectionTests {
 		verify(authenticationSuccessHandler).onAuthenticationSuccess(any(), any(), eq(tokenIntrospectionAuthentication));
 		verify(authenticationSuccessHandler).onAuthenticationSuccess(any(), any(), eq(tokenIntrospectionAuthentication));
 	}
 	}
 
 
+	@Test
+	public void requestWhenIntrospectionRequestIncludesIssuerPathThenActive() throws Exception {
+		this.spring.register(AuthorizationServerConfigurationCustomTokenIntrospectionEndpoint.class).autowire();
+
+		RegisteredClient introspectRegisteredClient = TestRegisteredClients.registeredClient2().build();
+		this.registeredClientRepository.save(introspectRegisteredClient);
+
+		RegisteredClient authorizedRegisteredClient = TestRegisteredClients.registeredClient().build();
+		this.registeredClientRepository.save(authorizedRegisteredClient);
+
+		OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(authorizedRegisteredClient).build();
+		this.authorizationService.save(authorization);
+
+		OAuth2AccessToken accessToken = authorization.getAccessToken().getToken();
+
+		Authentication clientPrincipal = new OAuth2ClientAuthenticationToken(
+				introspectRegisteredClient, ClientAuthenticationMethod.CLIENT_SECRET_BASIC, introspectRegisteredClient.getClientSecret());
+		OAuth2TokenIntrospectionAuthenticationToken tokenIntrospectionAuthentication =
+				new OAuth2TokenIntrospectionAuthenticationToken(
+						accessToken.getTokenValue(), clientPrincipal, null, null);
+
+		when(authenticationConverter.convert(any())).thenReturn(tokenIntrospectionAuthentication);
+		when(authenticationProvider.supports(eq(OAuth2TokenIntrospectionAuthenticationToken.class))).thenReturn(true);
+		when(authenticationProvider.authenticate(any())).thenReturn(tokenIntrospectionAuthentication);
+
+		String issuer = "https://example.com:8443/issuer1";
+
+		// @formatter:off
+		this.mvc.perform(post(issuer.concat(authorizationServerSettings.getTokenIntrospectionEndpoint()))
+						.params(getTokenIntrospectionRequestParameters(accessToken, OAuth2TokenType.ACCESS_TOKEN))
+						.header(HttpHeaders.AUTHORIZATION, getAuthorizationHeader(introspectRegisteredClient)))
+				.andExpect(status().isOk());
+		// @formatter:on
+	}
+
 	private static MultiValueMap<String, String> getTokenIntrospectionRequestParameters(OAuth2Token token,
 	private static MultiValueMap<String, String> getTokenIntrospectionRequestParameters(OAuth2Token token,
 			OAuth2TokenType tokenType) {
 			OAuth2TokenType tokenType) {
 		MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
 		MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();

+ 30 - 1
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2TokenRevocationTests.java

@@ -1,5 +1,5 @@
 /*
 /*
- * Copyright 2020-2022 the original author or authors.
+ * Copyright 2020-2024 the original author or authors.
  *
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * you may not use this file except in compliance with the License.
@@ -201,6 +201,35 @@ public class OAuth2TokenRevocationTests {
 		assertThat(refreshToken.isInvalidated()).isFalse();
 		assertThat(refreshToken.isInvalidated()).isFalse();
 	}
 	}
 
 
+	@Test
+	public void requestWhenRevokeAccessTokenAndRequestIncludesIssuerPathThenRevoked() throws Exception {
+		this.spring.register(AuthorizationServerConfiguration.class).autowire();
+
+		RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
+		this.registeredClientRepository.save(registeredClient);
+
+		OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient).build();
+		OAuth2AccessToken token = authorization.getAccessToken().getToken();
+		OAuth2TokenType tokenType = OAuth2TokenType.ACCESS_TOKEN;
+		this.authorizationService.save(authorization);
+
+		String issuer = "https://example.com:8443/issuer1";
+
+		// @formatter:off
+		this.mvc.perform(post(issuer.concat(DEFAULT_TOKEN_REVOCATION_ENDPOINT_URI))
+						.params(getTokenRevocationRequestParameters(token, tokenType))
+						.header(HttpHeaders.AUTHORIZATION, "Basic " + encodeBasicAuth(
+								registeredClient.getClientId(), registeredClient.getClientSecret())))
+				.andExpect(status().isOk());
+		// @formatter:on
+
+		OAuth2Authorization updatedAuthorization = this.authorizationService.findById(authorization.getId());
+		OAuth2Authorization.Token<OAuth2AccessToken> accessToken = updatedAuthorization.getAccessToken();
+		assertThat(accessToken.isInvalidated()).isTrue();
+		OAuth2Authorization.Token<OAuth2RefreshToken> refreshToken = updatedAuthorization.getRefreshToken();
+		assertThat(refreshToken.isInvalidated()).isFalse();
+	}
+
 	@Test
 	@Test
 	public void requestWhenTokenRevocationEndpointCustomizedThenUsed() throws Exception {
 	public void requestWhenTokenRevocationEndpointCustomizedThenUsed() throws Exception {
 		this.spring.register(AuthorizationServerConfigurationCustomTokenRevocationEndpoint.class).autowire();
 		this.spring.register(AuthorizationServerConfigurationCustomTokenRevocationEndpoint.class).autowire();

+ 9 - 9
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcClientRegistrationTests.java

@@ -1,5 +1,5 @@
 /*
 /*
- * Copyright 2020-2023 the original author or authors.
+ * Copyright 2020-2024 the original author or authors.
  *
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * you may not use this file except in compliance with the License.
@@ -137,6 +137,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
  */
  */
 @ExtendWith(SpringTestContextExtension.class)
 @ExtendWith(SpringTestContextExtension.class)
 public class OidcClientRegistrationTests {
 public class OidcClientRegistrationTests {
+	private static final String ISSUER = "https://example.com:8443/issuer1";
 	private static final String DEFAULT_TOKEN_ENDPOINT_URI = "/oauth2/token";
 	private static final String DEFAULT_TOKEN_ENDPOINT_URI = "/oauth2/token";
 	private static final String DEFAULT_OIDC_CLIENT_REGISTRATION_ENDPOINT_URI = "/connect/register";
 	private static final String DEFAULT_OIDC_CLIENT_REGISTRATION_ENDPOINT_URI = "/connect/register";
 	private static final HttpMessageConverter<OAuth2AccessTokenResponse> accessTokenHttpResponseConverter =
 	private static final HttpMessageConverter<OAuth2AccessTokenResponse> accessTokenHttpResponseConverter =
@@ -374,7 +375,7 @@ public class OidcClientRegistrationTests {
 
 
 		when(authenticationProvider.authenticate(any())).thenThrow(new OAuth2AuthenticationException("error"));
 		when(authenticationProvider.authenticate(any())).thenThrow(new OAuth2AuthenticationException("error"));
 
 
-		this.mvc.perform(get(DEFAULT_OIDC_CLIENT_REGISTRATION_ENDPOINT_URI)
+		this.mvc.perform(get(ISSUER.concat(DEFAULT_OIDC_CLIENT_REGISTRATION_ENDPOINT_URI))
 				.param(OAuth2ParameterNames.CLIENT_ID, "invalid").with(jwt()));
 				.param(OAuth2ParameterNames.CLIENT_ID, "invalid").with(jwt()));
 
 
 		verify(authenticationFailureHandler).onAuthenticationFailure(any(), any(), any());
 		verify(authenticationFailureHandler).onAuthenticationFailure(any(), any(), any());
@@ -399,7 +400,7 @@ public class OidcClientRegistrationTests {
 
 
 		OidcClientRegistration clientRegistrationResponse = registerClient(clientRegistration);
 		OidcClientRegistration clientRegistrationResponse = registerClient(clientRegistration);
 
 
-		this.mvc.perform(post(DEFAULT_TOKEN_ENDPOINT_URI)
+		this.mvc.perform(post(ISSUER.concat(DEFAULT_TOKEN_ENDPOINT_URI))
 						.param(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())
 						.param(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())
 						.param(OAuth2ParameterNames.SCOPE, "scope1")
 						.param(OAuth2ParameterNames.SCOPE, "scope1")
 						.with(httpBasic(clientRegistrationResponse.getClientId(), clientRegistrationResponse.getClientSecret())))
 						.with(httpBasic(clientRegistrationResponse.getClientId(), clientRegistrationResponse.getClientSecret())))
@@ -436,7 +437,7 @@ public class OidcClientRegistrationTests {
 		JwtClaimsSet jwtClaimsSet = JwtClaimsSet.builder()
 		JwtClaimsSet jwtClaimsSet = JwtClaimsSet.builder()
 				.issuer(clientRegistrationResponse.getClientId())
 				.issuer(clientRegistrationResponse.getClientId())
 				.subject(clientRegistrationResponse.getClientId())
 				.subject(clientRegistrationResponse.getClientId())
-				.audience(Collections.singletonList(asUrl(this.authorizationServerSettings.getIssuer(), this.authorizationServerSettings.getTokenEndpoint())))
+				.audience(Collections.singletonList(asUrl(ISSUER, this.authorizationServerSettings.getTokenEndpoint())))
 				.issuedAt(issuedAt)
 				.issuedAt(issuedAt)
 				.expiresAt(expiresAt)
 				.expiresAt(expiresAt)
 				.build();
 				.build();
@@ -447,7 +448,7 @@ public class OidcClientRegistrationTests {
 
 
 		Jwt jwtAssertion = jwtClientAssertionEncoder.encode(JwtEncoderParameters.from(jwsHeader, jwtClaimsSet));
 		Jwt jwtAssertion = jwtClientAssertionEncoder.encode(JwtEncoderParameters.from(jwsHeader, jwtClaimsSet));
 
 
-		this.mvc.perform(post(DEFAULT_TOKEN_ENDPOINT_URI)
+		this.mvc.perform(post(ISSUER.concat(DEFAULT_TOKEN_ENDPOINT_URI))
 						.param(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())
 						.param(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())
 						.param(OAuth2ParameterNames.SCOPE, "scope1")
 						.param(OAuth2ParameterNames.SCOPE, "scope1")
 						.param(OAuth2ParameterNames.CLIENT_ASSERTION_TYPE, "urn:ietf:params:oauth:client-assertion-type:jwt-bearer")
 						.param(OAuth2ParameterNames.CLIENT_ASSERTION_TYPE, "urn:ietf:params:oauth:client-assertion-type:jwt-bearer")
@@ -520,7 +521,7 @@ public class OidcClientRegistrationTests {
 		// @formatter:on
 		// @formatter:on
 		Jwt jwtAssertion = jwtClientAssertionEncoder.encode(JwtEncoderParameters.from(jwsHeader, jwtClaimsSet));
 		Jwt jwtAssertion = jwtClientAssertionEncoder.encode(JwtEncoderParameters.from(jwsHeader, jwtClaimsSet));
 
 
-		MvcResult mvcResult = this.mvc.perform(post(DEFAULT_TOKEN_ENDPOINT_URI)
+		MvcResult mvcResult = this.mvc.perform(post(ISSUER.concat(DEFAULT_TOKEN_ENDPOINT_URI))
 				.param(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())
 				.param(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())
 				.param(OAuth2ParameterNames.SCOPE, clientRegistrationScope)
 				.param(OAuth2ParameterNames.SCOPE, clientRegistrationScope)
 				.param(OAuth2ParameterNames.CLIENT_ASSERTION_TYPE, "urn:ietf:params:oauth:client-assertion-type:jwt-bearer")
 				.param(OAuth2ParameterNames.CLIENT_ASSERTION_TYPE, "urn:ietf:params:oauth:client-assertion-type:jwt-bearer")
@@ -539,7 +540,7 @@ public class OidcClientRegistrationTests {
 		httpHeaders.setBearerAuth(accessToken.getTokenValue());
 		httpHeaders.setBearerAuth(accessToken.getTokenValue());
 
 
 		// Register the client
 		// Register the client
-		mvcResult = this.mvc.perform(post(DEFAULT_OIDC_CLIENT_REGISTRATION_ENDPOINT_URI)
+		mvcResult = this.mvc.perform(post(ISSUER.concat(DEFAULT_OIDC_CLIENT_REGISTRATION_ENDPOINT_URI))
 				.headers(httpHeaders)
 				.headers(httpHeaders)
 				.contentType(MediaType.APPLICATION_JSON)
 				.contentType(MediaType.APPLICATION_JSON)
 				.content(getClientRegistrationRequestContent(clientRegistration)))
 				.content(getClientRegistrationRequestContent(clientRegistration)))
@@ -557,7 +558,7 @@ public class OidcClientRegistrationTests {
 		return JwtClaimsSet.builder()
 		return JwtClaimsSet.builder()
 				.issuer(registeredClient.getClientId())
 				.issuer(registeredClient.getClientId())
 				.subject(registeredClient.getClientId())
 				.subject(registeredClient.getClientId())
-				.audience(Collections.singletonList(asUrl(this.authorizationServerSettings.getIssuer(), this.authorizationServerSettings.getTokenEndpoint())))
+				.audience(Collections.singletonList(asUrl(ISSUER, this.authorizationServerSettings.getTokenEndpoint())))
 				.issuedAt(issuedAt)
 				.issuedAt(issuedAt)
 				.expiresAt(expiresAt);
 				.expiresAt(expiresAt);
 	}
 	}
@@ -734,7 +735,6 @@ public class OidcClientRegistrationTests {
 		@Bean
 		@Bean
 		AuthorizationServerSettings authorizationServerSettings() {
 		AuthorizationServerSettings authorizationServerSettings() {
 			return AuthorizationServerSettings.builder()
 			return AuthorizationServerSettings.builder()
-					.issuer("https://auth-server:9000")
 					.build();
 					.build();
 		}
 		}
 
 

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

@@ -1,5 +1,5 @@
 /*
 /*
- * Copyright 2020-2023 the original author or authors.
+ * Copyright 2020-2024 the original author or authors.
  *
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * you may not use this file except in compliance with the License.
@@ -63,7 +63,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
 @ExtendWith(SpringTestContextExtension.class)
 @ExtendWith(SpringTestContextExtension.class)
 public class OidcProviderConfigurationTests {
 public class OidcProviderConfigurationTests {
 	private static final String DEFAULT_OIDC_PROVIDER_CONFIGURATION_ENDPOINT_URI = "/.well-known/openid-configuration";
 	private static final String DEFAULT_OIDC_PROVIDER_CONFIGURATION_ENDPOINT_URI = "/.well-known/openid-configuration";
-	private static final String ISSUER_URL = "https://example.com";
+	private static final String ISSUER = "https://example.com";
 
 
 	public final SpringTestContext spring = new SpringTestContext();
 	public final SpringTestContext spring = new SpringTestContext();
 
 
@@ -77,9 +77,29 @@ public class OidcProviderConfigurationTests {
 	public void requestWhenConfigurationRequestAndIssuerSetThenReturnDefaultConfigurationResponse() throws Exception {
 	public void requestWhenConfigurationRequestAndIssuerSetThenReturnDefaultConfigurationResponse() throws Exception {
 		this.spring.register(AuthorizationServerConfiguration.class).autowire();
 		this.spring.register(AuthorizationServerConfiguration.class).autowire();
 
 
-		this.mvc.perform(get(ISSUER_URL.concat(DEFAULT_OIDC_PROVIDER_CONFIGURATION_ENDPOINT_URI)))
+		this.mvc.perform(get(ISSUER.concat(DEFAULT_OIDC_PROVIDER_CONFIGURATION_ENDPOINT_URI)))
 				.andExpect(status().is2xxSuccessful())
 				.andExpect(status().is2xxSuccessful())
-				.andExpectAll(defaultConfigurationMatchers());
+				.andExpectAll(defaultConfigurationMatchers(ISSUER));
+	}
+
+	@Test
+	public void requestWhenConfigurationRequestIncludesIssuerPathThenConfigurationResponseHasIssuerPath() throws Exception {
+		this.spring.register(AuthorizationServerConfigurationWithIssuerNotSet.class).autowire();
+
+		String issuer = "https://example.com:8443/issuer1";
+		this.mvc.perform(get(issuer.concat(DEFAULT_OIDC_PROVIDER_CONFIGURATION_ENDPOINT_URI)))
+				.andExpect(status().is2xxSuccessful())
+				.andExpectAll(defaultConfigurationMatchers(issuer));
+
+		issuer = "https://example.com:8443/path1/issuer2";
+		this.mvc.perform(get(issuer.concat(DEFAULT_OIDC_PROVIDER_CONFIGURATION_ENDPOINT_URI)))
+				.andExpect(status().is2xxSuccessful())
+				.andExpectAll(defaultConfigurationMatchers(issuer));
+
+		issuer = "https://example.com:8443/path1/path2/issuer3";
+		this.mvc.perform(get(issuer.concat(DEFAULT_OIDC_PROVIDER_CONFIGURATION_ENDPOINT_URI)))
+				.andExpect(status().is2xxSuccessful())
+				.andExpectAll(defaultConfigurationMatchers(issuer));
 	}
 	}
 
 
 	// gh-632
 	// gh-632
@@ -87,10 +107,10 @@ public class OidcProviderConfigurationTests {
 	public void requestWhenConfigurationRequestAndUserAuthenticatedThenReturnConfigurationResponse() throws Exception {
 	public void requestWhenConfigurationRequestAndUserAuthenticatedThenReturnConfigurationResponse() throws Exception {
 		this.spring.register(AuthorizationServerConfiguration.class).autowire();
 		this.spring.register(AuthorizationServerConfiguration.class).autowire();
 
 
-		this.mvc.perform(get(ISSUER_URL.concat(DEFAULT_OIDC_PROVIDER_CONFIGURATION_ENDPOINT_URI))
+		this.mvc.perform(get(ISSUER.concat(DEFAULT_OIDC_PROVIDER_CONFIGURATION_ENDPOINT_URI))
 				.with(user("user")))
 				.with(user("user")))
 				.andExpect(status().is2xxSuccessful())
 				.andExpect(status().is2xxSuccessful())
-				.andExpectAll(defaultConfigurationMatchers());
+				.andExpectAll(defaultConfigurationMatchers(ISSUER));
 	}
 	}
 
 
 	// gh-616
 	// gh-616
@@ -98,7 +118,7 @@ public class OidcProviderConfigurationTests {
 	public void requestWhenConfigurationRequestAndConfigurationCustomizerSetThenReturnCustomConfigurationResponse() throws Exception {
 	public void requestWhenConfigurationRequestAndConfigurationCustomizerSetThenReturnCustomConfigurationResponse() throws Exception {
 		this.spring.register(AuthorizationServerConfigurationWithProviderConfigurationCustomizer.class).autowire();
 		this.spring.register(AuthorizationServerConfigurationWithProviderConfigurationCustomizer.class).autowire();
 
 
-		this.mvc.perform(get(ISSUER_URL.concat(DEFAULT_OIDC_PROVIDER_CONFIGURATION_ENDPOINT_URI)))
+		this.mvc.perform(get(ISSUER.concat(DEFAULT_OIDC_PROVIDER_CONFIGURATION_ENDPOINT_URI)))
 				.andExpect(status().is2xxSuccessful())
 				.andExpect(status().is2xxSuccessful())
 				.andExpect(jsonPath(OAuth2AuthorizationServerMetadataClaimNames.SCOPES_SUPPORTED,
 				.andExpect(jsonPath(OAuth2AuthorizationServerMetadataClaimNames.SCOPES_SUPPORTED,
 						hasItems(OidcScopes.OPENID, OidcScopes.PROFILE, OidcScopes.EMAIL)));
 						hasItems(OidcScopes.OPENID, OidcScopes.PROFILE, OidcScopes.EMAIL)));
@@ -108,35 +128,35 @@ public class OidcProviderConfigurationTests {
 	public void requestWhenConfigurationRequestAndClientRegistrationEnabledThenConfigurationResponseIncludesRegistrationEndpoint() throws Exception {
 	public void requestWhenConfigurationRequestAndClientRegistrationEnabledThenConfigurationResponseIncludesRegistrationEndpoint() throws Exception {
 		this.spring.register(AuthorizationServerConfigurationWithClientRegistrationEnabled.class).autowire();
 		this.spring.register(AuthorizationServerConfigurationWithClientRegistrationEnabled.class).autowire();
 
 
-		this.mvc.perform(get(ISSUER_URL.concat(DEFAULT_OIDC_PROVIDER_CONFIGURATION_ENDPOINT_URI)))
+		this.mvc.perform(get(ISSUER.concat(DEFAULT_OIDC_PROVIDER_CONFIGURATION_ENDPOINT_URI)))
 				.andExpect(status().is2xxSuccessful())
 				.andExpect(status().is2xxSuccessful())
-				.andExpectAll(defaultConfigurationMatchers())
-				.andExpect(jsonPath("$.registration_endpoint").value(ISSUER_URL.concat(this.authorizationServerSettings.getOidcClientRegistrationEndpoint())));
+				.andExpectAll(defaultConfigurationMatchers(ISSUER))
+				.andExpect(jsonPath("$.registration_endpoint").value(ISSUER.concat(this.authorizationServerSettings.getOidcClientRegistrationEndpoint())));
 	}
 	}
 
 
-	private ResultMatcher[] defaultConfigurationMatchers() {
+	private ResultMatcher[] defaultConfigurationMatchers(String issuer) {
 		// @formatter:off
 		// @formatter:off
 		return new ResultMatcher[] {
 		return new ResultMatcher[] {
-				jsonPath("issuer").value(ISSUER_URL),
-				jsonPath("authorization_endpoint").value(ISSUER_URL.concat(this.authorizationServerSettings.getAuthorizationEndpoint())),
-				jsonPath("token_endpoint").value(ISSUER_URL.concat(this.authorizationServerSettings.getTokenEndpoint())),
+				jsonPath("issuer").value(issuer),
+				jsonPath("authorization_endpoint").value(issuer.concat(this.authorizationServerSettings.getAuthorizationEndpoint())),
+				jsonPath("token_endpoint").value(issuer.concat(this.authorizationServerSettings.getTokenEndpoint())),
 				jsonPath("$.token_endpoint_auth_methods_supported[0]").value(ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue()),
 				jsonPath("$.token_endpoint_auth_methods_supported[0]").value(ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue()),
 				jsonPath("$.token_endpoint_auth_methods_supported[1]").value(ClientAuthenticationMethod.CLIENT_SECRET_POST.getValue()),
 				jsonPath("$.token_endpoint_auth_methods_supported[1]").value(ClientAuthenticationMethod.CLIENT_SECRET_POST.getValue()),
 				jsonPath("$.token_endpoint_auth_methods_supported[2]").value(ClientAuthenticationMethod.CLIENT_SECRET_JWT.getValue()),
 				jsonPath("$.token_endpoint_auth_methods_supported[2]").value(ClientAuthenticationMethod.CLIENT_SECRET_JWT.getValue()),
 				jsonPath("$.token_endpoint_auth_methods_supported[3]").value(ClientAuthenticationMethod.PRIVATE_KEY_JWT.getValue()),
 				jsonPath("$.token_endpoint_auth_methods_supported[3]").value(ClientAuthenticationMethod.PRIVATE_KEY_JWT.getValue()),
-				jsonPath("jwks_uri").value(ISSUER_URL.concat(this.authorizationServerSettings.getJwkSetEndpoint())),
-				jsonPath("userinfo_endpoint").value(ISSUER_URL.concat(this.authorizationServerSettings.getOidcUserInfoEndpoint())),
-				jsonPath("end_session_endpoint").value(ISSUER_URL.concat(this.authorizationServerSettings.getOidcLogoutEndpoint())),
+				jsonPath("jwks_uri").value(issuer.concat(this.authorizationServerSettings.getJwkSetEndpoint())),
+				jsonPath("userinfo_endpoint").value(issuer.concat(this.authorizationServerSettings.getOidcUserInfoEndpoint())),
+				jsonPath("end_session_endpoint").value(issuer.concat(this.authorizationServerSettings.getOidcLogoutEndpoint())),
 				jsonPath("response_types_supported").value(OAuth2AuthorizationResponseType.CODE.getValue()),
 				jsonPath("response_types_supported").value(OAuth2AuthorizationResponseType.CODE.getValue()),
 				jsonPath("$.grant_types_supported[0]").value(AuthorizationGrantType.AUTHORIZATION_CODE.getValue()),
 				jsonPath("$.grant_types_supported[0]").value(AuthorizationGrantType.AUTHORIZATION_CODE.getValue()),
 				jsonPath("$.grant_types_supported[1]").value(AuthorizationGrantType.CLIENT_CREDENTIALS.getValue()),
 				jsonPath("$.grant_types_supported[1]").value(AuthorizationGrantType.CLIENT_CREDENTIALS.getValue()),
 				jsonPath("$.grant_types_supported[2]").value(AuthorizationGrantType.REFRESH_TOKEN.getValue()),
 				jsonPath("$.grant_types_supported[2]").value(AuthorizationGrantType.REFRESH_TOKEN.getValue()),
-				jsonPath("revocation_endpoint").value(ISSUER_URL.concat(this.authorizationServerSettings.getTokenRevocationEndpoint())),
+				jsonPath("revocation_endpoint").value(issuer.concat(this.authorizationServerSettings.getTokenRevocationEndpoint())),
 				jsonPath("$.revocation_endpoint_auth_methods_supported[0]").value(ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue()),
 				jsonPath("$.revocation_endpoint_auth_methods_supported[0]").value(ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue()),
 				jsonPath("$.revocation_endpoint_auth_methods_supported[1]").value(ClientAuthenticationMethod.CLIENT_SECRET_POST.getValue()),
 				jsonPath("$.revocation_endpoint_auth_methods_supported[1]").value(ClientAuthenticationMethod.CLIENT_SECRET_POST.getValue()),
 				jsonPath("$.revocation_endpoint_auth_methods_supported[2]").value(ClientAuthenticationMethod.CLIENT_SECRET_JWT.getValue()),
 				jsonPath("$.revocation_endpoint_auth_methods_supported[2]").value(ClientAuthenticationMethod.CLIENT_SECRET_JWT.getValue()),
 				jsonPath("$.revocation_endpoint_auth_methods_supported[3]").value(ClientAuthenticationMethod.PRIVATE_KEY_JWT.getValue()),
 				jsonPath("$.revocation_endpoint_auth_methods_supported[3]").value(ClientAuthenticationMethod.PRIVATE_KEY_JWT.getValue()),
-				jsonPath("introspection_endpoint").value(ISSUER_URL.concat(this.authorizationServerSettings.getTokenIntrospectionEndpoint())),
+				jsonPath("introspection_endpoint").value(issuer.concat(this.authorizationServerSettings.getTokenIntrospectionEndpoint())),
 				jsonPath("$.introspection_endpoint_auth_methods_supported[0]").value(ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue()),
 				jsonPath("$.introspection_endpoint_auth_methods_supported[0]").value(ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue()),
 				jsonPath("$.introspection_endpoint_auth_methods_supported[1]").value(ClientAuthenticationMethod.CLIENT_SECRET_POST.getValue()),
 				jsonPath("$.introspection_endpoint_auth_methods_supported[1]").value(ClientAuthenticationMethod.CLIENT_SECRET_POST.getValue()),
 				jsonPath("$.introspection_endpoint_auth_methods_supported[2]").value(ClientAuthenticationMethod.CLIENT_SECRET_JWT.getValue()),
 				jsonPath("$.introspection_endpoint_auth_methods_supported[2]").value(ClientAuthenticationMethod.CLIENT_SECRET_JWT.getValue()),
@@ -218,7 +238,18 @@ public class OidcProviderConfigurationTests {
 		@Bean
 		@Bean
 		AuthorizationServerSettings authorizationServerSettings() {
 		AuthorizationServerSettings authorizationServerSettings() {
 			return AuthorizationServerSettings.builder()
 			return AuthorizationServerSettings.builder()
-					.issuer(ISSUER_URL)
+					.issuer(ISSUER)
+					.build();
+		}
+
+	}
+
+	@EnableWebSecurity
+	static class AuthorizationServerConfigurationWithIssuerNotSet extends AuthorizationServerConfiguration {
+
+		@Bean
+		AuthorizationServerSettings authorizationServerSettings() {
+			return AuthorizationServerSettings.builder()
 					.build();
 					.build();
 		}
 		}
 
 
@@ -306,7 +337,7 @@ public class OidcProviderConfigurationTests {
 
 
 		@Bean
 		@Bean
 		AuthorizationServerSettings authorizationServerSettings() {
 		AuthorizationServerSettings authorizationServerSettings() {
-			return AuthorizationServerSettings.builder().issuer(ISSUER_URL + "?param=value").build();
+			return AuthorizationServerSettings.builder().issuer(ISSUER + "?param=value").build();
 		}
 		}
 	}
 	}
 
 
@@ -315,7 +346,7 @@ public class OidcProviderConfigurationTests {
 
 
 		@Bean
 		@Bean
 		AuthorizationServerSettings authorizationServerSettings() {
 		AuthorizationServerSettings authorizationServerSettings() {
-			return AuthorizationServerSettings.builder().issuer(ISSUER_URL + "#fragment").build();
+			return AuthorizationServerSettings.builder().issuer(ISSUER + "#fragment").build();
 		}
 		}
 	}
 	}
 
 
@@ -324,7 +355,7 @@ public class OidcProviderConfigurationTests {
 
 
 		@Bean
 		@Bean
 		AuthorizationServerSettings authorizationServerSettings() {
 		AuthorizationServerSettings authorizationServerSettings() {
-			return AuthorizationServerSettings.builder().issuer(ISSUER_URL + "?param=value#fragment").build();
+			return AuthorizationServerSettings.builder().issuer(ISSUER + "?param=value#fragment").build();
 		}
 		}
 	}
 	}
 
 
@@ -333,7 +364,7 @@ public class OidcProviderConfigurationTests {
 
 
 		@Bean
 		@Bean
 		AuthorizationServerSettings authorizationServerSettings() {
 		AuthorizationServerSettings authorizationServerSettings() {
-			return AuthorizationServerSettings.builder().issuer(ISSUER_URL + "?").build();
+			return AuthorizationServerSettings.builder().issuer(ISSUER + "?").build();
 		}
 		}
 	}
 	}
 
 
@@ -342,7 +373,7 @@ public class OidcProviderConfigurationTests {
 
 
 		@Bean
 		@Bean
 		AuthorizationServerSettings authorizationServerSettings() {
 		AuthorizationServerSettings authorizationServerSettings() {
-			return AuthorizationServerSettings.builder().issuer(ISSUER_URL + "#").build();
+			return AuthorizationServerSettings.builder().issuer(ISSUER + "#").build();
 		}
 		}
 	}
 	}
 
 

+ 6 - 4
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcTests.java

@@ -1,5 +1,5 @@
 /*
 /*
- * Copyright 2020-2023 the original author or authors.
+ * Copyright 2020-2024 the original author or authors.
  *
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * you may not use this file except in compliance with the License.
@@ -303,9 +303,11 @@ public class OidcTests {
 		RegisteredClient registeredClient = TestRegisteredClients.registeredClient().scope(OidcScopes.OPENID).build();
 		RegisteredClient registeredClient = TestRegisteredClients.registeredClient().scope(OidcScopes.OPENID).build();
 		this.registeredClientRepository.save(registeredClient);
 		this.registeredClientRepository.save(registeredClient);
 
 
+		String issuer = "https://example.com:8443/issuer1";
+
 		// Login
 		// Login
 		MultiValueMap<String, String> authorizationRequestParameters = getAuthorizationRequestParameters(registeredClient);
 		MultiValueMap<String, String> authorizationRequestParameters = getAuthorizationRequestParameters(registeredClient);
-		MvcResult mvcResult = this.mvc.perform(get(DEFAULT_AUTHORIZATION_ENDPOINT_URI)
+		MvcResult mvcResult = this.mvc.perform(get(issuer.concat(DEFAULT_AUTHORIZATION_ENDPOINT_URI))
 						.queryParams(authorizationRequestParameters)
 						.queryParams(authorizationRequestParameters)
 						.with(user("user")))
 						.with(user("user")))
 				.andExpect(status().is3xxRedirection())
 				.andExpect(status().is3xxRedirection())
@@ -319,7 +321,7 @@ public class OidcTests {
 		OAuth2Authorization authorization = this.authorizationService.findByToken(authorizationCode, AUTHORIZATION_CODE_TOKEN_TYPE);
 		OAuth2Authorization authorization = this.authorizationService.findByToken(authorizationCode, AUTHORIZATION_CODE_TOKEN_TYPE);
 
 
 		// Get ID Token
 		// Get ID Token
-		mvcResult = this.mvc.perform(post(DEFAULT_TOKEN_ENDPOINT_URI)
+		mvcResult = this.mvc.perform(post(issuer.concat(DEFAULT_TOKEN_ENDPOINT_URI))
 						.params(getTokenRequestParameters(registeredClient, authorization))
 						.params(getTokenRequestParameters(registeredClient, authorization))
 						.header(HttpHeaders.AUTHORIZATION, "Basic " + encodeBasicAuth(
 						.header(HttpHeaders.AUTHORIZATION, "Basic " + encodeBasicAuth(
 								registeredClient.getClientId(), registeredClient.getClientSecret())))
 								registeredClient.getClientId(), registeredClient.getClientSecret())))
@@ -334,7 +336,7 @@ public class OidcTests {
 		String idToken = (String) accessTokenResponse.getAdditionalParameters().get(OidcParameterNames.ID_TOKEN);
 		String idToken = (String) accessTokenResponse.getAdditionalParameters().get(OidcParameterNames.ID_TOKEN);
 
 
 		// Logout
 		// Logout
-		mvcResult = this.mvc.perform(post(DEFAULT_OIDC_LOGOUT_ENDPOINT_URI)
+		mvcResult = this.mvc.perform(post(issuer.concat(DEFAULT_OIDC_LOGOUT_ENDPOINT_URI))
 						.param("id_token_hint", idToken)
 						.param("id_token_hint", idToken)
 						.session(session))
 						.session(session))
 				.andExpect(status().is3xxRedirection())
 				.andExpect(status().is3xxRedirection())

+ 21 - 3
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcUserInfoTests.java

@@ -1,5 +1,5 @@
 /*
 /*
- * Copyright 2020-2022 the original author or authors.
+ * Copyright 2020-2024 the original author or authors.
  *
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * you may not use this file except in compliance with the License.
@@ -24,11 +24,12 @@ import java.util.Set;
 import java.util.function.Consumer;
 import java.util.function.Consumer;
 import java.util.function.Function;
 import java.util.function.Function;
 
 
+import jakarta.servlet.http.HttpServletResponse;
+
 import com.nimbusds.jose.jwk.JWKSet;
 import com.nimbusds.jose.jwk.JWKSet;
 import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
 import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
 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;
-import jakarta.servlet.http.HttpServletResponse;
 import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.Test;
@@ -193,6 +194,24 @@ public class OidcUserInfoTests {
 		// @formatter:on
 		// @formatter:on
 	}
 	}
 
 
+	@Test
+	public void requestWhenUserInfoRequestIncludesIssuerPathThenUserInfoResponse() throws Exception {
+		this.spring.register(AuthorizationServerConfiguration.class).autowire();
+
+		OAuth2Authorization authorization = createAuthorization();
+		this.authorizationService.save(authorization);
+
+		String issuer = "https://example.com:8443/issuer1";
+
+		OAuth2AccessToken accessToken = authorization.getAccessToken().getToken();
+		// @formatter:off
+		this.mvc.perform(get(issuer.concat(DEFAULT_OIDC_USER_INFO_ENDPOINT_URI))
+				.header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken.getTokenValue()))
+				.andExpect(status().is2xxSuccessful())
+				.andExpectAll(userInfoResponse());
+		// @formatter:on
+	}
+
 	@Test
 	@Test
 	public void requestWhenUserInfoEndpointCustomizedThenUsed() throws Exception {
 	public void requestWhenUserInfoEndpointCustomizedThenUsed() throws Exception {
 		this.spring.register(CustomUserInfoConfiguration.class).autowire();
 		this.spring.register(CustomUserInfoConfiguration.class).autowire();
@@ -512,7 +531,6 @@ public class OidcUserInfoTests {
 		@Bean
 		@Bean
 		AuthorizationServerSettings authorizationServerSettings() {
 		AuthorizationServerSettings authorizationServerSettings() {
 			return AuthorizationServerSettings.builder()
 			return AuthorizationServerSettings.builder()
-					.issuer("https://auth-server:9000")
 					.build();
 					.build();
 		}
 		}
 
 

+ 5 - 1
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2DeviceAuthorizationEndpointFilterTests.java

@@ -1,5 +1,5 @@
 /*
 /*
- * Copyright 2020-2023 the original author or authors.
+ * Copyright 2020-2024 the original author or authors.
  *
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * you may not use this file except in compliance with the License.
@@ -21,6 +21,7 @@ import java.time.temporal.ChronoUnit;
 
 
 import jakarta.servlet.FilterChain;
 import jakarta.servlet.FilterChain;
 import jakarta.servlet.http.HttpServletRequest;
 import jakarta.servlet.http.HttpServletRequest;
+
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.Test;
@@ -404,6 +405,9 @@ public class OAuth2DeviceAuthorizationEndpointFilterTests {
 		request.setRequestURI(AUTHORIZATION_URI);
 		request.setRequestURI(AUTHORIZATION_URI);
 		request.setServletPath(AUTHORIZATION_URI);
 		request.setServletPath(AUTHORIZATION_URI);
 		request.setRemoteAddr(REMOTE_ADDRESS);
 		request.setRemoteAddr(REMOTE_ADDRESS);
+		request.setScheme("https");
+		request.setServerName("provider.com");
+		request.setServerPort(-1);
 		return request;
 		return request;
 	}
 	}
 
 

+ 2 - 4
samples/demo-authorizationserver/src/main/java/sample/config/AuthorizationServerConfig.java

@@ -1,5 +1,5 @@
 /*
 /*
- * Copyright 2020-2023 the original author or authors.
+ * Copyright 2020-2024 the original author or authors.
  *
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * you may not use this file except in compliance with the License.
@@ -43,8 +43,6 @@ import org.springframework.security.oauth2.core.oidc.OidcScopes;
 import org.springframework.security.oauth2.jwt.JwtDecoder;
 import org.springframework.security.oauth2.jwt.JwtDecoder;
 import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationConsentService;
 import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationConsentService;
 import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationService;
 import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationService;
-import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
-import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
 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;
@@ -94,7 +92,7 @@ public class AuthorizationServerConfig {
 		 */
 		 */
 		DeviceClientAuthenticationConverter deviceClientAuthenticationConverter =
 		DeviceClientAuthenticationConverter deviceClientAuthenticationConverter =
 				new DeviceClientAuthenticationConverter(
 				new DeviceClientAuthenticationConverter(
-						authorizationServerSettings.getDeviceAuthorizationEndpoint());
+						"/**" + authorizationServerSettings.getDeviceAuthorizationEndpoint());
 		DeviceClientAuthenticationProvider deviceClientAuthenticationProvider =
 		DeviceClientAuthenticationProvider deviceClientAuthenticationProvider =
 				new DeviceClientAuthenticationProvider(registeredClientRepository);
 				new DeviceClientAuthenticationProvider(registeredClientRepository);