1 /*
2 * MIT License
3 *
4 * Copyright (c) 2010-2024 The Waffle Project Contributors: https://github.com/Waffle/waffle/graphs/contributors
5 *
6 * Permission is hereby granted, free of charge, to any person obtaining a copy
7 * of this software and associated documentation files (the "Software"), to deal
8 * in the Software without restriction, including without limitation the rights
9 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 * copies of the Software, and to permit persons to whom the Software is
11 * furnished to do so, subject to the following conditions:
12 *
13 * The above copyright notice and this permission notice shall be included in all
14 * copies or substantial portions of the Software.
15 *
16 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 * SOFTWARE.
23 */
24 package waffle.shiro.negotiate;
25
26 import java.io.IOException;
27 import java.util.ArrayList;
28 import java.util.Base64;
29 import java.util.List;
30 import java.util.Locale;
31
32 import javax.servlet.ServletRequest;
33 import javax.servlet.ServletResponse;
34 import javax.servlet.http.HttpServletRequest;
35 import javax.servlet.http.HttpServletResponse;
36
37 import org.apache.shiro.authc.AuthenticationException;
38 import org.apache.shiro.authc.AuthenticationToken;
39 import org.apache.shiro.subject.Subject;
40 import org.apache.shiro.web.filter.authc.AuthenticatingFilter;
41 import org.apache.shiro.web.filter.authc.FormAuthenticationFilter;
42 import org.apache.shiro.web.util.WebUtils;
43 import org.slf4j.Logger;
44 import org.slf4j.LoggerFactory;
45
46 import waffle.util.AuthorizationHeader;
47 import waffle.util.NtlmServletRequest;
48
49 /**
50 * A authentication filter that implements the HTTP Negotiate mechanism. The current user is authenticated, providing
51 * single-sign-on. Derived from net.skorgenes.security.jsecurity.negotiate.NegotiateAuthenticationFilter. see:
52 * https://bitbucket.org/lothor
53 * /shiro-negotiate/src/7b25efde130b9cbcacf579b3f926c532d919aa23/src/main/java/net/skorgenes/
54 * security/jsecurity/negotiate/NegotiateAuthenticationFilter.java?at=default
55 *
56 * @since 1.0.0
57 */
58 public class NegotiateAuthenticationFilter extends AuthenticatingFilter {
59
60 /**
61 * This class's private logger.
62 */
63 private static final Logger LOGGER = LoggerFactory.getLogger(NegotiateAuthenticationFilter.class);
64
65 // TODO things (sometimes) break, depending on what user account is running tomcat:
66 // related to setSPN and running tomcat server as NT Service account vs. as normal user account.
67 // https://waffle.codeplex.com/discussions/254748
68 // setspn -A HTTP/<server-fqdn> <user_tomcat_running_under>
69 /** The Constant PROTOCOLS. */
70 private static final List<String> PROTOCOLS = new ArrayList<>();
71
72 /** The failure key attribute. */
73 private String failureKeyAttribute = FormAuthenticationFilter.DEFAULT_ERROR_KEY_ATTRIBUTE_NAME;
74
75 /** The remember me param. */
76 private String rememberMeParam = FormAuthenticationFilter.DEFAULT_REMEMBER_ME_PARAM;
77
78 /**
79 * Instantiates a new negotiate authentication filter.
80 */
81 public NegotiateAuthenticationFilter() {
82 NegotiateAuthenticationFilter.PROTOCOLS.add("Negotiate");
83 NegotiateAuthenticationFilter.PROTOCOLS.add("NTLM");
84 }
85
86 /**
87 * Gets the remember me param.
88 *
89 * @return the remember me param
90 */
91 public String getRememberMeParam() {
92 return this.rememberMeParam;
93 }
94
95 /**
96 * Sets the request parameter name to look for when acquiring the rememberMe boolean value. Unless overridden by
97 * calling this method, the default is <code>rememberMe</code>. <br>
98 * <br>
99 * RememberMe will be <code>true</code> if the parameter value equals any of those supported by
100 * {@link org.apache.shiro.web.util.WebUtils#isTrue(javax.servlet.ServletRequest, String)
101 * WebUtils.isTrue(request,value)}, <code>false</code> otherwise.
102 *
103 * @param value
104 * the name of the request param to check for acquiring the rememberMe boolean value.
105 */
106 public void setRememberMeParam(final String value) {
107 this.rememberMeParam = value;
108 }
109
110 @Override
111 protected boolean isRememberMe(final ServletRequest request) {
112 return WebUtils.isTrue(request, this.getRememberMeParam());
113 }
114
115 @Override
116 protected AuthenticationToken createToken(final ServletRequest request, final ServletResponse response) {
117 final String authorization = this.getAuthzHeader(request);
118 final String[] elements = authorization.split(" ", -1);
119 final byte[] inToken = Base64.getDecoder().decode(elements[1]);
120
121 // maintain a connection-based session for NTLM tokens
122 // TODO see about changing this parameter to ServletRequest in waffle
123 final String connectionId = NtlmServletRequest.getConnectionId((HttpServletRequest) request);
124 final String securityPackage = elements[0];
125
126 // TODO see about changing this parameter to ServletRequest in waffle
127 final AuthorizationHeader authorizationHeader = new AuthorizationHeader((HttpServletRequest) request);
128 final boolean ntlmPost = authorizationHeader.isNtlmType1PostAuthorizationHeader();
129
130 NegotiateAuthenticationFilter.LOGGER.debug("security package: {}, connection id: {}, ntlmPost: {}",
131 securityPackage, connectionId, Boolean.valueOf(ntlmPost));
132
133 final boolean rememberMe = this.isRememberMe(request);
134 final String host = this.getHost(request);
135
136 return new NegotiateToken(inToken, new byte[0], connectionId, securityPackage, ntlmPost, rememberMe, host);
137 }
138
139 @Override
140 protected boolean onLoginSuccess(final AuthenticationToken token, final Subject subject,
141 final ServletRequest request, final ServletResponse response) throws Exception {
142 request.setAttribute("MY_SUBJECT", ((NegotiateToken) token).getSubject());
143 return true;
144 }
145
146 @Override
147 protected boolean onLoginFailure(final AuthenticationToken token, final AuthenticationException e,
148 final ServletRequest request, final ServletResponse response) {
149 if (e instanceof AuthenticationInProgressException) {
150 // negotiate is processing
151 final String protocol = this.getAuthzHeaderProtocol(request);
152 NegotiateAuthenticationFilter.LOGGER.debug("Negotiation in progress for protocol: {}", protocol);
153 this.sendChallengeDuringNegotiate(protocol, response, ((NegotiateToken) token).getOut());
154 return false;
155 }
156 NegotiateAuthenticationFilter.LOGGER.warn("login exception: {}", e.getMessage());
157
158 // do not send token.out bytes, this was a login failure.
159 this.sendChallengeOnFailure(response);
160
161 this.setFailureAttribute(request, e);
162 return true;
163 }
164
165 /**
166 * Sets the failure attribute.
167 *
168 * @param request
169 * the request
170 * @param ae
171 * the ae
172 */
173 protected void setFailureAttribute(final ServletRequest request, final AuthenticationException ae) {
174 final String className = ae.getClass().getName();
175 request.setAttribute(this.getFailureKeyAttribute(), className);
176 }
177
178 /**
179 * Gets the failure key attribute.
180 *
181 * @return the failure key attribute
182 */
183 public String getFailureKeyAttribute() {
184 return this.failureKeyAttribute;
185 }
186
187 /**
188 * Sets the failure key attribute.
189 *
190 * @param value
191 * the new failure key attribute
192 */
193 public void setFailureKeyAttribute(final String value) {
194 this.failureKeyAttribute = value;
195 }
196
197 @Override
198 protected boolean onAccessDenied(final ServletRequest request, final ServletResponse response) throws Exception {
199 // false by default or we wouldn't be in
200 boolean loggedIn = false;
201 // this method
202 if (this.isLoginAttempt(request)) {
203 loggedIn = this.executeLogin(request, response);
204 } else {
205 NegotiateAuthenticationFilter.LOGGER.debug("authorization required, supported protocols: {}",
206 NegotiateAuthenticationFilter.PROTOCOLS);
207 this.sendChallengeInitiateNegotiate(response);
208 }
209 return loggedIn;
210 }
211
212 /**
213 * Returns the {@link org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter#AUTHORIZATION_HEADER
214 * AUTHORIZATION_HEADER} from the specified ServletRequest.
215 * <p/>
216 * This implementation merely casts the request to an <code>HttpServletRequest</code> and returns the header:
217 * <p/>
218 * <code>HttpServletRequest httpRequest = {@link WebUtils#toHttp(javax.servlet.ServletRequest) toHttp(reaquest)};<br/>
219 * return httpRequest.getHeader({@link org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter#AUTHORIZATION_HEADER AUTHORIZATION_HEADER});</code>
220 *
221 * @param request
222 * the incoming <code>ServletRequest</code>
223 *
224 * @return the <code>Authorization</code> header's value.
225 */
226 private String getAuthzHeader(final ServletRequest request) {
227 final HttpServletRequest httpRequest = WebUtils.toHttp(request);
228 return httpRequest.getHeader("Authorization");
229 }
230
231 /**
232 * Gets the authz header protocol.
233 *
234 * @param request
235 * the request
236 *
237 * @return the authz header protocol
238 */
239 private String getAuthzHeaderProtocol(final ServletRequest request) {
240 final String authzHeader = this.getAuthzHeader(request);
241 return authzHeader.substring(0, authzHeader.indexOf(' '));
242 }
243
244 /**
245 * Determines whether the incoming request is an attempt to log in.
246 * <p/>
247 * The default implementation obtains the value of the request's
248 * {@link org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter#AUTHORIZATION_HEADER AUTHORIZATION_HEADER}
249 * , and if it is not <code>null</code>, delegates to
250 * {@link org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter#isLoginAttempt(String)
251 * isLoginAttempt(authzHeaderValue)}. If the header is <code>null</code>, <code>false</code> is returned.
252 *
253 * @param request
254 * incoming ServletRequest
255 *
256 * @return true if the incoming request is an attempt to log in based, false otherwise
257 */
258 private boolean isLoginAttempt(final ServletRequest request) {
259 final String authzHeader = this.getAuthzHeader(request);
260 return authzHeader != null && this.isLoginAttempt(authzHeader);
261 }
262
263 /**
264 * Default implementation that returns <code>true</code> if the specified <code>authzHeader</code> starts with the
265 * same (case-insensitive) characters specified by any of the configured protocols (Negotiate or NTLM),
266 * <code>false</code> otherwise.
267 *
268 * @param authzHeader
269 * the 'Authorization' header value (guaranteed to be non-null if the
270 * {@link #isLoginAttempt(javax.servlet.ServletRequest)} method is not overriden).
271 *
272 * @return <code>true</code> if the authzHeader value matches any of the configured protocols (Negotiate or NTLM).
273 */
274 boolean isLoginAttempt(final String authzHeader) {
275 for (final String protocol : NegotiateAuthenticationFilter.PROTOCOLS) {
276 if (authzHeader.toLowerCase(Locale.ENGLISH).startsWith(protocol.toLowerCase(Locale.ENGLISH))) {
277 return true;
278 }
279 }
280 return false;
281 }
282
283 /**
284 * Builds the challenge for authorization by setting a HTTP <code>401</code> (Unauthorized) status as well as the
285 * response's {@link org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter#AUTHENTICATE_HEADER
286 * AUTHENTICATE_HEADER}.
287 *
288 * @param protocols
289 * protocols for which to send a challenge. In initial cases, will be all supported protocols. In the
290 * midst of negotiation, will be only the protocol being negotiated.
291 * @param response
292 * outgoing ServletResponse
293 * @param out
294 * token.out or null
295 */
296 private void sendChallenge(final List<String> protocols, final ServletResponse response, final byte[] out) {
297 final HttpServletResponse httpResponse = WebUtils.toHttp(response);
298 this.sendAuthenticateHeader(protocols, out, httpResponse);
299 httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
300 }
301
302 /**
303 * Send challenge initiate negotiate.
304 *
305 * @param response
306 * the response
307 */
308 void sendChallengeInitiateNegotiate(final ServletResponse response) {
309 this.sendChallenge(NegotiateAuthenticationFilter.PROTOCOLS, response, null);
310 }
311
312 /**
313 * Send challenge during negotiate.
314 *
315 * @param protocol
316 * the protocol
317 * @param response
318 * the response
319 * @param out
320 * the out
321 */
322 void sendChallengeDuringNegotiate(final String protocol, final ServletResponse response, final byte[] out) {
323 final List<String> protocolsList = new ArrayList<>();
324 protocolsList.add(protocol);
325 this.sendChallenge(protocolsList, response, out);
326 }
327
328 /**
329 * Send challenge on failure.
330 *
331 * @param response
332 * the response
333 */
334 void sendChallengeOnFailure(final ServletResponse response) {
335 final HttpServletResponse httpResponse = WebUtils.toHttp(response);
336 this.sendUnauthorized(NegotiateAuthenticationFilter.PROTOCOLS, null, httpResponse);
337 httpResponse.setHeader("Connection", "close");
338 try {
339 httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED);
340 httpResponse.flushBuffer();
341 } catch (final IOException e) {
342 throw new RuntimeException(e);
343 }
344 }
345
346 /**
347 * Send authenticate header.
348 *
349 * @param protocolsList
350 * the protocols list
351 * @param out
352 * the out
353 * @param httpResponse
354 * the http response
355 */
356 private void sendAuthenticateHeader(final List<String> protocolsList, final byte[] out,
357 final HttpServletResponse httpResponse) {
358 this.sendUnauthorized(protocolsList, out, httpResponse);
359 httpResponse.setHeader("Connection", "keep-alive");
360 }
361
362 /**
363 * Send unauthorized.
364 *
365 * @param protocols
366 * the protocols
367 * @param out
368 * the out
369 * @param response
370 * the response
371 */
372 private void sendUnauthorized(final List<String> protocols, final byte[] out, final HttpServletResponse response) {
373 for (final String protocol : protocols) {
374 if (out == null || out.length == 0) {
375 response.addHeader("WWW-Authenticate", protocol);
376 } else {
377 response.setHeader("WWW-Authenticate", protocol + " " + Base64.getEncoder().encodeToString(out));
378 }
379 }
380 }
381
382 }