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 = (IWindowsAuthProvider) Class.forName(authProvider).getConstructor().newInstance();
378             } catch (final ClassNotFoundException | IllegalArgumentException | SecurityException
379                     | InstantiationException | IllegalAccessException | InvocationTargetException
380                     | NoSuchMethodException e) {
381                 throw new ServletException(e);
382             }
383         }
384 
385         if (this.auth == null) {
386             this.auth = new WindowsAuthProviderImpl();
387         }
388 
389         if (providerNames != null) {
390             this.providers = new SecurityFilterProviderCollection(providerNames, this.auth);
391         }
392 
393         // create default providers if none specified
394         if (this.providers == null) {
395             NegotiateSecurityFilter.LOGGER.debug("initializing default security filter providers");
396             this.providers = new SecurityFilterProviderCollection(this.auth);
397         }
398 
399         // apply provider implementation parameters
400         NegotiateSecurityFilter.LOGGER.debug("[waffle.servlet.NegotiateSecurityFilter] load provider parameters");
401         for (final Map.Entry<String, String> implParameter : implParameters.entrySet()) {
402             final String[] classAndParameter = implParameter.getKey().split("/", 2);
403             if (classAndParameter.length == 2) {
404                 try {
405 
406                     NegotiateSecurityFilter.LOGGER.debug("setting {}, {}={}", classAndParameter[0],
407                             classAndParameter[1], implParameter.getValue());
408 
409                     final SecurityFilterProvider provider = this.providers.getByClassName(classAndParameter[0]);
410                     provider.initParameter(classAndParameter[1], implParameter.getValue());
411 
412                 } catch (final ClassNotFoundException e) {
413                     NegotiateSecurityFilter.LOGGER.error("invalid class: {} in {}", classAndParameter[0],
414                             implParameter.getKey());
415                     throw new ServletException(e);
416                 } catch (final Exception e) {
417                     NegotiateSecurityFilter.LOGGER.error("Error setting {} in {}", classAndParameter[0],
418                             classAndParameter[1]);
419                     throw new ServletException(e);
420                 }
421             } else {
422                 NegotiateSecurityFilter.LOGGER.error("Invalid parameter: {}", implParameter.getKey());
423                 throw new ServletException("Invalid parameter: " + implParameter.getKey());
424             }
425         }
426 
427         NegotiateSecurityFilter.LOGGER.info("[waffle.servlet.NegotiateSecurityFilter] started");
428     }
429 
430     /**
431      * Set the principal format.
432      *
433      * @param format
434      *            Principal format.
435      */
436     public void setPrincipalFormat(final String format) {
437         this.principalFormat = PrincipalFormat.valueOf(format.toUpperCase(Locale.ENGLISH));
438         NegotiateSecurityFilter.LOGGER.info("principal format: {}", this.principalFormat);
439     }
440 
441     /**
442      * Principal format.
443      *
444      * @return Principal format.
445      */
446     public PrincipalFormat getPrincipalFormat() {
447         return this.principalFormat;
448     }
449 
450     /**
451      * Set the principal format.
452      *
453      * @param format
454      *            Role format.
455      */
456     public void setRoleFormat(final String format) {
457         this.roleFormat = PrincipalFormat.valueOf(format.toUpperCase(Locale.ENGLISH));
458         NegotiateSecurityFilter.LOGGER.info("role format: {}", this.roleFormat);
459     }
460 
461     /**
462      * Principal format.
463      *
464      * @return Role format.
465      */
466     public PrincipalFormat getRoleFormat() {
467         return this.roleFormat;
468     }
469 
470     /**
471      * Send a 401 Unauthorized along with protocol authentication headers.
472      *
473      * @param response
474      *            HTTP Response
475      * @param close
476      *            Close connection.
477      */
478     private void sendUnauthorized(final HttpServletResponse response, final boolean close) {
479         try {
480             this.providers.sendUnauthorized(response);
481             if (close) {
482                 response.setHeader("Connection", "close");
483             } else {
484                 response.setHeader("Connection", "keep-alive");
485             }
486             response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
487             response.flushBuffer();
488         } catch (final IOException e) {
489             throw new RuntimeException(e);
490         }
491     }
492 
493     /**
494      * Windows auth provider.
495      *
496      * @return IWindowsAuthProvider.
497      */
498     public IWindowsAuthProvider getAuth() {
499         return this.auth;
500     }
501 
502     /**
503      * Set Windows auth provider.
504      *
505      * @param provider
506      *            Class implements IWindowsAuthProvider.
507      */
508     public void setAuth(final IWindowsAuthProvider provider) {
509         this.auth = provider;
510     }
511 
512     /**
513      * True if guest login is allowed.
514      *
515      * @return True if guest login is allowed, false otherwise.
516      */
517     public boolean isAllowGuestLogin() {
518         return this.allowGuestLogin;
519     }
520 
521     /**
522      * Enable/Disable impersonation.
523      *
524      * @param value
525      *            true to enable impersonation, false otherwise
526      */
527     public void setImpersonate(final boolean value) {
528         this.impersonate = value;
529     }
530 
531     /**
532      * Checks if is impersonate.
533      *
534      * @return true if impersonation is enabled, false otherwise
535      */
536     public boolean isImpersonate() {
537         return this.impersonate;
538     }
539 
540     /**
541      * Security filter providers.
542      *
543      * @return A collection of security filter providers.
544      */
545     public SecurityFilterProviderCollection getProviders() {
546         return this.providers;
547     }
548 
549     private static boolean isWindows() {
550         if (NegotiateSecurityFilter.windows == null) {
551             NegotiateSecurityFilter.windows = System.getProperty("os.name").toLowerCase(Locale.ENGLISH).contains("win");
552         }
553         return NegotiateSecurityFilter.windows.booleanValue();
554     }
555 
556 }