/*
 * Copyright 2004-present the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.security.config.annotation.web.configurers;

import jakarta.servlet.http.HttpServletRequest;

import org.springframework.context.ApplicationContext;
import org.springframework.security.authentication.AuthenticationDetailsSource;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.authority.FactorGrantedAuthority;
import org.springframework.security.core.userdetails.AuthenticationUserDetailsService;
import org.springframework.security.core.userdetails.UserDetailsByNameServiceWrapper;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.authentication.Http403ForbiddenEntryPoint;
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider;
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken;
import org.springframework.security.web.authentication.preauth.PreAuthenticatedGrantedAuthoritiesWebAuthenticationDetails;
import org.springframework.security.web.authentication.preauth.x509.SubjectDnX509PrincipalExtractor;
import org.springframework.security.web.authentication.preauth.x509.X509AuthenticationFilter;
import org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor;
import org.springframework.security.web.context.RequestAttributeSecurityContextRepository;

/**
 * Adds X509 based pre authentication to an application. Since validating the certificate
 * happens when the client connects, the requesting and validation of the client
 * certificate should be performed by the container. Spring Security will then use the
 * certificate to look up the {@link Authentication} for the user.
 *
 * <h2>Security Filters</h2>
 * <p>
 * The following Filters are populated
 *
 * <ul>
 * <li>{@link X509AuthenticationFilter}</li>
 * </ul>
 *
 * <h2>Shared Objects Created</h2>
 * <p>
 * The following shared objects are created
 *
 * <ul>
 * <li>{@link AuthenticationEntryPoint} is populated with an
 * {@link Http403ForbiddenEntryPoint}</li>
 * <li>A {@link PreAuthenticatedAuthenticationProvider} is populated into
 * {@link HttpSecurity#authenticationProvider(org.springframework.security.authentication.AuthenticationProvider)}
 * </li>
 * </ul>
 *
 * <h2>Shared Objects Used</h2>
 * <p>
 * The following shared objects are used:
 *
 * <ul>
 * <li>A {@link UserDetailsService} shared object is used if no
 * {@link AuthenticationUserDetailsService} is specified</li>
 * </ul>
 *
 * @author Rob Winch
 * @author Ngoc Nhan
 * @since 3.2
 */
