package org.apereo.cas.web.flow;

import org.apereo.cas.authentication.Credential;
import org.apereo.cas.authentication.adaptive.AdaptiveAuthenticationPolicy;
import org.apereo.cas.support.spnego.authentication.principal.SpnegoCredential;
import org.apereo.cas.support.spnego.util.SpnegoConstants;
import org.apereo.cas.util.EncodingUtils;
import org.apereo.cas.web.flow.resolver.CasDelegatingWebflowEventResolver;
import org.apereo.cas.web.flow.resolver.CasWebflowEventResolver;
import org.apereo.cas.web.support.WebUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.StringUtils;
import org.springframework.webflow.execution.RequestContext;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.nio.charset.Charset;

/**
 * Second action of a SPNEGO flow : decode the gssapi-data and build a new
 * {@link SpnegoCredential}.
 * <p>
 * Once AbstractNonInteractiveCredentialsAction has executed the authentication
 * procedure, this action check whether a principal is present in Credential and
 * add corresponding response headers.</p>
 *
 * @author Arnaud Lesueur
 * @author Marc-Antoine Garrigue
 * @see <a href="http://ietfreport.isoc.org/idref/rfc4559/#page-2">RFC 4559</a>
 * @since 3.1
 */
public class SpnegoCredentialsAction extends AbstractNonInteractiveCredentialsAction {
    private static final Logger LOGGER = LoggerFactory.getLogger(SpnegoCredentialsAction.class);
    private final boolean ntlm;
    private final String messageBeginPrefix;

    /**
     * Behavior in case of SPNEGO authentication failure :
     * <ul><li>True : if SPNEGO is the last authentication method with no fallback.</li>
     * <li>False : if an interactive view (eg: login page) should be send to user as SPNEGO failure fallback</li>
     * </ul>
     */
    private boolean send401OnAuthenticationFailure = true;

    public SpnegoCredentialsAction(final CasDelegatingWebflowEventResolver initialAuthenticationAttemptWebflowEventResolver,
                                   final CasWebflowEventResolver serviceTicketRequestWebflowEventResolver,
                                   final AdaptiveAuthenticationPolicy adaptiveAuthenticationPolicy, final boolean ntlm,
                                   final boolean send401OnAuthenticationFailure) {
        super(initialAuthenticationAttemptWebflowEventResolver, serviceTicketRequestWebflowEventResolver, adaptiveAuthenticationPolicy);
        this.ntlm = ntlm;
        this.messageBeginPrefix = (ntlm ? SpnegoConstants.NTLM : SpnegoConstants.NEGOTIATE) + ' ';
        this.send401OnAuthenticationFailure = send401OnAuthenticationFailure;
    }

    @Override
    protected Credential constructCredentialsFromRequest(final RequestContext context) {
        final HttpServletRequest request = WebUtils.getHttpServletRequest(context);

        final String authorizationHeader = request.getHeader(SpnegoConstants.HEADER_AUTHORIZATION);
        LOGGER.debug("SPNEGO Authorization header located as [{}]", authorizationHeader);
                
        if (StringUtils.hasText(authorizationHeader)
                && authorizationHeader.startsWith(this.messageBeginPrefix)
                && authorizationHeader.length() > this.messageBeginPrefix.length()) {

            LOGGER.debug("SPNEGO Authorization header found with [{}] bytes",
                    authorizationHeader.length() - this.messageBeginPrefix.length());

            final byte[] token = EncodingUtils.decodeBase64(authorizationHeader.substring(this.messageBeginPrefix.length()));
            if (token == null) {
                LOGGER.warn("Could not decode authorization header in Base64");
                return null;
            }
            LOGGER.debug("Obtained token: [{}]. Creating SPNEGO credential...", new String(token, Charset.defaultCharset()));
            return new SpnegoCredential(token);
        }

        LOGGER.warn("SPNEGO Authorization header not found under [{}] or it does not begin with the prefix [{}]",
                SpnegoConstants.HEADER_AUTHORIZATION, this.messageBeginPrefix);
        return null;
    }

    @Override
    protected void onError(final RequestContext context) {
        setResponseHeader(context);
    }

    @Override
    protected void onSuccess(final RequestContext context) {
        setResponseHeader(context);
    }

    /**
     * Sets the response header based on the retrieved token.
     *
     * @param context    the context
     */
    private void setResponseHeader(final RequestContext context) {
        final Credential credential = WebUtils.getCredential(context);
        
        if (credential == null) {
            LOGGER.debug("No credential was provided. No response header set.");
            return;
        }

        final HttpServletResponse response = WebUtils.getHttpServletResponse(context);
        final SpnegoCredential spnegoCredentials = (SpnegoCredential) credential;
        final byte[] nextToken = spnegoCredentials.getNextToken();
        if (nextToken != null) {
            LOGGER.debug("Obtained output token: [{}]", new String(nextToken, Charset.defaultCharset()));
            response.setHeader(SpnegoConstants.HEADER_AUTHENTICATE, (this.ntlm
                    ? SpnegoConstants.NTLM : SpnegoConstants.NEGOTIATE)
                    + ' ' + EncodingUtils.encodeBase64(nextToken));
        } else {
            LOGGER.debug("Unable to obtain the output token required.");
        }

        if (spnegoCredentials.getPrincipal() == null && this.send401OnAuthenticationFailure) {
            LOGGER.debug("Setting HTTP Status to 401");
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        }
    }
}
