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

import java.io.IOException;
import java.util.Base64;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.jspecify.annotations.Nullable;

import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.authentication.AuthenticationDetailsSource;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.context.SecurityContextHolderStrategy;
import org.springframework.security.kerberos.authentication.KerberosServiceAuthenticationProvider;
import org.springframework.security.kerberos.authentication.KerberosServiceRequestToken;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.security.web.authentication.session.NullAuthenticatedSessionStrategy;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
import org.springframework.security.web.context.RequestAttributeSecurityContextRepository;
import org.springframework.security.web.context.SecurityContextRepository;
import org.springframework.util.Assert;
import org.springframework.web.filter.OncePerRequestFilter;

/**
 * Parses the SPNEGO authentication Header, which was generated by the browser and creates
 * a {@link KerberosServiceRequestToken} out of it. It will then call the
 * {@link AuthenticationManager}.
 *
 * <p>
 * A typical Spring Security configuration might look like this:
 * </p>
 *
 * <pre>
 * &lt;beans xmlns=&quot;https://www.springframework.org/schema/beans&quot;
 * xmlns:xsi=&quot;https://www.w3.org/2001/XMLSchema-instance&quot; xmlns:sec=&quot;https://www.springframework.org/schema/security&quot;
 * xsi:schemaLocation=&quot;https://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans-2.0.xsd
 * 	https://www.springframework.org/schema/security https://www.springframework.org/schema/security/spring-security-3.0.xsd&quot;&gt;
 *
 * &lt;sec:http entry-point-ref=&quot;spnegoEntryPoint&quot;&gt;
 * 	&lt;sec:intercept-url pattern=&quot;/secure/**&quot; access=&quot;IS_AUTHENTICATED_FULLY&quot; /&gt;
 * 	&lt;sec:custom-filter ref=&quot;spnegoAuthenticationProcessingFilter&quot; position=&quot;BASIC_AUTH_FILTER&quot; /&gt;
 * &lt;/sec:http&gt;
 *
 * &lt;bean id=&quot;spnegoEntryPoint&quot; class=&quot;org.springframework.security.kerberos.web.authentication.SpnegoEntryPoint&quot; /&gt;
 *
 * &lt;bean id=&quot;spnegoAuthenticationProcessingFilter&quot;
 * 	class=&quot;org.springframework.security.kerberos.web.authentication.SpnegoAuthenticationProcessingFilter&quot;&gt;
 * 	&lt;property name=&quot;authenticationManager&quot; ref=&quot;authenticationManager&quot; /&gt;
 * &lt;/bean&gt;
 *
 * &lt;sec:authentication-manager alias=&quot;authenticationManager&quot;&gt;
 * 	&lt;sec:authentication-provider ref=&quot;kerberosServiceAuthenticationProvider&quot; /&gt;
 * &lt;/sec:authentication-manager&gt;
 *
 * &lt;bean id=&quot;kerberosServiceAuthenticationProvider&quot;
 * 	class=&quot;org.springframework.security.kerberos.authentication.KerberosServiceAuthenticationProvider&quot;&gt;
 * 	&lt;property name=&quot;ticketValidator&quot;&gt;
 * 		&lt;bean class=&quot;org.springframework.security.kerberos.authentication.sun.SunJaasKerberosTicketValidator&quot;&gt;
 * 			&lt;property name=&quot;servicePrincipal&quot; value=&quot;HTTP/web.springsource.com&quot; /&gt;
 * 			&lt;property name=&quot;keyTabLocation&quot; value=&quot;classpath:http-java.keytab&quot; /&gt;
 * 		&lt;/bean&gt;
 * 	&lt;/property&gt;
 * 	&lt;property name=&quot;userDetailsService&quot; ref=&quot;inMemoryUserDetailsService&quot; /&gt;
 * &lt;/bean&gt;
 *
 * &lt;bean id=&quot;inMemoryUserDetailsService&quot;
 * 	class=&quot;org.springframework.security.core.userdetails.memory.InMemoryDaoImpl&quot;&gt;
 * 	&lt;property name=&quot;userProperties&quot;&gt;
 * 		&lt;value&gt;
 * 			mike@SECPOD.DE=notUsed,ROLE_ADMIN
 * 		&lt;/value&gt;
 * 	&lt;/property&gt;
 * &lt;/bean&gt;
 * &lt;/beans&gt;
 * </pre>
 *
 * <p>
 * If you get a "GSSException: Channel binding mismatch (Mechanism level:ChannelBinding
 * not provided!) have a look at this
 * <a href="https://bugs.sun.com/view_bug.do?bug_id=6851973">bug</a>.
 * </p>
 * <p>
 * A workaround until this is fixed in the JVM is to change
 * </p>
 * HKEY_LOCAL_MACHINE\System \CurrentControlSet\Control\LSA\SuppressExtendedProtection to
 * 0x02
 *
 * @author Mike Wiesner
 * @author Jeremy Stone
 * @author Denis Angilella
 * @since 1.0
 * @see KerberosServiceAuthenticationProvider
 * @see SpnegoEntryPoint
 */
