/*
 * Copyright 2004-present 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.config.annotation.web.configurers.oauth2.server.authorization;

import java.net.URI;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;

import com.nimbusds.jose.jwk.source.JWKSource;

import org.springframework.context.ApplicationListener;
import org.springframework.context.event.GenericApplicationListenerAdapter;
import org.springframework.context.event.SmartApplicationListener;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.annotation.web.configurers.ExceptionHandlingConfigurer;
import org.springframework.security.context.DelegatingApplicationListener;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.core.session.SessionRegistryImpl;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.OAuth2Token;
import org.springframework.security.oauth2.core.oidc.OidcScopes;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationContext;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationException;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContext;
import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContextHolder;
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
import org.springframework.security.oauth2.server.authorization.web.NimbusJwkSetEndpointFilter;
import org.springframework.security.web.authentication.HttpStatusEntryPoint;
import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;
import org.springframework.security.web.context.SecurityContextHolderFilter;
import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;
import org.springframework.security.web.util.matcher.OrRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
import org.springframework.web.util.UriComponentsBuilder;

/**
 * An {@link AbstractHttpConfigurer} for OAuth 2.1 Authorization Server support.
 *
 * @author Joe Grandja
 * @author Daniel Garnier-Moiroux
 * @author Gerardo Roza
 * @author Ovidiu Popa
 * @author Gaurav Tiwari
 * @since 7.0
 * @see AbstractHttpConfigurer
 * @see OAuth2ClientAuthenticationConfigurer
 * @see OAuth2AuthorizationServerMetadataEndpointConfigurer
 * @see OAuth2AuthorizationEndpointConfigurer
 * @see OAuth2PushedAuthorizationRequestEndpointConfigurer
 * @see OAuth2TokenEndpointConfigurer
 * @see OAuth2TokenIntrospectionEndpointConfigurer
 * @see OAuth2TokenRevocationEndpointConfigurer
 * @see OAuth2DeviceAuthorizationEndpointConfigurer
 * @see OAuth2DeviceVerificationEndpointConfigurer
 * @see OAuth2ClientRegistrationEndpointConfigurer
 * @see OidcConfigurer
 * @see RegisteredClientRepository
 * @see OAuth2AuthorizationService
 * @see OAuth2AuthorizationConsentService
 * @see NimbusJwkSetEndpointFilter
 */
