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