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