/*
 * 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.web.server;

import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.security.interfaces.RSAPublicKey;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import reactor.core.publisher.Mono;
import reactor.util.context.Context;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.context.ApplicationContext;
import org.springframework.core.Ordered;
import org.springframework.core.ResolvableType;
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
import org.springframework.core.convert.converter.Converter;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.authentication.DelegatingReactiveAuthenticationManager;
import org.springframework.security.authentication.ReactiveAuthenticationManager;
import org.springframework.security.authentication.ReactiveAuthenticationManagerResolver;
import org.springframework.security.authentication.ott.GenerateOneTimeTokenRequest;
import org.springframework.security.authentication.ott.OneTimeToken;
import org.springframework.security.authentication.ott.reactive.InMemoryReactiveOneTimeTokenService;
import org.springframework.security.authentication.ott.reactive.OneTimeTokenReactiveAuthenticationManager;
import org.springframework.security.authentication.ott.reactive.ReactiveOneTimeTokenService;
import org.springframework.security.authorization.AuthenticatedReactiveAuthorizationManager;
import org.springframework.security.authorization.AuthorityReactiveAuthorizationManager;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.ReactiveAuthorizationManager;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.ObjectPostProcessor;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
import org.springframework.security.core.session.ReactiveSessionRegistry;
import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
import org.springframework.security.oauth2.client.InMemoryReactiveOAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthorizationCodeReactiveAuthenticationManager;
import org.springframework.security.oauth2.client.authentication.OAuth2LoginReactiveAuthenticationManager;
import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest;
import org.springframework.security.oauth2.client.endpoint.ReactiveOAuth2AccessTokenResponseClient;
import org.springframework.security.oauth2.client.endpoint.WebClientReactiveAuthorizationCodeTokenResponseClient;
import org.springframework.security.oauth2.client.oidc.authentication.OidcAuthorizationCodeReactiveAuthenticationManager;
import org.springframework.security.oauth2.client.oidc.server.session.InMemoryReactiveOidcSessionRegistry;
import org.springframework.security.oauth2.client.oidc.server.session.ReactiveOidcSessionRegistry;
import org.springframework.security.oauth2.client.oidc.session.OidcSessionInformation;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcReactiveOAuth2UserService;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
import org.springframework.security.oauth2.client.userinfo.DefaultReactiveOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.client.userinfo.ReactiveOAuth2UserService;
import org.springframework.security.oauth2.client.web.server.AuthenticatedPrincipalServerOAuth2AuthorizedClientRepository;
import org.springframework.security.oauth2.client.web.server.OAuth2AuthorizationCodeGrantWebFilter;
import org.springframework.security.oauth2.client.web.server.OAuth2AuthorizationRequestRedirectWebFilter;
import org.springframework.security.oauth2.client.web.server.ServerAuthorizationRequestRepository;
import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizationCodeAuthenticationTokenConverter;
import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizationRequestResolver;
import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizedClientRepository;
import org.springframework.security.oauth2.client.web.server.WebSessionOAuth2ServerAuthorizationRequestRepository;
import org.springframework.security.oauth2.client.web.server.authentication.OAuth2LoginAuthenticationWebFilter;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder;
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder;
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoderFactory;
import org.springframework.security.oauth2.server.resource.authentication.JwtReactiveAuthenticationManager;
import org.springframework.security.oauth2.server.resource.authentication.OpaqueTokenReactiveAuthenticationManager;
import org.springframework.security.oauth2.server.resource.authentication.ReactiveJwtAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenIntrospector;
import org.springframework.security.oauth2.server.resource.introspection.SpringReactiveOpaqueTokenIntrospector;
import org.springframework.security.oauth2.server.resource.web.access.server.BearerTokenServerAccessDeniedHandler;
import org.springframework.security.oauth2.server.resource.web.server.BearerTokenServerAuthenticationEntryPoint;
import org.springframework.security.oauth2.server.resource.web.server.authentication.ServerBearerTokenAuthenticationConverter;
import org.springframework.security.web.PortMapper;
import org.springframework.security.web.authentication.logout.LogoutHandler;
import org.springframework.security.web.authentication.preauth.x509.SubjectX500PrincipalExtractor;
import org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor;
import org.springframework.security.web.server.DefaultServerRedirectStrategy;
import org.springframework.security.web.server.DelegatingServerAuthenticationEntryPoint;
import org.springframework.security.web.server.DelegatingServerAuthenticationEntryPoint.DelegateEntry;
import org.springframework.security.web.server.ExchangeMatcherRedirectWebFilter;
import org.springframework.security.web.server.MatcherSecurityWebFilterChain;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.security.web.server.ServerAuthenticationEntryPoint;
import org.springframework.security.web.server.ServerRedirectStrategy;
import org.springframework.security.web.server.WebFilterExchange;
import org.springframework.security.web.server.authentication.AnonymousAuthenticationWebFilter;
import org.springframework.security.web.server.authentication.AuthenticationConverterServerWebExchangeMatcher;
import org.springframework.security.web.server.authentication.AuthenticationWebFilter;
import org.springframework.security.web.server.authentication.ConcurrentSessionControlServerAuthenticationSuccessHandler;
import org.springframework.security.web.server.authentication.DelegatingServerAuthenticationSuccessHandler;
import org.springframework.security.web.server.authentication.HttpBasicServerAuthenticationEntryPoint;
import org.springframework.security.web.server.authentication.HttpStatusServerEntryPoint;
import org.springframework.security.web.server.authentication.InvalidateLeastUsedServerMaximumSessionsExceededHandler;
import org.springframework.security.web.server.authentication.ReactivePreAuthenticatedAuthenticationManager;
import org.springframework.security.web.server.authentication.RedirectServerAuthenticationEntryPoint;
import org.springframework.security.web.server.authentication.RedirectServerAuthenticationFailureHandler;
import org.springframework.security.web.server.authentication.RedirectServerAuthenticationSuccessHandler;
import org.springframework.security.web.server.authentication.RegisterSessionServerAuthenticationSuccessHandler;
import org.springframework.security.web.server.authentication.ServerAuthenticationConverter;
import org.springframework.security.web.server.authentication.ServerAuthenticationEntryPointFailureHandler;
import org.springframework.security.web.server.authentication.ServerAuthenticationFailureHandler;
import org.springframework.security.web.server.authentication.ServerAuthenticationSuccessHandler;
import org.springframework.security.web.server.authentication.ServerFormLoginAuthenticationConverter;
import org.springframework.security.web.server.authentication.ServerHttpBasicAuthenticationConverter;
import org.springframework.security.web.server.authentication.ServerMaximumSessionsExceededHandler;
import org.springframework.security.web.server.authentication.ServerX509AuthenticationConverter;
import org.springframework.security.web.server.authentication.SessionLimit;
import org.springframework.security.web.server.authentication.WebFilterChainServerAuthenticationSuccessHandler;
import org.springframework.security.web.server.authentication.logout.DelegatingServerLogoutHandler;
import org.springframework.security.web.server.authentication.logout.LogoutWebFilter;
import org.springframework.security.web.server.authentication.logout.SecurityContextServerLogoutHandler;
import org.springframework.security.web.server.authentication.logout.ServerLogoutHandler;
import org.springframework.security.web.server.authentication.logout.ServerLogoutSuccessHandler;
import org.springframework.security.web.server.authentication.ott.DefaultServerGenerateOneTimeTokenRequestResolver;
import org.springframework.security.web.server.authentication.ott.GenerateOneTimeTokenWebFilter;
import org.springframework.security.web.server.authentication.ott.ServerGenerateOneTimeTokenRequestResolver;
import org.springframework.security.web.server.authentication.ott.ServerOneTimeTokenAuthenticationConverter;
import org.springframework.security.web.server.authentication.ott.ServerOneTimeTokenGenerationSuccessHandler;
import org.springframework.security.web.server.authorization.AuthorizationContext;
import org.springframework.security.web.server.authorization.AuthorizationWebFilter;
import org.springframework.security.web.server.authorization.DelegatingReactiveAuthorizationManager;
import org.springframework.security.web.server.authorization.ExceptionTranslationWebFilter;
import org.springframework.security.web.server.authorization.IpAddressReactiveAuthorizationManager;
import org.springframework.security.web.server.authorization.ServerAccessDeniedHandler;
import org.springframework.security.web.server.authorization.ServerWebExchangeDelegatingServerAccessDeniedHandler;
import org.springframework.security.web.server.context.NoOpServerSecurityContextRepository;
import org.springframework.security.web.server.context.ReactorContextWebFilter;
import org.springframework.security.web.server.context.SecurityContextServerWebExchangeWebFilter;
import org.springframework.security.web.server.context.ServerSecurityContextRepository;
import org.springframework.security.web.server.context.WebSessionServerSecurityContextRepository;
import org.springframework.security.web.server.csrf.CsrfServerLogoutHandler;
import org.springframework.security.web.server.csrf.CsrfToken;
import org.springframework.security.web.server.csrf.CsrfWebFilter;
import org.springframework.security.web.server.csrf.ServerCsrfTokenRepository;
import org.springframework.security.web.server.csrf.ServerCsrfTokenRequestHandler;
import org.springframework.security.web.server.csrf.WebSessionServerCsrfTokenRepository;
import org.springframework.security.web.server.header.CacheControlServerHttpHeadersWriter;
import org.springframework.security.web.server.header.CompositeServerHttpHeadersWriter;
import org.springframework.security.web.server.header.ContentSecurityPolicyServerHttpHeadersWriter;
import org.springframework.security.web.server.header.ContentTypeOptionsServerHttpHeadersWriter;
import org.springframework.security.web.server.header.CrossOriginEmbedderPolicyServerHttpHeadersWriter;
import org.springframework.security.web.server.header.CrossOriginEmbedderPolicyServerHttpHeadersWriter.CrossOriginEmbedderPolicy;
import org.springframework.security.web.server.header.CrossOriginOpenerPolicyServerHttpHeadersWriter;
import org.springframework.security.web.server.header.CrossOriginOpenerPolicyServerHttpHeadersWriter.CrossOriginOpenerPolicy;
import org.springframework.security.web.server.header.CrossOriginResourcePolicyServerHttpHeadersWriter;
import org.springframework.security.web.server.header.CrossOriginResourcePolicyServerHttpHeadersWriter.CrossOriginResourcePolicy;
import org.springframework.security.web.server.header.FeaturePolicyServerHttpHeadersWriter;
import org.springframework.security.web.server.header.HttpHeaderWriterWebFilter;
import org.springframework.security.web.server.header.PermissionsPolicyServerHttpHeadersWriter;
import org.springframework.security.web.server.header.ReferrerPolicyServerHttpHeadersWriter;
import org.springframework.security.web.server.header.ReferrerPolicyServerHttpHeadersWriter.ReferrerPolicy;
import org.springframework.security.web.server.header.ServerHttpHeadersWriter;
import org.springframework.security.web.server.header.StrictTransportSecurityServerHttpHeadersWriter;
import org.springframework.security.web.server.header.XFrameOptionsServerHttpHeadersWriter;
import org.springframework.security.web.server.header.XXssProtectionServerHttpHeadersWriter;
import org.springframework.security.web.server.savedrequest.NoOpServerRequestCache;
import org.springframework.security.web.server.savedrequest.ServerRequestCache;
import org.springframework.security.web.server.savedrequest.ServerRequestCacheWebFilter;
import org.springframework.security.web.server.savedrequest.WebSessionServerRequestCache;
import org.springframework.security.web.server.transport.HttpsRedirectWebFilter;
import org.springframework.security.web.server.ui.DefaultResourcesWebFilter;
import org.springframework.security.web.server.ui.LoginPageGeneratingWebFilter;
import org.springframework.security.web.server.ui.LogoutPageGeneratingWebFilter;
import org.springframework.security.web.server.ui.OneTimeTokenSubmitPageGeneratingWebFilter;
import org.springframework.security.web.server.util.matcher.AndServerWebExchangeMatcher;
import org.springframework.security.web.server.util.matcher.MediaTypeServerWebExchangeMatcher;
import org.springframework.security.web.server.util.matcher.NegatedServerWebExchangeMatcher;
import org.springframework.security.web.server.util.matcher.OrServerWebExchangeMatcher;
import org.springframework.security.web.server.util.matcher.PathPatternParserServerWebExchangeMatcher;
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcherEntry;
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.cors.reactive.CorsConfigurationSource;
import org.springframework.web.cors.reactive.CorsProcessor;
import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.web.cors.reactive.DefaultCorsProcessor;
import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerMapping;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.ServerWebExchangeDecorator;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import org.springframework.web.server.WebSession;
import org.springframework.web.server.adapter.WebHttpHandlerBuilder;
import org.springframework.web.server.session.DefaultWebSessionManager;
import org.springframework.web.util.pattern.PathPatternParser;

/**
 * A {@link ServerHttpSecurity} is similar to Spring Security's {@code HttpSecurity} but
 * for WebFlux. It allows configuring web based security for specific http requests. By
 * default it will be applied to all requests, but can be restricted using
 * {@link #securityMatcher(ServerWebExchangeMatcher)} or other similar methods.
 *
 * A minimal configuration can be found below:
 *
 * <pre class="code">
 * &#064;Configuration
 * &#064;EnableWebFluxSecurity
 * public class MyMinimalSecurityConfiguration {
 *
 *     &#064;Bean
 *     public MapReactiveUserDetailsService userDetailsService() {
 *         UserDetails user = User.withDefaultPasswordEncoder()
 *             .username("user")
 *             .password("password")
 *             .roles("USER")
 *             .build();
 *         return new MapReactiveUserDetailsService(user);
 *     }
 * }
 * </pre>
 *
 * Below is the same as our minimal configuration, but explicitly declaring the
 * {@code ServerHttpSecurity}.
 *
 * <pre class="code">
 * &#064;Configuration
 * &#064;EnableWebFluxSecurity
 * public class MyExplicitSecurityConfiguration {
 *
 *     &#064;Bean
 *     public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
 *         http
 *             .authorizeExchange((authorize) -> authorize.anyExchange().authenticated())
 *             .httpBasic(Customizer.withDefaults())
 *               .formLogin();
 *             return http.build();
 *     }
 *
 *     &#064;Bean
 *     public MapReactiveUserDetailsService userDetailsService() {
 *         UserDetails user = User.withDefaultPasswordEncoder()
 *             .username("user")
 *             .password("password")
 *             .roles("USER")
 *             .build();
 *         return new MapReactiveUserDetailsService(user);
 *     }
 * }
 * </pre>
 *
 * @author Rob Winch
 * @author Vedran Pavic
 * @author Rafiullah Hamedy
 * @author Eddú Meléndez
 * @author Joe Grandja
 * @author Parikshit Dutta
 * @author Ankur Pathak
 * @author Alexey Nesterov
 * @author Yanming Zhou
 * @since 5.0
 */
public class ServerHttpSecurity {

	private ServerWebExchangeMatcher securityMatcher = ServerWebExchangeMatchers.anyExchange();

	private AuthorizeExchangeSpec authorizeExchange;

	private HttpsRedirectSpec httpsRedirectSpec;

	private HeaderSpec headers = new HeaderSpec();

	private CsrfSpec csrf = new CsrfSpec();

	private CorsSpec cors = new CorsSpec();

	private ExceptionHandlingSpec exceptionHandling = new ExceptionHandlingSpec();

	private HttpBasicSpec httpBasic;

	private PasswordManagementSpec passwordManagement;

	private X509Spec x509;

	private final RequestCacheSpec requestCache = new RequestCacheSpec();

	private FormLoginSpec formLogin;

	private OAuth2LoginSpec oauth2Login;

	private OAuth2ResourceServerSpec resourceServer;

	private OAuth2ClientSpec client;

	private OidcLogoutSpec oidcLogout;

	private LogoutSpec logout = new LogoutSpec();

	private LoginPageSpec loginPage = new LoginPageSpec();

	private SessionManagementSpec sessionManagement;

	private ReactiveAuthenticationManager authenticationManager;

	private ServerSecurityContextRepository securityContextRepository;

	private ServerAuthenticationEntryPoint authenticationEntryPoint;

	private List<DelegateEntry> defaultEntryPoints = new ArrayList<>();

	private ServerAccessDeniedHandler accessDeniedHandler;

	private List<ServerWebExchangeDelegatingServerAccessDeniedHandler.DelegateEntry> defaultAccessDeniedHandlers = new ArrayList<>();

	private List<WebFilter> webFilters = new ArrayList<>();

	private ApplicationContext context;

	private Throwable built;

	private AnonymousSpec anonymous;

	private OneTimeTokenLoginSpec oneTimeTokenLogin;

	protected ServerHttpSecurity() {
	}

	/**
	 * The ServerExchangeMatcher that determines which requests apply to this HttpSecurity
	 * instance.
	 * @param matcher the ServerExchangeMatcher that determines which requests apply to
	 * this HttpSecurity instance. Default is all requests.
	 * @return the {@link ServerHttpSecurity} to continue configuring
	 */
	public ServerHttpSecurity securityMatcher(ServerWebExchangeMatcher matcher) {
		Assert.notNull(matcher, "matcher cannot be null");
		this.securityMatcher = matcher;
		return this;
	}

	/**
	 * Adds a {@link WebFilter} at a specific position.
	 * @param webFilter the {@link WebFilter} to add
	 * @param order the place to insert the {@link WebFilter}
	 * @return the {@link ServerHttpSecurity} to continue configuring
	 */
	public ServerHttpSecurity addFilterAt(WebFilter webFilter, SecurityWebFiltersOrder order) {
		this.webFilters.add(new OrderedWebFilter(webFilter, order.getOrder()));
		return this;
	}

	/**
	 *
	 * Adds a {@link WebFilter} before specific position.
	 * @param webFilter the {@link WebFilter} to add
	 * @param order the place before which to insert the {@link WebFilter}
	 * @return the {@link ServerHttpSecurity} to continue configuring
	 * @since 5.2.0
	 */
	public ServerHttpSecurity addFilterBefore(WebFilter webFilter, SecurityWebFiltersOrder order) {
		this.webFilters.add(new OrderedWebFilter(webFilter, order.getOrder() - 1));
		return this;
	}

	/**
	 * Adds a {@link WebFilter} after specific position.
	 * @param webFilter the {@link WebFilter} to add
	 * @param order the place after which to insert the {@link WebFilter}
	 * @return the {@link ServerHttpSecurity} to continue configuring
	 * @since 5.2.0
	 */
	public ServerHttpSecurity addFilterAfter(WebFilter webFilter, SecurityWebFiltersOrder order) {
		this.webFilters.add(new OrderedWebFilter(webFilter, order.getOrder() + 1));
		return this;
	}

	/**
	 * Gets the ServerExchangeMatcher that determines which requests apply to this
	 * HttpSecurity instance.
	 * @return the ServerExchangeMatcher that determines which requests apply to this
	 * HttpSecurity instance.
	 */
	private ServerWebExchangeMatcher getSecurityMatcher() {
		return this.securityMatcher;
	}

	/**
	 * The strategy used with {@code ReactorContextWebFilter}. It does impact how the
	 * {@code SecurityContext} is saved which is configured on a per
	 * {@link AuthenticationWebFilter} basis.
	 * @param securityContextRepository the repository to use
	 * @return the {@link ServerHttpSecurity} to continue configuring
	 */
	public ServerHttpSecurity securityContextRepository(ServerSecurityContextRepository securityContextRepository) {
		Assert.notNull(securityContextRepository, "securityContextRepository cannot be null");
		this.securityContextRepository = securityContextRepository;
		return this;
	}

	/**
	 * Configures HTTPS redirection rules. If the default is used:
	 *
	 * <pre class="code">
	 *  &#064;Bean
	 * 	public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
	 * 	    http
	 * 	        // ...
	 * 	        .redirectToHttps(withDefaults());
	 * 	    return http.build();
	 * 	}
	 * </pre>
	 *
	 * Then all non-HTTPS requests will be redirected to HTTPS.
	 *
	 * Typically, all requests should be HTTPS; however, the focus for redirection can
	 * also be narrowed:
	 *
	 * <pre class="code">
	 *  &#064;Bean
	 * 	public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
	 * 	    http
	 * 	        // ...
	 * 	        .redirectToHttps((redirectToHttps) -&gt;
	 * 	        	redirectToHttps
	 * 	            	.httpsRedirectWhen((serverWebExchange) -&gt;
	 * 	            		serverWebExchange.getRequest().getHeaders().containsKey("X-Requires-Https"))
	 * 	            );
	 * 	    return http.build();
	 * 	}
	 * </pre>
	 * @param httpsRedirectCustomizer the {@link Customizer} to provide more options for
	 * the {@link HttpsRedirectSpec}
	 * @return the {@link ServerHttpSecurity} to customize
	 */
	public ServerHttpSecurity redirectToHttps(Customizer<HttpsRedirectSpec> httpsRedirectCustomizer) {
		this.httpsRedirectSpec = new HttpsRedirectSpec();
		httpsRedirectCustomizer.customize(this.httpsRedirectSpec);
		return this;
	}

	/**
	 * Configures <a href=
	 * "https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)_Prevention_Cheat_Sheet">CSRF
	 * Protection</a> which is enabled by default. You can disable it using:
	 *
	 * <pre class="code">
	 *  &#064;Bean
	 *  public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
	 *      http
	 *          // ...
	 *          .csrf((csrf) -&gt;
	 *              csrf.disabled()
	 *          );
	 *      return http.build();
	 *  }
	 * </pre>
	 *
	 * Additional configuration options can be seen below:
	 *
	 *
	 * <pre class="code">
	 *  &#064;Bean
	 *  public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
	 *      http
	 *          // ...
	 *          .csrf((csrf) -&gt;
	 *              csrf
	 *                  // Handle CSRF failures
	 *                  .accessDeniedHandler(accessDeniedHandler)
	 *                  // Custom persistence of CSRF Token
	 *                  .csrfTokenRepository(csrfTokenRepository)
	 *                  // custom matching when CSRF protection is enabled
	 *                  .requireCsrfProtectionMatcher(matcher)
	 *          );
	 *      return http.build();
	 *  }
	 * </pre>
	 * @param csrfCustomizer the {@link Customizer} to provide more options for the
	 * {@link CsrfSpec}
	 * @return the {@link ServerHttpSecurity} to customize
	 */
	public ServerHttpSecurity csrf(Customizer<CsrfSpec> csrfCustomizer) {
		if (this.csrf == null) {
			this.csrf = new CsrfSpec();
		}
		csrfCustomizer.customize(this.csrf);
		return this;
	}

	/**
	 * Configures CORS headers. By default if a {@link CorsConfigurationSource} Bean is
	 * found, it will be used to create a {@link CorsWebFilter}. If
	 * {@link CorsSpec#configurationSource(CorsConfigurationSource)} is invoked it will be
	 * used instead. If neither has been configured, the Cors configuration will do
	 * nothing.
	 * @param corsCustomizer the {@link Customizer} to provide more options for the
	 * {@link CorsSpec}
	 * @return the {@link ServerHttpSecurity} to customize
	 */
	public ServerHttpSecurity cors(Customizer<CorsSpec> corsCustomizer) {
		if (this.cors == null) {
			this.cors = new CorsSpec();
		}
		corsCustomizer.customize(this.cors);
		return this;
	}

	/**
	 * Enables and Configures anonymous authentication. Anonymous Authentication is
	 * disabled by default.
	 *
	 * <pre class="code">
	 *  &#064;Bean
	 *  public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
	 *      http
	 *          // ...
	 *          .anonymous((anonymous) -&gt;
	 *              anonymous
	 *                  .key("key")
	 *                  .authorities("ROLE_ANONYMOUS")
	 *          );
	 *      return http.build();
	 *  }
	 * </pre>
	 * @param anonymousCustomizer the {@link Customizer} to provide more options for the
	 * {@link AnonymousSpec}
	 * @return the {@link ServerHttpSecurity} to customize
	 */
	public ServerHttpSecurity anonymous(Customizer<AnonymousSpec> anonymousCustomizer) {
		if (this.anonymous == null) {
			this.anonymous = new AnonymousSpec();
		}
		anonymousCustomizer.customize(this.anonymous);
		return this;
	}

	/**
	 * Configures HTTP Basic authentication. An example configuration is provided below:
	 *
	 * <pre class="code">
	 *  &#064;Bean
	 *  public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
	 *      http
	 *          // ...
	 *          .httpBasic((httpBasic) -&gt;
	 *              httpBasic
	 *                  // used for authenticating the credentials
	 *                  .authenticationManager(authenticationManager)
	 *                  // Custom persistence of the authentication
	 *                  .securityContextRepository(securityContextRepository)
	 *              );
	 *      return http.build();
	 *  }
	 * </pre>
	 * @param httpBasicCustomizer the {@link Customizer} to provide more options for the
	 * {@link HttpBasicSpec}
	 * @return the {@link ServerHttpSecurity} to customize
	 */
	public ServerHttpSecurity httpBasic(Customizer<HttpBasicSpec> httpBasicCustomizer) {
		if (this.httpBasic == null) {
			this.httpBasic = new HttpBasicSpec();
		}
		httpBasicCustomizer.customize(this.httpBasic);
		return this;
	}

