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