public class SpnegoAuthenticationProcessingFilter extends OncePerRequestFilter {

	private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder
		.getContextHolderStrategy();

	private SecurityContextRepository securityContextRepository = new RequestAttributeSecurityContextRepository();

	private AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource = new WebAuthenticationDetailsSource();

	private @Nullable AuthenticationManager authenticationManager;

	private @Nullable AuthenticationSuccessHandler successHandler;

	private @Nullable AuthenticationFailureHandler failureHandler;

	private SessionAuthenticationStrategy sessionStrategy = new NullAuthenticatedSessionStrategy();

	private boolean skipIfAlreadyAuthenticated = true;

	private boolean stopFilterChainOnSuccessfulAuthentication = false;

	/**
	 * Authentication header prefix sent by IE/Windows when the domain controller fails to
	 * issue a Kerberos ticket for the URL.
	 *
	 * "TlRMTVNTUA" is the base64 encoding of "NTLMSSP". This will be followed by the
	 * actual token.
	 **/
	private static final String NTLMSSP_PREFIX = "Negotiate TlRMTVNTUA";

	@Override
	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
			throws ServletException, IOException {

		if (this.skipIfAlreadyAuthenticated) {
			Authentication existingAuth = SecurityContextHolder.getContext().getAuthentication();

			if (existingAuth != null && existingAuth.isAuthenticated()
					&& !(existingAuth instanceof AnonymousAuthenticationToken)) {
				chain.doFilter(request, response);
				return;
			}
		}

		String header = request.getHeader("Authorization");

		if (header != null && ((header.startsWith("Negotiate ") && !header.startsWith(NTLMSSP_PREFIX))
				|| header.startsWith("Kerberos "))) {
			if (this.logger.isDebugEnabled()) {
				this.logger.debug("Received Negotiate Header for request " + request.getRequestURL() + ": " + header);
			}
			byte[] base64Token = header.substring(header.indexOf(" ") + 1).getBytes("UTF-8");
			byte[] kerberosTicket = Base64.getDecoder().decode(base64Token);
			KerberosServiceRequestToken authenticationRequest = new KerberosServiceRequestToken(kerberosTicket);
			authenticationRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
			Authentication authentication;
			try {
				if (this.authenticationManager == null) {
					throw new IllegalStateException("authenticationManager must be set");
				}
				authentication = this.authenticationManager.authenticate(authenticationRequest);
			}
			catch (AuthenticationException ex) {
				// That shouldn't happen, as it is most likely a wrong
				// configuration on the server side
				this.logger.warn("Negotiate Header was invalid: " + header, ex);
				this.securityContextHolderStrategy.clearContext();
				if (this.failureHandler != null) {
					this.failureHandler.onAuthenticationFailure(request, response, ex);
				}
				else {
					response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
					response.flushBuffer();
				}
				return;
			}
			this.sessionStrategy.onAuthentication(authentication, request, response);

			SecurityContext context = this.securityContextHolderStrategy.createEmptyContext();
			context.setAuthentication(authentication);
			this.securityContextHolderStrategy.setContext(context);
			this.securityContextRepository.saveContext(context, request, response);
			if (this.successHandler != null) {
				this.successHandler.onAuthenticationSuccess(request, response, authentication);
			}
			if (this.stopFilterChainOnSuccessfulAuthentication) {
				return;
			}
		}

		chain.doFilter(request, response);

	}