	/**
	 * Configures Session Management. An example configuration is provided below:
	 * <pre class="code">
	 *  &#064;Bean
	 *  SecurityWebFilterChain filterChain(ServerHttpSecurity http, ReactiveSessionRegistry sessionRegistry) {
	 *      http
	 *          // ...
	 *          .sessionManagement((sessionManagement) -> sessionManagement
	 *              .concurrentSessions((concurrentSessions) -> concurrentSessions
	 *                  .maxSessions(1)
	 *                  .maxSessionsPreventsLogin(true)
	 *                  .sessionRegistry(sessionRegistry)
	 *              )
	 *          );
	 *      return http.build();
	 *  }
	 * </pre>
	 * @param customizer the {@link Customizer} to provide more options for the
	 * {@link SessionManagementSpec}
	 * @return the {@link ServerHttpSecurity} to continue configuring
	 * @since 6.3
	 */
	public ServerHttpSecurity sessionManagement(Customizer<SessionManagementSpec> customizer) {
		if (this.sessionManagement == null) {
			this.sessionManagement = new SessionManagementSpec();
		}
		customizer.customize(this.sessionManagement);
		return this;
	}

	/**
	 * Configures password management. An example configuration is provided below:
	 *
	 * <pre class="code">
	 *  &#064;Bean
	 *  public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
	 *      http
	 *          // ...
	 *          .passwordManagement(passwordManagement -&gt;
	 *          	// Custom change password page.
	 *          	passwordManagement.changePasswordPage("/custom-change-password-page")
	 *          );
	 *      return http.build();
	 *  }
	 * </pre>
	 * @param passwordManagementCustomizer the {@link Customizer} to provide more options
	 * for the {@link PasswordManagementSpec}
	 * @return the {@link ServerHttpSecurity} to customize
	 * @since 5.6
	 */
	public ServerHttpSecurity passwordManagement(Customizer<PasswordManagementSpec> passwordManagementCustomizer) {
		if (this.passwordManagement == null) {
			this.passwordManagement = new PasswordManagementSpec();
		}
		passwordManagementCustomizer.customize(this.passwordManagement);
		return this;
	}

	/**
	 * Configures form based authentication. An example configuration is provided below:
	 *
	 * <pre class="code">
	 *  &#064;Bean
	 *  public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
	 *      http
	 *          // ...
	 *          .formLogin((formLogin) -&gt;
	 *              formLogin
	 *              	// used for authenticating the credentials
	 *              	.authenticationManager(authenticationManager)
	 *              	// Custom persistence of the authentication
	 *              	.securityContextRepository(securityContextRepository)
	 *              	// expect a log in page at "/authenticate"
	 *              	// a POST "/authenticate" is where authentication occurs
	 *              	// error page at "/authenticate?error"
	 *              	.loginPage("/authenticate")
	 *          );
	 *      return http.build();
	 *  }
	 * </pre>
	 * @param formLoginCustomizer the {@link Customizer} to provide more options for the
	 * {@link FormLoginSpec}
	 * @return the {@link ServerHttpSecurity} to customize
	 */
	public ServerHttpSecurity formLogin(Customizer<FormLoginSpec> formLoginCustomizer) {
		if (this.formLogin == null) {
			this.formLogin = new FormLoginSpec();
		}
		formLoginCustomizer.customize(this.formLogin);
		return this;
	}

	/**
	 * Configures x509 authentication using a certificate provided by a client.
	 *
	 * <pre class="code">
	 *  &#064;Bean
	 *  public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
	 *      http
	 *          .x509((x509) -&gt;
	 *              x509
	 *          	    .authenticationManager(authenticationManager)
	 *                  .principalExtractor(principalExtractor)
	 *          );
	 *      return http.build();
	 *  }
	 * </pre>
	 *
	 * Note that if extractor is not specified, {@link SubjectX500PrincipalExtractor} will
	 * be used. If authenticationManager is not specified,
	 * {@link ReactivePreAuthenticatedAuthenticationManager} will be used.
	 * @param x509Customizer the {@link Customizer} to provide more options for the
	 * {@link X509Spec}
	 * @return the {@link ServerHttpSecurity} to customize
	 * @since 5.2
	 */
	public ServerHttpSecurity x509(Customizer<X509Spec> x509Customizer) {
		if (this.x509 == null) {
			this.x509 = new X509Spec();
		}
		x509Customizer.customize(this.x509);
		return this;
	}

	/**
	 * Configures authentication support using an OAuth 2.0 and/or OpenID Connect 1.0
	 * Provider.
	 *
	 * <pre class="code">
	 *  &#064;Bean
	 *  public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
	 *      http
	 *          // ...
	 *          .oauth2Login((oauth2Login) -&gt;
	 *              oauth2Login
	 *                  .authenticationConverter(authenticationConverter)
	 *                  .authenticationManager(manager)
	 *          );
	 *      return http.build();
	 *  }
	 * </pre>
	 * @param oauth2LoginCustomizer the {@link Customizer} to provide more options for the
	 * {@link OAuth2LoginSpec}
	 * @return the {@link ServerHttpSecurity} to customize
	 */
	public ServerHttpSecurity oauth2Login(Customizer<OAuth2LoginSpec> oauth2LoginCustomizer) {
		if (this.oauth2Login == null) {
			this.oauth2Login = new OAuth2LoginSpec();
		}
		oauth2LoginCustomizer.customize(this.oauth2Login);
		return this;
	}

	/**
	 * Configures the OAuth2 client.
	 *
	 * <pre class="code">
	 *  &#064;Bean
	 *  public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
	 *      http
	 *          // ...
	 *          .oauth2Client((oauth2Client) -&gt;
	 *              oauth2Client
	 *                  .clientRegistrationRepository(clientRegistrationRepository)
	 *                  .authorizedClientRepository(authorizedClientRepository)
	 *          );
	 *      return http.build();
	 *  }
	 * </pre>
	 * @param oauth2ClientCustomizer the {@link Customizer} to provide more options for
	 * the {@link OAuth2ClientSpec}
	 * @return the {@link ServerHttpSecurity} to customize
	 */
	public ServerHttpSecurity oauth2Client(Customizer<OAuth2ClientSpec> oauth2ClientCustomizer) {
		if (this.client == null) {
			this.client = new OAuth2ClientSpec();
		}
		oauth2ClientCustomizer.customize(this.client);
		return this;
	}

	/**
	 * Configures OAuth 2.0 Resource Server support.
	 *
	 * <pre class="code">
	 *  &#064;Bean
	 *  public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
	 *      http
	 *          // ...
	 *          .oauth2ResourceServer((oauth2ResourceServer) -&gt;
	 *              oauth2ResourceServer
	 *                  .jwt((jwt) -&gt;
	 *                      jwt
	 *                          .publicKey(publicKey())
	 *                  )
	 *          );
	 *      return http.build();
	 *  }
	 * </pre>
	 * @param oauth2ResourceServerCustomizer the {@link Customizer} to provide more
	 * options for the {@link OAuth2ResourceServerSpec}
	 * @return the {@link ServerHttpSecurity} to customize
	 */
	public ServerHttpSecurity oauth2ResourceServer(
			Customizer<OAuth2ResourceServerSpec> oauth2ResourceServerCustomizer) {
		if (this.resourceServer == null) {
			this.resourceServer = new OAuth2ResourceServerSpec();
		}
		oauth2ResourceServerCustomizer.customize(this.resourceServer);
		return this;
	}

	/**
	 * Configures OIDC Connect 1.0 Logout support.
	 *
	 * <pre class="code">
	 *  &#064;Bean
	 *  public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
	 *      http
	 *          // ...
	 *          .oidcLogout((logout) -&gt; logout
	 *              .backChannel(Customizer.withDefaults())
	 *          );
	 *      return http.build();
	 *  }
	 * </pre>
	 * @param oidcLogoutCustomizer the {@link Customizer} to provide more options for the
	 * {@link OidcLogoutSpec}
	 * @return the {@link ServerHttpSecurity} to customize
	 * @since 6.2
	 */
	public ServerHttpSecurity oidcLogout(Customizer<OidcLogoutSpec> oidcLogoutCustomizer) {
		if (this.oidcLogout == null) {
			this.oidcLogout = new OidcLogoutSpec();
		}
		oidcLogoutCustomizer.customize(this.oidcLogout);
		return this;
	}

	/**
	 * Configures HTTP Response Headers. The default headers are:
	 *
	 * <pre>
	 * Cache-Control: no-cache, no-store, max-age=0, must-revalidate
	 * Pragma: no-cache
	 * Expires: 0
	 * X-Content-Type-Options: nosniff
	 * Strict-Transport-Security: max-age=31536000 ; includeSubDomains
	 * X-Frame-Options: DENY
	 * X-XSS-Protection: 0
	 * </pre>
	 *
	 * such that "Strict-Transport-Security" is only added on secure requests.
	 *
	 * An example configuration is provided below:
	 *
	 * <pre class="code">
	 *  &#064;Bean
	 *  public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
	 *      http
	 *          // ...
	 *          .headers((headers) -&gt;
	 *              headers
	 *                  // customize frame options to be same origin
	 *                  .frameOptions((frameOptions) -&gt;
	 *                      frameOptions
	 *                          .mode(XFrameOptionsServerHttpHeadersWriter.Mode.SAMEORIGIN)
	 *                   )
	 *                  // disable cache control
	 *                  .cache((cache) -&gt;
	 *                      cache
	 *                          .disable()
	 *                  )
	 *          );
	 *      return http.build();
	 *  }
	 * </pre>
	 * @param headerCustomizer the {@link Customizer} to provide more options for the
	 * {@link HeaderSpec}
	 * @return the {@link ServerHttpSecurity} to customize
	 */
	public ServerHttpSecurity headers(Customizer<HeaderSpec> headerCustomizer) {
		if (this.headers == null) {
			this.headers = new HeaderSpec();
		}
		headerCustomizer.customize(this.headers);
		return this;
	}

	/**
	 * Configures exception handling (i.e. handles when authentication is requested). An
	 * example configuration can be found below:
	 *
	 * <pre class="code">
	 *  &#064;Bean
	 *  public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
	 *      http
	 *          // ...
	 *          .exceptionHandling((exceptionHandling) -&gt;
	 *              exceptionHandling
	 *                  // customize how to request for authentication
	 *                  .authenticationEntryPoint(entryPoint)
	 *          );
	 *      return http.build();
	 *  }
	 * </pre>
	 * @param exceptionHandlingCustomizer the {@link Customizer} to provide more options
	 * for the {@link ExceptionHandlingSpec}
	 * @return the {@link ServerHttpSecurity} to customize
	 */
	public ServerHttpSecurity exceptionHandling(Customizer<ExceptionHandlingSpec> exceptionHandlingCustomizer) {
		if (this.exceptionHandling == null) {
			this.exceptionHandling = new ExceptionHandlingSpec();
		}
		exceptionHandlingCustomizer.customize(this.exceptionHandling);
		return this;
	}

	/**
	 * Configures authorization. An example configuration can be found below:
	 *
	 * <pre class="code">
	 *  &#064;Bean
	 *  public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
	 *      http
	 *          // ...
	 *          .authorizeExchange((exchanges) -&gt;
	 *              exchanges
	 *                  // any URL that starts with /admin/ requires the role "ROLE_ADMIN"
	 *                  .pathMatchers("/admin/**").hasRole("ADMIN")
	 *                  // a POST to /users requires the role "USER_POST"
	 *                  .pathMatchers(HttpMethod.POST, "/users").hasAuthority("USER_POST")
	 *                  // a request to /users/{username} requires the current authentication's username
	 *                  // to be equal to the {username}
	 *                  .pathMatchers("/users/{username}").access((authentication, context) -&gt;
	 *                      authentication
	 *                          .map(Authentication::getName)
	 *                          .map((username) -&gt; username.equals(context.getVariables().get("username")))
	 *                          .map(AuthorizationDecision::new)
	 *                  )
	 *                  // allows providing a custom matching strategy that requires the role "ROLE_CUSTOM"
	 *                  .matchers(customMatcher).hasRole("CUSTOM")
	 *                  // any other request requires the user to be authenticated
	 *                  .anyExchange().authenticated()
	 *          );
	 *      return http.build();
	 *  }
	 * </pre>
	 * @param authorizeExchangeCustomizer the {@link Customizer} to provide more options
	 * for the {@link AuthorizeExchangeSpec}
	 * @return the {@link ServerHttpSecurity} to customize
	 */
	public ServerHttpSecurity authorizeExchange(Customizer<AuthorizeExchangeSpec> authorizeExchangeCustomizer) {
		if (this.authorizeExchange == null) {
			this.authorizeExchange = new AuthorizeExchangeSpec();
		}
		authorizeExchangeCustomizer.customize(this.authorizeExchange);
		return this;
	}

	/**
	 * Configures log out. An example configuration can be found below:
	 *
	 * <pre class="code">
	 *  &#064;Bean
	 *  public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
	 *      http
	 *          // ...
	 *          .logout((logout) -&gt;
	 *              logout
	 *                  // configures how log out is done
	 *                  .logoutHandler(logoutHandler)
	 *                  // log out will be performed on POST /signout
	 *                  .logoutUrl("/signout")
	 *                  // configure what is done on logout success
	 *                  .logoutSuccessHandler(successHandler)
	 *          );
	 *      return http.build();
	 *  }
	 * </pre>
	 * @param logoutCustomizer the {@link Customizer} to provide more options for the
	 * {@link LogoutSpec}
	 * @return the {@link ServerHttpSecurity} to customize
	 */
	public ServerHttpSecurity logout(Customizer<LogoutSpec> logoutCustomizer) {
		if (this.logout == null) {
			this.logout = new LogoutSpec();
		}
		logoutCustomizer.customize(this.logout);
		return this;
	}

	/**
	 * Configures the request cache which is used when a flow is interrupted (i.e. due to
	 * requesting credentials) so that the request can be replayed after authentication.
	 * An example configuration can be found below:
	 *
	 * <pre class="code">
	 *  &#064;Bean
	 *  public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
	 *      http
	 *          // ...
	 *          .requestCache((requestCache) -&gt;
	 *              requestCache
	 *                  // configures how the request is cached
	 *                  .requestCache(customRequestCache)
	 *          );
	 *      return http.build();
	 *  }
	 * </pre>
	 * @param requestCacheCustomizer the {@link Customizer} to provide more options for
	 * the {@link RequestCacheSpec}
	 * @return the {@link ServerHttpSecurity} to customize
	 */
	public ServerHttpSecurity requestCache(Customizer<RequestCacheSpec> requestCacheCustomizer) {
		requestCacheCustomizer.customize(this.requestCache);
		return this;
	}

	/**
	 * Configure the default authentication manager.
	 * @param manager the authentication manager to use
	 * @return the {@code ServerHttpSecurity} to customize
	 */
	public ServerHttpSecurity authenticationManager(ReactiveAuthenticationManager manager) {
		this.authenticationManager = manager;
		return this;
	}

	/**
	 * Configures One-Time Token Login Support.
	 *
	 * <h2>Example Configuration</h2>
	 *
	 * <pre>
	 * &#064;Configuration
	 * &#064;EnableWebFluxSecurity
	 * public class SecurityConfig {
	 *
	 * 	&#064;Bean
	 * 	public SecurityWebFilterChain securityFilterChain(ServerHttpSecurity http) throws Exception {
	 * 		http
	 * 			// ...
	 * 			.oneTimeTokenLogin(Customizer.withDefaults());
	 * 		return http.build();
	 * 	}
	 *
	 * 	&#064;Bean
	 * 	public ServerOneTimeTokenGenerationSuccessHandler oneTimeTokenGenerationSuccessHandler() {
	 * 		return new MyMagicLinkServerOneTimeTokenGenerationSuccessHandler();
	 * 	}
	 *
	 * }
	 * </pre>
	 * @param oneTimeTokenLoginCustomizer the {@link Customizer} to provide more options
	 * for the {@link OneTimeTokenLoginSpec}
	 * @return the {@link ServerHttpSecurity} for further customizations
	 */
	public ServerHttpSecurity oneTimeTokenLogin(Customizer<OneTimeTokenLoginSpec> oneTimeTokenLoginCustomizer) {
		if (this.oneTimeTokenLogin == null) {
			this.oneTimeTokenLogin = new OneTimeTokenLoginSpec();
		}
		oneTimeTokenLoginCustomizer.customize(this.oneTimeTokenLogin);
		return this;
	}

	/**
	 * Builds the {@link SecurityWebFilterChain}
	 * @return the {@link SecurityWebFilterChain}
	 */
	public SecurityWebFilterChain build() {
		if (this.built != null) {
			throw new IllegalStateException(
					"This has already been built with the following stacktrace. " + buildToString());
		}
		this.built = new RuntimeException("First Build Invocation").fillInStackTrace();
		if (this.headers != null) {
			this.headers.configure(this);
		}
		WebFilter securityContextRepositoryWebFilter = securityContextRepositoryWebFilter();
		this.webFilters.add(securityContextRepositoryWebFilter);
		if (this.sessionManagement != null) {
			this.sessionManagement.configure(this);
		}
		if (this.httpsRedirectSpec != null) {
			this.httpsRedirectSpec.configure(this);
		}
		if (this.x509 != null) {
			this.x509.configure(this);
		}
		if (this.csrf != null) {
			this.csrf.configure(this);
		}
		if (this.cors != null) {
			this.cors.configure(this);
		}
		if (this.httpBasic != null) {
			if (this.httpBasic.authenticationManager == null) {
				this.httpBasic.authenticationManager(this.authenticationManager);
			}
			if (this.httpBasic.securityContextRepository != null) {
				this.httpBasic.securityContextRepository(this.httpBasic.securityContextRepository);
			}
			else if (this.securityContextRepository != null) {
				this.httpBasic.securityContextRepository(this.securityContextRepository);
			}
			else {
				this.httpBasic.securityContextRepository(NoOpServerSecurityContextRepository.getInstance());
			}
			this.httpBasic.configure(this);
		}
		if (this.passwordManagement != null) {
			this.passwordManagement.configure(this);
		}
		if (this.formLogin != null) {
			if (this.formLogin.authenticationManager == null) {
				this.formLogin.authenticationManager(this.authenticationManager);
			}
			if (this.formLogin.securityContextRepository != null) {
				this.formLogin.securityContextRepository(this.formLogin.securityContextRepository);
			}
			else if (this.securityContextRepository != null) {
				this.formLogin.securityContextRepository(this.securityContextRepository);
			}
			else {
				this.formLogin.securityContextRepository(new WebSessionServerSecurityContextRepository());
			}
			this.formLogin.configure(this);
		}
		if (this.oauth2Login != null) {
			if (this.oauth2Login.securityContextRepository != null) {
				this.oauth2Login.securityContextRepository(this.oauth2Login.securityContextRepository);
			}
			else if (this.securityContextRepository != null) {
				this.oauth2Login.securityContextRepository(this.securityContextRepository);
			}
			else {
				this.oauth2Login.securityContextRepository(new WebSessionServerSecurityContextRepository());
			}
			this.oauth2Login.configure(this);
		}
		if (this.resourceServer != null) {
			this.resourceServer.configure(this);
		}
		if (this.oidcLogout != null) {
			this.oidcLogout.configure(this);
		}
		if (this.client != null) {
			this.client.configure(this);
		}
		if (this.anonymous != null) {
			this.anonymous.configure(this);
		}
		this.loginPage.configure(this);
		if (this.logout != null) {
			this.logout.configure(this);
		}
		this.requestCache.configure(this);
		if (this.oneTimeTokenLogin != null) {
			if (this.oneTimeTokenLogin.securityContextRepository != null) {
				this.oneTimeTokenLogin.securityContextRepository(this.oneTimeTokenLogin.securityContextRepository);
			}
			else if (this.securityContextRepository != null) {
				this.oneTimeTokenLogin.securityContextRepository(this.securityContextRepository);
			}
			else {
				this.oneTimeTokenLogin.securityContextRepository(new WebSessionServerSecurityContextRepository());
			}
			this.oneTimeTokenLogin.configure(this);
		}
		this.addFilterAt(new SecurityContextServerWebExchangeWebFilter(),
				SecurityWebFiltersOrder.SECURITY_CONTEXT_SERVER_WEB_EXCHANGE);
		if (this.authorizeExchange != null) {
			ServerAuthenticationEntryPoint authenticationEntryPoint = getAuthenticationEntryPoint();
			ExceptionTranslationWebFilter exceptionTranslationWebFilter = new ExceptionTranslationWebFilter();
			if (authenticationEntryPoint != null) {
				exceptionTranslationWebFilter.setAuthenticationEntryPoint(authenticationEntryPoint);
			}
			ServerAccessDeniedHandler accessDeniedHandler = getAccessDeniedHandler();
			if (accessDeniedHandler != null) {
				exceptionTranslationWebFilter.setAccessDeniedHandler(accessDeniedHandler);
			}
			this.addFilterAt(exceptionTranslationWebFilter, SecurityWebFiltersOrder.EXCEPTION_TRANSLATION);
			this.authorizeExchange.configure(this);
		}
		AnnotationAwareOrderComparator.sort(this.webFilters);
		List<WebFilter> sortedWebFilters = new ArrayList<>();
		this.webFilters.forEach((f) -> {
			if (f instanceof OrderedWebFilter) {
				f = ((OrderedWebFilter) f).webFilter;
			}
			sortedWebFilters.add(f);
		});
		sortedWebFilters.add(0, new ServerWebExchangeReactorContextWebFilter());
		return new MatcherSecurityWebFilterChain(getSecurityMatcher(), sortedWebFilters);
	}

	private String buildToString() {
		try (StringWriter writer = new StringWriter()) {
			try (PrintWriter printer = new PrintWriter(writer)) {
				printer.println();
				printer.println();
				this.built.printStackTrace(printer);
				printer.println();
				printer.println();
				return writer.toString();
			}
		}
		catch (IOException ex) {
			throw new RuntimeException(ex);
		}
	}

	private ServerAuthenticationEntryPoint getAuthenticationEntryPoint() {
		if (this.authenticationEntryPoint != null || this.defaultEntryPoints.isEmpty()) {
			return this.authenticationEntryPoint;
		}
		if (this.defaultEntryPoints.size() == 1) {
			return this.defaultEntryPoints.get(0).getEntryPoint();
		}
		DelegatingServerAuthenticationEntryPoint result = new DelegatingServerAuthenticationEntryPoint(
				this.defaultEntryPoints);
		result.setDefaultEntryPoint(this.defaultEntryPoints.get(this.defaultEntryPoints.size() - 1).getEntryPoint());
		return result;
	}

	private ServerAccessDeniedHandler getAccessDeniedHandler() {
		if (this.accessDeniedHandler != null || this.defaultAccessDeniedHandlers.isEmpty()) {
			return this.accessDeniedHandler;
		}
		if (this.defaultAccessDeniedHandlers.size() == 1) {
			return this.defaultAccessDeniedHandlers.get(0).getAccessDeniedHandler();
		}
		ServerWebExchangeDelegatingServerAccessDeniedHandler result = new ServerWebExchangeDelegatingServerAccessDeniedHandler(
				this.defaultAccessDeniedHandlers);
		result.setDefaultAccessDeniedHandler(
				this.defaultAccessDeniedHandlers.get(this.defaultAccessDeniedHandlers.size() - 1)
					.getAccessDeniedHandler());
		return result;
	}

	/**
	 * Creates a new instance.
	 * @return the new {@link ServerHttpSecurity} instance
	 */
	public static ServerHttpSecurity http() {
		return new ServerHttpSecurity();
	}

	private WebFilter securityContextRepositoryWebFilter() {
		ServerSecurityContextRepository repository = (this.securityContextRepository != null)
				? this.securityContextRepository : new WebSessionServerSecurityContextRepository();
		WebFilter result = new ReactorContextWebFilter(repository);
		return new OrderedWebFilter(result, SecurityWebFiltersOrder.REACTOR_CONTEXT.getOrder());
	}

	private <T> T getBean(Class<T> beanClass) {
		if (this.context == null) {
			return null;
		}
		return this.context.getBean(beanClass);
	}

	private <T> T getBeanOrDefault(Class<T> beanClass, T defaultInstance) {
		if (this.context == null) {
			return defaultInstance;
		}
		return this.context.getBeanProvider(beanClass).getIfUnique(() -> defaultInstance);
	}

	private <T> ObjectProvider<T> getBeanProvider(ResolvableType type) {
		if (this.context == null) {
			return new ObjectProvider<>() {
				@Override
				public Iterator<T> iterator() {
					return Collections.emptyIterator();
				}
			};
		}
		return this.context.getBeanProvider(type);
	}

	private <T> T getBeanOrNull(Class<T> beanClass) {
		return getBeanOrNull(ResolvableType.forClass(beanClass));
	}

	@SuppressWarnings("unchecked")
	private <T> T getBeanOrNull(ResolvableType type) {
		if (this.context == null) {
			return null;
		}
		return (T) this.context.getBeanProvider(type).getIfUnique();
	}

	private <T> T getBeanOrNull(String beanName, Class<T> requiredClass) {
		if (this.context == null) {
			return null;
		}
		try {
			return this.context.getBean(beanName, requiredClass);
		}
		catch (Exception ex) {
			return null;
		}
	}

	private <T> String[] getBeanNamesForTypeOrEmpty(Class<T> beanClass) {
		if (this.context == null) {
			return new String[0];
		}
		return this.context.getBeanNamesForType(beanClass);
	}

	ApplicationContext getApplicationContext() {
		return this.context;
	}

