1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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
62
63 public class NegotiateSecurityFilter implements Filter {
64
65
66 private static final Logger LOGGER = LoggerFactory.getLogger(NegotiateSecurityFilter.class);
67
68
69 private static final String PRINCIPALSESSIONKEY = NegotiateSecurityFilter.class.getName() + ".PRINCIPAL";
70
71
72 private static Boolean windows;
73
74
75 private PrincipalFormat principalFormat = PrincipalFormat.FQN;
76
77
78 private PrincipalFormat roleFormat = PrincipalFormat.FQN;
79
80
81 private SecurityFilterProviderCollection providers;
82
83
84 private IWindowsAuthProvider auth;
85
86
87 private String[] excludePatterns;
88
89
90 private boolean allowGuestLogin = true;
91
92
93 private boolean impersonate;
94
95
96 private boolean excludeBearerAuthorization;
97
98
99 private boolean excludeCorsPreflight;
100
101
102 private boolean disableSSO;
103
104
105
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
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
134 if (this.disableSSO) {
135 NegotiateSecurityFilter.LOGGER.debug("SSO is disabled, resuming filter chain");
136 chain.doFilter(request, response);
137 return;
138 }
139
140
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
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
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
170 return;
171 }
172
173
174 if (!authorizationHeader.isNull()) {
175
176
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
253
254
255
256
257
258
259
260
261
262
263
264
265
266
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
280 return false;
281 }
282
283 if (this.providers.isPrincipalException(request)) {
284
285 return false;
286 }
287
288
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
295
296
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
395 if (this.providers == null) {
396 NegotiateSecurityFilter.LOGGER.debug("initializing default security filter providers");
397 this.providers = new SecurityFilterProviderCollection(this.auth);
398 }
399
400
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
433
434
435
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
444
445
446
447 public PrincipalFormat getPrincipalFormat() {
448 return this.principalFormat;
449 }
450
451
452
453
454
455
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
464
465
466
467 public PrincipalFormat getRoleFormat() {
468 return this.roleFormat;
469 }
470
471
472
473
474
475
476
477
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
496
497
498
499 public IWindowsAuthProvider getAuth() {
500 return this.auth;
501 }
502
503
504
505
506
507
508
509 public void setAuth(final IWindowsAuthProvider provider) {
510 this.auth = provider;
511 }
512
513
514
515
516
517
518 public boolean isAllowGuestLogin() {
519 return this.allowGuestLogin;
520 }
521
522
523
524
525
526
527
528 public void setImpersonate(final boolean value) {
529 this.impersonate = value;
530 }
531
532
533
534
535
536
537 public boolean isImpersonate() {
538 return this.impersonate;
539 }
540
541
542
543
544
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 }