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); } }