View Javadoc
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.servlet;
15  
16  import java.io.IOException;
17  import java.lang.reflect.InvocationTargetException;
18  import java.security.Principal;
19  import java.util.Enumeration;
20  import java.util.HashMap;
21  import java.util.Locale;
22  import java.util.Map;
23  import java.util.Map.Entry;
24  
25  import javax.security.auth.Subject;
26  import javax.servlet.Filter;
27  import javax.servlet.FilterChain;
28  import javax.servlet.FilterConfig;
29  import javax.servlet.ServletException;
30  import javax.servlet.ServletRequest;
31  import javax.servlet.ServletResponse;
32  import javax.servlet.http.HttpServletRequest;
33  import javax.servlet.http.HttpServletResponse;
34  import javax.servlet.http.HttpSession;
35  
36  import org.slf4j.Logger;
37  import org.slf4j.LoggerFactory;
38  
39  import waffle.servlet.spi.SecurityFilterProvider;
40  import waffle.servlet.spi.SecurityFilterProviderCollection;
41  import waffle.util.AuthorizationHeader;
42  import waffle.windows.auth.IWindowsAuthProvider;
43  import waffle.windows.auth.IWindowsIdentity;
44  import waffle.windows.auth.IWindowsImpersonationContext;
45  import waffle.windows.auth.PrincipalFormat;
46  import waffle.windows.auth.impl.WindowsAuthProviderImpl;
47  
48  /**
49   * A Negotiate (NTLM/Kerberos) Security Filter.
50   *
51   * @author dblock[at]dblock[dot]org
52   */
53  public class NegotiateSecurityFilter implements Filter {
54  
55      /** The Constant LOGGER. */
56      private static final Logger              LOGGER              = LoggerFactory
57                                                                           .getLogger(NegotiateSecurityFilter.class);
58      
59      /** The principal format. */
60      private PrincipalFormat                  principalFormat     = PrincipalFormat.FQN;
61      
62      /** The role format. */
63      private PrincipalFormat                  roleFormat          = PrincipalFormat.FQN;
64      
65      /** The providers. */
66      private SecurityFilterProviderCollection providers;
67      
68      /** The auth. */
69      private IWindowsAuthProvider             auth;
70      
71      /** The allow guest login. */
72      private boolean                          allowGuestLogin     = true;
73      
74      /** The impersonate. */
75      private boolean                          impersonate;
76      
77      /** The Constant PRINCIPALSESSIONKEY. */
78      private static final String              PRINCIPALSESSIONKEY = NegotiateSecurityFilter.class.getName()
79                                                                           + ".PRINCIPAL";
80  
81      /**
82       * Instantiates a new negotiate security filter.
83       */
84      public NegotiateSecurityFilter() {
85          NegotiateSecurityFilter.LOGGER.debug("[waffle.servlet.NegotiateSecurityFilter] loaded");
86      }
87  
88      /* (non-Javadoc)
89       * @see javax.servlet.Filter#destroy()
90       */
91      @Override
92      public void destroy() {
93          NegotiateSecurityFilter.LOGGER.info("[waffle.servlet.NegotiateSecurityFilter] stopped");
94      }
95  
96      /* (non-Javadoc)
97       * @see javax.servlet.Filter#doFilter(javax.servlet.ServletRequest, javax.servlet.ServletResponse, javax.servlet.FilterChain)
98       */
99      @Override
100     public void doFilter(final ServletRequest sreq, final ServletResponse sres, final FilterChain chain)
101             throws IOException, ServletException {
102 
103         final HttpServletRequest request = (HttpServletRequest) sreq;
104         final HttpServletResponse response = (HttpServletResponse) sres;
105 
106         NegotiateSecurityFilter.LOGGER.debug("{} {}, contentlength: {}", request.getMethod(), request.getRequestURI(),
107                 Integer.valueOf(request.getContentLength()));
108 
109         if (this.doFilterPrincipal(request, response, chain)) {
110             // previously authenticated user
111             return;
112         }
113 
114         final AuthorizationHeader authorizationHeader = new AuthorizationHeader(request);
115 
116         // authenticate user
117         if (!authorizationHeader.isNull()) {
118 
119             // log the user in using the token
120             IWindowsIdentity windowsIdentity;
121             try {
122                 windowsIdentity = this.providers.doFilter(request, response);
123                 if (windowsIdentity == null) {
124                     return;
125                 }
126             } catch (final IOException e) {
127                 NegotiateSecurityFilter.LOGGER.warn("error logging in user: {}", e.getMessage());
128                 NegotiateSecurityFilter.LOGGER.trace("{}", e);
129                 this.sendUnauthorized(response, true);
130                 return;
131             }
132 
133             IWindowsImpersonationContext ctx = null;
134             try {
135                 if (!this.allowGuestLogin && windowsIdentity.isGuest()) {
136                     NegotiateSecurityFilter.LOGGER.warn("guest login disabled: {}", windowsIdentity.getFqn());
137                     this.sendUnauthorized(response, true);
138                     return;
139                 }
140 
141                 NegotiateSecurityFilter.LOGGER.debug("logged in user: {} ({})", windowsIdentity.getFqn(), windowsIdentity.getSidString());
142 
143                 final HttpSession session = request.getSession(true);
144                 if (session == null) {
145                     throw new ServletException("Expected HttpSession");
146                 }
147 
148                 Subject subject = (Subject) session.getAttribute("javax.security.auth.subject");
149                 if (subject == null) {
150                     subject = new Subject();
151                 }
152 
153                 WindowsPrincipal windowsPrincipal = null;
154                 if (this.impersonate) {
155                     windowsPrincipal = new AutoDisposableWindowsPrincipal(windowsIdentity, this.principalFormat,
156                             this.roleFormat);
157                 } else {
158                     windowsPrincipal = new WindowsPrincipal(windowsIdentity, this.principalFormat, this.roleFormat);
159                 }
160 
161                 NegotiateSecurityFilter.LOGGER.debug("roles: {}", windowsPrincipal.getRolesString());
162                 subject.getPrincipals().add(windowsPrincipal);
163                 session.setAttribute("javax.security.auth.subject", subject);
164 
165                 NegotiateSecurityFilter.LOGGER.info("successfully logged in user: {}", windowsIdentity.getFqn());
166 
167                 request.getSession().setAttribute(NegotiateSecurityFilter.PRINCIPALSESSIONKEY, windowsPrincipal);
168 
169                 final NegotiateRequestWrapper requestWrapper = new NegotiateRequestWrapper(request, windowsPrincipal);
170 
171                 if (this.impersonate) {
172                     NegotiateSecurityFilter.LOGGER.debug("impersonating user");
173                     ctx = windowsIdentity.impersonate();
174                 }
175 
176                 chain.doFilter(requestWrapper, response);
177             } finally {
178                 if (this.impersonate && ctx != null) {
179                     NegotiateSecurityFilter.LOGGER.debug("terminating impersonation");
180                     ctx.revertToSelf();
181                 } else {
182                     windowsIdentity.dispose();
183                 }
184             }
185 
186             return;
187         }
188 
189         NegotiateSecurityFilter.LOGGER.debug("authorization required");
190         this.sendUnauthorized(response, false);
191     }
192 
193     /**
194      * Filter for a previously logged on user.
195      *
196      * @param request
197      *            HTTP request.
198      * @param response
199      *            HTTP response.
200      * @param chain
201      *            Filter chain.
202      * @return True if a user already authenticated.
203      * @throws IOException
204      *             Signals that an I/O exception has occurred.
205      * @throws ServletException
206      *             the servlet exception
207      */
208     private boolean doFilterPrincipal(final HttpServletRequest request, final HttpServletResponse response,
209             final FilterChain chain) throws IOException, ServletException {
210         Principal principal = request.getUserPrincipal();
211         if (principal == null) {
212             final HttpSession session = request.getSession(false);
213             if (session != null) {
214                 principal = (Principal) session.getAttribute(NegotiateSecurityFilter.PRINCIPALSESSIONKEY);
215             }
216         }
217 
218         if (principal == null) {
219             // no principal in this request
220             return false;
221         }
222 
223         if (this.providers.isPrincipalException(request)) {
224             // the providers signal to authenticate despite an existing principal, eg. NTLM post
225             return false;
226         }
227 
228         // user already authenticated
229 
230         if (principal instanceof WindowsPrincipal) {
231             NegotiateSecurityFilter.LOGGER.debug("previously authenticated Windows user: {}", principal.getName());
232             final WindowsPrincipal windowsPrincipal = (WindowsPrincipal) principal;
233 
234             if (this.impersonate && windowsPrincipal.getIdentity() == null) {
235                 // This can happen when the session has been serialized then de-serialized
236                 // and because the IWindowsIdentity field is transient. In this case re-ask an
237                 // authentication to get a new identity.
238                 return false;
239             }
240 
241             final NegotiateRequestWrapper requestWrapper = new NegotiateRequestWrapper(request, windowsPrincipal);
242 
243             IWindowsImpersonationContext ctx = null;
244             if (this.impersonate) {
245                 NegotiateSecurityFilter.LOGGER.debug("re-impersonating user");
246                 ctx = windowsPrincipal.getIdentity().impersonate();
247             }
248             try {
249                 chain.doFilter(requestWrapper, response);
250             } finally {
251                 if (this.impersonate && ctx != null) {
252                     NegotiateSecurityFilter.LOGGER.debug("terminating impersonation");
253                     ctx.revertToSelf();
254                 }
255             }
256         } else {
257             NegotiateSecurityFilter.LOGGER.debug("previously authenticated user: {}", principal.getName());
258             chain.doFilter(request, response);
259         }
260         return true;
261     }
262 
263     /* (non-Javadoc)
264      * @see javax.servlet.Filter#init(javax.servlet.FilterConfig)
265      */
266     @SuppressWarnings("unchecked")
267     @Override
268     public void init(final FilterConfig filterConfig) throws ServletException {
269         final Map<String, String> implParameters = new HashMap<String, String>();
270 
271         String authProvider = null;
272         String[] providerNames = null;
273         if (filterConfig != null) {
274             final Enumeration<String> parameterNames = filterConfig.getInitParameterNames();
275             while (parameterNames.hasMoreElements()) {
276                 final String parameterName = parameterNames.nextElement();
277                 final String parameterValue = filterConfig.getInitParameter(parameterName);
278                 NegotiateSecurityFilter.LOGGER.debug("{}={}", parameterName, parameterValue);
279                 if (parameterName.equals("principalFormat")) {
280                     this.principalFormat = PrincipalFormat.valueOf(parameterValue.toUpperCase(Locale.ENGLISH));
281                 } else if (parameterName.equals("roleFormat")) {
282                     this.roleFormat = PrincipalFormat.valueOf(parameterValue.toUpperCase(Locale.ENGLISH));
283                 } else if (parameterName.equals("allowGuestLogin")) {
284                     this.allowGuestLogin = Boolean.parseBoolean(parameterValue);
285                 } else if (parameterName.equals("impersonate")) {
286                     this.impersonate = Boolean.parseBoolean(parameterValue);
287                 } else if (parameterName.equals("securityFilterProviders")) {
288                     providerNames = parameterValue.split("\\s+");
289                 } else if (parameterName.equals("authProvider")) {
290                     authProvider = parameterValue;
291                 } else {
292                     implParameters.put(parameterName, parameterValue);
293                 }
294             }
295         }
296 
297         if (authProvider != null) {
298             try {
299                 this.auth = (IWindowsAuthProvider) Class.forName(authProvider).getConstructor().newInstance();
300             } catch (final ClassNotFoundException e) {
301                 NegotiateSecurityFilter.LOGGER.error("error loading '{}': {}", authProvider, e.getMessage());
302                 NegotiateSecurityFilter.LOGGER.trace("{}", e);
303                 throw new ServletException(e);
304             } catch (final IllegalArgumentException e) {
305                 NegotiateSecurityFilter.LOGGER.error("error loading '{}': {}", authProvider, e.getMessage());
306                 NegotiateSecurityFilter.LOGGER.trace("{}", e);
307                 throw new ServletException(e);
308             } catch (final SecurityException e) {
309                 NegotiateSecurityFilter.LOGGER.error("error loading '{}': {}", authProvider, e.getMessage());
310                 NegotiateSecurityFilter.LOGGER.trace("{}", e);
311                 throw new ServletException(e);
312             } catch (final InstantiationException e) {
313                 NegotiateSecurityFilter.LOGGER.error("error loading '{}': {}", authProvider, e.getMessage());
314                 NegotiateSecurityFilter.LOGGER.trace("{}", e);
315                 throw new ServletException(e);
316             } catch (final IllegalAccessException e) {
317                 NegotiateSecurityFilter.LOGGER.error("error loading '{}': {}", authProvider, e.getMessage());
318                 NegotiateSecurityFilter.LOGGER.trace("{}", e);
319                 throw new ServletException(e);
320             } catch (final InvocationTargetException e) {
321                 NegotiateSecurityFilter.LOGGER.error("error loading '{}': {}", authProvider, e.getMessage());
322                 NegotiateSecurityFilter.LOGGER.trace("{}", e);
323                 throw new ServletException(e);
324             } catch (final NoSuchMethodException e) {
325                 NegotiateSecurityFilter.LOGGER.error("error loading '{}': {}", authProvider, e.getMessage());
326                 NegotiateSecurityFilter.LOGGER.trace("{}", e);
327                 throw new ServletException(e);
328             }
329         }
330 
331         if (this.auth == null) {
332             this.auth = new WindowsAuthProviderImpl();
333         }
334 
335         if (providerNames != null) {
336             this.providers = new SecurityFilterProviderCollection(providerNames, this.auth);
337         }
338 
339         // create default providers if none specified
340         if (this.providers == null) {
341             NegotiateSecurityFilter.LOGGER.debug("initializing default security filter providers");
342             this.providers = new SecurityFilterProviderCollection(this.auth);
343         }
344 
345         // apply provider implementation parameters
346         for (final Entry<String, String> implParameter : implParameters.entrySet()) {
347             final String[] classAndParameter = implParameter.getKey().split("/", 2);
348             if (classAndParameter.length == 2) {
349                 try {
350 
351                     NegotiateSecurityFilter.LOGGER.debug("setting {}, {}={}", classAndParameter[0], classAndParameter[1],
352                             implParameter.getValue());
353 
354                     final SecurityFilterProvider provider = this.providers.getByClassName(classAndParameter[0]);
355                     provider.initParameter(classAndParameter[1], implParameter.getValue());
356 
357                 } catch (final ClassNotFoundException e) {
358                     NegotiateSecurityFilter.LOGGER.error("invalid class: {} in {}", classAndParameter[0], implParameter.getKey());
359                     throw new ServletException(e);
360                 } catch (final Exception e) {
361                     NegotiateSecurityFilter.LOGGER.error("{}: error setting '{}': {}", classAndParameter[0], classAndParameter[1],
362                             e.getMessage());
363                     NegotiateSecurityFilter.LOGGER.trace("{}", e);
364                     throw new ServletException(e);
365                 }
366             } else {
367                 NegotiateSecurityFilter.LOGGER.error("Invalid parameter: {}", implParameter.getKey());
368                 throw new ServletException("Invalid parameter: " + implParameter.getKey());
369             }
370         }
371 
372         NegotiateSecurityFilter.LOGGER.info("[waffle.servlet.NegotiateSecurityFilter] started");
373     }
374 
375     /**
376      * Set the principal format.
377      * 
378      * @param format
379      *            Principal format.
380      */
381     public void setPrincipalFormat(final String format) {
382         this.principalFormat = PrincipalFormat.valueOf(format.toUpperCase(Locale.ENGLISH));
383         NegotiateSecurityFilter.LOGGER.info("principal format: {}", this.principalFormat);
384     }
385 
386     /**
387      * Principal format.
388      * 
389      * @return Principal format.
390      */
391     public PrincipalFormat getPrincipalFormat() {
392         return this.principalFormat;
393     }
394 
395     /**
396      * Set the principal format.
397      * 
398      * @param format
399      *            Role format.
400      */
401     public void setRoleFormat(final String format) {
402         this.roleFormat = PrincipalFormat.valueOf(format.toUpperCase(Locale.ENGLISH));
403         NegotiateSecurityFilter.LOGGER.info("role format: {}", this.roleFormat);
404     }
405 
406     /**
407      * Principal format.
408      * 
409      * @return Role format.
410      */
411     public PrincipalFormat getRoleFormat() {
412         return this.roleFormat;
413     }
414 
415     /**
416      * Send a 401 Unauthorized along with protocol authentication headers.
417      * 
418      * @param response
419      *            HTTP Response
420      * @param close
421      *            Close connection.
422      */
423     private void sendUnauthorized(final HttpServletResponse response, final boolean close) {
424         try {
425             this.providers.sendUnauthorized(response);
426             if (close) {
427                 response.setHeader("Connection", "close");
428             } else {
429                 response.setHeader("Connection", "keep-alive");
430             }
431             response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
432             response.flushBuffer();
433         } catch (final IOException e) {
434             throw new RuntimeException(e);
435         }
436     }
437 
438     /**
439      * Windows auth provider.
440      * 
441      * @return IWindowsAuthProvider.
442      */
443     public IWindowsAuthProvider getAuth() {
444         return this.auth;
445     }
446 
447     /**
448      * Set Windows auth provider.
449      * 
450      * @param provider
451      *            Class implements IWindowsAuthProvider.
452      */
453     public void setAuth(final IWindowsAuthProvider provider) {
454         this.auth = provider;
455     }
456 
457     /**
458      * True if guest login is allowed.
459      * 
460      * @return True if guest login is allowed, false otherwise.
461      */
462     public boolean isAllowGuestLogin() {
463         return this.allowGuestLogin;
464     }
465 
466     /**
467      * Enable/Disable impersonation.
468      *
469      * @param value
470      *            true to enable impersonation, false otherwise
471      */
472     public void setImpersonate(final boolean value) {
473         this.impersonate = value;
474     }
475 
476     /**
477      * Checks if is impersonate.
478      *
479      * @return true if impersonation is enabled, false otherwise
480      */
481     public boolean isImpersonate() {
482         return this.impersonate;
483     }
484 
485     /**
486      * Security filter providers.
487      * 
488      * @return A collection of security filter providers.
489      */
490     public SecurityFilterProviderCollection getProviders() {
491         return this.providers;
492     }
493 }