package org.apereo.cas.web.flow;
import org.apereo.cas.support.spnego.util.SpnegoConstants;
import org.apereo.cas.web.support.WebUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.StringUtils;
import org.springframework.webflow.action.AbstractAction;
import org.springframework.webflow.execution.Event;
import org.springframework.webflow.execution.RequestContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
/**
* First action of a SPNEGO flow : negotiation.
* <p>The server checks if the
* negotiation string is in the request header and this is a supported browser:
* <ul>
* <li>If found do nothing and return {@code success()}</li>
* <li>else add a WWW-Authenticate response header and a 401 response status,
* then return {@code success()}</li>
* </ul>
*
* @author Arnaud Lesueur
* @author Marc-Antoine Garrigue
* @author Scott Battaglia
* @author John Gasper
* @see <a href="http://ietfreport.isoc.org/idref/rfc4559/#page-2">RFC 4559</a>
* @since 3.1
*/
public class SpnegoNegociateCredentialsAction extends AbstractAction {
private static final Logger LOGGER = LoggerFactory.getLogger(SpnegoNegociateCredentialsAction.class);
/** Whether this is using the NTLM protocol or not. */
private final boolean ntlm;
/**
* Sets whether mixed mode authentication should be enabled. If it is
* enabled then control is allowed to pass back to the Spring Webflow
* instead of immediately terminating the page after issuing the
* unauthorized (401) header. This has the effect of displaying the login
* page on unsupported/configured browsers.
* <p>
* If this is set to false then the page is immediately closed after the
* unauthorized header is sent. This is ideal in environments that only
* want to use Windows Integrated Auth/SPNEGO and not forms auth.
*/
private final boolean mixedModeAuthentication;
/**
* Sets supported browsers by their user agent. The user agent
* header defined will be compared against this list. The user agents configured
* here need not be an exact match. So longer is the user agent identifier
* configured in this list is "found" in the user agent header retrieved,
* the check will pass.
*/
private final List<String> supportedBrowser;
private final String messageBeginPrefix;
/**
* Instantiates a new Spnego negociate credentials action.
* Also add to the list of supported browser user agents the following:
* <ul>
* <li>{@code MSIE}</li>
* <li>{@code Trident}</li>
* <li>{@code Firefox}</li>
* <li>{@code AppleWebKit}</li>
* </ul>
*
* @param supportedBrowser the supported browsers list
* @param ntlm Sets the ntlm. Generates the message prefix as well.
* @param mixedModeAuthenticationEnabled should mixed mode authentication be allowed. Default is false.
*
* @since 4.1
*/
public SpnegoNegociateCredentialsAction(final List<String> supportedBrowser, final boolean ntlm, final boolean mixedModeAuthenticationEnabled) {
super();
this.ntlm = ntlm;
this.messageBeginPrefix = constructMessagePrefix();
this.mixedModeAuthentication = mixedModeAuthenticationEnabled;
this.supportedBrowser = supportedBrowser;
this.supportedBrowser.add("MSIE");
this.supportedBrowser.add("Trident");
this.supportedBrowser.add("Firefox");
this.supportedBrowser.add("AppleWebKit");
}
@Override
protected Event doExecute(final RequestContext context) {
final HttpServletRequest request = WebUtils.getHttpServletRequest(context);
final HttpServletResponse response = WebUtils.getHttpServletResponse(context);
final String authorizationHeader = request.getHeader(SpnegoConstants.HEADER_AUTHORIZATION);
final String userAgent = WebUtils.getHttpServletRequestUserAgent(request);
LOGGER.debug("Authorization header [{}], User Agent header [{}]", authorizationHeader, userAgent);
if (!StringUtils.hasText(userAgent) || this.supportedBrowser.isEmpty()) {
LOGGER.warn("User Agent header [{}] is empty, or no browsers are supported", userAgent);
return error();
}
if (!isSupportedBrowser(userAgent)) {
LOGGER.warn("User Agent header [{}] is not supported in the list of supported browsers [{}]",
userAgent, this.supportedBrowser);
return error();
}
if (!StringUtils.hasText(authorizationHeader)
|| !authorizationHeader.startsWith(this.messageBeginPrefix)
|| authorizationHeader.length() <= this.messageBeginPrefix
.length()) {
final String wwwHeader = this.ntlm ? SpnegoConstants.NTLM : SpnegoConstants.NEGOTIATE;
LOGGER.debug("Authorization header not found or does not match the message prefix [{}]. Sending [{}] header [{}]",
this.messageBeginPrefix, SpnegoConstants.HEADER_AUTHENTICATE, wwwHeader);
response.setHeader(SpnegoConstants.HEADER_AUTHENTICATE, wwwHeader);
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
// The responseComplete flag tells the pausing view-state not to render the response
// because another object has taken care of it. If mixed mode authentication is allowed
// then responseComplete should not be called so that webflow will display the login page.
if (!this.mixedModeAuthentication) {
LOGGER.debug("Mixed-mode authentication is disabled. Executing completion of response");
context.getExternalContext().recordResponseComplete();
} else {
LOGGER.debug("Mixed-mode authentication is enabled");
}
}
return success();
}
/**
* Construct message prefix.
*
* @return if {@link #ntlm} is enabled, {@link SpnegoConstants#NTLM}, otherwise
* {@link SpnegoConstants#NEGOTIATE}. An extra space is appended to the end.
*/
protected String constructMessagePrefix() {
return (this.ntlm ? SpnegoConstants.NTLM : SpnegoConstants.NEGOTIATE) + ' ';
}
/**
* Checks if is supported browser.
*
* @param userAgent the user agent
* @return true, if supported browser
*/
protected boolean isSupportedBrowser(final String userAgent) {
return supportedBrowser.stream().anyMatch(userAgent::contains);
}
}