	protected void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
		this.context = applicationContext;
	}

	/**
	 * Configures authorization
	 *
	 * @author Rob Winch
	 * @since 5.0
	 * @see #authorizeExchange(Customizer)
	 */
	public class AuthorizeExchangeSpec extends AbstractServerWebExchangeMatcherRegistry<AuthorizeExchangeSpec.Access> {

		private static final String REQUEST_MAPPING_HANDLER_MAPPING_BEAN_NAME = "requestMappingHandlerMapping";

		private DelegatingReactiveAuthorizationManager.Builder managerBldr = DelegatingReactiveAuthorizationManager
			.builder();

		private ServerWebExchangeMatcher matcher;

		private boolean anyExchangeRegistered;

		private PathPatternParser pathPatternParser;

		private ObjectPostProcessor<ReactiveAuthorizationManager<ServerWebExchange>> postProcessor = ObjectPostProcessor
			.identity();

		public AuthorizeExchangeSpec() {
			ResolvableType type = ResolvableType.forClassWithGenerics(ObjectPostProcessor.class,
					ResolvableType.forClassWithGenerics(ReactiveAuthorizationManager.class, ServerWebExchange.class));
			ObjectProvider<ObjectPostProcessor<ReactiveAuthorizationManager<ServerWebExchange>>> postProcessor = getBeanProvider(
					type);
			postProcessor.ifUnique((p) -> this.postProcessor = p);
		}

		/**
		 * Disables authorization.
		 * @return the {@link Access} to continue configuring
		 */
		@Override
		public Access anyExchange() {
			Access result = super.anyExchange();
			this.anyExchangeRegistered = true;
			return result;
		}

		@Override
		protected PathPatternParser getPathPatternParser() {
			if (this.pathPatternParser != null) {
				return this.pathPatternParser;
			}
			RequestMappingHandlerMapping requestMappingHandlerMapping = getBeanOrNull(
					REQUEST_MAPPING_HANDLER_MAPPING_BEAN_NAME, RequestMappingHandlerMapping.class);
			if (requestMappingHandlerMapping != null) {
				this.pathPatternParser = requestMappingHandlerMapping.getPathPatternParser();
			}
			if (this.pathPatternParser == null) {
				this.pathPatternParser = PathPatternParser.defaultInstance;
			}
			return this.pathPatternParser;
		}

		@Override
		protected Access registerMatcher(ServerWebExchangeMatcher matcher) {
			Assert.state(!this.anyExchangeRegistered, () -> "Cannot register " + matcher
					+ " which would be unreachable because anyExchange() has already been registered.");
			Assert.state(this.matcher == null,
					() -> "The matcher " + matcher + " does not have an access rule defined");
			this.matcher = matcher;
			return new Access();
		}

		protected void configure(ServerHttpSecurity http) {
			Assert.state(this.matcher == null,
					() -> "The matcher " + this.matcher + " does not have an access rule defined");
			ReactiveAuthorizationManager<ServerWebExchange> manager = this.managerBldr.build();
			manager = this.postProcessor.postProcess(manager);
			AuthorizationWebFilter result = new AuthorizationWebFilter(manager);
			http.addFilterAt(result, SecurityWebFiltersOrder.AUTHORIZATION);
		}

		/**
		 * Configures the access for a particular set of exchanges.
		 */
		public final class Access {

			/**
			 * Allow access for anyone
			 * @return the {@link AuthorizeExchangeSpec} to configure
			 */
			public AuthorizeExchangeSpec permitAll() {
				return access((a, e) -> Mono.just(new AuthorizationDecision(true)));
			}

			/**
			 * Deny access for everyone
			 * @return the {@link AuthorizeExchangeSpec} to configure
			 */
			public AuthorizeExchangeSpec denyAll() {
				return access((a, e) -> Mono.just(new AuthorizationDecision(false)));
			}

			/**
			 * Require a specific role. This is a shorcut for
			 * {@link #hasAuthority(String)}
			 * @param role the role (i.e. "USER" would require "ROLE_USER")
			 * @return the {@link AuthorizeExchangeSpec} to configure
			 */
			public AuthorizeExchangeSpec hasRole(String role) {
				return access(AuthorityReactiveAuthorizationManager.hasRole(role));
			}

			/**
			 * Require any specific role. This is a shortcut for
			 * {@link #hasAnyAuthority(String...)}
			 * @param roles the roles (i.e. "USER" would require "ROLE_USER")
			 * @return the {@link AuthorizeExchangeSpec} to configure
			 */
			public AuthorizeExchangeSpec hasAnyRole(String... roles) {
				return access(AuthorityReactiveAuthorizationManager.hasAnyRole(roles));
			}

			/**
			 * Require a specific authority.
			 * @param authority the authority to require (i.e. "USER" would require
			 * authority of "USER").
			 * @return the {@link AuthorizeExchangeSpec} to configure
			 */
			public AuthorizeExchangeSpec hasAuthority(String authority) {
				return access(AuthorityReactiveAuthorizationManager.hasAuthority(authority));
			}

			/**
			 * Require any authority
			 * @param authorities the authorities to require (i.e. "USER" would require
			 * authority of "USER").
			 * @return the {@link AuthorizeExchangeSpec} to configure
			 */
			public AuthorizeExchangeSpec hasAnyAuthority(String... authorities) {
				return access(AuthorityReactiveAuthorizationManager.hasAnyAuthority(authorities));
			}

			/**
			 * Require an authenticated user
			 * @return the {@link AuthorizeExchangeSpec} to configure
			 */
			public AuthorizeExchangeSpec authenticated() {
				return access(AuthenticatedReactiveAuthorizationManager.authenticated());
			}

			/**
			 * Require a specific IP address or range using an IP/Netmask (e.g.
			 * 192.168.1.0/24).
			 * @param ipAddress the address or range of addresses from which the request
			 * must come.
			 * @return the {@link AuthorizeExchangeSpec} to configure
			 * @since 5.7
			 */
			public AuthorizeExchangeSpec hasIpAddress(String ipAddress) {
				return access(IpAddressReactiveAuthorizationManager.hasIpAddress(ipAddress));
			}

			/**
			 * Allows plugging in a custom authorization strategy
			 * @param manager the authorization manager to use
			 * @return the {@link AuthorizeExchangeSpec} to configure
			 */
			public AuthorizeExchangeSpec access(ReactiveAuthorizationManager<AuthorizationContext> manager) {
				AuthorizeExchangeSpec.this.managerBldr
					.add(new ServerWebExchangeMatcherEntry<>(AuthorizeExchangeSpec.this.matcher, manager));
				AuthorizeExchangeSpec.this.matcher = null;
				return AuthorizeExchangeSpec.this;
			}

		}

	}

	/**
	 * Configures how sessions are managed.
	 */
	public class SessionManagementSpec {

		private ConcurrentSessionsSpec concurrentSessions;

		private ServerAuthenticationSuccessHandler authenticationSuccessHandler;

		private ReactiveSessionRegistry sessionRegistry;

		private SessionLimit sessionLimit = SessionLimit.UNLIMITED;

		private ServerMaximumSessionsExceededHandler maximumSessionsExceededHandler;

		/**
		 * Configures how many sessions are allowed for a given user.
		 * @param customizer the customizer to provide more options
		 * @return the {@link SessionManagementSpec} to customize
		 */
		public SessionManagementSpec concurrentSessions(Customizer<ConcurrentSessionsSpec> customizer) {
			if (this.concurrentSessions == null) {
				this.concurrentSessions = new ConcurrentSessionsSpec();
			}
			customizer.customize(this.concurrentSessions);
			return this;
		}

		void configure(ServerHttpSecurity http) {
			if (this.concurrentSessions != null) {
				ReactiveSessionRegistry reactiveSessionRegistry = getSessionRegistry();
				ConcurrentSessionControlServerAuthenticationSuccessHandler concurrentSessionControlStrategy = new ConcurrentSessionControlServerAuthenticationSuccessHandler(
						reactiveSessionRegistry, getMaximumSessionsExceededHandler());
				concurrentSessionControlStrategy.setSessionLimit(this.sessionLimit);
				RegisterSessionServerAuthenticationSuccessHandler registerSessionAuthenticationStrategy = new RegisterSessionServerAuthenticationSuccessHandler(
						reactiveSessionRegistry);
				this.authenticationSuccessHandler = new DelegatingServerAuthenticationSuccessHandler(
						concurrentSessionControlStrategy, registerSessionAuthenticationStrategy);
				SessionRegistryWebFilter sessionRegistryWebFilter = new SessionRegistryWebFilter(
						reactiveSessionRegistry);
				configureSuccessHandlerOnAuthenticationFilters();
				http.addFilterAfter(sessionRegistryWebFilter, SecurityWebFiltersOrder.HTTP_HEADERS_WRITER);
			}
		}

		private ServerMaximumSessionsExceededHandler getMaximumSessionsExceededHandler() {
			if (this.maximumSessionsExceededHandler != null) {
				return this.maximumSessionsExceededHandler;
			}
			DefaultWebSessionManager webSessionManager = getBeanOrNull(
					WebHttpHandlerBuilder.WEB_SESSION_MANAGER_BEAN_NAME, DefaultWebSessionManager.class);
			if (webSessionManager != null) {
				this.maximumSessionsExceededHandler = new InvalidateLeastUsedServerMaximumSessionsExceededHandler(
						webSessionManager.getSessionStore());
			}
			if (this.maximumSessionsExceededHandler == null) {
				throw new IllegalStateException(
						"Could not create a default ServerMaximumSessionsExceededHandler. Please provide "
								+ "a ServerMaximumSessionsExceededHandler via DSL");
			}
			return this.maximumSessionsExceededHandler;
		}

		private void configureSuccessHandlerOnAuthenticationFilters() {
			if (ServerHttpSecurity.this.formLogin != null) {
				ServerHttpSecurity.this.formLogin.defaultSuccessHandlers.add(0, this.authenticationSuccessHandler);
			}
			if (ServerHttpSecurity.this.oauth2Login != null) {
				ServerHttpSecurity.this.oauth2Login.defaultSuccessHandlers.add(0, this.authenticationSuccessHandler);
			}
			if (ServerHttpSecurity.this.httpBasic != null) {
				ServerHttpSecurity.this.httpBasic.defaultSuccessHandlers.add(0, this.authenticationSuccessHandler);
			}
		}

		private ReactiveSessionRegistry getSessionRegistry() {
			if (this.sessionRegistry == null) {
				this.sessionRegistry = getBeanOrNull(ReactiveSessionRegistry.class);
			}
			if (this.sessionRegistry == null) {
				throw new IllegalStateException(
						"A ReactiveSessionRegistry is needed for concurrent session management");
			}
			return this.sessionRegistry;
		}

		/**
		 * Configures how many sessions are allowed for a given user.
		 */
		public class ConcurrentSessionsSpec {

			/**
			 * Sets the {@link ReactiveSessionRegistry} to use.
			 * @param reactiveSessionRegistry the {@link ReactiveSessionRegistry} to use
			 * @return the {@link ConcurrentSessionsSpec} to continue customizing
			 */
			public ConcurrentSessionsSpec sessionRegistry(ReactiveSessionRegistry reactiveSessionRegistry) {
				SessionManagementSpec.this.sessionRegistry = reactiveSessionRegistry;
				return this;
			}

			/**
			 * Sets the maximum number of sessions allowed for any user. You can use
			 * {@link SessionLimit#of(int)} to specify a positive integer or
			 * {@link SessionLimit#UNLIMITED} to allow unlimited sessions. To customize
			 * the maximum number of sessions on a per-user basis, you can provide a
			 * custom {@link SessionLimit} implementation, like so: <pre>
			 *     http
			 *         .sessionManagement((sessions) -> sessions
			 *             .concurrentSessions((concurrency) -> concurrency
			 *                 .maximumSessions((authentication) -> {
			 *                     if (authentication.getName().equals("admin")) {
			 *                         return Mono.empty() // unlimited sessions for admin
			 *                     }
			 *                     return Mono.just(1); // one session for every other user
			 *                 })
			 *             )
			 *         )
			 * </pre>
			 * @param sessionLimit the maximum number of sessions allowed for any user
			 * @return the {@link ConcurrentSessionsSpec} to continue customizing
			 */
			public ConcurrentSessionsSpec maximumSessions(SessionLimit sessionLimit) {
				Assert.notNull(sessionLimit, "sessionLimit cannot be null");
				SessionManagementSpec.this.sessionLimit = sessionLimit;
				return this;
			}

			/**
			 * Sets the {@link ServerMaximumSessionsExceededHandler} to use when the
			 * maximum number of sessions is exceeded.
			 * @param maximumSessionsExceededHandler the
			 * {@link ServerMaximumSessionsExceededHandler} to use
			 * @return the {@link ConcurrentSessionsSpec} to continue customizing
			 */
			public ConcurrentSessionsSpec maximumSessionsExceededHandler(
					ServerMaximumSessionsExceededHandler maximumSessionsExceededHandler) {
				Assert.notNull(maximumSessionsExceededHandler, "maximumSessionsExceededHandler cannot be null");
				SessionManagementSpec.this.maximumSessionsExceededHandler = maximumSessionsExceededHandler;
				return this;
			}

		}

		private static final class SessionRegistryWebFilter implements WebFilter {

			private final ReactiveSessionRegistry sessionRegistry;

			private SessionRegistryWebFilter(ReactiveSessionRegistry sessionRegistry) {
				Assert.notNull(sessionRegistry, "sessionRegistry cannot be null");
				this.sessionRegistry = sessionRegistry;
			}

			@Override
			public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
				return chain.filter(new SessionRegistryWebExchange(exchange));
			}

			private final class SessionRegistryWebExchange extends ServerWebExchangeDecorator {

				private final Mono<WebSession> sessionMono;

				private SessionRegistryWebExchange(ServerWebExchange delegate) {
					super(delegate);
					this.sessionMono = delegate.getSession()
						.flatMap((session) -> SessionRegistryWebFilter.this.sessionRegistry
							.updateLastAccessTime(session.getId())
							.thenReturn(session))
						.map(SessionRegistryWebSession::new);
				}

				@Override
				public Mono<WebSession> getSession() {
					return this.sessionMono;
				}

			}

			private final class SessionRegistryWebSession implements WebSession {

				private final WebSession session;

				private SessionRegistryWebSession(WebSession session) {
					this.session = session;
				}

				@Override
				public String getId() {
					return this.session.getId();
				}

				@Override
				public Map<String, Object> getAttributes() {
					return this.session.getAttributes();
				}

				@Override
				public void start() {
					this.session.start();
				}

				@Override
				public boolean isStarted() {
					return this.session.isStarted();
				}

				@Override
				public Mono<Void> changeSessionId() {
					String currentId = this.session.getId();
					return this.session.changeSessionId()
						.then(Mono.defer(
								() -> SessionRegistryWebFilter.this.sessionRegistry.removeSessionInformation(currentId)
									.flatMap((information) -> {
										information = information.withSessionId(this.session.getId());
										return SessionRegistryWebFilter.this.sessionRegistry
											.saveSessionInformation(information);
									})));
				}

				@Override
				public Mono<Void> invalidate() {
					String currentId = this.session.getId();
					return this.session.invalidate()
						.then(Mono.defer(() -> SessionRegistryWebFilter.this.sessionRegistry
							.removeSessionInformation(currentId)))
						.then();
				}

				@Override
				public Mono<Void> save() {
					return this.session.save();
				}

				@Override
				public boolean isExpired() {
					return this.session.isExpired();
				}

				@Override
				public Instant getCreationTime() {
					return this.session.getCreationTime();
				}

				@Override
				public Instant getLastAccessTime() {
					return this.session.getLastAccessTime();
				}

				@Override
				public void setMaxIdleTime(Duration maxIdleTime) {
					this.session.setMaxIdleTime(maxIdleTime);
				}

				@Override
				public Duration getMaxIdleTime() {
					return this.session.getMaxIdleTime();
				}

			}

		}

	}

	/**
	 * Configures HTTPS redirection rules
	 *
	 * @author Josh Cummings
	 * @since 5.1
	 * @see #redirectToHttps(Customizer)
	 */
	public class HttpsRedirectSpec {

		private ServerWebExchangeMatcher serverWebExchangeMatcher;

		private PortMapper portMapper;

		/**
		 * Configures when this filter should redirect to https
		 *
		 * By default, the filter will redirect whenever an exchange's scheme is not https
		 * @param matchers the list of conditions that, when any are met, the filter
		 * should redirect to https
		 * @return the {@link HttpsRedirectSpec} for additional configuration
		 */
		public HttpsRedirectSpec httpsRedirectWhen(ServerWebExchangeMatcher... matchers) {
			this.serverWebExchangeMatcher = new OrServerWebExchangeMatcher(matchers);
			return this;
		}

		/**
		 * Configures when this filter should redirect to https
		 *
		 * By default, the filter will redirect whenever an exchange's scheme is not https
		 * @param when determines when to redirect to https
		 * @return the {@link HttpsRedirectSpec} for additional configuration
		 */
		public HttpsRedirectSpec httpsRedirectWhen(Function<ServerWebExchange, Boolean> when) {
			ServerWebExchangeMatcher matcher = (e) -> when.apply(e) ? ServerWebExchangeMatcher.MatchResult.match()
					: ServerWebExchangeMatcher.MatchResult.notMatch();
			return httpsRedirectWhen(matcher);
		}

		/**
		 * Configures a custom HTTPS port to redirect to
		 * @param portMapper the {@link PortMapper} to use
		 * @return the {@link HttpsRedirectSpec} for additional configuration
		 */
		public HttpsRedirectSpec portMapper(PortMapper portMapper) {
			this.portMapper = portMapper;
			return this;
		}

		protected void configure(ServerHttpSecurity http) {
			HttpsRedirectWebFilter httpsRedirectWebFilter = new HttpsRedirectWebFilter();
			if (this.serverWebExchangeMatcher != null) {
				httpsRedirectWebFilter.setRequiresHttpsRedirectMatcher(this.serverWebExchangeMatcher);
			}
			if (this.portMapper != null) {
				httpsRedirectWebFilter.setPortMapper(this.portMapper);
			}
			http.addFilterAt(httpsRedirectWebFilter, SecurityWebFiltersOrder.HTTPS_REDIRECT);
		}

	}

	/**
	 * Configures <a href=
	 * "https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)_Prevention_Cheat_Sheet">CSRF
	 * Protection</a>
	 *
	 * @author Rob Winch
	 * @since 5.0
	 * @see #csrf(Customizer)
	 */
	public final class CsrfSpec {

		private CsrfSpec() {
		}

		private CsrfWebFilter filter = new CsrfWebFilter();

		private ServerCsrfTokenRepository csrfTokenRepository = new WebSessionServerCsrfTokenRepository();

		private boolean specifiedRequireCsrfProtectionMatcher;

		/**
		 * Configures the {@link ServerAccessDeniedHandler} used when a CSRF token is
		 * invalid. Default is to send an
		 * {@link org.springframework.http.HttpStatus#FORBIDDEN}.
		 * @param accessDeniedHandler the access denied handler.
		 * @return the {@link CsrfSpec} for additional configuration
		 */
		public CsrfSpec accessDeniedHandler(ServerAccessDeniedHandler accessDeniedHandler) {
			this.filter.setAccessDeniedHandler(accessDeniedHandler);
			return this;
		}

		/**
		 * Configures the {@link ServerCsrfTokenRepository} used to persist the CSRF
		 * Token. Default is
		 * {@link org.springframework.security.web.server.csrf.WebSessionServerCsrfTokenRepository}.
		 * @param csrfTokenRepository the repository to use
		 * @return the {@link CsrfSpec} for additional configuration
		 */
		public CsrfSpec csrfTokenRepository(ServerCsrfTokenRepository csrfTokenRepository) {
			this.csrfTokenRepository = csrfTokenRepository;
			return this;
		}

		/**
		 * Configures the {@link ServerWebExchangeMatcher} used to determine when CSRF
		 * protection is enabled. Default is PUT, POST, DELETE requests.
		 * @param requireCsrfProtectionMatcher the matcher to use
		 * @return the {@link CsrfSpec} for additional configuration
		 */
		public CsrfSpec requireCsrfProtectionMatcher(ServerWebExchangeMatcher requireCsrfProtectionMatcher) {
			this.filter.setRequireCsrfProtectionMatcher(requireCsrfProtectionMatcher);
			this.specifiedRequireCsrfProtectionMatcher = true;
			return this;
		}

		/**
		 * Specifies a {@link ServerCsrfTokenRequestHandler} that is used to make the
		 * {@code CsrfToken} available as an exchange attribute.
		 * @param requestHandler the {@link ServerCsrfTokenRequestHandler} to use
		 * @return the {@link CsrfSpec} for additional configuration
		 * @since 5.8
		 */
		public CsrfSpec csrfTokenRequestHandler(ServerCsrfTokenRequestHandler requestHandler) {
			this.filter.setRequestHandler(requestHandler);
			return this;
		}

		/**
		 * Disables CSRF Protection. Disabling CSRF Protection is only recommended when
		 * the application is never used within a browser.
		 * @return the {@link ServerHttpSecurity} to continue configuring
		 */
		public ServerHttpSecurity disable() {
			ServerHttpSecurity.this.csrf = null;
			return ServerHttpSecurity.this;
		}

		protected void configure(ServerHttpSecurity http) {
			if (this.csrfTokenRepository != null) {
				this.filter.setCsrfTokenRepository(this.csrfTokenRepository);
				if (ServerHttpSecurity.this.logout != null) {
					ServerHttpSecurity.this.logout
						.addLogoutHandler(new CsrfServerLogoutHandler(this.csrfTokenRepository));
				}
			}
			http.addFilterAt(this.filter, SecurityWebFiltersOrder.CSRF);
		}

	}

	/**
	 * Configures exception handling
	 *
	 * @author Rob Winch
	 * @since 5.0
	 * @see #exceptionHandling(Customizer)
	 */
	public final class ExceptionHandlingSpec {

		private ExceptionHandlingSpec() {
		}

		/**
		 * Configures what to do when the application request authentication
		 * @param authenticationEntryPoint the entry point to use
		 * @return the {@link ExceptionHandlingSpec} to configure
		 */
		public ExceptionHandlingSpec authenticationEntryPoint(ServerAuthenticationEntryPoint authenticationEntryPoint) {
			ServerHttpSecurity.this.authenticationEntryPoint = authenticationEntryPoint;
			return this;
		}

		/**
		 * Configures what to do when an authenticated user does not hold a required
		 * authority
		 * @param accessDeniedHandler the access denied handler to use
		 * @return the {@link ExceptionHandlingSpec} to configure
		 *
		 * @since 5.0.5
		 */
		public ExceptionHandlingSpec accessDeniedHandler(ServerAccessDeniedHandler accessDeniedHandler) {
			ServerHttpSecurity.this.accessDeniedHandler = accessDeniedHandler;
			return this;
		}

	}

	/**
	 * Configures the request cache which is used when a flow is interrupted (i.e. due to
	 * requesting credentials) so that the request can be replayed after authentication.
	 *
	 * @author Rob Winch
	 * @since 5.0
	 * @see #requestCache(Customizer)
	 */
	public final class RequestCacheSpec {

		private ServerRequestCache requestCache = new WebSessionServerRequestCache();

		private RequestCacheSpec() {
		}

		/**
		 * Configures the cache used
		 * @param requestCache the request cache
		 * @return the {@link RequestCacheSpec} to configure
		 */
		public RequestCacheSpec requestCache(ServerRequestCache requestCache) {
			Assert.notNull(requestCache, "requestCache cannot be null");
			this.requestCache = requestCache;
			return this;
		}

		protected void configure(ServerHttpSecurity http) {
			ServerRequestCacheWebFilter filter = new ServerRequestCacheWebFilter();
			filter.setRequestCache(this.requestCache);
			http.addFilterAt(filter, SecurityWebFiltersOrder.SERVER_REQUEST_CACHE);
		}

		/**
		 * Disables the {@link RequestCacheSpec}
		 * @return the {@link ServerHttpSecurity} to continue configuring
		 */
		public ServerHttpSecurity disable() {
			this.requestCache = NoOpServerRequestCache.getInstance();
			return ServerHttpSecurity.this;
		}

	}

	/**
	 * Configures HTTP Basic Authentication
	 *
	 * @author Rob Winch
	 * @since 5.0
	 * @see #httpBasic(Customizer)
	 */
	public final class HttpBasicSpec {

		private final ServerWebExchangeMatcher xhrMatcher = (exchange) -> Mono.just(exchange.getRequest().getHeaders())
			.filter((h) -> h.getOrEmpty("X-Requested-With").contains("XMLHttpRequest"))
			.flatMap((h) -> ServerWebExchangeMatcher.MatchResult.match())
			.switchIfEmpty(ServerWebExchangeMatcher.MatchResult.notMatch());

		private ReactiveAuthenticationManager authenticationManager;

		private ServerSecurityContextRepository securityContextRepository;

		private ServerAuthenticationEntryPoint entryPoint;

		private ServerAuthenticationFailureHandler authenticationFailureHandler;

		private final List<ServerAuthenticationSuccessHandler> defaultSuccessHandlers = new ArrayList<>(
				List.of(new WebFilterChainServerAuthenticationSuccessHandler()));

		private List<ServerAuthenticationSuccessHandler> authenticationSuccessHandlers = new ArrayList<>();

		private HttpBasicSpec() {
			List<DelegateEntry> entryPoints = new ArrayList<>();
			entryPoints
				.add(new DelegateEntry(this.xhrMatcher, new HttpStatusServerEntryPoint(HttpStatus.UNAUTHORIZED)));
			DelegatingServerAuthenticationEntryPoint defaultEntryPoint = new DelegatingServerAuthenticationEntryPoint(
					entryPoints);
			defaultEntryPoint.setDefaultEntryPoint(new HttpBasicServerAuthenticationEntryPoint());
			this.entryPoint = defaultEntryPoint;
		}

		/**
		 * The {@link ServerAuthenticationSuccessHandler} used after authentication
		 * success. Defaults to {@link WebFilterChainServerAuthenticationSuccessHandler}.
		 * Note that this method clears previously added success handlers via
		 * {@link #authenticationSuccessHandler(Consumer)}
		 * @param authenticationSuccessHandler the success handler to use
		 * @return the {@link HttpBasicSpec} to continue configuring
		 * @since 6.3
		 */
		public HttpBasicSpec authenticationSuccessHandler(
				ServerAuthenticationSuccessHandler authenticationSuccessHandler) {
			Assert.notNull(authenticationSuccessHandler, "authenticationSuccessHandler cannot be null");
			authenticationSuccessHandler((handlers) -> {
				handlers.clear();
				handlers.add(authenticationSuccessHandler);
			});
			return this;
		}

		/**
		 * Allows customizing the list of {@link ServerAuthenticationSuccessHandler}. The
		 * default list contains a
		 * {@link WebFilterChainServerAuthenticationSuccessHandler}.
		 * @param handlersConsumer the handlers consumer
		 * @return the {@link HttpBasicSpec} to continue configuring
		 * @since 6.3
		 */
		public HttpBasicSpec authenticationSuccessHandler(
				Consumer<List<ServerAuthenticationSuccessHandler>> handlersConsumer) {
			Assert.notNull(handlersConsumer, "handlersConsumer cannot be null");
			handlersConsumer.accept(this.authenticationSuccessHandlers);
			return this;
		}

		/**
		 * The {@link ReactiveAuthenticationManager} used to authenticate. Defaults to
		 * {@link ServerHttpSecurity#authenticationManager(ReactiveAuthenticationManager)}.
		 * @param authenticationManager the authentication manager to use
		 * @return the {@link HttpBasicSpec} to continue configuring
		 */
		public HttpBasicSpec authenticationManager(ReactiveAuthenticationManager authenticationManager) {
			this.authenticationManager = authenticationManager;
			return this;
		}

		/**
		 * The {@link ServerSecurityContextRepository} used to save the
		 * {@code Authentication}. Defaults to
		 * {@link NoOpServerSecurityContextRepository}. For the {@code SecurityContext} to
		 * be loaded on subsequent requests the {@link ReactorContextWebFilter} must be
		 * configured to be able to load the value (they are not implicitly linked).
		 * @param securityContextRepository the repository to use
		 * @return the {@link HttpBasicSpec} to continue configuring
		 */
		public HttpBasicSpec securityContextRepository(ServerSecurityContextRepository securityContextRepository) {
			this.securityContextRepository = securityContextRepository;
			return this;
		}

		/**
		 * Allows easily setting the entry point.
		 * @param authenticationEntryPoint the {@link ServerAuthenticationEntryPoint} to
		 * use
		 * @return {@link HttpBasicSpec} for additional customization
		 * @since 5.2.0
		 */
		public HttpBasicSpec authenticationEntryPoint(ServerAuthenticationEntryPoint authenticationEntryPoint) {
			Assert.notNull(authenticationEntryPoint, "authenticationEntryPoint cannot be null");
			this.entryPoint = authenticationEntryPoint;
			return this;
		}

		public HttpBasicSpec authenticationFailureHandler(
				ServerAuthenticationFailureHandler authenticationFailureHandler) {
			Assert.notNull(authenticationFailureHandler, "authenticationFailureHandler cannot be null");
			this.authenticationFailureHandler = authenticationFailureHandler;
			return this;
		}

		/**
		 * Disables HTTP Basic authentication.
		 * @return the {@link ServerHttpSecurity} to continue configuring
		 */
		public ServerHttpSecurity disable() {
			ServerHttpSecurity.this.httpBasic = null;
			return ServerHttpSecurity.this;
		}

		protected void configure(ServerHttpSecurity http) {
			MediaTypeServerWebExchangeMatcher restMatcher = new MediaTypeServerWebExchangeMatcher(
					MediaType.APPLICATION_ATOM_XML, MediaType.APPLICATION_FORM_URLENCODED, MediaType.APPLICATION_JSON,
					MediaType.APPLICATION_OCTET_STREAM, MediaType.APPLICATION_XML, MediaType.MULTIPART_FORM_DATA,
					MediaType.TEXT_XML);
			restMatcher.setIgnoredMediaTypes(Collections.singleton(MediaType.ALL));
			ServerWebExchangeMatcher notHtmlMatcher = new NegatedServerWebExchangeMatcher(
					new MediaTypeServerWebExchangeMatcher(MediaType.TEXT_HTML));
			ServerWebExchangeMatcher restNotHtmlMatcher = new AndServerWebExchangeMatcher(
					Arrays.asList(notHtmlMatcher, restMatcher));
			ServerWebExchangeMatcher preferredMatcher = new OrServerWebExchangeMatcher(
					Arrays.asList(this.xhrMatcher, restNotHtmlMatcher));
			ServerHttpSecurity.this.defaultEntryPoints.add(new DelegateEntry(preferredMatcher, this.entryPoint));
			AuthenticationWebFilter authenticationFilter = new AuthenticationWebFilter(this.authenticationManager);
			authenticationFilter.setAuthenticationFailureHandler(authenticationFailureHandler());
			authenticationFilter.setServerAuthenticationConverter(new ServerHttpBasicAuthenticationConverter());
			authenticationFilter.setSecurityContextRepository(this.securityContextRepository);
			authenticationFilter.setAuthenticationSuccessHandler(getAuthenticationSuccessHandler(http));
			http.addFilterAt(authenticationFilter, SecurityWebFiltersOrder.HTTP_BASIC);
		}

		private ServerAuthenticationSuccessHandler getAuthenticationSuccessHandler(ServerHttpSecurity http) {
			if (this.authenticationSuccessHandlers.isEmpty()) {
				return new DelegatingServerAuthenticationSuccessHandler(this.defaultSuccessHandlers);
			}
			return new DelegatingServerAuthenticationSuccessHandler(this.authenticationSuccessHandlers);
		}

		private ServerAuthenticationFailureHandler authenticationFailureHandler() {
			if (this.authenticationFailureHandler != null) {
				return this.authenticationFailureHandler;
			}
			return new ServerAuthenticationEntryPointFailureHandler(this.entryPoint);
		}

	}

	/**
	 * Configures password management.
	 *
	 * @author Evgeniy Cheban
	 * @since 5.6
	 * @see #passwordManagement(Customizer)
	 */
	public final class PasswordManagementSpec {

		private static final String WELL_KNOWN_CHANGE_PASSWORD_PATTERN = "/.well-known/change-password";

		private static final String DEFAULT_CHANGE_PASSWORD_PAGE = "/change-password";

		private String changePasswordPage = DEFAULT_CHANGE_PASSWORD_PAGE;

		/**
		 * Sets the change password page. Defaults to
		 * {@link PasswordManagementSpec#DEFAULT_CHANGE_PASSWORD_PAGE}.
		 * @param changePasswordPage the change password page
		 * @return the {@link PasswordManagementSpec} to continue configuring
		 */
		public PasswordManagementSpec changePasswordPage(String changePasswordPage) {
			Assert.hasText(changePasswordPage, "changePasswordPage cannot be empty");
			this.changePasswordPage = changePasswordPage;
			return this;
		}

		protected void configure(ServerHttpSecurity http) {
			ExchangeMatcherRedirectWebFilter changePasswordWebFilter = new ExchangeMatcherRedirectWebFilter(
					new PathPatternParserServerWebExchangeMatcher(WELL_KNOWN_CHANGE_PASSWORD_PATTERN),
					this.changePasswordPage);
			http.addFilterBefore(changePasswordWebFilter, SecurityWebFiltersOrder.AUTHENTICATION);
		}

		private PasswordManagementSpec() {
		}

	}

	/**
	 * Configures Form Based authentication
	 *
	 * @author Rob Winch
	 * @since 5.0
	 * @see #formLogin(Customizer)
	 */
	public final class FormLoginSpec {

		private final RedirectServerAuthenticationSuccessHandler defaultSuccessHandler = new RedirectServerAuthenticationSuccessHandler(
				"/");

		private final List<ServerAuthenticationSuccessHandler> defaultSuccessHandlers = new ArrayList<>(
				List.of(this.defaultSuccessHandler));

		private RedirectServerAuthenticationEntryPoint defaultEntryPoint;

		private ReactiveAuthenticationManager authenticationManager;

		private ServerSecurityContextRepository securityContextRepository;

		private ServerAuthenticationEntryPoint authenticationEntryPoint;

		private boolean isEntryPointExplicit;

		private ServerWebExchangeMatcher requiresAuthenticationMatcher;

		private ServerAuthenticationFailureHandler authenticationFailureHandler;

		private List<ServerAuthenticationSuccessHandler> authenticationSuccessHandlers = new ArrayList<>();

		private FormLoginSpec() {
		}

		/**
		 * The {@link ReactiveAuthenticationManager} used to authenticate. Defaults to
		 * {@link ServerHttpSecurity#authenticationManager(ReactiveAuthenticationManager)}.
		 * @param authenticationManager the authentication manager to use
		 * @return the {@link FormLoginSpec} to continue configuring
		 */
		public FormLoginSpec authenticationManager(ReactiveAuthenticationManager authenticationManager) {
			this.authenticationManager = authenticationManager;
			return this;
		}

		/**
		 * The {@link ServerAuthenticationSuccessHandler} used after authentication
		 * success. Defaults to {@link RedirectServerAuthenticationSuccessHandler}. Note
		 * that this method clears previously added success handlers via
		 * {@link #authenticationSuccessHandler(Consumer)}
		 * @param authenticationSuccessHandler the success handler to use
		 * @return the {@link FormLoginSpec} to continue configuring
		 */
		public FormLoginSpec authenticationSuccessHandler(
				ServerAuthenticationSuccessHandler authenticationSuccessHandler) {
			Assert.notNull(authenticationSuccessHandler, "authenticationSuccessHandler cannot be null");
			authenticationSuccessHandler((handlers) -> {
				handlers.clear();
				handlers.add(authenticationSuccessHandler);
			});
			return this;
		}

		/**
		 * Allows customizing the list of {@link ServerAuthenticationSuccessHandler}. The
		 * default list contains a {@link RedirectServerAuthenticationSuccessHandler} that
		 * redirects to "/".
		 * @param handlersConsumer the handlers consumer
		 * @return the {@link FormLoginSpec} to continue configuring
		 * @since 6.3
		 */
		public FormLoginSpec authenticationSuccessHandler(
				Consumer<List<ServerAuthenticationSuccessHandler>> handlersConsumer) {
			Assert.notNull(handlersConsumer, "handlersConsumer cannot be null");
			handlersConsumer.accept(this.authenticationSuccessHandlers);
			return this;
		}

		/**
		 * Configures the log in page to redirect to, the authentication failure page, and
		 * when authentication is performed. The default is that Spring Security will
		 * generate a log in page at "/login" and a log out page at "/logout". If this is
		 * customized:
		 * <ul>
		 * <li>The default log in &amp; log out page are no longer provided</li>
		 * <li>The application must render a log in page at the provided URL</li>
		 * <li>The application must render an authentication error page at the provided
		 * URL + "?error"</li>
		 * <li>Authentication will occur for POST to the provided URL</li>
		 * </ul>
		 * @param loginPage the url to redirect to which provides a form to log in (i.e.
		 * "/login")
		 * @return the {@link FormLoginSpec} to continue configuring
		 * @see #authenticationEntryPoint(ServerAuthenticationEntryPoint)
		 * @see #requiresAuthenticationMatcher(ServerWebExchangeMatcher)
		 * @see #authenticationFailureHandler(ServerAuthenticationFailureHandler)
		 */
		public FormLoginSpec loginPage(String loginPage) {
			this.defaultEntryPoint = new RedirectServerAuthenticationEntryPoint(loginPage);
			this.authenticationEntryPoint = this.defaultEntryPoint;
			if (this.requiresAuthenticationMatcher == null) {
				this.requiresAuthenticationMatcher = ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, loginPage);
			}
			if (this.authenticationFailureHandler == null) {
				this.authenticationFailureHandler = new RedirectServerAuthenticationFailureHandler(
						loginPage + "?error");
			}
			return this;
		}

		/**
		 * How to request for authentication. The default is that Spring Security will
		 * generate a log in page at "/login".
		 * @param authenticationEntryPoint the entry point to use
		 * @return the {@link FormLoginSpec} to continue configuring
		 * @see #loginPage(String)
		 */
		public FormLoginSpec authenticationEntryPoint(ServerAuthenticationEntryPoint authenticationEntryPoint) {
			this.authenticationEntryPoint = authenticationEntryPoint;
			return this;
		}

		/**
		 * Configures when authentication is performed. The default is a POST to "/login".
		 * @param requiresAuthenticationMatcher the matcher to use
		 * @return the {@link FormLoginSpec} to continue configuring
		 * @see #loginPage(String)
		 */
		public FormLoginSpec requiresAuthenticationMatcher(ServerWebExchangeMatcher requiresAuthenticationMatcher) {
			this.requiresAuthenticationMatcher = requiresAuthenticationMatcher;
			return this;
		}

		/**
		 * Configures how a failed authentication is handled. The default is to redirect
		 * to "/login?error".
		 * @param authenticationFailureHandler the handler to use
		 * @return the {@link FormLoginSpec} to continue configuring
		 * @see #loginPage(String)
		 */
		public FormLoginSpec authenticationFailureHandler(
				ServerAuthenticationFailureHandler authenticationFailureHandler) {
			this.authenticationFailureHandler = authenticationFailureHandler;
			return this;
		}

		/**
		 * The {@link ServerSecurityContextRepository} used to save the
		 * {@code Authentication}. Defaults to
		 * {@link WebSessionServerSecurityContextRepository}. For the
		 * {@code SecurityContext} to be loaded on subsequent requests the
		 * {@link ReactorContextWebFilter} must be configured to be able to load the value
		 * (they are not implicitly linked).
		 * @param securityContextRepository the repository to use
		 * @return the {@link FormLoginSpec} to continue configuring
		 */
		public FormLoginSpec securityContextRepository(ServerSecurityContextRepository securityContextRepository) {
			this.securityContextRepository = securityContextRepository;
			return this;
		}

		/**
		 * Disables HTTP Basic authentication.
		 * @return the {@link ServerHttpSecurity} to continue configuring
		 */
		public ServerHttpSecurity disable() {
			ServerHttpSecurity.this.formLogin = null;
			return ServerHttpSecurity.this;
		}

		protected void configure(ServerHttpSecurity http) {
			if (this.authenticationEntryPoint == null) {
				this.isEntryPointExplicit = false;
				loginPage("/login");
			}
			else {
				this.isEntryPointExplicit = true;
			}
			if (http.requestCache != null) {
				ServerRequestCache requestCache = http.requestCache.requestCache;
				this.defaultSuccessHandler.setRequestCache(requestCache);
				if (this.defaultEntryPoint != null) {
					this.defaultEntryPoint.setRequestCache(requestCache);
				}
			}
			MediaTypeServerWebExchangeMatcher htmlMatcher = new MediaTypeServerWebExchangeMatcher(MediaType.TEXT_HTML);
			htmlMatcher.setIgnoredMediaTypes(Collections.singleton(MediaType.ALL));
			ServerHttpSecurity.this.defaultEntryPoints.add(0,
					new DelegateEntry(htmlMatcher, this.authenticationEntryPoint));
			AuthenticationWebFilter authenticationFilter = new AuthenticationWebFilter(this.authenticationManager);
			authenticationFilter.setRequiresAuthenticationMatcher(this.requiresAuthenticationMatcher);
			authenticationFilter.setAuthenticationFailureHandler(this.authenticationFailureHandler);
			authenticationFilter.setServerAuthenticationConverter(new ServerFormLoginAuthenticationConverter());
			authenticationFilter.setAuthenticationSuccessHandler(getAuthenticationSuccessHandler(http));
			authenticationFilter.setSecurityContextRepository(this.securityContextRepository);
			http.addFilterAt(authenticationFilter, SecurityWebFiltersOrder.FORM_LOGIN);
		}

		private ServerAuthenticationSuccessHandler getAuthenticationSuccessHandler(ServerHttpSecurity http) {
			if (this.authenticationSuccessHandlers.isEmpty()) {
				return new DelegatingServerAuthenticationSuccessHandler(this.defaultSuccessHandlers);
			}
			return new DelegatingServerAuthenticationSuccessHandler(this.authenticationSuccessHandlers);
		}

	}

	private final class LoginPageSpec {

		private LoginPageSpec() {
		}

		protected void configure(ServerHttpSecurity http) {
			if (http.authenticationEntryPoint != null) {
				return;
			}
			if (http.formLogin != null && http.formLogin.isEntryPointExplicit
					|| http.oauth2Login != null && StringUtils.hasText(http.oauth2Login.loginPage)
					|| http.oneTimeTokenLogin != null && StringUtils.hasText(http.oneTimeTokenLogin.loginPage)) {
				return;
			}
			LoginPageGeneratingWebFilter loginPage = null;
			if (http.formLogin != null && !http.formLogin.isEntryPointExplicit) {
				loginPage = new LoginPageGeneratingWebFilter();
				loginPage.setFormLoginEnabled(true);
			}
			if (http.oauth2Login != null) {
				Map<String, String> urlToText = http.oauth2Login.getLinks();
				if (loginPage == null) {
					loginPage = new LoginPageGeneratingWebFilter();
				}
				loginPage.setOauth2AuthenticationUrlToClientName(urlToText);
			}
			if (http.oneTimeTokenLogin != null) {
				if (loginPage == null) {
					loginPage = new LoginPageGeneratingWebFilter();
				}
				loginPage.setOneTimeTokenEnabled(true);
				loginPage.setGenerateOneTimeTokenUrl(http.oneTimeTokenLogin.tokenGeneratingUrl);
			}
			if (loginPage != null) {
				http.addFilterAt(loginPage, SecurityWebFiltersOrder.LOGIN_PAGE_GENERATING);
				http.addFilterBefore(DefaultResourcesWebFilter.css(), SecurityWebFiltersOrder.LOGIN_PAGE_GENERATING);
				if (http.logout != null) {
					http.addFilterAt(new LogoutPageGeneratingWebFilter(),
							SecurityWebFiltersOrder.LOGOUT_PAGE_GENERATING);
				}
			}
		}

	}

	/**
	 * Configures HTTP Response Headers.
	 *
	 * @author Rob Winch
	 * @since 5.0
	 * @see #headers(Customizer)
	 */
	public final class HeaderSpec {

		private final List<ServerHttpHeadersWriter> writers;

		private CacheControlServerHttpHeadersWriter cacheControl = new CacheControlServerHttpHeadersWriter();

		private ContentTypeOptionsServerHttpHeadersWriter contentTypeOptions = new ContentTypeOptionsServerHttpHeadersWriter();

		private StrictTransportSecurityServerHttpHeadersWriter hsts = new StrictTransportSecurityServerHttpHeadersWriter();

		private XFrameOptionsServerHttpHeadersWriter frameOptions = new XFrameOptionsServerHttpHeadersWriter();

		private XXssProtectionServerHttpHeadersWriter xss = new XXssProtectionServerHttpHeadersWriter();

		private FeaturePolicyServerHttpHeadersWriter featurePolicy = new FeaturePolicyServerHttpHeadersWriter();

		private PermissionsPolicyServerHttpHeadersWriter permissionsPolicy = new PermissionsPolicyServerHttpHeadersWriter();

		private ContentSecurityPolicyServerHttpHeadersWriter contentSecurityPolicy = new ContentSecurityPolicyServerHttpHeadersWriter();

		private ReferrerPolicyServerHttpHeadersWriter referrerPolicy = new ReferrerPolicyServerHttpHeadersWriter();

		private CrossOriginOpenerPolicyServerHttpHeadersWriter crossOriginOpenerPolicy = new CrossOriginOpenerPolicyServerHttpHeadersWriter();

		private CrossOriginEmbedderPolicyServerHttpHeadersWriter crossOriginEmbedderPolicy = new CrossOriginEmbedderPolicyServerHttpHeadersWriter();

		private CrossOriginResourcePolicyServerHttpHeadersWriter crossOriginResourcePolicy = new CrossOriginResourcePolicyServerHttpHeadersWriter();

		private HeaderSpec() {
			this.writers = new ArrayList<>(Arrays.asList(this.cacheControl, this.contentTypeOptions, this.hsts,
					this.frameOptions, this.xss, this.featurePolicy, this.permissionsPolicy, this.contentSecurityPolicy,
					this.referrerPolicy, this.crossOriginOpenerPolicy, this.crossOriginEmbedderPolicy,
					this.crossOriginResourcePolicy));
		}

		/**
		 * Disables http response headers
		 * @return the {@link ServerHttpSecurity} to continue configuring
		 */
		public ServerHttpSecurity disable() {
			ServerHttpSecurity.this.headers = null;
			return ServerHttpSecurity.this;
		}

		/**
		 * Configures cache control headers
		 * @param cacheCustomizer the {@link Customizer} to provide more options for the
		 * {@link CacheSpec}
		 * @return the {@link HeaderSpec} to customize
		 */
		public HeaderSpec cache(Customizer<CacheSpec> cacheCustomizer) {
			cacheCustomizer.customize(new CacheSpec());
			return this;
		}

		/**
		 * Configures content type response headers
		 * @param contentTypeOptionsCustomizer the {@link Customizer} to provide more
		 * options for the {@link ContentTypeOptionsSpec}
		 * @return the {@link HeaderSpec} to customize
		 */
		public HeaderSpec contentTypeOptions(Customizer<ContentTypeOptionsSpec> contentTypeOptionsCustomizer) {
			contentTypeOptionsCustomizer.customize(new ContentTypeOptionsSpec());
			return this;
		}

		/**
		 * Configures frame options response headers
		 * @param frameOptionsCustomizer the {@link Customizer} to provide more options
		 * for the {@link FrameOptionsSpec}
		 * @return the {@link HeaderSpec} to customize
		 */
		public HeaderSpec frameOptions(Customizer<FrameOptionsSpec> frameOptionsCustomizer) {
			frameOptionsCustomizer.customize(new FrameOptionsSpec());
			return this;
		}

		/**
		 * Configures custom headers writer
		 * @param serverHttpHeadersWriter the {@link ServerHttpHeadersWriter} to provide
		 * custom headers writer
		 * @return the {@link HeaderSpec} to customize
		 * @since 5.3.0
		 */
		public HeaderSpec writer(ServerHttpHeadersWriter serverHttpHeadersWriter) {
			Assert.notNull(serverHttpHeadersWriter, "serverHttpHeadersWriter cannot be null");
			this.writers.add(serverHttpHeadersWriter);
			return this;
		}

		/**
		 * Configures the Strict Transport Security response headers
		 * @param hstsCustomizer the {@link Customizer} to provide more options for the
		 * {@link HstsSpec}
		 * @return the {@link HeaderSpec} to customize
		 */
		public HeaderSpec hsts(Customizer<HstsSpec> hstsCustomizer) {
			hstsCustomizer.customize(new HstsSpec());
			return this;
		}

		protected void configure(ServerHttpSecurity http) {
			ServerHttpHeadersWriter writer = new CompositeServerHttpHeadersWriter(this.writers);
			HttpHeaderWriterWebFilter result = new HttpHeaderWriterWebFilter(writer);
			http.addFilterAt(result, SecurityWebFiltersOrder.HTTP_HEADERS_WRITER);
		}

		/**
		 * Configures x-xss-protection response header.
		 * @param xssProtectionCustomizer the {@link Customizer} to provide more options
		 * for the {@link XssProtectionSpec}
		 * @return the {@link HeaderSpec} to customize
		 */
		public HeaderSpec xssProtection(Customizer<XssProtectionSpec> xssProtectionCustomizer) {
			xssProtectionCustomizer.customize(new XssProtectionSpec());
			return this;
		}

		/**
		 * Configures {@code Content-Security-Policy} response header.
		 * @param contentSecurityPolicyCustomizer the {@link Customizer} to provide more
		 * options for the {@link ContentSecurityPolicySpec}
		 * @return the {@link HeaderSpec} to customize
		 */
		public HeaderSpec contentSecurityPolicy(Customizer<ContentSecurityPolicySpec> contentSecurityPolicyCustomizer) {
			contentSecurityPolicyCustomizer.customize(new ContentSecurityPolicySpec());
			return this;
		}

		/**
		 * Configures {@code Feature-Policy} response header.
		 * @param policyDirectives the policy
		 * @return the {@link FeaturePolicySpec} to configure
		 * @deprecated For removal in 7.0. Use {@link #permissionsPolicy(Customizer)}
		 * instead.
		 */
		@Deprecated
		public FeaturePolicySpec featurePolicy(String policyDirectives) {
			return new FeaturePolicySpec(policyDirectives);
		}

		/**
		 * Configures {@code Permissions-Policy} response header.
		 * @param permissionsPolicyCustomizer the {@link Customizer} to provide more
		 * options for the {@link PermissionsPolicySpec}
		 * @return the {@link HeaderSpec} to customize
		 */
		public HeaderSpec permissionsPolicy(Customizer<PermissionsPolicySpec> permissionsPolicyCustomizer) {
			permissionsPolicyCustomizer.customize(new PermissionsPolicySpec());
			return this;
		}

		/**
		 * Configures {@code Referrer-Policy} response header.
		 * @param referrerPolicyCustomizer the {@link Customizer} to provide more options
		 * for the {@link ReferrerPolicySpec}
		 * @return the {@link HeaderSpec} to customize
		 */
		public HeaderSpec referrerPolicy(Customizer<ReferrerPolicySpec> referrerPolicyCustomizer) {
			referrerPolicyCustomizer.customize(new ReferrerPolicySpec());
			return this;
		}

		/**
		 * Configures the <a href=
		 * "https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Opener-Policy">
		 * Cross-Origin-Opener-Policy</a> header.
		 * @return the {@link HeaderSpec} to customize
		 * @since 5.7
		 * @see CrossOriginOpenerPolicyServerHttpHeadersWriter
		 */
		public HeaderSpec crossOriginOpenerPolicy(
				Customizer<CrossOriginOpenerPolicySpec> crossOriginOpenerPolicyCustomizer) {
			crossOriginOpenerPolicyCustomizer.customize(new CrossOriginOpenerPolicySpec());
			return this;
		}

		/**
		 * Configures the <a href=
		 * "https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Embedder-Policy">
		 * Cross-Origin-Embedder-Policy</a> header.
		 * @return the {@link HeaderSpec} to customize
		 * @since 5.7
		 * @see CrossOriginEmbedderPolicyServerHttpHeadersWriter
		 */
		public HeaderSpec crossOriginEmbedderPolicy(
				Customizer<CrossOriginEmbedderPolicySpec> crossOriginEmbedderPolicyCustomizer) {
			crossOriginEmbedderPolicyCustomizer.customize(new CrossOriginEmbedderPolicySpec());
			return this;
		}

		/**
		 * Configures the <a href=
		 * "https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Resource-Policy">
		 * Cross-Origin-Resource-Policy</a> header.
		 * @return the {@link HeaderSpec} to customize
		 * @since 5.7
		 * @see CrossOriginResourcePolicyServerHttpHeadersWriter
		 */
		public HeaderSpec crossOriginResourcePolicy(
				Customizer<CrossOriginResourcePolicySpec> crossOriginResourcePolicyCustomizer) {
			crossOriginResourcePolicyCustomizer.customize(new CrossOriginResourcePolicySpec());
			return this;
		}

		/**
		 * Configures cache control headers
		 *
		 * @see HeaderSpec#cache(Customizer)
		 */
		public final class CacheSpec {

			private CacheSpec() {
			}

			/**
			 * Disables cache control response headers
			 * @return the {@link HeaderSpec} to configure
			 */
			public HeaderSpec disable() {
				HeaderSpec.this.writers.remove(HeaderSpec.this.cacheControl);
				return HeaderSpec.this;
			}

		}

		/**
		 * The content type headers
		 *
		 * @see HeaderSpec#contentTypeOptions(Customizer)
		 */
		public final class ContentTypeOptionsSpec {

			private ContentTypeOptionsSpec() {
			}

			/**
			 * Disables the content type options response header
			 * @return the {@link HeaderSpec} to configure
			 */
			public HeaderSpec disable() {
				HeaderSpec.this.writers.remove(HeaderSpec.this.contentTypeOptions);
				return HeaderSpec.this;
			}

		}

		/**
		 * Configures frame options response header
		 *
		 * @see HeaderSpec#frameOptions(Customizer)
		 */
		public final class FrameOptionsSpec {

			private FrameOptionsSpec() {
			}

			/**
			 * The mode to configure. Default is
			 * {@link org.springframework.security.web.server.header.XFrameOptionsServerHttpHeadersWriter.Mode#DENY}
			 * @param mode the mode to use
			 * @return the {@link HeaderSpec} to configure
			 */
			public HeaderSpec mode(XFrameOptionsServerHttpHeadersWriter.Mode mode) {
				HeaderSpec.this.frameOptions.setMode(mode);
				return HeaderSpec.this;
			}

			/**
			 * Disables frame options response header
			 * @return the {@link HeaderSpec} to continue configuring
			 */
			public HeaderSpec disable() {
				HeaderSpec.this.writers.remove(HeaderSpec.this.frameOptions);
				return HeaderSpec.this;
			}

		}

		/**
		 * Configures Strict Transport Security response header
		 *
		 * @see HeaderSpec#hsts(Customizer)
		 */
		public final class HstsSpec {

			private HstsSpec() {
			}

			/**
			 * Configures the max age. Default is one year.
			 * @param maxAge the max age
			 * @return the {@link HstsSpec} to continue configuring
			 */
			public HstsSpec maxAge(Duration maxAge) {
				HeaderSpec.this.hsts.setMaxAge(maxAge);
				return this;
			}

			/**
			 * Configures if subdomains should be included. Default is true
			 * @param includeSubDomains if subdomains should be included
			 * @return the {@link HstsSpec} to continue configuring
			 */
			public HstsSpec includeSubdomains(boolean includeSubDomains) {
				HeaderSpec.this.hsts.setIncludeSubDomains(includeSubDomains);
				return this;
			}

			/**
			 * <p>
			 * Configures if preload should be included. Default is false
			 * </p>
			 *
			 * <p>
			 * See <a href="https://hstspreload.org/">Website hstspreload.org</a> for
			 * additional details.
			 * </p>
			 * @param preload if subdomains should be included
			 * @return the {@link HstsSpec} to continue configuring
			 * @since 5.2.0
			 */
			public HstsSpec preload(boolean preload) {
				HeaderSpec.this.hsts.setPreload(preload);
				return this;
			}

			/**
			 * Disables strict transport security response header
			 * @return the {@link HeaderSpec} to continue configuring
			 */
			public HeaderSpec disable() {
				HeaderSpec.this.writers.remove(HeaderSpec.this.hsts);
				return HeaderSpec.this;
			}

		}

		/**
		 * Configures x-xss-protection response header
		 *
		 * @see HeaderSpec#xssProtection(Customizer)
		 */
		public final class XssProtectionSpec {

			private XssProtectionSpec() {
			}

			/**
			 * Disables the x-xss-protection response header
			 * @return the {@link HeaderSpec} to continue configuring
			 */
			public HeaderSpec disable() {
				HeaderSpec.this.writers.remove(HeaderSpec.this.xss);
				return HeaderSpec.this;
			}

			/**
			 * Sets the value of x-xss-protection header. OWASP recommends using
			 * {@link XXssProtectionServerHttpHeadersWriter.HeaderValue#DISABLED}.
			 * @param headerValue the headerValue
			 * @return the {@link HeaderSpec} to continue configuring
			 * @since 5.8
			 */
			public HeaderSpec headerValue(XXssProtectionServerHttpHeadersWriter.HeaderValue headerValue) {
				HeaderSpec.this.xss.setHeaderValue(headerValue);
				return HeaderSpec.this;
			}

		}

		/**
		 * Configures {@code Content-Security-Policy} response header.
		 *
		 * @since 5.1
		 * @see HeaderSpec#contentSecurityPolicy(Customizer)
		 */
		public final class ContentSecurityPolicySpec {

			private static final String DEFAULT_SRC_SELF_POLICY = "default-src 'self'";

			private ContentSecurityPolicySpec() {
				HeaderSpec.this.contentSecurityPolicy.setPolicyDirectives(DEFAULT_SRC_SELF_POLICY);
			}

			/**
			 * Whether to include the {@code Content-Security-Policy-Report-Only} header
			 * in the response. Otherwise, defaults to the {@code Content-Security-Policy}
			 * header.
			 * @param reportOnly whether to only report policy violations
			 * @return the {@link HeaderSpec} to continue configuring
			 */
			public HeaderSpec reportOnly(boolean reportOnly) {
				HeaderSpec.this.contentSecurityPolicy.setReportOnly(reportOnly);
				return HeaderSpec.this;
			}

			/**
			 * Sets the security policy directive(s) to be used in the response header.
			 * @param policyDirectives the security policy directive(s)
			 * @return the {@link HeaderSpec} to continue configuring
			 */
			public HeaderSpec policyDirectives(String policyDirectives) {
				HeaderSpec.this.contentSecurityPolicy.setPolicyDirectives(policyDirectives);
				return HeaderSpec.this;
			}

			private ContentSecurityPolicySpec(String policyDirectives) {
				HeaderSpec.this.contentSecurityPolicy.setPolicyDirectives(policyDirectives);
			}

		}

		/**
		 * Configures {@code Feature-Policy} response header.
		 *
		 * @since 5.1
		 * @see #featurePolicy(String)
		 */
		public final class FeaturePolicySpec {

			private FeaturePolicySpec(String policyDirectives) {
				HeaderSpec.this.featurePolicy.setPolicyDirectives(policyDirectives);
			}

			/**
			 * Allows method chaining to continue configuring the
			 * {@link ServerHttpSecurity}.
			 * @return the {@link HeaderSpec} to continue configuring
			 * @deprecated For removal in 7.0. Use {@link #featurePolicy(String)} instead
			 */
			@Deprecated(since = "6.1", forRemoval = true)
			public HeaderSpec and() {
				return HeaderSpec.this;
			}

		}

		/**
		 * Configures {@code Permissions-Policy} response header.
		 *
		 * @since 5.5
		 * @see HeaderSpec#permissionsPolicy(Customizer)
		 */
		public final class PermissionsPolicySpec {

			private PermissionsPolicySpec() {
			}

			/**
			 * Sets the policy to be used in the response header.
			 * @param policy a permissions policy
			 * @return the {@link PermissionsPolicySpec} to continue configuring
			 */
			public PermissionsPolicySpec policy(String policy) {
				HeaderSpec.this.permissionsPolicy.setPolicy(policy);
				return this;
			}

		}

		/**
		 * Configures {@code Referrer-Policy} response header.
		 *
		 * @since 5.1
		 * @see HeaderSpec#referrerPolicy(Customizer)
		 */
		public final class ReferrerPolicySpec {

			private ReferrerPolicySpec() {
			}

			private ReferrerPolicySpec(ReferrerPolicy referrerPolicy) {
				HeaderSpec.this.referrerPolicy.setPolicy(referrerPolicy);
			}

			/**
			 * Sets the policy to be used in the response header.
			 * @param referrerPolicy a referrer policy
			 * @return the {@link ReferrerPolicySpec} to continue configuring
			 */
			public ReferrerPolicySpec policy(ReferrerPolicy referrerPolicy) {
				HeaderSpec.this.referrerPolicy.setPolicy(referrerPolicy);
				return this;
			}

		}

		/**
		 * Configures the Cross-Origin-Opener-Policy header
		 *
		 * @since 5.7
		 */
		public final class CrossOriginOpenerPolicySpec {

			private CrossOriginOpenerPolicySpec() {
			}

			/**
			 * Sets the value to be used in the `Cross-Origin-Opener-Policy` header
			 * @param openerPolicy a opener policy
			 * @return the {@link CrossOriginOpenerPolicySpec} to continue configuring
			 */
			public CrossOriginOpenerPolicySpec policy(CrossOriginOpenerPolicy openerPolicy) {
				HeaderSpec.this.crossOriginOpenerPolicy.setPolicy(openerPolicy);
				return this;
			}

		}

		/**
		 * Configures the Cross-Origin-Embedder-Policy header
		 *
		 * @since 5.7
		 */
		public final class CrossOriginEmbedderPolicySpec {

			private CrossOriginEmbedderPolicySpec() {
			}

			/**
			 * Sets the value to be used in the `Cross-Origin-Embedder-Policy` header
			 * @param embedderPolicy a opener policy
			 * @return the {@link CrossOriginEmbedderPolicySpec} to continue configuring
			 */
			public CrossOriginEmbedderPolicySpec policy(CrossOriginEmbedderPolicy embedderPolicy) {
				HeaderSpec.this.crossOriginEmbedderPolicy.setPolicy(embedderPolicy);
				return this;
			}

		}

		/**
		 * Configures the Cross-Origin-Resource-Policy header
		 *
		 * @since 5.7
		 */
		public final class CrossOriginResourcePolicySpec {

			private CrossOriginResourcePolicySpec() {
			}

			/**
			 * Sets the value to be used in the `Cross-Origin-Resource-Policy` header
			 * @param resourcePolicy a opener policy
			 * @return the {@link CrossOriginResourcePolicySpec} to continue configuring
			 */
			public CrossOriginResourcePolicySpec policy(CrossOriginResourcePolicy resourcePolicy) {
				HeaderSpec.this.crossOriginResourcePolicy.setPolicy(resourcePolicy);
				return this;
			}

		}

	}

	/**
	 * Configures log out
	 *
	 * @author Shazin Sadakath
	 * @since 5.0
	 * @see #logout(Customizer)
	 */
	public final class LogoutSpec {

		private LogoutWebFilter logoutWebFilter = new LogoutWebFilter();

		private final SecurityContextServerLogoutHandler DEFAULT_LOGOUT_HANDLER = new SecurityContextServerLogoutHandler();

		private List<ServerLogoutHandler> logoutHandlers = new ArrayList<>(Arrays.asList(this.DEFAULT_LOGOUT_HANDLER));

		private LogoutSpec() {
		}

		/**
		 * Configures the logout handler. Default is
		 * {@code SecurityContextServerLogoutHandler}. This clears any previous handlers
		 * configured.
		 * @param logoutHandler
		 * @return the {@link LogoutSpec} to configure
		 */
		public LogoutSpec logoutHandler(ServerLogoutHandler logoutHandler) {
			Assert.notNull(logoutHandler, "logoutHandler cannot be null");
			this.logoutHandlers.clear();
			return addLogoutHandler(logoutHandler);
		}

		private LogoutSpec addLogoutHandler(ServerLogoutHandler logoutHandler) {
			Assert.notNull(logoutHandler, "logoutHandler cannot be null");
			this.logoutHandlers.add(logoutHandler);
			return this;
		}

		/**
		 * Allows managing the list of {@link ServerLogoutHandler} instances.
		 * @param handlersConsumer {@link Consumer} for managing the list of handlers.
		 * @return the {@link LogoutSpec} to configure
		 * @since 7.0
		 */
		public LogoutSpec logoutHandler(Consumer<List<ServerLogoutHandler>> handlersConsumer) {
			Assert.notNull(handlersConsumer, "consumer cannot be null");
			handlersConsumer.accept(this.logoutHandlers);
			return this;
		}

		/**
		 * Configures what URL a POST to will trigger a log out.
		 * @param logoutUrl the url to trigger a log out (i.e. "/signout" would mean a
		 * POST to "/signout" would trigger log out)
		 * @return the {@link LogoutSpec} to configure
		 */
		public LogoutSpec logoutUrl(String logoutUrl) {
			Assert.notNull(logoutUrl, "logoutUrl must not be null");
			ServerWebExchangeMatcher requiresLogout = ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST,
					logoutUrl);
			return requiresLogout(requiresLogout);
		}

		/**
		 * Configures when the log out will be triggered.
		 * @param requiresLogout the matcher to determine when log out is triggered
		 * @return the {@link LogoutSpec} to configure
		 */
		public LogoutSpec requiresLogout(ServerWebExchangeMatcher requiresLogout) {
			this.logoutWebFilter.setRequiresLogoutMatcher(requiresLogout);
			return this;
		}

		public LogoutSpec logoutSuccessHandler(ServerLogoutSuccessHandler handler) {
			this.logoutWebFilter.setLogoutSuccessHandler(handler);
			return this;
		}

		/**
		 * Disables log out
		 * @return the {@link ServerHttpSecurity} to continue configuring
		 */
		public ServerHttpSecurity disable() {
			ServerHttpSecurity.this.logout = null;
			return ServerHttpSecurity.this;
		}

		private ServerLogoutHandler createLogoutHandler() {
			ServerSecurityContextRepository securityContextRepository = ServerHttpSecurity.this.securityContextRepository;
			if (securityContextRepository != null) {
				this.DEFAULT_LOGOUT_HANDLER.setSecurityContextRepository(securityContextRepository);
			}
			if (this.logoutHandlers.isEmpty()) {
				return null;
			}
			if (this.logoutHandlers.size() == 1) {
				return this.logoutHandlers.get(0);
			}
			return new DelegatingServerLogoutHandler(this.logoutHandlers);
		}

		protected void configure(ServerHttpSecurity http) {
			ServerLogoutHandler logoutHandler = createLogoutHandler();
			if (logoutHandler != null) {
				this.logoutWebFilter.setLogoutHandler(logoutHandler);
			}
			http.addFilterAt(this.logoutWebFilter, SecurityWebFiltersOrder.LOGOUT);
		}

	}

	private static class OrderedWebFilter implements WebFilter, Ordered {

		private final WebFilter webFilter;

		private final int order;

		OrderedWebFilter(WebFilter webFilter, int order) {
			this.webFilter = webFilter;
			this.order = order;
		}

		@Override
		public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
			return this.webFilter.filter(exchange, chain);
		}

		@Override
		public int getOrder() {
			return this.order;
		}

		@Override
		public String toString() {
			return "OrderedWebFilter{" + "webFilter=" + this.webFilter + ", order=" + this.order + '}';
		}

	}

	/**
	 * Workaround https://jira.spring.io/projects/SPR/issues/SPR-17213
	 */
	static class ServerWebExchangeReactorContextWebFilter implements WebFilter {

		@Override
		public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
			return chain.filter(exchange).contextWrite(Context.of(ServerWebExchange.class, exchange));
		}

	}

	/**
	 * Configures CORS support within Spring Security. This ensures that the
	 * {@link CorsWebFilter} is place in the correct order.
	 */
	public final class CorsSpec {

		private CorsWebFilter corsFilter;

		private CorsSpec() {
		}

		/**
		 * Configures the {@link CorsConfigurationSource} to be used
		 * @param source the source to use
		 * @return the {@link CorsSpec} for additional configuration
		 */
		public CorsSpec configurationSource(CorsConfigurationSource source) {
			this.corsFilter = new CorsWebFilter(source);
			return this;
		}

		/**
		 * Disables CORS support within Spring Security.
		 * @return the {@link ServerHttpSecurity} to continue configuring
		 */
		public ServerHttpSecurity disable() {
			ServerHttpSecurity.this.cors = null;
			return ServerHttpSecurity.this;
		}

		protected void configure(ServerHttpSecurity http) {
			CorsWebFilter corsFilter = getCorsFilter();
			if (corsFilter != null) {
				http.addFilterAt(this.corsFilter, SecurityWebFiltersOrder.CORS);
			}
		}

		private CorsWebFilter getCorsFilter() {
			if (this.corsFilter != null) {
				return this.corsFilter;
			}
			CorsConfigurationSource source = getBeanOrNull(CorsConfigurationSource.class);
			if (source == null) {
				return null;
			}
			CorsProcessor processor = getBeanOrNull(CorsProcessor.class);
			if (processor == null) {
				processor = new DefaultCorsProcessor();
			}
			this.corsFilter = new CorsWebFilter(source, processor);
			return this.corsFilter;
		}

	}

	/**
	 * Configures X509 authentication
	 *
	 * @author Alexey Nesterov
	 * @since 5.2
	 * @see #x509(Customizer)
	 */
	public final class X509Spec {

		private X509PrincipalExtractor principalExtractor;

		private ReactiveAuthenticationManager authenticationManager;

		private ServerAuthenticationConverter serverAuthenticationConverter;

		private X509Spec() {
		}

		public X509Spec principalExtractor(X509PrincipalExtractor principalExtractor) {
			this.principalExtractor = principalExtractor;
			return this;
		}

		public X509Spec authenticationManager(ReactiveAuthenticationManager authenticationManager) {
			this.authenticationManager = authenticationManager;
			return this;
		}

		public X509Spec serverAuthenticationConverter(ServerAuthenticationConverter serverAuthenticationConverter) {
			this.serverAuthenticationConverter = serverAuthenticationConverter;
			return this;
		}

		protected void configure(ServerHttpSecurity http) {
			ReactiveAuthenticationManager authenticationManager = getAuthenticationManager();
			X509PrincipalExtractor principalExtractor = getPrincipalExtractor();
			ServerAuthenticationConverter converter = getServerAuthenticationConverter(principalExtractor);
			AuthenticationWebFilter filter = new AuthenticationWebFilter(authenticationManager);
			filter.setServerAuthenticationConverter(converter);
			http.addFilterAt(filter, SecurityWebFiltersOrder.AUTHENTICATION);
		}

		private X509PrincipalExtractor getPrincipalExtractor() {
			if (this.principalExtractor != null) {
				return this.principalExtractor;
			}
			return new SubjectX500PrincipalExtractor();
		}

		private ReactiveAuthenticationManager getAuthenticationManager() {
			if (this.authenticationManager != null) {
				return this.authenticationManager;
			}
			ReactiveUserDetailsService userDetailsService = getBean(ReactiveUserDetailsService.class);
			return new ReactivePreAuthenticatedAuthenticationManager(userDetailsService);
		}

		private ServerAuthenticationConverter getServerAuthenticationConverter(X509PrincipalExtractor extractor) {
			if (this.serverAuthenticationConverter != null) {
				return this.serverAuthenticationConverter;
			}
			return new ServerX509AuthenticationConverter(extractor);
		}

	}

	public final class OAuth2LoginSpec {

		private ReactiveClientRegistrationRepository clientRegistrationRepository;

		private ServerOAuth2AuthorizedClientRepository authorizedClientRepository;

		private ServerAuthorizationRequestRepository<OAuth2AuthorizationRequest> authorizationRequestRepository;

		private ReactiveAuthenticationManager authenticationManager;

		private ServerSecurityContextRepository securityContextRepository;

		private ServerAuthenticationConverter authenticationConverter;

		private ServerOAuth2AuthorizationRequestResolver authorizationRequestResolver;

		private ServerRedirectStrategy authorizationRedirectStrategy;

		private ServerWebExchangeMatcher authenticationMatcher;

		private ReactiveOidcSessionRegistry oidcSessionRegistry;

		private final RedirectServerAuthenticationSuccessHandler defaultAuthenticationSuccessHandler = new RedirectServerAuthenticationSuccessHandler();

		private final List<ServerAuthenticationSuccessHandler> defaultSuccessHandlers = new ArrayList<>(
				List.of(this.defaultAuthenticationSuccessHandler));

		private List<ServerAuthenticationSuccessHandler> authenticationSuccessHandlers = new ArrayList<>();

		private ServerAuthenticationFailureHandler authenticationFailureHandler;

		private String loginPage;

		private OAuth2LoginSpec() {
		}

		/**
		 * Configures the {@link ReactiveAuthenticationManager} to use. The default is
		 * {@link OAuth2AuthorizationCodeReactiveAuthenticationManager}
		 * @param authenticationManager the manager to use
		 * @return the {@link OAuth2LoginSpec} to customize
		 */
		public OAuth2LoginSpec authenticationManager(ReactiveAuthenticationManager authenticationManager) {
			this.authenticationManager = authenticationManager;
			return this;
		}

		/**
		 * The {@link ServerSecurityContextRepository} used to save the
		 * {@code Authentication}. Defaults to
		 * {@link WebSessionServerSecurityContextRepository}.
		 * @param securityContextRepository the repository to use
		 * @return the {@link OAuth2LoginSpec} to continue configuring
		 * @since 5.2
		 */
		public OAuth2LoginSpec securityContextRepository(ServerSecurityContextRepository securityContextRepository) {
			this.securityContextRepository = securityContextRepository;
			return this;
		}

		/**
		 * Configures the {@link ReactiveOidcSessionRegistry} to use when logins use OIDC.
		 * Default is to look the value up as a Bean, or else use an
		 * {@link InMemoryReactiveOidcSessionRegistry}.
		 * @param oidcSessionRegistry the registry to use
		 * @return the {@link OidcLogoutSpec} to customize
		 * @since 6.2
		 */
		public OAuth2LoginSpec oidcSessionRegistry(ReactiveOidcSessionRegistry oidcSessionRegistry) {
			Assert.notNull(oidcSessionRegistry, "oidcSessionRegistry cannot be null");
			this.oidcSessionRegistry = oidcSessionRegistry;
			return this;
		}

		/**
		 * The {@link ServerAuthenticationSuccessHandler} used after authentication
		 * success. Defaults to {@link RedirectServerAuthenticationSuccessHandler}
		 * redirecting to "/". Note that this method clears previously added success
		 * handlers via {@link #authenticationSuccessHandler(Consumer)}
		 * @param authenticationSuccessHandler the success handler to use
		 * @return the {@link OAuth2LoginSpec} to customize
		 * @since 5.2
		 */
		public OAuth2LoginSpec authenticationSuccessHandler(
				ServerAuthenticationSuccessHandler authenticationSuccessHandler) {
			Assert.notNull(authenticationSuccessHandler, "authenticationSuccessHandler cannot be null");
			authenticationSuccessHandler((handlers) -> {
				handlers.clear();
				handlers.add(authenticationSuccessHandler);
			});
			return this;
		}

		/**
		 * Allows customizing the list of {@link ServerAuthenticationSuccessHandler}. The
		 * default list contains a {@link RedirectServerAuthenticationSuccessHandler} that
		 * redirects to "/".
		 * @param handlersConsumer the handlers consumer
		 * @return the {@link OAuth2LoginSpec} to continue configuring
		 * @since 6.3
		 */
		public OAuth2LoginSpec authenticationSuccessHandler(
				Consumer<List<ServerAuthenticationSuccessHandler>> handlersConsumer) {
			Assert.notNull(handlersConsumer, "handlersConsumer cannot be null");
			handlersConsumer.accept(this.authenticationSuccessHandlers);
			return this;
		}

		/**
		 * The {@link ServerAuthenticationFailureHandler} used after authentication
		 * failure. Defaults to {@link RedirectServerAuthenticationFailureHandler}
		 * redirecting to "/login?error".
		 * @param authenticationFailureHandler the failure handler to use
		 * @return the {@link OAuth2LoginSpec} to customize
		 * @since 5.2
		 */
		public OAuth2LoginSpec authenticationFailureHandler(
				ServerAuthenticationFailureHandler authenticationFailureHandler) {
			Assert.notNull(authenticationFailureHandler, "authenticationFailureHandler cannot be null");
			this.authenticationFailureHandler = authenticationFailureHandler;
			return this;
		}

		/**
		 * Gets the {@link ReactiveAuthenticationManager} to use. First tries an
		 * explicitly configured manager, and defaults to
		 * {@link OAuth2AuthorizationCodeReactiveAuthenticationManager}
		 * @return the {@link ReactiveAuthenticationManager} to use
		 */
		private ReactiveAuthenticationManager getAuthenticationManager() {
			if (this.authenticationManager == null) {
				this.authenticationManager = createDefault();
			}
			return this.authenticationManager;
		}

		private ReactiveAuthenticationManager createDefault() {
			ReactiveOAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> client = getAccessTokenResponseClient();
			OAuth2LoginReactiveAuthenticationManager oauth2Manager = new OAuth2LoginReactiveAuthenticationManager(
					client, getOauth2UserService());
			GrantedAuthoritiesMapper authoritiesMapper = getBeanOrNull(GrantedAuthoritiesMapper.class);
			if (authoritiesMapper != null) {
				oauth2Manager.setAuthoritiesMapper(authoritiesMapper);
			}
			boolean oidcAuthenticationProviderEnabled = ClassUtils
				.isPresent("org.springframework.security.oauth2.jwt.JwtDecoder", this.getClass().getClassLoader());
			if (!oidcAuthenticationProviderEnabled) {
				return oauth2Manager;
			}
			OidcAuthorizationCodeReactiveAuthenticationManager oidc = new OidcAuthorizationCodeReactiveAuthenticationManager(
					client, getOidcUserService());
			ResolvableType type = ResolvableType.forClassWithGenerics(ReactiveJwtDecoderFactory.class,
					ClientRegistration.class);
			ReactiveJwtDecoderFactory<ClientRegistration> jwtDecoderFactory = getBeanOrNull(type);
			if (jwtDecoderFactory != null) {
				oidc.setJwtDecoderFactory(jwtDecoderFactory);
			}
			if (authoritiesMapper != null) {
				oidc.setAuthoritiesMapper(authoritiesMapper);
			}
			return new DelegatingReactiveAuthenticationManager(oidc, oauth2Manager);
		}

		/**
		 * Sets the converter to use
		 * @param authenticationConverter the converter to use
		 * @return the {@link OAuth2LoginSpec} to customize
		 */
		public OAuth2LoginSpec authenticationConverter(ServerAuthenticationConverter authenticationConverter) {
			this.authenticationConverter = authenticationConverter;
			return this;
		}

		private ServerAuthenticationConverter getAuthenticationConverter(
				ReactiveClientRegistrationRepository clientRegistrationRepository) {
			if (this.authenticationConverter != null) {
				return this.authenticationConverter;
			}
			ServerOAuth2AuthorizationCodeAuthenticationTokenConverter delegate = new ServerOAuth2AuthorizationCodeAuthenticationTokenConverter(
					clientRegistrationRepository);
			delegate.setAuthorizationRequestRepository(getAuthorizationRequestRepository());
			ServerAuthenticationConverter authenticationConverter = (exchange) -> delegate.convert(exchange)
				.onErrorMap(OAuth2AuthorizationException.class,
						(e) -> new OAuth2AuthenticationException(e.getError(), e.getError().toString()));
			this.authenticationConverter = authenticationConverter;
			return authenticationConverter;
		}

		public OAuth2LoginSpec clientRegistrationRepository(
				ReactiveClientRegistrationRepository clientRegistrationRepository) {
			this.clientRegistrationRepository = clientRegistrationRepository;
			return this;
		}

		public OAuth2LoginSpec authorizedClientService(ReactiveOAuth2AuthorizedClientService authorizedClientService) {
			this.authorizedClientRepository = new AuthenticatedPrincipalServerOAuth2AuthorizedClientRepository(
					authorizedClientService);
			return this;
		}

		public OAuth2LoginSpec authorizedClientRepository(
				ServerOAuth2AuthorizedClientRepository authorizedClientRepository) {
			this.authorizedClientRepository = authorizedClientRepository;
			return this;
		}

		/**
		 * Sets the repository to use for storing {@link OAuth2AuthorizationRequest}'s.
		 * @param authorizationRequestRepository the repository to use for storing
		 * {@link OAuth2AuthorizationRequest}'s
		 * @return the {@link OAuth2LoginSpec} for further configuration
		 * @since 5.2
		 */
		public OAuth2LoginSpec authorizationRequestRepository(
				ServerAuthorizationRequestRepository<OAuth2AuthorizationRequest> authorizationRequestRepository) {
			this.authorizationRequestRepository = authorizationRequestRepository;
			return this;
		}

		/**
		 * Sets the resolver used for resolving {@link OAuth2AuthorizationRequest}'s.
		 * @param authorizationRequestResolver the resolver used for resolving
		 * {@link OAuth2AuthorizationRequest}'s
		 * @return the {@link OAuth2LoginSpec} for further configuration
		 * @since 5.2
		 */
		public OAuth2LoginSpec authorizationRequestResolver(
				ServerOAuth2AuthorizationRequestResolver authorizationRequestResolver) {
			this.authorizationRequestResolver = authorizationRequestResolver;
			return this;
		}

		/**
		 * Sets the redirect strategy for Authorization Endpoint redirect URI.
		 * @param authorizationRedirectStrategy the redirect strategy
		 * @return the {@link OAuth2LoginSpec} for further configuration
		 */
		public OAuth2LoginSpec authorizationRedirectStrategy(ServerRedirectStrategy authorizationRedirectStrategy) {
			this.authorizationRedirectStrategy = authorizationRedirectStrategy;
			return this;
		}

		/**
		 * Sets the {@link ServerWebExchangeMatcher matcher} used for determining if the
		 * request is an authentication request.
		 * @param authenticationMatcher the {@link ServerWebExchangeMatcher matcher} used
		 * for determining if the request is an authentication request
		 * @return the {@link OAuth2LoginSpec} for further configuration
		 * @since 5.2
		 */
		public OAuth2LoginSpec authenticationMatcher(ServerWebExchangeMatcher authenticationMatcher) {
			this.authenticationMatcher = authenticationMatcher;
			return this;
		}

		private ServerWebExchangeMatcher getAuthenticationMatcher() {
			if (this.authenticationMatcher == null) {
				this.authenticationMatcher = createAttemptAuthenticationRequestMatcher();
			}
			return this.authenticationMatcher;
		}

		/**
		 * Specifies the URL to send users to if login is required. A default login page
		 * will be generated when this attribute is not specified.
		 * @param loginPage the URL to send users to if login is required
		 * @return the {@link OAuth2LoginSpec} for further configuration
		 * @since 6.4
		 */
		public OAuth2LoginSpec loginPage(String loginPage) {
			Assert.hasText(loginPage, "loginPage cannot be empty");
			this.loginPage = loginPage;
			return this;
		}

		protected void configure(ServerHttpSecurity http) {
			ReactiveClientRegistrationRepository clientRegistrationRepository = getClientRegistrationRepository();
			ServerOAuth2AuthorizedClientRepository authorizedClientRepository = getAuthorizedClientRepository();
			OAuth2AuthorizationRequestRedirectWebFilter oauthRedirectFilter = getRedirectWebFilter();
			ServerAuthorizationRequestRepository<OAuth2AuthorizationRequest> authorizationRequestRepository = getAuthorizationRequestRepository();
			oauthRedirectFilter.setAuthorizationRequestRepository(authorizationRequestRepository);
			oauthRedirectFilter.setAuthorizationRedirectStrategy(getAuthorizationRedirectStrategy());
			oauthRedirectFilter.setRequestCache(http.requestCache.requestCache);

			ReactiveAuthenticationManager manager = getAuthenticationManager();
			ReactiveOidcSessionRegistry sessionRegistry = getOidcSessionRegistry();
			AuthenticationWebFilter authenticationFilter = (sessionRegistry != null)
					? new OidcSessionRegistryAuthenticationWebFilter(manager, authorizedClientRepository,
							sessionRegistry)
					: new OAuth2LoginAuthenticationWebFilter(manager, authorizedClientRepository);
			authenticationFilter.setRequiresAuthenticationMatcher(getAuthenticationMatcher());
			authenticationFilter
				.setServerAuthenticationConverter(getAuthenticationConverter(clientRegistrationRepository));
			authenticationFilter.setAuthenticationSuccessHandler(getAuthenticationSuccessHandler(http));
			authenticationFilter.setAuthenticationFailureHandler(getAuthenticationFailureHandler());
			authenticationFilter.setSecurityContextRepository(this.securityContextRepository);

			setDefaultEntryPoints(http);
			if (sessionRegistry != null) {
				http.addFilterAfter(new OidcSessionRegistryWebFilter(sessionRegistry),
						SecurityWebFiltersOrder.HTTP_HEADERS_WRITER);
			}
			http.addFilterAt(oauthRedirectFilter, SecurityWebFiltersOrder.HTTP_BASIC);
			http.addFilterAt(authenticationFilter, SecurityWebFiltersOrder.AUTHENTICATION);
		}

		private void setDefaultEntryPoints(ServerHttpSecurity http) {
			MediaTypeServerWebExchangeMatcher htmlMatcher = new MediaTypeServerWebExchangeMatcher(
					MediaType.APPLICATION_XHTML_XML, new MediaType("image", "*"), MediaType.TEXT_HTML,
					MediaType.TEXT_PLAIN);
			htmlMatcher.setIgnoredMediaTypes(Collections.singleton(MediaType.ALL));
			ServerWebExchangeMatcher xhrMatcher = (exchange) -> {
				if (exchange.getRequest().getHeaders().getOrEmpty("X-Requested-With").contains("XMLHttpRequest")) {
					return ServerWebExchangeMatcher.MatchResult.match();
				}
				return ServerWebExchangeMatcher.MatchResult.notMatch();
			};
			ServerWebExchangeMatcher notXhrMatcher = new NegatedServerWebExchangeMatcher(xhrMatcher);
			ServerWebExchangeMatcher defaultEntryPointMatcher = new AndServerWebExchangeMatcher(notXhrMatcher,
					htmlMatcher);
			String loginPage = "/login";
			if (StringUtils.hasText(this.loginPage)) {
				loginPage = this.loginPage;
			}
			else {
				Map<String, String> urlToText = http.oauth2Login.getLinks();
				String providerLoginPage = null;
				if (urlToText.size() == 1) {
					providerLoginPage = urlToText.keySet().iterator().next();
				}
				if (providerLoginPage != null) {
					ServerWebExchangeMatcher loginPageMatcher = new PathPatternParserServerWebExchangeMatcher(
							loginPage);
					ServerWebExchangeMatcher faviconMatcher = new PathPatternParserServerWebExchangeMatcher(
							"/favicon.ico");
					ServerWebExchangeMatcher defaultLoginPageMatcher = new AndServerWebExchangeMatcher(
							new OrServerWebExchangeMatcher(loginPageMatcher, faviconMatcher), defaultEntryPointMatcher);

					ServerWebExchangeMatcher matcher = new AndServerWebExchangeMatcher(notXhrMatcher,
							new NegatedServerWebExchangeMatcher(defaultLoginPageMatcher));
					RedirectServerAuthenticationEntryPoint entryPoint = new RedirectServerAuthenticationEntryPoint(
							providerLoginPage);
					entryPoint.setRequestCache(http.requestCache.requestCache);
					http.defaultEntryPoints.add(new DelegateEntry(matcher, entryPoint));
				}
			}
			RedirectServerAuthenticationEntryPoint defaultEntryPoint = new RedirectServerAuthenticationEntryPoint(
					loginPage);
			defaultEntryPoint.setRequestCache(http.requestCache.requestCache);
			http.defaultEntryPoints.add(new DelegateEntry(defaultEntryPointMatcher, defaultEntryPoint));
		}

		private ReactiveOidcSessionRegistry getOidcSessionRegistry() {
			if (ServerHttpSecurity.this.oidcLogout == null && this.oidcSessionRegistry == null) {
				return null;
			}
			if (this.oidcSessionRegistry == null) {
				this.oidcSessionRegistry = getBeanOrNull(ReactiveOidcSessionRegistry.class);
			}
			if (this.oidcSessionRegistry == null) {
				this.oidcSessionRegistry = new InMemoryReactiveOidcSessionRegistry();
			}
			return this.oidcSessionRegistry;
		}

		private ServerAuthenticationSuccessHandler getAuthenticationSuccessHandler(ServerHttpSecurity http) {
			this.defaultAuthenticationSuccessHandler.setRequestCache(http.requestCache.requestCache);
			if (this.authenticationSuccessHandlers.isEmpty()) {
				return new DelegatingServerAuthenticationSuccessHandler(this.defaultSuccessHandlers);
			}
			return new DelegatingServerAuthenticationSuccessHandler(this.authenticationSuccessHandlers);
		}

		private ServerAuthenticationFailureHandler getAuthenticationFailureHandler() {
			if (this.authenticationFailureHandler == null) {
				this.authenticationFailureHandler = new RedirectServerAuthenticationFailureHandler("/login?error");
			}
			return this.authenticationFailureHandler;
		}

		private ServerWebExchangeMatcher createAttemptAuthenticationRequestMatcher() {
			return new PathPatternParserServerWebExchangeMatcher("/login/oauth2/code/{registrationId}");
		}

		private ReactiveOAuth2UserService<OidcUserRequest, OidcUser> getOidcUserService() {
			ResolvableType type = ResolvableType.forClassWithGenerics(ReactiveOAuth2UserService.class,
					OidcUserRequest.class, OidcUser.class);
			ReactiveOAuth2UserService<OidcUserRequest, OidcUser> bean = getBeanOrNull(type);
			if (bean != null) {
				return bean;
			}
			OidcReactiveOAuth2UserService reactiveOAuth2UserService = new OidcReactiveOAuth2UserService();
			reactiveOAuth2UserService.setOauth2UserService(getOauth2UserService());
			return reactiveOAuth2UserService;
		}

		private ReactiveOAuth2UserService<OAuth2UserRequest, OAuth2User> getOauth2UserService() {
			ResolvableType type = ResolvableType.forClassWithGenerics(ReactiveOAuth2UserService.class,
					OAuth2UserRequest.class, OAuth2User.class);
			ReactiveOAuth2UserService<OAuth2UserRequest, OAuth2User> bean = getBeanOrNull(type);
			if (bean != null) {
				return bean;
			}
			return new DefaultReactiveOAuth2UserService();
		}

		private Map<String, String> getLinks() {
			Iterable<ClientRegistration> registrations = getBeanOrNull(
					ResolvableType.forClassWithGenerics(Iterable.class, ClientRegistration.class));
			if (registrations == null) {
				return Collections.emptyMap();
			}
			Map<String, String> result = new HashMap<>();
			registrations.iterator().forEachRemaining((r) -> {
				if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(r.getAuthorizationGrantType())) {
					result.put("/oauth2/authorization/" + r.getRegistrationId(), r.getClientName());
				}
			});
			return result;
		}

		private ReactiveOAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> getAccessTokenResponseClient() {
			ResolvableType type = ResolvableType.forClassWithGenerics(ReactiveOAuth2AccessTokenResponseClient.class,
					OAuth2AuthorizationCodeGrantRequest.class);
			ReactiveOAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> bean = getBeanOrNull(type);
			if (bean != null) {
				return bean;
			}
			return new WebClientReactiveAuthorizationCodeTokenResponseClient();
		}

		private ReactiveClientRegistrationRepository getClientRegistrationRepository() {
			if (this.clientRegistrationRepository == null) {
				this.clientRegistrationRepository = getBeanOrNull(ReactiveClientRegistrationRepository.class);
			}
			return this.clientRegistrationRepository;
		}

		private OAuth2AuthorizationRequestRedirectWebFilter getRedirectWebFilter() {
			ServerOAuth2AuthorizationRequestResolver authorizationRequestResolver = this.authorizationRequestResolver;
			if (authorizationRequestResolver == null) {
				authorizationRequestResolver = getBeanOrNull(ServerOAuth2AuthorizationRequestResolver.class);
			}
			if (authorizationRequestResolver != null) {
				return new OAuth2AuthorizationRequestRedirectWebFilter(authorizationRequestResolver);
			}
			return new OAuth2AuthorizationRequestRedirectWebFilter(getClientRegistrationRepository());
		}

		private ServerOAuth2AuthorizedClientRepository getAuthorizedClientRepository() {
			ServerOAuth2AuthorizedClientRepository result = this.authorizedClientRepository;
			if (result == null) {
				result = getBeanOrNull(ServerOAuth2AuthorizedClientRepository.class);
			}
			if (result == null) {
				ReactiveOAuth2AuthorizedClientService authorizedClientService = getAuthorizedClientService();
				if (authorizedClientService != null) {
					result = new AuthenticatedPrincipalServerOAuth2AuthorizedClientRepository(authorizedClientService);
				}
			}
			return result;
		}

		private ServerAuthorizationRequestRepository<OAuth2AuthorizationRequest> getAuthorizationRequestRepository() {
			if (this.authorizationRequestRepository == null) {
				this.authorizationRequestRepository = new WebSessionOAuth2ServerAuthorizationRequestRepository();
			}
			return this.authorizationRequestRepository;
		}

		private ServerRedirectStrategy getAuthorizationRedirectStrategy() {
			if (this.authorizationRedirectStrategy == null) {
				this.authorizationRedirectStrategy = new DefaultServerRedirectStrategy();
			}
			return this.authorizationRedirectStrategy;
		}

		private ReactiveOAuth2AuthorizedClientService getAuthorizedClientService() {
			ReactiveOAuth2AuthorizedClientService bean = getBeanOrNull(ReactiveOAuth2AuthorizedClientService.class);
			if (bean != null) {
				return bean;
			}
			return new InMemoryReactiveOAuth2AuthorizedClientService(getClientRegistrationRepository());
		}

		private static final class OidcSessionRegistryWebFilter implements WebFilter {

			private final ReactiveOidcSessionRegistry oidcSessionRegistry;

			OidcSessionRegistryWebFilter(ReactiveOidcSessionRegistry oidcSessionRegistry) {
				this.oidcSessionRegistry = oidcSessionRegistry;
			}

			@Override
			public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
				return chain.filter(new OidcSessionRegistryServerWebExchange(exchange));
			}

			private final class OidcSessionRegistryServerWebExchange extends ServerWebExchangeDecorator {

				private final Mono<WebSession> sessionMono;

				protected OidcSessionRegistryServerWebExchange(ServerWebExchange delegate) {
					super(delegate);
					this.sessionMono = delegate.getSession().map(OidcSessionRegistryWebSession::new);
				}

				@Override
				public Mono<WebSession> getSession() {
					return this.sessionMono;
				}

				private final class OidcSessionRegistryWebSession implements WebSession {

					private final WebSession session;

					OidcSessionRegistryWebSession(WebSession session) {
						this.session = session;
					}

					@Override
					public String getId() {
						return this.session.getId();
					}

					@Override
					public Map<String, Object> getAttributes() {
						return this.session.getAttributes();
					}

					@Override
					public void start() {
						this.session.start();
					}

					@Override
					public boolean isStarted() {
						return this.session.isStarted();
					}

					@Override
					public Mono<Void> changeSessionId() {
						String currentId = this.session.getId();
						return this.session.changeSessionId()
							.then(Mono.defer(() -> OidcSessionRegistryWebFilter.this.oidcSessionRegistry
								.removeSessionInformation(currentId)
								.flatMap((information) -> {
									information = information.withSessionId(this.session.getId());
									return OidcSessionRegistryWebFilter.this.oidcSessionRegistry
										.saveSessionInformation(information);
								})));
					}

					@Override
					public Mono<Void> invalidate() {
						String currentId = this.session.getId();
						return this.session.invalidate()
							.then(Mono.defer(() -> OidcSessionRegistryWebFilter.this.oidcSessionRegistry
								.removeSessionInformation(currentId)
								.then(Mono.empty())));
					}

					@Override
					public Mono<Void> save() {
						return this.session.save();
					}

					@Override
					public boolean isExpired() {
						return this.session.isExpired();
					}

					@Override
					public Instant getCreationTime() {
						return this.session.getCreationTime();
					}

					@Override
					public Instant getLastAccessTime() {
						return this.session.getLastAccessTime();
					}

					@Override
					public void setMaxIdleTime(Duration maxIdleTime) {
						this.session.setMaxIdleTime(maxIdleTime);
					}

					@Override
					public Duration getMaxIdleTime() {
						return this.session.getMaxIdleTime();
					}

				}

			}

		}

		static final class OidcSessionRegistryAuthenticationWebFilter extends OAuth2LoginAuthenticationWebFilter {

			private final Log logger = LogFactory.getLog(getClass());

			private final ReactiveOidcSessionRegistry oidcSessionRegistry;

			OidcSessionRegistryAuthenticationWebFilter(ReactiveAuthenticationManager authenticationManager,
					ServerOAuth2AuthorizedClientRepository authorizedClientRepository,
					ReactiveOidcSessionRegistry oidcSessionRegistry) {
				super(authenticationManager, authorizedClientRepository);
				this.oidcSessionRegistry = oidcSessionRegistry;
			}

			@Override
			protected Mono<Void> onAuthenticationSuccess(Authentication authentication,
					WebFilterExchange webFilterExchange) {
				if (!(authentication.getPrincipal() instanceof OidcUser user)) {
					return super.onAuthenticationSuccess(authentication, webFilterExchange);
				}
				return webFilterExchange.getExchange().getSession().doOnNext((session) -> {
					if (this.logger.isTraceEnabled()) {
						this.logger.trace(String.format("Linking a provider [%s] session to this client's session",
								user.getIssuer()));
					}
				}).flatMap((session) -> {
					Mono<CsrfToken> csrfToken = webFilterExchange.getExchange().getAttribute(CsrfToken.class.getName());
					return (csrfToken != null)
							? csrfToken.map((token) -> new OidcSessionInformation(session.getId(),
									Map.of(token.getHeaderName(), token.getToken()), user))
							: Mono.just(new OidcSessionInformation(session.getId(), Map.of(), user));
				})
					.flatMap(this.oidcSessionRegistry::saveSessionInformation)
					.then(super.onAuthenticationSuccess(authentication, webFilterExchange));
			}

		}

	}

	public final class OAuth2ClientSpec {

		private ReactiveClientRegistrationRepository clientRegistrationRepository;

		private ServerAuthenticationConverter authenticationConverter;

		private ServerOAuth2AuthorizedClientRepository authorizedClientRepository;

		private ReactiveAuthenticationManager authenticationManager;

		private ServerAuthorizationRequestRepository<OAuth2AuthorizationRequest> authorizationRequestRepository;

		private ServerOAuth2AuthorizationRequestResolver authorizationRequestResolver;

		private ServerRedirectStrategy authorizationRedirectStrategy;

		private OAuth2ClientSpec() {
		}

		/**
		 * Sets the converter to use
		 * @param authenticationConverter the converter to use
		 * @return the {@link OAuth2ClientSpec} to customize
		 */
		public OAuth2ClientSpec authenticationConverter(ServerAuthenticationConverter authenticationConverter) {
			this.authenticationConverter = authenticationConverter;
			return this;
		}

		private ServerAuthenticationConverter getAuthenticationConverter() {
			if (this.authenticationConverter == null) {
				ServerOAuth2AuthorizationCodeAuthenticationTokenConverter authenticationConverter = new ServerOAuth2AuthorizationCodeAuthenticationTokenConverter(
						getClientRegistrationRepository());
				authenticationConverter.setAuthorizationRequestRepository(getAuthorizationRequestRepository());
				this.authenticationConverter = authenticationConverter;
			}
			return this.authenticationConverter;
		}

		/**
		 * Configures the {@link ReactiveAuthenticationManager} to use. The default is
		 * {@link OAuth2AuthorizationCodeReactiveAuthenticationManager}
		 * @param authenticationManager the manager to use
		 * @return the {@link OAuth2ClientSpec} to customize
		 */
		public OAuth2ClientSpec authenticationManager(ReactiveAuthenticationManager authenticationManager) {
			this.authenticationManager = authenticationManager;
			return this;
		}

		/**
		 * Gets the {@link ReactiveAuthenticationManager} to use. First tries an
		 * explicitly configured manager, and defaults to
		 * {@link OAuth2AuthorizationCodeReactiveAuthenticationManager}
		 * @return the {@link ReactiveAuthenticationManager} to use
		 */
		private ReactiveAuthenticationManager getAuthenticationManager() {
			if (this.authenticationManager == null) {
				this.authenticationManager = new OAuth2AuthorizationCodeReactiveAuthenticationManager(
						getAuthorizationCodeTokenResponseClient());
			}
			return this.authenticationManager;
		}

		private ReactiveOAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> getAuthorizationCodeTokenResponseClient() {
			ResolvableType resolvableType = ResolvableType.forClassWithGenerics(
					ReactiveOAuth2AccessTokenResponseClient.class, OAuth2AuthorizationCodeGrantRequest.class);
			ReactiveOAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient = getBeanOrNull(
					resolvableType);
			if (accessTokenResponseClient == null) {
				accessTokenResponseClient = new WebClientReactiveAuthorizationCodeTokenResponseClient();
			}
			return accessTokenResponseClient;
		}

		/**
		 * Configures the {@link ReactiveClientRegistrationRepository}. Default is to look
		 * the value up as a Bean.
		 * @param clientRegistrationRepository the repository to use
		 * @return the {@link OAuth2ClientSpec} to customize
		 */
		public OAuth2ClientSpec clientRegistrationRepository(
				ReactiveClientRegistrationRepository clientRegistrationRepository) {
			this.clientRegistrationRepository = clientRegistrationRepository;
			return this;
		}

		/**
		 * Configures the {@link ReactiveClientRegistrationRepository}. Default is to look
		 * the value up as a Bean.
		 * @param authorizedClientRepository the repository to use
		 * @return the {@link OAuth2ClientSpec} to customize
		 */
		public OAuth2ClientSpec authorizedClientRepository(
				ServerOAuth2AuthorizedClientRepository authorizedClientRepository) {
			this.authorizedClientRepository = authorizedClientRepository;
			return this;
		}

		/**
		 * Sets the repository to use for storing {@link OAuth2AuthorizationRequest}'s.
		 * @param authorizationRequestRepository the repository to use for storing
		 * {@link OAuth2AuthorizationRequest}'s
		 * @return the {@link OAuth2ClientSpec} to customize
		 * @since 5.2
		 */
		public OAuth2ClientSpec authorizationRequestRepository(
				ServerAuthorizationRequestRepository<OAuth2AuthorizationRequest> authorizationRequestRepository) {
			this.authorizationRequestRepository = authorizationRequestRepository;
			return this;
		}

		private ServerAuthorizationRequestRepository<OAuth2AuthorizationRequest> getAuthorizationRequestRepository() {
			if (this.authorizationRequestRepository == null) {
				this.authorizationRequestRepository = new WebSessionOAuth2ServerAuthorizationRequestRepository();
			}
			return this.authorizationRequestRepository;
		}

		/**
		 * Sets the resolver used for resolving {@link OAuth2AuthorizationRequest}'s.
		 * @param authorizationRequestResolver the resolver used for resolving
		 * {@link OAuth2AuthorizationRequest}'s
		 * @return the {@link OAuth2ClientSpec} to customize
		 * @since 6.1
		 */
		public OAuth2ClientSpec authorizationRequestResolver(
				ServerOAuth2AuthorizationRequestResolver authorizationRequestResolver) {
			this.authorizationRequestResolver = authorizationRequestResolver;
			return this;
		}

		private OAuth2AuthorizationRequestRedirectWebFilter getRedirectWebFilter() {
			if (this.authorizationRequestResolver != null) {
				return new OAuth2AuthorizationRequestRedirectWebFilter(this.authorizationRequestResolver);
			}
			return new OAuth2AuthorizationRequestRedirectWebFilter(getClientRegistrationRepository());
		}

		/**
		 * Sets the redirect strategy for Authorization Endpoint redirect URI.
		 * @param authorizationRedirectStrategy the redirect strategy
		 * @return the {@link OAuth2ClientSpec} for further configuration
		 */
		public OAuth2ClientSpec authorizationRedirectStrategy(ServerRedirectStrategy authorizationRedirectStrategy) {
			this.authorizationRedirectStrategy = authorizationRedirectStrategy;
			return this;
		}

		private ServerRedirectStrategy getAuthorizationRedirectStrategy() {
			if (this.authorizationRedirectStrategy == null) {
				this.authorizationRedirectStrategy = new DefaultServerRedirectStrategy();
			}
			return this.authorizationRedirectStrategy;
		}

		protected void configure(ServerHttpSecurity http) {
			ServerOAuth2AuthorizedClientRepository authorizedClientRepository = getAuthorizedClientRepository();
			ServerAuthenticationConverter authenticationConverter = getAuthenticationConverter();
			ReactiveAuthenticationManager authenticationManager = getAuthenticationManager();
			OAuth2AuthorizationCodeGrantWebFilter codeGrantWebFilter = new OAuth2AuthorizationCodeGrantWebFilter(
					authenticationManager, authenticationConverter, authorizedClientRepository);
			codeGrantWebFilter.setAuthorizationRequestRepository(getAuthorizationRequestRepository());
			if (http.requestCache != null) {
				codeGrantWebFilter.setRequestCache(http.requestCache.requestCache);
			}

			OAuth2AuthorizationRequestRedirectWebFilter oauthRedirectFilter = getRedirectWebFilter();
			oauthRedirectFilter.setAuthorizationRequestRepository(getAuthorizationRequestRepository());
			oauthRedirectFilter.setAuthorizationRedirectStrategy(getAuthorizationRedirectStrategy());
			if (http.requestCache != null) {
				oauthRedirectFilter.setRequestCache(http.requestCache.requestCache);
			}

			http.addFilterAt(codeGrantWebFilter, SecurityWebFiltersOrder.OAUTH2_AUTHORIZATION_CODE);
			http.addFilterAt(oauthRedirectFilter, SecurityWebFiltersOrder.HTTP_BASIC);
		}

		private ReactiveClientRegistrationRepository getClientRegistrationRepository() {
			if (this.clientRegistrationRepository != null) {
				return this.clientRegistrationRepository;
			}
			return getBeanOrNull(ReactiveClientRegistrationRepository.class);
		}

		private ServerOAuth2AuthorizedClientRepository getAuthorizedClientRepository() {
			if (this.authorizedClientRepository != null) {
				return this.authorizedClientRepository;
			}
			ServerOAuth2AuthorizedClientRepository result = getBeanOrNull(ServerOAuth2AuthorizedClientRepository.class);
			if (result != null) {
				return result;
			}
			ReactiveOAuth2AuthorizedClientService authorizedClientService = getAuthorizedClientService();
			if (authorizedClientService != null) {
				return new AuthenticatedPrincipalServerOAuth2AuthorizedClientRepository(authorizedClientService);
			}
			return null;
		}

		private ReactiveOAuth2AuthorizedClientService getAuthorizedClientService() {
			ReactiveOAuth2AuthorizedClientService bean = getBeanOrNull(ReactiveOAuth2AuthorizedClientService.class);
			if (bean != null) {
				return bean;
			}
			return new InMemoryReactiveOAuth2AuthorizedClientService(getClientRegistrationRepository());
		}

	}

	/**
	 * Configures OAuth2 Resource Server Support
	 */
	public class OAuth2ResourceServerSpec {

		private ServerAuthenticationEntryPoint entryPoint = new BearerTokenServerAuthenticationEntryPoint();

		private ServerAuthenticationFailureHandler authenticationFailureHandler;

		private ServerAccessDeniedHandler accessDeniedHandler = new BearerTokenServerAccessDeniedHandler();

		private ServerAuthenticationConverter bearerTokenConverter = new ServerBearerTokenAuthenticationConverter();

		private AuthenticationConverterServerWebExchangeMatcher authenticationConverterServerWebExchangeMatcher;

		private JwtSpec jwt;

		private OpaqueTokenSpec opaqueToken;

		private ReactiveAuthenticationManagerResolver<ServerWebExchange> authenticationManagerResolver;

		/**
		 * Configures the {@link ServerAccessDeniedHandler} to use for requests
		 * authenticating with
		 * <a href="https://tools.ietf.org/html/rfc6750#section-1.2" target=
		 * "_blank">Bearer Token</a>s. requests.
		 * @param accessDeniedHandler the {@link ServerAccessDeniedHandler} to use
		 * @return the {@link OAuth2ResourceServerSpec} for additional configuration
		 * @since 5.2
		 */
		public OAuth2ResourceServerSpec accessDeniedHandler(ServerAccessDeniedHandler accessDeniedHandler) {
			Assert.notNull(accessDeniedHandler, "accessDeniedHandler cannot be null");
			this.accessDeniedHandler = accessDeniedHandler;
			return this;
		}

		/**
		 * Configures the {@link ServerAuthenticationEntryPoint} to use for requests
		 * authenticating with
		 * <a href="https://tools.ietf.org/html/rfc6750#section-1.2" target=
		 * "_blank">Bearer Token</a>s.
		 * @param entryPoint the {@link ServerAuthenticationEntryPoint} to use
		 * @return the {@link OAuth2ResourceServerSpec} for additional configuration
		 * @since 5.2
		 */
		public OAuth2ResourceServerSpec authenticationEntryPoint(ServerAuthenticationEntryPoint entryPoint) {
			Assert.notNull(entryPoint, "entryPoint cannot be null");
			this.entryPoint = entryPoint;
			return this;
		}

		public OAuth2ResourceServerSpec authenticationFailureHandler(
				ServerAuthenticationFailureHandler authenticationFailureHandler) {
			this.authenticationFailureHandler = authenticationFailureHandler;
			return this;
		}

		/**
		 * Configures the {@link ServerAuthenticationConverter} to use for requests
		 * authenticating with
		 * <a href="https://tools.ietf.org/html/rfc6750#section-1.2" target=
		 * "_blank">Bearer Token</a>s.
		 * @param bearerTokenConverter The {@link ServerAuthenticationConverter} to use
		 * @return The {@link OAuth2ResourceServerSpec} for additional configuration
		 * @since 5.2
		 */
		public OAuth2ResourceServerSpec bearerTokenConverter(ServerAuthenticationConverter bearerTokenConverter) {
			Assert.notNull(bearerTokenConverter, "bearerTokenConverter cannot be null");
			this.bearerTokenConverter = bearerTokenConverter;
			return this;
		}

		/**
		 * Configures the {@link ReactiveAuthenticationManagerResolver}
		 * @param authenticationManagerResolver the
		 * {@link ReactiveAuthenticationManagerResolver}
		 * @return the {@link OAuth2ResourceServerSpec} for additional configuration
		 * @since 5.3
		 */
		public OAuth2ResourceServerSpec authenticationManagerResolver(
				ReactiveAuthenticationManagerResolver<ServerWebExchange> authenticationManagerResolver) {
			Assert.notNull(authenticationManagerResolver, "authenticationManagerResolver cannot be null");
			this.authenticationManagerResolver = authenticationManagerResolver;
			return this;
		}

		/**
		 * Enables JWT Resource Server support.
		 * @param jwtCustomizer the {@link Customizer} to provide more options for the
		 * {@link JwtSpec}
		 * @return the {@link OAuth2ResourceServerSpec} to customize
		 */
		public OAuth2ResourceServerSpec jwt(Customizer<JwtSpec> jwtCustomizer) {
			if (this.jwt == null) {
				this.jwt = new JwtSpec();
			}
			jwtCustomizer.customize(this.jwt);
			return this;
		}

		/**
		 * Enables Opaque Token Resource Server support.
		 * @param opaqueTokenCustomizer the {@link Customizer} to provide more options for
		 * the {@link OpaqueTokenSpec}
		 * @return the {@link OAuth2ResourceServerSpec} to customize
		 */
		public OAuth2ResourceServerSpec opaqueToken(Customizer<OpaqueTokenSpec> opaqueTokenCustomizer) {
			if (this.opaqueToken == null) {
				this.opaqueToken = new OpaqueTokenSpec();
			}
			opaqueTokenCustomizer.customize(this.opaqueToken);
			return this;
		}

		protected void configure(ServerHttpSecurity http) {
			this.authenticationConverterServerWebExchangeMatcher = new AuthenticationConverterServerWebExchangeMatcher(
					this.bearerTokenConverter);
			registerDefaultAccessDeniedHandler(http);
			registerDefaultAuthenticationEntryPoint(http);
			registerDefaultCsrfOverride(http);
			validateConfiguration();
			if (this.authenticationManagerResolver != null) {
				AuthenticationWebFilter oauth2 = new AuthenticationWebFilter(this.authenticationManagerResolver);
				oauth2.setServerAuthenticationConverter(this.bearerTokenConverter);
				oauth2.setAuthenticationFailureHandler(authenticationFailureHandler());
				http.addFilterAt(oauth2, SecurityWebFiltersOrder.AUTHENTICATION);
			}
			else if (this.jwt != null) {
				this.jwt.configure(http);
			}
			else if (this.opaqueToken != null) {
				this.opaqueToken.configure(http);
			}
		}

		private void validateConfiguration() {
			if (this.authenticationManagerResolver == null) {
				Assert.state(this.jwt != null || this.opaqueToken != null,
						"Jwt and Opaque Token are the only supported formats for bearer tokens "
								+ "in Spring Security and neither was found. Make sure to configure JWT "
								+ "via http.oauth2ResourceServer().jwt() or Opaque Tokens via "
								+ "http.oauth2ResourceServer().opaqueToken().");
				Assert.state(this.jwt == null || this.opaqueToken == null,
						"Spring Security only supports JWTs or Opaque Tokens, not both at the " + "same time.");
			}
			else {
				Assert.state(this.jwt == null && this.opaqueToken == null,
						"If an authenticationManagerResolver() is configured, then it takes "
								+ "precedence over any jwt() or opaqueToken() configuration.");
			}
		}

		private void registerDefaultAccessDeniedHandler(ServerHttpSecurity http) {
			if (http.exceptionHandling != null) {
				http.defaultAccessDeniedHandlers
					.add(new ServerWebExchangeDelegatingServerAccessDeniedHandler.DelegateEntry(
							this.authenticationConverterServerWebExchangeMatcher,
							OAuth2ResourceServerSpec.this.accessDeniedHandler));
			}
		}

		private void registerDefaultAuthenticationEntryPoint(ServerHttpSecurity http) {
			if (http.exceptionHandling != null) {
				http.defaultEntryPoints.add(new DelegateEntry(this.authenticationConverterServerWebExchangeMatcher,
						OAuth2ResourceServerSpec.this.entryPoint));
			}
		}

		private void registerDefaultCsrfOverride(ServerHttpSecurity http) {
			if (http.csrf != null && !http.csrf.specifiedRequireCsrfProtectionMatcher) {
				AndServerWebExchangeMatcher matcher = new AndServerWebExchangeMatcher(
						CsrfWebFilter.DEFAULT_CSRF_MATCHER,
						new NegatedServerWebExchangeMatcher(this.authenticationConverterServerWebExchangeMatcher));
				http.csrf((csrf) -> csrf.requireCsrfProtectionMatcher(matcher));
			}
		}

		private ServerAuthenticationFailureHandler authenticationFailureHandler() {
			if (this.authenticationFailureHandler != null) {
				return this.authenticationFailureHandler;
			}
			return new ServerAuthenticationEntryPointFailureHandler(this.entryPoint);
		}

		/**
		 * Configures JWT Resource Server Support
		 */
		public class JwtSpec {

			private ReactiveAuthenticationManager authenticationManager;

			private ReactiveJwtDecoder jwtDecoder;

			private Converter<Jwt, ? extends Mono<? extends AbstractAuthenticationToken>> jwtAuthenticationConverter;

			/**
			 * Configures the {@link ReactiveAuthenticationManager} to use
			 * @param authenticationManager the authentication manager to use
			 * @return the {@code JwtSpec} for additional configuration
			 */
			public JwtSpec authenticationManager(ReactiveAuthenticationManager authenticationManager) {
				Assert.notNull(authenticationManager, "authenticationManager cannot be null");
				this.authenticationManager = authenticationManager;
				return this;
			}

			/**
			 * Configures the {@link Converter} to use for converting a {@link Jwt} into
			 * an {@link AbstractAuthenticationToken}.
			 * @param jwtAuthenticationConverter the converter to use
			 * @return the {@code JwtSpec} for additional configuration
			 * @since 5.1.1
			 */
			public JwtSpec jwtAuthenticationConverter(
					Converter<Jwt, ? extends Mono<? extends AbstractAuthenticationToken>> jwtAuthenticationConverter) {
				Assert.notNull(jwtAuthenticationConverter, "jwtAuthenticationConverter cannot be null");
				this.jwtAuthenticationConverter = jwtAuthenticationConverter;
				return this;
			}

			/**
			 * Configures the {@link ReactiveJwtDecoder} to use
			 * @param jwtDecoder the decoder to use
			 * @return the {@code JwtSpec} for additional configuration
			 */
			public JwtSpec jwtDecoder(ReactiveJwtDecoder jwtDecoder) {
				this.jwtDecoder = jwtDecoder;
				return this;
			}

			/**
			 * Configures a {@link ReactiveJwtDecoder} that leverages the provided
			 * {@link RSAPublicKey}
			 * @param publicKey the public key to use.
			 * @return the {@code JwtSpec} for additional configuration
			 */
			public JwtSpec publicKey(RSAPublicKey publicKey) {
				this.jwtDecoder = new NimbusReactiveJwtDecoder(publicKey);
				return this;
			}

			/**
			 * Configures a {@link ReactiveJwtDecoder} using
			 * <a target="_blank" href="https://tools.ietf.org/html/rfc7517">JSON Web Key
			 * (JWK)</a> URL
			 * @param jwkSetUri the URL to use.
			 * @return the {@code JwtSpec} for additional configuration
			 */
			public JwtSpec jwkSetUri(String jwkSetUri) {
				this.jwtDecoder = new NimbusReactiveJwtDecoder(jwkSetUri);
				return this;
			}

			protected void configure(ServerHttpSecurity http) {
				ReactiveAuthenticationManager authenticationManager = getAuthenticationManager();
				AuthenticationWebFilter oauth2 = new AuthenticationWebFilter(authenticationManager);
				oauth2.setServerAuthenticationConverter(OAuth2ResourceServerSpec.this.bearerTokenConverter);
				oauth2.setAuthenticationFailureHandler(authenticationFailureHandler());
				http.addFilterAt(oauth2, SecurityWebFiltersOrder.AUTHENTICATION);
			}

			protected ReactiveJwtDecoder getJwtDecoder() {
				return (this.jwtDecoder != null) ? this.jwtDecoder : getBean(ReactiveJwtDecoder.class);
			}

			protected Converter<Jwt, ? extends Mono<? extends AbstractAuthenticationToken>> getJwtAuthenticationConverter() {
				if (this.jwtAuthenticationConverter != null) {
					return this.jwtAuthenticationConverter;
				}

				if (getBeanNamesForTypeOrEmpty(ReactiveJwtAuthenticationConverter.class).length > 0) {
					return getBean(ReactiveJwtAuthenticationConverter.class);
				}
				else {
					return new ReactiveJwtAuthenticationConverter();
				}
			}

			private ReactiveAuthenticationManager getAuthenticationManager() {
				if (this.authenticationManager != null) {
					return this.authenticationManager;
				}
				ReactiveJwtDecoder jwtDecoder = getJwtDecoder();
				Converter<Jwt, ? extends Mono<? extends AbstractAuthenticationToken>> jwtAuthenticationConverter = getJwtAuthenticationConverter();
				JwtReactiveAuthenticationManager authenticationManager = new JwtReactiveAuthenticationManager(
						jwtDecoder);
				authenticationManager.setJwtAuthenticationConverter(jwtAuthenticationConverter);
				return authenticationManager;
			}

		}

		/**
		 * Configures Opaque Token Resource Server support
		 *
		 * @author Josh Cummings
		 * @since 5.2
		 */
		public final class OpaqueTokenSpec {

			private String introspectionUri;

			private String clientId;

			private String clientSecret;

			private Supplier<ReactiveOpaqueTokenIntrospector> introspector;

			private ReactiveOpaqueTokenAuthenticationConverter authenticationConverter;

			private OpaqueTokenSpec() {
			}

			/**
			 * Configures the URI of the Introspection endpoint
			 * @param introspectionUri The URI of the Introspection endpoint
			 * @return the {@code OpaqueTokenSpec} for additional configuration
			 */
			public OpaqueTokenSpec introspectionUri(String introspectionUri) {
				Assert.hasText(introspectionUri, "introspectionUri cannot be empty");
				this.introspectionUri = introspectionUri;
				this.introspector = () -> SpringReactiveOpaqueTokenIntrospector
					.withIntrospectionUri(this.introspectionUri)
					.clientId(this.clientId)
					.clientSecret(this.clientSecret)
					.build();
				return this;
			}

			/**
			 * Configures the credentials for Introspection endpoint
			 * @param clientId The clientId part of the credentials
			 * @param clientSecret The clientSecret part of the credentials
			 * @return the {@code OpaqueTokenSpec} for additional configuration
			 */
			public OpaqueTokenSpec introspectionClientCredentials(String clientId, String clientSecret) {
				Assert.hasText(clientId, "clientId cannot be empty");
				Assert.notNull(clientSecret, "clientSecret cannot be null");
				this.clientId = clientId;
				this.clientSecret = clientSecret;
				this.introspector = () -> SpringReactiveOpaqueTokenIntrospector
					.withIntrospectionUri(this.introspectionUri)
					.clientId(this.clientId)
					.clientSecret(this.clientSecret)
					.build();
				return this;
			}

			public OpaqueTokenSpec introspector(ReactiveOpaqueTokenIntrospector introspector) {
				Assert.notNull(introspector, "introspector cannot be null");
				this.introspector = () -> introspector;
				return this;
			}

			public OpaqueTokenSpec authenticationConverter(
					ReactiveOpaqueTokenAuthenticationConverter authenticationConverter) {
				Assert.notNull(authenticationConverter, "authenticationConverter cannot be null");
				this.authenticationConverter = authenticationConverter;
				return this;
			}

			protected ReactiveAuthenticationManager getAuthenticationManager() {
				OpaqueTokenReactiveAuthenticationManager authenticationManager = new OpaqueTokenReactiveAuthenticationManager(
						getIntrospector());
				ReactiveOpaqueTokenAuthenticationConverter authenticationConverter = getAuthenticationConverter();
				if (authenticationConverter != null) {
					authenticationManager.setAuthenticationConverter(authenticationConverter);
				}
				return authenticationManager;
			}

			protected ReactiveOpaqueTokenIntrospector getIntrospector() {
				if (this.introspector != null) {
					return this.introspector.get();
				}
				return getBean(ReactiveOpaqueTokenIntrospector.class);
			}

			protected ReactiveOpaqueTokenAuthenticationConverter getAuthenticationConverter() {
				if (this.authenticationConverter != null) {
					return this.authenticationConverter;
				}
				return getBeanOrNull(ReactiveOpaqueTokenAuthenticationConverter.class);
			}

			protected void configure(ServerHttpSecurity http) {
				ReactiveAuthenticationManager authenticationManager = getAuthenticationManager();
				AuthenticationWebFilter oauth2 = new AuthenticationWebFilter(authenticationManager);
				oauth2.setServerAuthenticationConverter(OAuth2ResourceServerSpec.this.bearerTokenConverter);
				oauth2.setAuthenticationFailureHandler(authenticationFailureHandler());
				http.addFilterAt(oauth2, SecurityWebFiltersOrder.AUTHENTICATION);
			}

		}

	}

	/**
	 * Configures OIDC 1.0 Logout support
	 *
	 * @author Josh Cummings
	 * @since 6.2
	 */
	public final class OidcLogoutSpec {

		private ReactiveClientRegistrationRepository clientRegistrationRepository;

		private ReactiveOidcSessionRegistry sessionRegistry;

		private BackChannelLogoutConfigurer backChannel;

		/**
		 * Configures the {@link ReactiveClientRegistrationRepository}. Default is to look
		 * the value up as a Bean.
		 * @param clientRegistrationRepository the repository to use
		 * @return the {@link OidcLogoutSpec} to customize
		 */
		public OidcLogoutSpec clientRegistrationRepository(
				ReactiveClientRegistrationRepository clientRegistrationRepository) {
			Assert.notNull(clientRegistrationRepository, "clientRegistrationRepository cannot be null");
			this.clientRegistrationRepository = clientRegistrationRepository;
			return this;
		}

		/**
		 * Configures the {@link ReactiveOidcSessionRegistry}. Default is to use the value
		 * from {@link OAuth2LoginSpec#oidcSessionRegistry}, then look the value up as a
		 * Bean, or else use an {@link InMemoryReactiveOidcSessionRegistry}.
		 * @param sessionRegistry the registry to use
		 * @return the {@link OidcLogoutSpec} to customize
		 */
		public OidcLogoutSpec oidcSessionRegistry(ReactiveOidcSessionRegistry sessionRegistry) {
			Assert.notNull(sessionRegistry, "sessionRegistry cannot be null");
			this.sessionRegistry = sessionRegistry;
			return this;
		}

		/**
		 * Configure OIDC Back-Channel Logout using the provided {@link Consumer}
		 * @return the {@link OidcLogoutSpec} for further configuration
		 */
		public OidcLogoutSpec backChannel(Customizer<BackChannelLogoutConfigurer> backChannelLogoutConfigurer) {
			if (this.backChannel == null) {
				this.backChannel = new OidcLogoutSpec.BackChannelLogoutConfigurer();
			}
			backChannelLogoutConfigurer.customize(this.backChannel);
			return this;
		}

		void configure(ServerHttpSecurity http) {
			if (this.backChannel != null) {
				this.backChannel.configure(http);
			}
		}

		private ReactiveClientRegistrationRepository getClientRegistrationRepository() {
			if (this.clientRegistrationRepository == null) {
				this.clientRegistrationRepository = getBeanOrNull(ReactiveClientRegistrationRepository.class);
			}
			return this.clientRegistrationRepository;
		}

		private ReactiveOidcSessionRegistry getSessionRegistry() {
			if (this.sessionRegistry == null && ServerHttpSecurity.this.oauth2Login == null) {
				return getBeanOrDefault(ReactiveOidcSessionRegistry.class, new InMemoryReactiveOidcSessionRegistry());
			}
			if (this.sessionRegistry == null) {
				return ServerHttpSecurity.this.oauth2Login.oidcSessionRegistry;
			}
			return this.sessionRegistry;
		}

		/**
		 * A configurer for configuring OIDC Back-Channel Logout
		 */
		public final class BackChannelLogoutConfigurer {

			private ServerAuthenticationConverter authenticationConverter;

			private final ReactiveAuthenticationManager authenticationManager = new OidcBackChannelLogoutReactiveAuthenticationManager();

			private Supplier<ServerLogoutHandler> logoutHandler = this::logoutHandler;

			private ServerAuthenticationConverter authenticationConverter() {
				if (this.authenticationConverter == null) {
					this.authenticationConverter = new OidcLogoutServerAuthenticationConverter(
							OidcLogoutSpec.this.getClientRegistrationRepository());
				}
				return this.authenticationConverter;
			}

			private ReactiveAuthenticationManager authenticationManager() {
				return this.authenticationManager;
			}

			private ServerLogoutHandler logoutHandler() {
				OidcBackChannelServerLogoutHandler logoutHandler = getBeanOrNull(
						OidcBackChannelServerLogoutHandler.class);
				if (logoutHandler != null) {
					return logoutHandler;
				}
				logoutHandler = new OidcBackChannelServerLogoutHandler(OidcLogoutSpec.this.getSessionRegistry());
				return logoutHandler;
			}

			/**
			 * Use this endpoint when invoking a back-channel logout.
			 *
			 * <p>
			 * The resulting {@link LogoutHandler} will {@code POST} the session cookie
			 * and CSRF token to this endpoint to invalidate the corresponding end-user
			 * session.
			 *
			 * <p>
			 * Supports URI templates like {@code {baseUrl}}, {@code {baseScheme}}, and
			 * {@code {basePort}}.
			 *
			 * <p>
			 * By default, the URI is set to
			 * {@code {baseUrl}/logout/connect/back-channel/{registrationId}}, meaning
			 * that the scheme and port of the original back-channel request is preserved,
			 * while the host and endpoint are changed.
			 *
			 * <p>
			 * If you are using Spring Security for the logout endpoint, the path part of
			 * this URI should match the value configured there.
			 *
			 * <p>
			 * Otherwise, this is handy in the event that your server configuration means
			 * that the scheme, server name, or port in the {@code Host} header are
			 * different from how you would address the same server internally.
			 * @param logoutUri the URI to request logout on the back-channel
			 * @return the {@link BackChannelLogoutConfigurer} for further customizations
			 * @since 6.2.4
			 */
			public BackChannelLogoutConfigurer logoutUri(String logoutUri) {
				this.logoutHandler = () -> {
					OidcBackChannelServerLogoutHandler logoutHandler = new OidcBackChannelServerLogoutHandler(
							OidcLogoutSpec.this.getSessionRegistry());
					logoutHandler.setLogoutUri(logoutUri);
					return logoutHandler;
				};
				return this;
			}

			/**
			 * Configure what and how per-session logout will be performed.
			 *
			 * <p>
			 * This overrides any value given to {@link #logoutUri(String)}
			 *
			 * <p>
			 * By default, the resulting {@link LogoutHandler} will {@code POST} the
			 * session cookie and OIDC logout token back to the original back-channel
			 * logout endpoint.
			 *
			 * <p>
			 * Using this method changes the underlying default that {@code POST}s the
			 * session cookie and CSRF token to your application's {@code /logout}
			 * endpoint. As such, it is recommended to call this instead of accepting the
			 * {@code /logout} default as this does not require any special CSRF
			 * configuration, even if you don't require other changes.
			 *
			 * <p>
			 * For example, configuring Back-Channel Logout in the following way:
			 *
			 * <pre>
			 * 	http
			 *     	.oidcLogout((oidc) -&gt; oidc
			 *     		.backChannel((backChannel) -&gt; backChannel
			 *     			.logoutHandler(new OidcBackChannelServerLogoutHandler())
			 *     		)
			 *     	);
			 * </pre>
			 *
			 * will make so that the per-session logout invocation no longer requires
			 * special CSRF configurations.
			 *
			 * <p>
			 * The default URI is
			 * {@code {baseUrl}/logout/connect/back-channel/{registrationId}}, which is
			 * simply an internal version of the same endpoint exposed to your
			 * Back-Channel services. You can use
			 * {@link OidcBackChannelServerLogoutHandler#setLogoutUri(String)} to alter
			 * the scheme, server name, or port in the {@code Host} header to accommodate
			 * how your application would address itself internally.
			 *
			 * <p>
			 * For example, if the way your application would internally call itself is on
			 * a different scheme and port than incoming traffic, you can configure the
			 * endpoint in the following way:
			 *
			 * <pre>
			 * 	http
			 * 		.oidcLogout((oidc) -&gt; oidc
			 * 			.backChannel((backChannel) -&gt; backChannel
			 * 				.logoutUri("http://localhost:9000/logout/connect/back-channel/{registrationId}")
			 * 			)
			 * 		);
			 * </pre>
			 *
			 * <p>
			 * You can also publish it as a {@code @Bean} as follows:
			 *
			 * <pre>
			 *	&commat;Bean
			 *	OidcBackChannelServerLogoutHandler oidcLogoutHandler() {
			 *  	OidcBackChannelServerLogoutHandler logoutHandler = new OidcBackChannelServerLogoutHandler();
			 *  	logoutHandler.setLogoutUri("http://localhost:9000/logout/connect/back-channel/{registrationId}");
			 *  	return logoutHandler;
			 *	}
			 * </pre>
			 *
			 * to have the same effect.
			 * @param logoutHandler the {@link ServerLogoutHandler} to use each individual
			 * session
			 * @return {@link BackChannelLogoutConfigurer} for further customizations
			 * @since 6.4
			 */
			public BackChannelLogoutConfigurer logoutHandler(ServerLogoutHandler logoutHandler) {
				this.logoutHandler = () -> logoutHandler;
				return this;
			}

			void configure(ServerHttpSecurity http) {
				ServerLogoutHandler oidcLogout = this.logoutHandler.get();
				ServerLogoutHandler sessionLogout = new SecurityContextServerLogoutHandler();
				LogoutSpec logout = ServerHttpSecurity.this.logout;
				if (logout != null) {
					sessionLogout = new DelegatingServerLogoutHandler(logout.logoutHandlers);
				}
				OidcBackChannelLogoutWebFilter filter = new OidcBackChannelLogoutWebFilter(authenticationConverter(),
						authenticationManager(), new EitherLogoutHandler(oidcLogout, sessionLogout));
				http.addFilterBefore(filter, SecurityWebFiltersOrder.CSRF);
			}

			private static final class EitherLogoutHandler implements ServerLogoutHandler {

				private final ServerLogoutHandler left;

				private final ServerLogoutHandler right;

				EitherLogoutHandler(ServerLogoutHandler left, ServerLogoutHandler right) {
					this.left = left;
					this.right = right;
				}

				@Override
				public Mono<Void> logout(WebFilterExchange exchange, Authentication authentication) {
					return exchange.getExchange().getFormData().flatMap((data) -> {
						if (data.getFirst("_spring_security_internal_logout") == null) {
							return this.left.logout(exchange, authentication);
						}
						else {
							return this.right.logout(exchange, authentication);
						}
					});
				}

			}

		}

	}

	/**
	 * Configures anonymous authentication
	 *
	 * @since 5.2.0
	 */
	public final class AnonymousSpec {

		private String key;

		private AnonymousAuthenticationWebFilter authenticationFilter;

		private Object principal = "anonymousUser";

		private List<GrantedAuthority> authorities = AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS");

		/**
		 * Sets the key to identify tokens created for anonymous authentication. Default
		 * is a secure randomly generated key.
		 * @param key the key to identify tokens created for anonymous authentication.
		 * Default is a secure randomly generated key.
		 * @return the {@link AnonymousSpec} for further customization of anonymous
		 * authentication
		 */
		public AnonymousSpec key(String key) {
			this.key = key;
			return this;
		}

		/**
		 * Sets the principal for {@link Authentication} objects of anonymous users
		 * @param principal used for the {@link Authentication} object of anonymous users
		 * @return the {@link AnonymousSpec} for further customization of anonymous
		 * authentication
		 */
		public AnonymousSpec principal(Object principal) {
			this.principal = principal;
			return this;
		}

		/**
		 * Sets the
		 * {@link org.springframework.security.core.Authentication#getAuthorities()} for
		 * anonymous users
		 * @param authorities Sets the
		 * {@link org.springframework.security.core.Authentication#getAuthorities()} for
		 * anonymous users
		 * @return the {@link AnonymousSpec} for further customization of anonymous
		 * authentication
		 */
		public AnonymousSpec authorities(List<GrantedAuthority> authorities) {
			this.authorities = authorities;
			return this;
		}

		/**
		 * Sets the
		 * {@link org.springframework.security.core.Authentication#getAuthorities()} for
		 * anonymous users
		 * @param authorities Sets the
		 * {@link org.springframework.security.core.Authentication#getAuthorities()} for
		 * anonymous users (i.e. "ROLE_ANONYMOUS")
		 * @return the {@link AnonymousSpec} for further customization of anonymous
		 * authentication
		 */
		public AnonymousSpec authorities(String... authorities) {
			return authorities(AuthorityUtils.createAuthorityList(authorities));
		}

		/**
		 * Sets the {@link AnonymousAuthenticationWebFilter} used to populate an anonymous
		 * user. If this is set, no attributes on the {@link AnonymousSpec} will be set on
		 * the {@link AnonymousAuthenticationWebFilter}.
		 * @param authenticationFilter the {@link AnonymousAuthenticationWebFilter} used
		 * to populate an anonymous user.
		 * @return the {@link AnonymousSpec} for further customization of anonymous
		 * authentication
		 */
		public AnonymousSpec authenticationFilter(AnonymousAuthenticationWebFilter authenticationFilter) {
			this.authenticationFilter = authenticationFilter;
			return this;
		}

		/**
		 * Disables anonymous authentication.
		 * @return the {@link ServerHttpSecurity} to continue configuring
		 */
		public ServerHttpSecurity disable() {
			ServerHttpSecurity.this.anonymous = null;
			return ServerHttpSecurity.this;
		}

		protected void configure(ServerHttpSecurity http) {
			if (this.authenticationFilter == null) {
				this.authenticationFilter = new AnonymousAuthenticationWebFilter(getKey(), this.principal,
						this.authorities);
			}
			http.addFilterAt(this.authenticationFilter, SecurityWebFiltersOrder.ANONYMOUS_AUTHENTICATION);
		}

		private String getKey() {
			if (this.key == null) {
				this.key = UUID.randomUUID().toString();
			}
			return this.key;
		}

		private AnonymousSpec() {
		}

	}

	/**
	 * Configures One-Time Token Login Support
	 *
	 * @author Max Batischev
	 * @since 6.4
	 * @see #oneTimeTokenLogin(Customizer)
	 */
	public final class OneTimeTokenLoginSpec {

		private ReactiveAuthenticationManager authenticationManager;

		private ReactiveOneTimeTokenService tokenService;

		private ServerAuthenticationConverter authenticationConverter = new ServerOneTimeTokenAuthenticationConverter();

		private ServerAuthenticationFailureHandler authenticationFailureHandler;

		private final RedirectServerAuthenticationSuccessHandler defaultSuccessHandler = new RedirectServerAuthenticationSuccessHandler(
				"/");

		private final List<ServerAuthenticationSuccessHandler> defaultSuccessHandlers = new ArrayList<>(
				List.of(this.defaultSuccessHandler));

		private final List<ServerAuthenticationSuccessHandler> authenticationSuccessHandlers = new ArrayList<>();

		private ServerOneTimeTokenGenerationSuccessHandler tokenGenerationSuccessHandler;

		private ServerSecurityContextRepository securityContextRepository;

		private ServerGenerateOneTimeTokenRequestResolver requestResolver;

		private String loginProcessingUrl = "/login/ott";

		private String defaultSubmitPageUrl = "/login/ott";

		private String tokenGeneratingUrl = "/ott/generate";

		private boolean submitPageEnabled = true;

		private String loginPage;

		protected void configure(ServerHttpSecurity http) {
			configureSubmitPage(http);
			configureOttGenerateFilter(http);
			configureOttAuthenticationFilter(http);
			configureDefaultEntryPoint(http);
		}

		private void configureOttAuthenticationFilter(ServerHttpSecurity http) {
			AuthenticationWebFilter ottWebFilter = new AuthenticationWebFilter(getAuthenticationManager());
			ottWebFilter.setServerAuthenticationConverter(this.authenticationConverter);
			ottWebFilter.setAuthenticationFailureHandler(getAuthenticationFailureHandler());
			ottWebFilter.setAuthenticationSuccessHandler(getAuthenticationSuccessHandler());
			ottWebFilter.setRequiresAuthenticationMatcher(
					ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, this.loginProcessingUrl));
			ottWebFilter.setSecurityContextRepository(this.securityContextRepository);
			http.addFilterAt(ottWebFilter, SecurityWebFiltersOrder.AUTHENTICATION);
		}

		private void configureSubmitPage(ServerHttpSecurity http) {
			if (!this.submitPageEnabled) {
				return;
			}
			OneTimeTokenSubmitPageGeneratingWebFilter submitPage = new OneTimeTokenSubmitPageGeneratingWebFilter();
			submitPage.setLoginProcessingUrl(this.loginProcessingUrl);

			if (StringUtils.hasText(this.defaultSubmitPageUrl)) {
				submitPage.setRequestMatcher(
						ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, this.defaultSubmitPageUrl));
			}
			http.addFilterAt(submitPage, SecurityWebFiltersOrder.ONE_TIME_TOKEN_SUBMIT_PAGE_GENERATING);
		}

		private void configureOttGenerateFilter(ServerHttpSecurity http) {
			GenerateOneTimeTokenWebFilter generateFilter = new GenerateOneTimeTokenWebFilter(getTokenService(),
					getTokenGenerationSuccessHandler());
			generateFilter
				.setRequestMatcher(ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, this.tokenGeneratingUrl));
			generateFilter.setGenerateRequestResolver(getRequestResolver());
			http.addFilterAt(generateFilter, SecurityWebFiltersOrder.ONE_TIME_TOKEN);
		}

		private void configureDefaultEntryPoint(ServerHttpSecurity http) {
			MediaTypeServerWebExchangeMatcher htmlMatcher = new MediaTypeServerWebExchangeMatcher(
					MediaType.APPLICATION_XHTML_XML, new MediaType("image", "*"), MediaType.TEXT_HTML,
					MediaType.TEXT_PLAIN);
			htmlMatcher.setIgnoredMediaTypes(Collections.singleton(MediaType.ALL));
			ServerWebExchangeMatcher xhrMatcher = (exchange) -> {
				if (exchange.getRequest().getHeaders().getOrEmpty("X-Requested-With").contains("XMLHttpRequest")) {
					return ServerWebExchangeMatcher.MatchResult.match();
				}
				return ServerWebExchangeMatcher.MatchResult.notMatch();
			};
			ServerWebExchangeMatcher notXhrMatcher = new NegatedServerWebExchangeMatcher(xhrMatcher);
			ServerWebExchangeMatcher defaultEntryPointMatcher = new AndServerWebExchangeMatcher(notXhrMatcher,
					htmlMatcher);
			String loginPage = "/login";
			if (this.loginPage != null) {
				loginPage = this.loginPage;
			}
			RedirectServerAuthenticationEntryPoint defaultEntryPoint = new RedirectServerAuthenticationEntryPoint(
					loginPage);
			defaultEntryPoint.setRequestCache(http.requestCache.requestCache);
			http.defaultEntryPoints.add(new DelegateEntry(defaultEntryPointMatcher, defaultEntryPoint));

		}

		/**
		 * Allows customizing the list of {@link ServerAuthenticationSuccessHandler}. The
		 * default list contains a {@link RedirectServerAuthenticationSuccessHandler} that
		 * redirects to "/".
		 * @param handlersConsumer the handlers consumer
		 * @return the {@link OneTimeTokenLoginSpec} to continue configuring
		 */
		public OneTimeTokenLoginSpec authenticationSuccessHandler(
				Consumer<List<ServerAuthenticationSuccessHandler>> handlersConsumer) {
			Assert.notNull(handlersConsumer, "handlersConsumer cannot be null");
			handlersConsumer.accept(this.authenticationSuccessHandlers);
			return this;
		}

		/**
		 * Specifies the {@link ServerAuthenticationSuccessHandler}
		 * @param authenticationSuccessHandler the
		 * {@link ServerAuthenticationSuccessHandler}.
		 */
		public OneTimeTokenLoginSpec authenticationSuccessHandler(
				ServerAuthenticationSuccessHandler authenticationSuccessHandler) {
			Assert.notNull(authenticationSuccessHandler, "authenticationSuccessHandler cannot be null");
			authenticationSuccessHandler((handlers) -> {
				handlers.clear();
				handlers.add(authenticationSuccessHandler);
			});
			return this;
		}

		private ServerAuthenticationSuccessHandler getAuthenticationSuccessHandler() {
			if (this.authenticationSuccessHandlers.isEmpty()) {
				return new DelegatingServerAuthenticationSuccessHandler(this.defaultSuccessHandlers);
			}
			return new DelegatingServerAuthenticationSuccessHandler(this.authenticationSuccessHandlers);
		}

		/**
		 * Specifies the {@link ServerAuthenticationFailureHandler} to use when
		 * authentication fails. The default is redirecting to "/login?error" using
		 * {@link RedirectServerAuthenticationFailureHandler}
		 * @param authenticationFailureHandler the
		 * {@link ServerAuthenticationFailureHandler} to use when authentication fails.
		 */
		public OneTimeTokenLoginSpec authenticationFailureHandler(
				ServerAuthenticationFailureHandler authenticationFailureHandler) {
			Assert.notNull(authenticationFailureHandler, "authenticationFailureHandler cannot be null");
			this.authenticationFailureHandler = authenticationFailureHandler;
			return this;
		}

		ServerAuthenticationFailureHandler getAuthenticationFailureHandler() {
			if (this.authenticationFailureHandler == null) {
				this.authenticationFailureHandler = new RedirectServerAuthenticationFailureHandler("/login?error");
			}
			return this.authenticationFailureHandler;
		}

		/**
		 * Specifies {@link ReactiveAuthenticationManager} for one time tokens. Default
		 * implementation is {@link OneTimeTokenReactiveAuthenticationManager}
		 * @param authenticationManager
		 */
		public OneTimeTokenLoginSpec authenticationManager(ReactiveAuthenticationManager authenticationManager) {
			Assert.notNull(authenticationManager, "authenticationManager cannot be null");
			this.authenticationManager = authenticationManager;
			return this;
		}

		ReactiveAuthenticationManager getAuthenticationManager() {
			if (this.authenticationManager == null) {
				ReactiveUserDetailsService userDetailsService = getBean(ReactiveUserDetailsService.class);
				return new OneTimeTokenReactiveAuthenticationManager(getTokenService(), userDetailsService);
			}
			return this.authenticationManager;
		}

		/**
		 * Configures the {@link ReactiveOneTimeTokenService} used to generate and consume
		 * {@link OneTimeToken}
		 * @param oneTimeTokenService
		 */
		public OneTimeTokenLoginSpec tokenService(ReactiveOneTimeTokenService oneTimeTokenService) {
			Assert.notNull(oneTimeTokenService, "oneTimeTokenService cannot be null");
			this.tokenService = oneTimeTokenService;
			return this;
		}

		ReactiveOneTimeTokenService getTokenService() {
			if (this.tokenService != null) {
				return this.tokenService;
			}
			ReactiveOneTimeTokenService oneTimeTokenService = getBeanOrNull(ReactiveOneTimeTokenService.class);
			if (oneTimeTokenService != null) {
				return oneTimeTokenService;
			}
			this.tokenService = new InMemoryReactiveOneTimeTokenService();
			return this.tokenService;
		}

		/**
		 * Use this {@link ServerAuthenticationConverter} when converting incoming
		 * requests to an {@link Authentication}. By default, the
		 * {@link ServerOneTimeTokenAuthenticationConverter} is used.
		 * @param authenticationConverter the {@link ServerAuthenticationConverter} to use
		 */
		public OneTimeTokenLoginSpec authenticationConverter(ServerAuthenticationConverter authenticationConverter) {
			Assert.notNull(authenticationConverter, "authenticationConverter cannot be null");
			this.authenticationConverter = authenticationConverter;
			return this;
		}

		/**
		 * Use this {@link ServerGenerateOneTimeTokenRequestResolver} when resolving
		 * {@link GenerateOneTimeTokenRequest} from {@link ServerWebExchange}. By default,
		 * the {@link DefaultServerGenerateOneTimeTokenRequestResolver} is used.
		 * @param requestResolver the
		 * {@link DefaultServerGenerateOneTimeTokenRequestResolver} to use
		 * @since 6.5
		 */
		public OneTimeTokenLoginSpec generateRequestResolver(
				ServerGenerateOneTimeTokenRequestResolver requestResolver) {
			Assert.notNull(requestResolver, "generateRequestResolver cannot be null");
			this.requestResolver = requestResolver;
			return this;
		}

		private ServerGenerateOneTimeTokenRequestResolver getRequestResolver() {
			if (this.requestResolver != null) {
				return this.requestResolver;
			}
			ServerGenerateOneTimeTokenRequestResolver bean = getBeanOrNull(
					ServerGenerateOneTimeTokenRequestResolver.class);
			this.requestResolver = Objects.requireNonNullElseGet(bean,
					DefaultServerGenerateOneTimeTokenRequestResolver::new);
			return this.requestResolver;
		}

		/**
		 * Specifies the URL to process the login request, defaults to {@code /login/ott}.
		 * Only POST requests are processed, for that reason make sure that you pass a
		 * valid CSRF token if CSRF protection is enabled.
		 * @param loginProcessingUrl
		 */
		public OneTimeTokenLoginSpec loginProcessingUrl(String loginProcessingUrl) {
			Assert.hasText(loginProcessingUrl, "loginProcessingUrl cannot be null or empty");
			this.loginProcessingUrl = loginProcessingUrl;
			return this;
		}

		/**
		 * Configures whether the default one-time token submit page should be shown. This
		 * will prevent the {@link OneTimeTokenSubmitPageGeneratingWebFilter} to be
		 * configured.
		 * @param show
		 */
		public OneTimeTokenLoginSpec showDefaultSubmitPage(boolean show) {
			this.submitPageEnabled = show;
			return this;
		}

		/**
		 * Sets the URL that the default submit page will be generated. Defaults to
		 * {@code /login/ott}. If you don't want to generate the default submit page you
		 * should use {@link #showDefaultSubmitPage(boolean)}. Note that this method
		 * always invoke {@link #showDefaultSubmitPage(boolean)} passing {@code true}.
		 * @param submitPageUrl
		 */
		public OneTimeTokenLoginSpec defaultSubmitPageUrl(String submitPageUrl) {
			Assert.hasText(submitPageUrl, "submitPageUrl cannot be null or empty");
			this.defaultSubmitPageUrl = submitPageUrl;
			showDefaultSubmitPage(true);
			return this;
		}

		/**
		 * Specifies strategy to be used to handle generated one-time tokens.
		 * @param oneTimeTokenGenerationSuccessHandler
		 */
		public OneTimeTokenLoginSpec tokenGenerationSuccessHandler(
				ServerOneTimeTokenGenerationSuccessHandler oneTimeTokenGenerationSuccessHandler) {
			Assert.notNull(oneTimeTokenGenerationSuccessHandler, "oneTimeTokenGenerationSuccessHandler cannot be null");
			this.tokenGenerationSuccessHandler = oneTimeTokenGenerationSuccessHandler;
			return this;
		}

		/**
		 * Specifies the URL that a One-Time Token generate request will be processed.
		 * Defaults to {@code /ott/generate}.
		 * @param tokenGeneratingUrl
		 */
		public OneTimeTokenLoginSpec tokenGeneratingUrl(String tokenGeneratingUrl) {
			Assert.hasText(tokenGeneratingUrl, "tokenGeneratingUrl cannot be null or empty");
			this.tokenGeneratingUrl = tokenGeneratingUrl;
			return this;
		}

		/**
		 * The {@link ServerSecurityContextRepository} used to save the
		 * {@code Authentication}. Defaults to
		 * {@link WebSessionServerSecurityContextRepository}. For the
		 * {@code SecurityContext} to be loaded on subsequent requests the
		 * {@link ReactorContextWebFilter} must be configured to be able to load the value
		 * (they are not implicitly linked).
		 * @param securityContextRepository the repository to use
		 * @return the {@link OneTimeTokenLoginSpec} to continue configuring
		 */
		public OneTimeTokenLoginSpec securityContextRepository(
				ServerSecurityContextRepository securityContextRepository) {
			this.securityContextRepository = securityContextRepository;
			return this;
		}

		private ServerOneTimeTokenGenerationSuccessHandler getTokenGenerationSuccessHandler() {
			if (this.tokenGenerationSuccessHandler == null) {
				this.tokenGenerationSuccessHandler = getBeanOrNull(ServerOneTimeTokenGenerationSuccessHandler.class);
			}
			if (this.tokenGenerationSuccessHandler == null) {
				throw new IllegalStateException("""
						A ServerOneTimeTokenGenerationSuccessHandler is required to enable oneTimeTokenLogin().
						Please provide it as a bean or pass it to the oneTimeTokenLogin() DSL.
						""");
			}
			return this.tokenGenerationSuccessHandler;
		}

		/**
		 * Specifies the URL to send users to if login is required. A default login page
		 * will be generated when this attribute is not specified.
		 * @param loginPage the URL to send users to if login is required
		 * @return the {@link OAuth2LoginSpec} for further configuration
		 * @since 6.5
		 */
		public OneTimeTokenLoginSpec loginPage(String loginPage) {
			Assert.hasText(loginPage, "loginPage cannot be empty");
			this.loginPage = loginPage;
			return this;
		}

	}

}
