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 = (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
394 if (this.providers == null) {
395 NegotiateSecurityFilter.LOGGER.debug("initializing default security filter providers");
396 this.providers = new SecurityFilterProviderCollection(this.auth);
397 }
398
399
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
432
433
434
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
443
444
445
446 public PrincipalFormat getPrincipalFormat() {
447 return this.principalFormat;
448 }
449
450
451
452
453
454
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
463
464
465
466 public PrincipalFormat getRoleFormat() {
467 return this.roleFormat;
468 }
469
470
471
472
473
474
475
476
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
495
496
497
498 public IWindowsAuthProvider getAuth() {
499 return this.auth;
500 }
501
502
503
504
505
506
507
508 public void setAuth(final IWindowsAuthProvider provider) {
509 this.auth = provider;
510 }
511
512
513
514
515
516
517 public boolean isAllowGuestLogin() {
518 return this.allowGuestLogin;
519 }
520
521
522
523
524
525
526
527 public void setImpersonate(final boolean value) {
528 this.impersonate = value;
529 }
530
531
532
533
534
535
536 public boolean isImpersonate() {
537 return this.impersonate;
538 }
539
540
541
542
543
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 }