	@Override
	public void afterPropertiesSet() throws ServletException {
		super.afterPropertiesSet();
		Assert.notNull(this.authenticationManager, "authenticationManager must be specified");
	}

	/**
	 * The authentication manager for validating the ticket.
	 * @param authenticationManager the authentication manager
	 */
	public void setAuthenticationManager(AuthenticationManager authenticationManager) {
		this.authenticationManager = authenticationManager;
	}

	/**
	 * <p>
	 * This handler is called after a successful authentication. One can add additional
	 * authentication behavior by setting this.
	 * </p>
	 * <p>
	 * Default is null, which means nothing additional happens
	 * </p>
	 * @param successHandler the authentication success handler
	 */
	public void setSuccessHandler(AuthenticationSuccessHandler successHandler) {
		this.successHandler = successHandler;
	}

	/**
	 * <p>
	 * This handler is called after a failure authentication. In most cases you only get
	 * Kerberos/SPNEGO failures with a wrong server or network configurations and not
	 * during runtime. If the client encounters an error, he will just stop the
	 * communication with server and therefore this handler will not be called in this
	 * case.
	 * </p>
	 * <p>
	 * Default is null, which means that the Filter returns the HTTP 500 code
	 * </p>
	 * @param failureHandler the authentication failure handler
	 */
	public void setFailureHandler(AuthenticationFailureHandler failureHandler) {
		this.failureHandler = failureHandler;
	}

	/**
	 * Should Kerberos authentication be skipped if a user is already authenticated for
	 * this request (e.g. in the HTTP session).
	 * @param skipIfAlreadyAuthenticated default is true
	 */
	public void setSkipIfAlreadyAuthenticated(boolean skipIfAlreadyAuthenticated) {
		this.skipIfAlreadyAuthenticated = skipIfAlreadyAuthenticated;
	}

	/**
	 * The session handling strategy which will be invoked immediately after an
	 * authentication request is successfully processed by the
	 * <tt>AuthenticationManager</tt>. Used, for example, to handle changing of the
	 * session identifier to prevent session fixation attacks.
	 * @param sessionStrategy the implementation to use. If not set a null implementation
	 * is used.
	 */
	public void setSessionAuthenticationStrategy(SessionAuthenticationStrategy sessionStrategy) {
		this.sessionStrategy = sessionStrategy;
	}

	/**
	 * Sets the authentication details source.
	 * @param authenticationDetailsSource the authentication details source
	 */
	public void setAuthenticationDetailsSource(
			AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource) {
		Assert.notNull(authenticationDetailsSource, "AuthenticationDetailsSource required");
		this.authenticationDetailsSource = authenticationDetailsSource;
	}

	/**
	 * If set to {@code false} (the default) and authentication is successful, the request
	 * will be processed by the next filter in the chain. If {@code true} and
	 * authentication is successful, the filter chain will stop here.
	 * @param shouldStop set to {@code true} to prevent the next filter in the chain from
	 * processing the request after a successful authentication.
	 * @since 1.0.2
	 */
	public void setStopFilterChainOnSuccessfulAuthentication(boolean shouldStop) {
		this.stopFilterChainOnSuccessfulAuthentication = shouldStop;
	}

	/**
	 * Sets the {@link SecurityContextRepository} to save the {@link SecurityContext} on
	 * authentication success. The default action is not to save the
	 * {@link SecurityContext}.
	 * @param securityContextRepository the {@link SecurityContextRepository} to use.
	 * Cannot be null.
	 */
	public void setSecurityContextRepository(SecurityContextRepository securityContextRepository) {
		Assert.notNull(securityContextRepository, "securityContextRepository cannot be null");
		this.securityContextRepository = securityContextRepository;
	}

	/**
	 * Sets the {@link SecurityContextHolderStrategy} to use. The default action is to use
	 * the {@link SecurityContextHolderStrategy} stored in {@link SecurityContextHolder}.
	 * @param securityContextHolderStrategy the {@link SecurityContextHolderStrategy} to
	 * use. Cannot be null.
	 */
	public void setSecurityContextHolderStrategy(SecurityContextHolderStrategy securityContextHolderStrategy) {
		Assert.notNull(securityContextHolderStrategy, "securityContextHolderStrategy cannot be null");
		this.securityContextHolderStrategy = securityContextHolderStrategy;
	}

}