public final class X509Configurer<H extends HttpSecurityBuilder<H>>
		extends AbstractHttpConfigurer<X509Configurer<H>, H> {

	private X509AuthenticationFilter x509AuthenticationFilter;

	private X509PrincipalExtractor x509PrincipalExtractor;

	private AuthenticationUserDetailsService<PreAuthenticatedAuthenticationToken> authenticationUserDetailsService;

	private AuthenticationDetailsSource<HttpServletRequest, PreAuthenticatedGrantedAuthoritiesWebAuthenticationDetails> authenticationDetailsSource;

	/**
	 * Creates a new instance
	 *
	 * @see HttpSecurity#x509(Customizer)
	 */
	public X509Configurer() {
	}

	/**
	 * Allows specifying the entire {@link X509AuthenticationFilter}. If this is
	 * specified, the properties on {@link X509Configurer} will not be populated on the
	 * {@link X509AuthenticationFilter}.
	 * @param x509AuthenticationFilter the {@link X509AuthenticationFilter} to use
	 * @return the {@link X509Configurer} for further customizations
	 */
	public X509Configurer<H> x509AuthenticationFilter(X509AuthenticationFilter x509AuthenticationFilter) {
		this.x509AuthenticationFilter = x509AuthenticationFilter;
		return this;
	}

	/**
	 * Specifies the {@link X509PrincipalExtractor}
	 * @param x509PrincipalExtractor the {@link X509PrincipalExtractor} to use
	 * @return the {@link X509Configurer} to use
	 */
	public X509Configurer<H> x509PrincipalExtractor(X509PrincipalExtractor x509PrincipalExtractor) {
		this.x509PrincipalExtractor = x509PrincipalExtractor;
		return this;
	}

	/**
	 * Specifies the {@link AuthenticationDetailsSource}
	 * @param authenticationDetailsSource the {@link AuthenticationDetailsSource} to use
	 * @return the {@link X509Configurer} to use
	 */
	public X509Configurer<H> authenticationDetailsSource(
			AuthenticationDetailsSource<HttpServletRequest, PreAuthenticatedGrantedAuthoritiesWebAuthenticationDetails> authenticationDetailsSource) {
		this.authenticationDetailsSource = authenticationDetailsSource;
		return this;
	}

	/**
	 * Shortcut for invoking
	 * {@link #authenticationUserDetailsService(AuthenticationUserDetailsService)} with a
	 * {@link UserDetailsByNameServiceWrapper}.
	 * @param userDetailsService the {@link UserDetailsService} to use
	 * @return the {@link X509Configurer} for further customizations
	 */
	public X509Configurer<H> userDetailsService(UserDetailsService userDetailsService) {
		UserDetailsByNameServiceWrapper<PreAuthenticatedAuthenticationToken> authenticationUserDetailsService = new UserDetailsByNameServiceWrapper<>();
		authenticationUserDetailsService.setUserDetailsService(userDetailsService);
		return authenticationUserDetailsService(authenticationUserDetailsService);
	}

	/**
	 * Specifies the {@link AuthenticationUserDetailsService} to use. If not specified,
	 * then the {@link UserDetailsService} bean will be used by default.
	 * @param authenticationUserDetailsService the
	 * {@link AuthenticationUserDetailsService} to use
	 * @return the {@link X509Configurer} for further customizations
	 */
	public X509Configurer<H> authenticationUserDetailsService(
			AuthenticationUserDetailsService<PreAuthenticatedAuthenticationToken> authenticationUserDetailsService) {
		this.authenticationUserDetailsService = authenticationUserDetailsService;
		return this;
	}

	/**
	 * Specifies the regex to extract the principal from the certificate. If not
	 * specified, the default expression from {@link SubjectDnX509PrincipalExtractor} is
	 * used.
	 * @param subjectPrincipalRegex the regex to extract the user principal from the
	 * certificate (i.e. "CN=(.*?)(?:,|$)").
	 * @return the {@link X509Configurer} for further customizations
	 * @deprecated Please use {{@link #x509PrincipalExtractor(X509PrincipalExtractor)}
	 * instead
	 */
	@Deprecated
	public X509Configurer<H> subjectPrincipalRegex(String subjectPrincipalRegex) {
		SubjectDnX509PrincipalExtractor principalExtractor = new SubjectDnX509PrincipalExtractor();
		principalExtractor.setSubjectDnRegex(subjectPrincipalRegex);
		this.x509PrincipalExtractor = principalExtractor;
		return this;
	}

	@Override
	public void init(H http) {
		PreAuthenticatedAuthenticationProvider authenticationProvider = new PreAuthenticatedAuthenticationProvider();
		authenticationProvider.setPreAuthenticatedUserDetailsService(getAuthenticationUserDetailsService(http));
		authenticationProvider.setGrantedAuthoritySupplier(
				() -> AuthorityUtils.createAuthorityList(FactorGrantedAuthority.X509_AUTHORITY));
		http.authenticationProvider(authenticationProvider)
			.setSharedObject(AuthenticationEntryPoint.class, new Http403ForbiddenEntryPoint());
		ExceptionHandlingConfigurer<H> exceptions = http.getConfigurer(ExceptionHandlingConfigurer.class);
		if (exceptions != null) {
			AuthenticationEntryPoint forbidden = new Http403ForbiddenEntryPoint();
			exceptions.defaultDeniedHandlerForMissingAuthority((ep) -> ep.defaultEntryPoint(forbidden),
					FactorGrantedAuthority.X509_AUTHORITY);
		}
	}

	@Override
	public void configure(H http) {
		X509AuthenticationFilter filter = getFilter(http.getSharedObject(AuthenticationManager.class), http);
		http.addFilter(filter);
	}

	private X509AuthenticationFilter getFilter(AuthenticationManager authenticationManager, H http) {
		if (this.x509AuthenticationFilter == null) {
			this.x509AuthenticationFilter = new X509AuthenticationFilter();
			this.x509AuthenticationFilter.setAuthenticationManager(authenticationManager);
			if (this.x509PrincipalExtractor != null) {
				this.x509AuthenticationFilter.setPrincipalExtractor(this.x509PrincipalExtractor);
			}
			if (this.authenticationDetailsSource != null) {
				this.x509AuthenticationFilter.setAuthenticationDetailsSource(this.authenticationDetailsSource);
			}
			this.x509AuthenticationFilter.setSecurityContextRepository(new RequestAttributeSecurityContextRepository());
			this.x509AuthenticationFilter.setSecurityContextHolderStrategy(getSecurityContextHolderStrategy());
			this.x509AuthenticationFilter = postProcess(this.x509AuthenticationFilter);
		}

		return this.x509AuthenticationFilter;
	}

	private AuthenticationUserDetailsService<PreAuthenticatedAuthenticationToken> getAuthenticationUserDetailsService(
			H http) {
		if (this.authenticationUserDetailsService == null) {
			userDetailsService(getSharedOrBean(http, UserDetailsService.class));
		}
		return this.authenticationUserDetailsService;
	}

	private <C> C getSharedOrBean(H http, Class<C> type) {
		C shared = http.getSharedObject(type);
		if (shared != null) {
			return shared;
		}
		ApplicationContext context = getBuilder().getSharedObject(ApplicationContext.class);
		if (context == null) {
			return null;
		}
		return context.getBeanProvider(type).getIfUnique();
	}

}
