SpnegoNegociateCredentialsAction(List,boolean,boolean)   A
last analyzed

Complexity

Conditions 1

Size

Total Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 12
c 0
b 0
f 0
cc 1
rs 9.4285
1
package org.apereo.cas.web.flow;
2
3
import org.apereo.cas.support.spnego.util.SpnegoConstants;
4
import org.apereo.cas.util.HttpRequestUtils;
5
import org.apereo.cas.web.support.WebUtils;
6
import org.slf4j.Logger;
7
import org.slf4j.LoggerFactory;
8
import org.springframework.util.StringUtils;
9
import org.springframework.webflow.action.AbstractAction;
10
import org.springframework.webflow.execution.Event;
11
import org.springframework.webflow.execution.RequestContext;
12
13
import javax.servlet.http.HttpServletRequest;
14
import javax.servlet.http.HttpServletResponse;
15
import java.util.List;
16
17
/**
18
 * First action of a SPNEGO flow : negotiation.
19
 * <p>The server checks if the
20
 * negotiation string is in the request header and this is a supported browser:
21
 * <ul>
22
 * <li>If found do nothing and return {@code success()}</li>
23
 * <li>else add a WWW-Authenticate response header and a 401 response status,
24
 * then return {@code success()}</li>
25
 * </ul>
26
 *
27
 * @author Arnaud Lesueur
28
 * @author Marc-Antoine Garrigue
29
 * @author Scott Battaglia
30
 * @author John Gasper
31
 * @see <a href="http://ietfreport.isoc.org/idref/rfc4559/#page-2">RFC 4559</a>
32
 * @since 3.1
33
 */
34
public class SpnegoNegociateCredentialsAction extends AbstractAction {
35
36
    private static final Logger LOGGER = LoggerFactory.getLogger(SpnegoNegociateCredentialsAction.class);
37
38
    /** Whether this is using the NTLM protocol or not. */
39
    private final boolean ntlm;
40
41
    /**
42
     * Sets whether mixed mode authentication should be enabled. If it is
43
     * enabled then control is allowed to pass back to the Spring Webflow
44
     * instead of immediately terminating the page after issuing the
45
     * unauthorized (401) header. This has the effect of displaying the login
46
     * page on unsupported/configured browsers.
47
     * <p>
48
     * If this is set to false then the page is immediately closed after the
49
     * unauthorized header is sent. This is ideal in environments that only
50
     * want to use Windows Integrated Auth/SPNEGO and not forms auth.
51
     */
52
    private final boolean mixedModeAuthentication;
53
54
    /**
55
     * Sets supported browsers by their user agent. The user agent
56
     * header defined will be compared against this list. The user agents configured
57
     * here need not be an exact match. So longer is the user agent identifier
58
     * configured in this list is "found" in the user agent header retrieved,
59
     * the check will pass.
60
     */
61
    private final List<String> supportedBrowser;
62
63
    private final String messageBeginPrefix;
64
65
    /**
66
     * Instantiates a new Spnego negociate credentials action.
67
     * Also add to the list of supported browser user agents the following:
68
     * <ul>
69
     *     <li>{@code MSIE}</li>
70
     *     <li>{@code Trident}</li>
71
     *     <li>{@code Firefox}</li>
72
     *     <li>{@code AppleWebKit}</li>
73
     * </ul>
74
     *
75
     * @param supportedBrowser the supported browsers list
76
     * @param ntlm Sets the ntlm. Generates the message prefix as well.
77
     * @param mixedModeAuthenticationEnabled should mixed mode authentication be allowed. Default is false.
78
     *
79
     * @since 4.1
80
     */
81
    public SpnegoNegociateCredentialsAction(final List<String> supportedBrowser, final boolean ntlm, final boolean mixedModeAuthenticationEnabled) {
82
        super();
83
84
        this.ntlm = ntlm;
85
        this.messageBeginPrefix = constructMessagePrefix();
86
        this.mixedModeAuthentication = mixedModeAuthenticationEnabled;
87
88
        this.supportedBrowser = supportedBrowser;
89
        this.supportedBrowser.add("MSIE");
90
        this.supportedBrowser.add("Trident");
91
        this.supportedBrowser.add("Firefox");
92
        this.supportedBrowser.add("AppleWebKit");
93
    }
94
95
    @Override
96
    protected Event doExecute(final RequestContext context) {
97
        final HttpServletRequest request = WebUtils.getHttpServletRequestFromExternalWebflowContext(context);
98
        final HttpServletResponse response = WebUtils.getHttpServletResponseFromExternalWebflowContext(context);
99
100
        final String authorizationHeader = request.getHeader(SpnegoConstants.HEADER_AUTHORIZATION);
101
        final String userAgent = HttpRequestUtils.getHttpServletRequestUserAgent(request);
102
103
        LOGGER.debug("Authorization header [{}], User Agent header [{}]", authorizationHeader, userAgent);
104
        if (!StringUtils.hasText(userAgent) || this.supportedBrowser.isEmpty()) {
105
            LOGGER.warn("User Agent header [{}] is empty, or no browsers are supported", userAgent);
106
            return error();
107
        }
108
109
        if (!isSupportedBrowser(userAgent)) {
110
            LOGGER.warn("User Agent header [{}] is not supported in the list of supported browsers [{}]",
111
                    userAgent, this.supportedBrowser);
112
            return error();
113
        }
114
115
        if (!StringUtils.hasText(authorizationHeader)
116
                || !authorizationHeader.startsWith(this.messageBeginPrefix)
117
                || authorizationHeader.length() <= this.messageBeginPrefix
118
                .length()) {
119
120
            final String wwwHeader = this.ntlm ? SpnegoConstants.NTLM : SpnegoConstants.NEGOTIATE;
121
            LOGGER.debug("Authorization header not found or does not match the message prefix [{}]. Sending [{}] header [{}]",
122
                    this.messageBeginPrefix, SpnegoConstants.HEADER_AUTHENTICATE, wwwHeader);
123
            response.setHeader(SpnegoConstants.HEADER_AUTHENTICATE, wwwHeader);
124
125
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
126
            // The responseComplete flag tells the pausing view-state not to render the response
127
            // because another object has taken care of it. If mixed mode authentication is allowed
128
            // then responseComplete should not be called so that webflow will display the login page.
129
            if (!this.mixedModeAuthentication) {
130
                LOGGER.debug("Mixed-mode authentication is disabled. Executing completion of response");
131
                context.getExternalContext().recordResponseComplete();
132
            } else {
133
                LOGGER.debug("Mixed-mode authentication is enabled");
134
            }
135
        }
136
        return success();
137
    }
138
139
    /**
140
     * Construct message prefix.
141
     *
142
     * @return if {@link #ntlm} is enabled, {@link SpnegoConstants#NTLM}, otherwise
143
     * {@link SpnegoConstants#NEGOTIATE}. An extra space is appended to the end.
144
     */
145
    protected String constructMessagePrefix() {
146
        return (this.ntlm ? SpnegoConstants.NTLM : SpnegoConstants.NEGOTIATE) + ' ';
147
    }
148
149
    /**
150
     * Checks if is supported browser.
151
     *
152
     * @param userAgent the user agent
153
     * @return true, if  supported browser
154
     */
155
    protected boolean isSupportedBrowser(final String userAgent) {
156
        return supportedBrowser.stream().anyMatch(userAgent::contains);
157
    }
158
}
159