public final class OAuth2AuthorizationServerConfigurer
		extends AbstractHttpConfigurer<OAuth2AuthorizationServerConfigurer, HttpSecurity> {

	private final Map<Class<? extends AbstractOAuth2Configurer>, AbstractOAuth2Configurer> configurers = createConfigurers();

	private RequestMatcher endpointsMatcher;

	/**
	 * Sets the repository of registered clients.
	 * @param registeredClientRepository the repository of registered clients
	 * @return the {@link OAuth2AuthorizationServerConfigurer} for further configuration
	 */
	public OAuth2AuthorizationServerConfigurer registeredClientRepository(
			RegisteredClientRepository registeredClientRepository) {
		Assert.notNull(registeredClientRepository, "registeredClientRepository cannot be null");
		getBuilder().setSharedObject(RegisteredClientRepository.class, registeredClientRepository);
		return this;
	}

	/**
	 * Sets the authorization service.
	 * @param authorizationService the authorization service
	 * @return the {@link OAuth2AuthorizationServerConfigurer} for further configuration
	 */
	public OAuth2AuthorizationServerConfigurer authorizationService(OAuth2AuthorizationService authorizationService) {
		Assert.notNull(authorizationService, "authorizationService cannot be null");
		getBuilder().setSharedObject(OAuth2AuthorizationService.class, authorizationService);
		return this;
	}

	/**
	 * Sets the authorization consent service.
	 * @param authorizationConsentService the authorization consent service
	 * @return the {@link OAuth2AuthorizationServerConfigurer} for further configuration
	 */
	public OAuth2AuthorizationServerConfigurer authorizationConsentService(
			OAuth2AuthorizationConsentService authorizationConsentService) {
		Assert.notNull(authorizationConsentService, "authorizationConsentService cannot be null");
		getBuilder().setSharedObject(OAuth2AuthorizationConsentService.class, authorizationConsentService);
		return this;
	}

	/**
	 * Sets the authorization server settings.
	 * @param authorizationServerSettings the authorization server settings
	 * @return the {@link OAuth2AuthorizationServerConfigurer} for further configuration
	 */
	public OAuth2AuthorizationServerConfigurer authorizationServerSettings(
			AuthorizationServerSettings authorizationServerSettings) {
		Assert.notNull(authorizationServerSettings, "authorizationServerSettings cannot be null");
		getBuilder().setSharedObject(AuthorizationServerSettings.class, authorizationServerSettings);
		return this;
	}

	/**
	 * Sets the token generator.
	 * @param tokenGenerator the token generator
	 * @return the {@link OAuth2AuthorizationServerConfigurer} for further configuration
	 */
	public OAuth2AuthorizationServerConfigurer tokenGenerator(
			OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator) {
		Assert.notNull(tokenGenerator, "tokenGenerator cannot be null");
		getBuilder().setSharedObject(OAuth2TokenGenerator.class, tokenGenerator);
		return this;
	}

	/**
	 * Configures OAuth 2.0 Client Authentication.
	 * @param clientAuthenticationCustomizer the {@link Customizer} providing access to
	 * the {@link OAuth2ClientAuthenticationConfigurer}
	 * @return the {@link OAuth2AuthorizationServerConfigurer} for further configuration
	 */
	public OAuth2AuthorizationServerConfigurer clientAuthentication(
			Customizer<OAuth2ClientAuthenticationConfigurer> clientAuthenticationCustomizer) {
		clientAuthenticationCustomizer.customize(getConfigurer(OAuth2ClientAuthenticationConfigurer.class));
		return this;
	}

	/**
	 * Configures the OAuth 2.0 Authorization Server Metadata Endpoint.
	 * @param authorizationServerMetadataEndpointCustomizer the {@link Customizer}
	 * providing access to the {@link OAuth2AuthorizationServerMetadataEndpointConfigurer}
	 * @return the {@link OAuth2AuthorizationServerConfigurer} for further configuration
	 */
	public OAuth2AuthorizationServerConfigurer authorizationServerMetadataEndpoint(
			Customizer<OAuth2AuthorizationServerMetadataEndpointConfigurer> authorizationServerMetadataEndpointCustomizer) {
		authorizationServerMetadataEndpointCustomizer
			.customize(getConfigurer(OAuth2AuthorizationServerMetadataEndpointConfigurer.class));
		return this;
	}

	/**
	 * Configures the OAuth 2.0 Authorization Endpoint.
	 * @param authorizationEndpointCustomizer the {@link Customizer} providing access to
	 * the {@link OAuth2AuthorizationEndpointConfigurer}
	 * @return the {@link OAuth2AuthorizationServerConfigurer} for further configuration
	 */
	public OAuth2AuthorizationServerConfigurer authorizationEndpoint(
			Customizer<OAuth2AuthorizationEndpointConfigurer> authorizationEndpointCustomizer) {
		authorizationEndpointCustomizer.customize(getConfigurer(OAuth2AuthorizationEndpointConfigurer.class));
		return this;
	}

	/**
	 * Configures the OAuth 2.0 Pushed Authorization Request Endpoint.
	 * @param pushedAuthorizationRequestEndpointCustomizer the {@link Customizer}
	 * providing access to the {@link OAuth2PushedAuthorizationRequestEndpointConfigurer}
	 * @return the {@link OAuth2AuthorizationServerConfigurer} for further configuration
	 */
	public OAuth2AuthorizationServerConfigurer pushedAuthorizationRequestEndpoint(
			Customizer<OAuth2PushedAuthorizationRequestEndpointConfigurer> pushedAuthorizationRequestEndpointCustomizer) {
		OAuth2PushedAuthorizationRequestEndpointConfigurer pushedAuthorizationRequestEndpointConfigurer = getConfigurer(
				OAuth2PushedAuthorizationRequestEndpointConfigurer.class);
		if (pushedAuthorizationRequestEndpointConfigurer == null) {
			addConfigurer(OAuth2PushedAuthorizationRequestEndpointConfigurer.class,
					new OAuth2PushedAuthorizationRequestEndpointConfigurer(this::postProcess));
			pushedAuthorizationRequestEndpointConfigurer = getConfigurer(
					OAuth2PushedAuthorizationRequestEndpointConfigurer.class);
		}
		pushedAuthorizationRequestEndpointCustomizer.customize(pushedAuthorizationRequestEndpointConfigurer);
		return this;
	}

	/**
	 * Configures the OAuth 2.0 Token Endpoint.
	 * @param tokenEndpointCustomizer the {@link Customizer} providing access to the
	 * {@link OAuth2TokenEndpointConfigurer}
	 * @return the {@link OAuth2AuthorizationServerConfigurer} for further configuration
	 */
	public OAuth2AuthorizationServerConfigurer tokenEndpoint(
			Customizer<OAuth2TokenEndpointConfigurer> tokenEndpointCustomizer) {
		tokenEndpointCustomizer.customize(getConfigurer(OAuth2TokenEndpointConfigurer.class));
		return this;
	}

	/**
	 * Configures the OAuth 2.0 Token Introspection Endpoint.
	 * @param tokenIntrospectionEndpointCustomizer the {@link Customizer} providing access
	 * to the {@link OAuth2TokenIntrospectionEndpointConfigurer}
	 * @return the {@link OAuth2AuthorizationServerConfigurer} for further configuration
	 */
	public OAuth2AuthorizationServerConfigurer tokenIntrospectionEndpoint(
			Customizer<OAuth2TokenIntrospectionEndpointConfigurer> tokenIntrospectionEndpointCustomizer) {
		tokenIntrospectionEndpointCustomizer.customize(getConfigurer(OAuth2TokenIntrospectionEndpointConfigurer.class));
		return this;
	}

	/**
	 * Configures the OAuth 2.0 Token Revocation Endpoint.
	 * @param tokenRevocationEndpointCustomizer the {@link Customizer} providing access to
	 * the {@link OAuth2TokenRevocationEndpointConfigurer}
	 * @return the {@link OAuth2AuthorizationServerConfigurer} for further configuration
	 */
	public OAuth2AuthorizationServerConfigurer tokenRevocationEndpoint(
			Customizer<OAuth2TokenRevocationEndpointConfigurer> tokenRevocationEndpointCustomizer) {
		tokenRevocationEndpointCustomizer.customize(getConfigurer(OAuth2TokenRevocationEndpointConfigurer.class));
		return this;
	}

	/**
	 * Configures the OAuth 2.0 Device Authorization Endpoint.
	 * @param deviceAuthorizationEndpointCustomizer the {@link Customizer} providing
	 * access to the {@link OAuth2DeviceAuthorizationEndpointConfigurer}
	 * @return the {@link OAuth2AuthorizationServerConfigurer} for further configuration
	 */
	public OAuth2AuthorizationServerConfigurer deviceAuthorizationEndpoint(
			Customizer<OAuth2DeviceAuthorizationEndpointConfigurer> deviceAuthorizationEndpointCustomizer) {
		OAuth2DeviceAuthorizationEndpointConfigurer deviceAuthorizationEndpointConfigurer = getConfigurer(
				OAuth2DeviceAuthorizationEndpointConfigurer.class);
		if (deviceAuthorizationEndpointConfigurer == null) {
			addConfigurer(OAuth2DeviceAuthorizationEndpointConfigurer.class,
					new OAuth2DeviceAuthorizationEndpointConfigurer(this::postProcess));
			deviceAuthorizationEndpointConfigurer = getConfigurer(OAuth2DeviceAuthorizationEndpointConfigurer.class);
			deviceVerificationEndpoint((configurer) -> {
			}); // Ensure the Device Verification Endpoint is enabled
		}
		deviceAuthorizationEndpointCustomizer.customize(deviceAuthorizationEndpointConfigurer);
		return this;
	}

	/**
	 * Configures the OAuth 2.0 Device Verification Endpoint.
	 * @param deviceVerificationEndpointCustomizer the {@link Customizer} providing access
	 * to the {@link OAuth2DeviceVerificationEndpointConfigurer}
	 * @return the {@link OAuth2AuthorizationServerConfigurer} for further configuration
	 */
	public OAuth2AuthorizationServerConfigurer deviceVerificationEndpoint(
			Customizer<OAuth2DeviceVerificationEndpointConfigurer> deviceVerificationEndpointCustomizer) {
		OAuth2DeviceVerificationEndpointConfigurer deviceVerificationEndpointConfigurer = getConfigurer(
				OAuth2DeviceVerificationEndpointConfigurer.class);
		if (deviceVerificationEndpointConfigurer == null) {
			addConfigurer(OAuth2DeviceVerificationEndpointConfigurer.class,
					new OAuth2DeviceVerificationEndpointConfigurer(this::postProcess));
			deviceVerificationEndpointConfigurer = getConfigurer(OAuth2DeviceVerificationEndpointConfigurer.class);
			deviceAuthorizationEndpoint((configurer) -> {
			}); // Ensure the Device Authorization Endpoint is enabled
		}
		deviceVerificationEndpointCustomizer.customize(deviceVerificationEndpointConfigurer);
		return this;
	}

	/**
	 * Configures the OAuth 2.0 Dynamic Client Registration Endpoint.
	 * @param clientRegistrationEndpointCustomizer the {@link Customizer} providing access
	 * to the {@link OAuth2ClientRegistrationEndpointConfigurer}
	 * @return the {@link OAuth2AuthorizationServerConfigurer} for further configuration
	 */
	public OAuth2AuthorizationServerConfigurer clientRegistrationEndpoint(
			Customizer<OAuth2ClientRegistrationEndpointConfigurer> clientRegistrationEndpointCustomizer) {
		OAuth2ClientRegistrationEndpointConfigurer clientRegistrationEndpointConfigurer = getConfigurer(
				OAuth2ClientRegistrationEndpointConfigurer.class);
		if (clientRegistrationEndpointConfigurer == null) {
			addConfigurer(OAuth2ClientRegistrationEndpointConfigurer.class,
					new OAuth2ClientRegistrationEndpointConfigurer(this::postProcess));
			clientRegistrationEndpointConfigurer = getConfigurer(OAuth2ClientRegistrationEndpointConfigurer.class);
		}
		clientRegistrationEndpointCustomizer.customize(clientRegistrationEndpointConfigurer);
		return this;
	}

	/**
	 * Configures OpenID Connect 1.0 support (disabled by default).
	 * @param oidcCustomizer the {@link Customizer} providing access to the
	 * {@link OidcConfigurer}
	 * @return the {@link OAuth2AuthorizationServerConfigurer} for further configuration
	 */
	public OAuth2AuthorizationServerConfigurer oidc(Customizer<OidcConfigurer> oidcCustomizer) {
		OidcConfigurer oidcConfigurer = getConfigurer(OidcConfigurer.class);
		if (oidcConfigurer == null) {
			addConfigurer(OidcConfigurer.class, new OidcConfigurer(this::postProcess));
			oidcConfigurer = getConfigurer(OidcConfigurer.class);
		}
		oidcCustomizer.customize(oidcConfigurer);
		return this;
	}

	/**
	 * Returns a {@link RequestMatcher} for the authorization server endpoints.
	 * @return a {@link RequestMatcher} for the authorization server endpoints
	 */
	public RequestMatcher getEndpointsMatcher() {
		// Return a deferred RequestMatcher
		// since endpointsMatcher is constructed in init(HttpSecurity).
		return (request) -> this.endpointsMatcher.matches(request);
	}

	@Override
	public void init(HttpSecurity httpSecurity) {
		AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils
			.getAuthorizationServerSettings(httpSecurity);
		validateAuthorizationServerSettings(authorizationServerSettings);

		if (isOidcEnabled()) {
			// Add OpenID Connect session tracking capabilities.
			initSessionRegistry(httpSecurity);
			SessionRegistry sessionRegistry = httpSecurity.getSharedObject(SessionRegistry.class);
			OAuth2AuthorizationEndpointConfigurer authorizationEndpointConfigurer = getConfigurer(
					OAuth2AuthorizationEndpointConfigurer.class);
			authorizationEndpointConfigurer.setSessionAuthenticationStrategy((authentication, request, response) -> {
				if (authentication instanceof OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication) {
					if (authorizationCodeRequestAuthentication.getScopes().contains(OidcScopes.OPENID)) {
						if (sessionRegistry.getSessionInformation(request.getSession().getId()) == null) {
							sessionRegistry.registerNewSession(request.getSession().getId(),
									((Authentication) authorizationCodeRequestAuthentication.getPrincipal())
										.getPrincipal());
						}
					}
				}
			});
		}
		else {
			// OpenID Connect is disabled.
			// Add an authentication validator that rejects authentication requests.
			Consumer<OAuth2AuthorizationCodeRequestAuthenticationContext> oidcAuthenticationRequestValidator = (
					authenticationContext) -> {
				OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication = authenticationContext
					.getAuthentication();
				if (authorizationCodeRequestAuthentication.getScopes().contains(OidcScopes.OPENID)) {
					OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.INVALID_SCOPE,
							"OpenID Connect 1.0 authentication requests are restricted.",
							"https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1");
					throw new OAuth2AuthorizationCodeRequestAuthenticationException(error,
							authorizationCodeRequestAuthentication);
				}
			};
			OAuth2AuthorizationEndpointConfigurer authorizationEndpointConfigurer = getConfigurer(
					OAuth2AuthorizationEndpointConfigurer.class);
			authorizationEndpointConfigurer
				.addAuthorizationCodeRequestAuthenticationValidator(oidcAuthenticationRequestValidator);
			OAuth2PushedAuthorizationRequestEndpointConfigurer pushedAuthorizationRequestEndpointConfigurer = getConfigurer(
					OAuth2PushedAuthorizationRequestEndpointConfigurer.class);
			if (pushedAuthorizationRequestEndpointConfigurer != null) {
				pushedAuthorizationRequestEndpointConfigurer
					.addAuthorizationCodeRequestAuthenticationValidator(oidcAuthenticationRequestValidator);
			}
		}

		List<RequestMatcher> requestMatchers = new ArrayList<>();
		this.configurers.values().forEach((configurer) -> {
			configurer.init(httpSecurity);
			requestMatchers.add(configurer.getRequestMatcher());
		});
		String jwkSetEndpointUri = authorizationServerSettings.isMultipleIssuersAllowed()
				? OAuth2ConfigurerUtils.withMultipleIssuersPattern(authorizationServerSettings.getJwkSetEndpoint())
				: authorizationServerSettings.getJwkSetEndpoint();
		requestMatchers.add(PathPatternRequestMatcher.withDefaults().matcher(HttpMethod.GET, jwkSetEndpointUri));
		this.endpointsMatcher = new OrRequestMatcher(requestMatchers);

		ExceptionHandlingConfigurer<HttpSecurity> exceptionHandling = httpSecurity
			.getConfigurer(ExceptionHandlingConfigurer.class);
		if (exceptionHandling != null) {
			List<RequestMatcher> preferredMatchers = new ArrayList<>();
			preferredMatchers.add(getRequestMatcher(OAuth2TokenEndpointConfigurer.class));
			preferredMatchers.add(getRequestMatcher(OAuth2TokenIntrospectionEndpointConfigurer.class));
			preferredMatchers.add(getRequestMatcher(OAuth2TokenRevocationEndpointConfigurer.class));
			RequestMatcher preferredMatcher = getRequestMatcher(OAuth2DeviceAuthorizationEndpointConfigurer.class);
			if (preferredMatcher != null) {
				preferredMatchers.add(preferredMatcher);
			}
			preferredMatcher = getRequestMatcher(OAuth2PushedAuthorizationRequestEndpointConfigurer.class);
			if (preferredMatcher != null) {
				preferredMatchers.add(preferredMatcher);
			}
			exceptionHandling.defaultAuthenticationEntryPointFor(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED),
					new OrRequestMatcher(preferredMatchers));
		}

		httpSecurity.csrf((csrf) -> csrf.ignoringRequestMatchers(this.endpointsMatcher));

		if (getConfigurer(OAuth2ClientRegistrationEndpointConfigurer.class) != null) {
			httpSecurity
				// Accept access tokens for Client Registration
				.oauth2ResourceServer((oauth2ResourceServer) -> oauth2ResourceServer.jwt(Customizer.withDefaults()));
		}

		OidcConfigurer oidcConfigurer = getConfigurer(OidcConfigurer.class);
		if (oidcConfigurer != null) {
			if (oidcConfigurer.getConfigurer(OidcUserInfoEndpointConfigurer.class) != null
					|| oidcConfigurer.getConfigurer(OidcClientRegistrationEndpointConfigurer.class) != null) {
				httpSecurity
					// Accept access tokens for User Info and/or Client Registration
					.oauth2ResourceServer(
							(oauth2ResourceServer) -> oauth2ResourceServer.jwt(Customizer.withDefaults()));

			}
		}
	}

	@Override
	public void configure(HttpSecurity httpSecurity) {
		OAuth2ClientRegistrationEndpointConfigurer clientRegistrationEndpointConfigurer = getConfigurer(
				OAuth2ClientRegistrationEndpointConfigurer.class);
		if (clientRegistrationEndpointConfigurer != null) {
			OAuth2AuthorizationServerMetadataEndpointConfigurer authorizationServerMetadataEndpointConfigurer = getConfigurer(
					OAuth2AuthorizationServerMetadataEndpointConfigurer.class);

			authorizationServerMetadataEndpointConfigurer.addDefaultAuthorizationServerMetadataCustomizer((builder) -> {
				AuthorizationServerContext authorizationServerContext = AuthorizationServerContextHolder.getContext();
				String issuer = authorizationServerContext.getIssuer();
				AuthorizationServerSettings authorizationServerSettings = authorizationServerContext
					.getAuthorizationServerSettings();

				String clientRegistrationEndpoint = UriComponentsBuilder.fromUriString(issuer)
					.path(authorizationServerSettings.getClientRegistrationEndpoint())
					.build()
					.toUriString();

				builder.clientRegistrationEndpoint(clientRegistrationEndpoint);
			});
		}

		OAuth2DeviceAuthorizationEndpointConfigurer deviceAuthorizationEndpointConfigurer = getConfigurer(
				OAuth2DeviceAuthorizationEndpointConfigurer.class);
		if (deviceAuthorizationEndpointConfigurer != null) {
			OAuth2AuthorizationServerMetadataEndpointConfigurer authorizationServerMetadataEndpointConfigurer = getConfigurer(
					OAuth2AuthorizationServerMetadataEndpointConfigurer.class);

			authorizationServerMetadataEndpointConfigurer.addDefaultAuthorizationServerMetadataCustomizer((builder) -> {
				AuthorizationServerContext authorizationServerContext = AuthorizationServerContextHolder.getContext();
				String issuer = authorizationServerContext.getIssuer();
				AuthorizationServerSettings authorizationServerSettings = authorizationServerContext
					.getAuthorizationServerSettings();

				String deviceAuthorizationEndpoint = UriComponentsBuilder.fromUriString(issuer)
					.path(authorizationServerSettings.getDeviceAuthorizationEndpoint())
					.build()
					.toUriString();

				builder.deviceAuthorizationEndpoint(deviceAuthorizationEndpoint);
				builder.grantType(AuthorizationGrantType.DEVICE_CODE.getValue());
			});
		}

		OAuth2PushedAuthorizationRequestEndpointConfigurer pushedAuthorizationRequestEndpointConfigurer = getConfigurer(
				OAuth2PushedAuthorizationRequestEndpointConfigurer.class);
		if (pushedAuthorizationRequestEndpointConfigurer != null) {
			OAuth2AuthorizationServerMetadataEndpointConfigurer authorizationServerMetadataEndpointConfigurer = getConfigurer(
					OAuth2AuthorizationServerMetadataEndpointConfigurer.class);

			authorizationServerMetadataEndpointConfigurer.addDefaultAuthorizationServerMetadataCustomizer((builder) -> {
				AuthorizationServerContext authorizationServerContext = AuthorizationServerContextHolder.getContext();
				String issuer = authorizationServerContext.getIssuer();
				AuthorizationServerSettings authorizationServerSettings = authorizationServerContext
					.getAuthorizationServerSettings();

				String pushedAuthorizationRequestEndpoint = UriComponentsBuilder.fromUriString(issuer)
					.path(authorizationServerSettings.getPushedAuthorizationRequestEndpoint())
					.build()
					.toUriString();

				builder.pushedAuthorizationRequestEndpoint(pushedAuthorizationRequestEndpoint);
			});
		}

		this.configurers.values().forEach((configurer) -> configurer.configure(httpSecurity));

		AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils
			.getAuthorizationServerSettings(httpSecurity);

		AuthorizationServerContextFilter authorizationServerContextFilter = new AuthorizationServerContextFilter(
				authorizationServerSettings);
		httpSecurity.addFilterAfter(postProcess(authorizationServerContextFilter), SecurityContextHolderFilter.class);

		JWKSource<com.nimbusds.jose.proc.SecurityContext> jwkSource = OAuth2ConfigurerUtils.getJwkSource(httpSecurity);
		if (jwkSource != null) {
			String jwkSetEndpointUri = authorizationServerSettings.isMultipleIssuersAllowed()
					? OAuth2ConfigurerUtils.withMultipleIssuersPattern(authorizationServerSettings.getJwkSetEndpoint())
					: authorizationServerSettings.getJwkSetEndpoint();
			NimbusJwkSetEndpointFilter jwkSetEndpointFilter = new NimbusJwkSetEndpointFilter(jwkSource,
					jwkSetEndpointUri);
			httpSecurity.addFilterBefore(postProcess(jwkSetEndpointFilter),
					AbstractPreAuthenticatedProcessingFilter.class);
		}
	}

	private boolean isOidcEnabled() {
		return getConfigurer(OidcConfigurer.class) != null;
	}

	private Map<Class<? extends AbstractOAuth2Configurer>, AbstractOAuth2Configurer> createConfigurers() {
		Map<Class<? extends AbstractOAuth2Configurer>, AbstractOAuth2Configurer> configurers = new LinkedHashMap<>();
		configurers.put(OAuth2ClientAuthenticationConfigurer.class,
				new OAuth2ClientAuthenticationConfigurer(this::postProcess));
		configurers.put(OAuth2AuthorizationServerMetadataEndpointConfigurer.class,
				new OAuth2AuthorizationServerMetadataEndpointConfigurer(this::postProcess));
		configurers.put(OAuth2AuthorizationEndpointConfigurer.class,
				new OAuth2AuthorizationEndpointConfigurer(this::postProcess));
		configurers.put(OAuth2TokenEndpointConfigurer.class, new OAuth2TokenEndpointConfigurer(this::postProcess));
		configurers.put(OAuth2TokenIntrospectionEndpointConfigurer.class,
				new OAuth2TokenIntrospectionEndpointConfigurer(this::postProcess));
		configurers.put(OAuth2TokenRevocationEndpointConfigurer.class,
				new OAuth2TokenRevocationEndpointConfigurer(this::postProcess));
		return configurers;
	}

	@SuppressWarnings("unchecked")
	<T> T getConfigurer(Class<T> type) {
		return (T) this.configurers.get(type);
	}

	private <T extends AbstractOAuth2Configurer> void addConfigurer(Class<T> configurerType, T configurer) {
		this.configurers.put(configurerType, configurer);
	}

	private <T extends AbstractOAuth2Configurer> RequestMatcher getRequestMatcher(Class<T> configurerType) {
		T configurer = getConfigurer(configurerType);
		return (configurer != null) ? configurer.getRequestMatcher() : null;
	}

	private static void validateAuthorizationServerSettings(AuthorizationServerSettings authorizationServerSettings) {
		if (authorizationServerSettings.getIssuer() != null) {
			URI issuerUri;
			try {
				issuerUri = new URI(authorizationServerSettings.getIssuer());
				issuerUri.toURL();
			}
			catch (Exception ex) {
				throw new IllegalArgumentException("issuer must be a valid URL", ex);
			}
			// rfc8414 https://datatracker.ietf.org/doc/html/rfc8414#section-2
			if (issuerUri.getQuery() != null || issuerUri.getFragment() != null) {
				throw new IllegalArgumentException("issuer cannot contain query or fragment component");
			}
		}
	}

	private static void initSessionRegistry(HttpSecurity httpSecurity) {
		SessionRegistry sessionRegistry = OAuth2ConfigurerUtils.getOptionalBean(httpSecurity, SessionRegistry.class);
		if (sessionRegistry == null) {
			sessionRegistry = new SessionRegistryImpl();
			registerDelegateApplicationListener(httpSecurity, (SessionRegistryImpl) sessionRegistry);
		}
		httpSecurity.setSharedObject(SessionRegistry.class, sessionRegistry);
	}

	private static void registerDelegateApplicationListener(HttpSecurity httpSecurity,
			ApplicationListener<?> delegate) {
		DelegatingApplicationListener delegatingApplicationListener = OAuth2ConfigurerUtils
			.getOptionalBean(httpSecurity, DelegatingApplicationListener.class);
		if (delegatingApplicationListener == null) {
			return;
		}
		SmartApplicationListener smartListener = new GenericApplicationListenerAdapter(delegate);
		delegatingApplicationListener.addListener(smartListener);
	}

}
