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 static org.assertj.core.api.Assertions.assertThat;
27  
28  import com.sun.jna.platform.win32.Advapi32Util;
29  import com.sun.jna.platform.win32.Secur32.EXTENDED_NAME_FORMAT;
30  import com.sun.jna.platform.win32.Secur32Util;
31  import com.sun.jna.platform.win32.Sspi;
32  import com.sun.jna.platform.win32.SspiUtil.ManagedSecBufferDesc;
33  
34  import jakarta.servlet.ServletException;
35  
36  import java.io.IOException;
37  import java.util.ArrayList;
38  import java.util.Base64;
39  
40  import javax.security.auth.Subject;
41  
42  import org.junit.jupiter.api.AfterEach;
43  import org.junit.jupiter.api.Assertions;
44  import org.junit.jupiter.api.BeforeEach;
45  import org.junit.jupiter.api.Test;
46  
47  import waffle.mock.MockWindowsAuthProvider;
48  import waffle.mock.MockWindowsIdentity;
49  import waffle.mock.http.SimpleFilterChain;
50  import waffle.mock.http.SimpleFilterConfig;
51  import waffle.mock.http.SimpleHttpRequest;
52  import waffle.mock.http.SimpleHttpResponse;
53  import waffle.windows.auth.IWindowsCredentialsHandle;
54  import waffle.windows.auth.PrincipalFormat;
55  import waffle.windows.auth.impl.WindowsAccountImpl;
56  import waffle.windows.auth.impl.WindowsAuthProviderImpl;
57  import waffle.windows.auth.impl.WindowsCredentialsHandleImpl;
58  import waffle.windows.auth.impl.WindowsSecurityContextImpl;
59  
60  /**
61   * Waffle Tomcat Security Filter Test.
62   */
63  class NegotiateSecurityFilterTest {
64  
65      /** The Constant NEGOTIATE. */
66      private static final String NEGOTIATE = "Negotiate";
67  
68      /** The Constant NTLM. */
69      private static final String NTLM = "NTLM";
70  
71      /** The filter. */
72      private NegotiateSecurityFilter filter;
73  
74      /**
75       * Sets the up.
76       *
77       * @throws ServletException
78       *             the servlet exception
79       */
80      @BeforeEach
81      void setUp() throws ServletException {
82          this.filter = new NegotiateSecurityFilter();
83          this.filter.setAuth(new WindowsAuthProviderImpl());
84          this.filter.init(null);
85      }
86  
87      /**
88       * Tear down.
89       */
90      @AfterEach
91      void tearDown() {
92          this.filter.destroy();
93      }
94  
95      /**
96       * Test challenge get.
97       *
98       * @throws IOException
99       *             Signals that an I/O exception has occurred.
100      * @throws ServletException
101      *             the servlet exception
102      */
103     @Test
104     void testChallengeGET() throws IOException, ServletException {
105         final SimpleHttpRequest request = new SimpleHttpRequest();
106         request.setMethod("GET");
107         final SimpleHttpResponse response = new SimpleHttpResponse();
108         this.filter.doFilter(request, response, null);
109         final String[] wwwAuthenticates = response.getHeaderValues("WWW-Authenticate");
110         Assertions.assertEquals(3, wwwAuthenticates.length);
111         Assertions.assertEquals(NegotiateSecurityFilterTest.NEGOTIATE, wwwAuthenticates[0]);
112         Assertions.assertEquals(NegotiateSecurityFilterTest.NTLM, wwwAuthenticates[1]);
113         Assertions.assertTrue(wwwAuthenticates[2].startsWith("Basic realm=\""));
114         Assertions.assertEquals(2, response.getHeaderNamesSize());
115         Assertions.assertEquals("keep-alive", response.getHeader("Connection"));
116         Assertions.assertEquals(401, response.getStatus());
117     }
118 
119     /**
120      * Test challenge post.
121      *
122      * @throws IOException
123      *             Signals that an I/O exception has occurred.
124      * @throws ServletException
125      *             the servlet exception
126      */
127     @Test
128     void testChallengePOST() throws IOException, ServletException {
129         final String securityPackage = NegotiateSecurityFilterTest.NEGOTIATE;
130         IWindowsCredentialsHandle clientCredentials = null;
131         WindowsSecurityContextImpl clientContext = null;
132         try {
133             // client credentials handle
134             clientCredentials = WindowsCredentialsHandleImpl.getCurrent(securityPackage);
135             clientCredentials.initialize();
136             // initial client security context
137             clientContext = new WindowsSecurityContextImpl();
138             clientContext.setPrincipalName(WindowsAccountImpl.getCurrentUsername());
139             clientContext.setCredentialsHandle(clientCredentials);
140             clientContext.setSecurityPackage(securityPackage);
141             clientContext.initialize(null, null, WindowsAccountImpl.getCurrentUsername());
142             final SimpleHttpRequest request = new SimpleHttpRequest();
143             request.setMethod("POST");
144             request.setContentLength(0);
145             final String clientToken = Base64.getEncoder().encodeToString(clientContext.getToken());
146             request.addHeader("Authorization", securityPackage + " " + clientToken);
147             final SimpleHttpResponse response = new SimpleHttpResponse();
148             this.filter.doFilter(request, response, null);
149             Assertions.assertTrue(response.getHeader("WWW-Authenticate").startsWith(securityPackage + " "));
150             Assertions.assertEquals("keep-alive", response.getHeader("Connection"));
151             Assertions.assertEquals(2, response.getHeaderNamesSize());
152             Assertions.assertEquals(401, response.getStatus());
153         } finally {
154             if (clientContext != null) {
155                 clientContext.dispose();
156             }
157             if (clientCredentials != null) {
158                 clientCredentials.dispose();
159             }
160         }
161     }
162 
163     /**
164      * Test negotiate.
165      *
166      * @throws IOException
167      *             Signals that an I/O exception has occurred.
168      * @throws ServletException
169      *             the servlet exception
170      */
171     @Test
172     void testNegotiate() throws IOException, ServletException {
173         final String securityPackage = NegotiateSecurityFilterTest.NEGOTIATE;
174         // client credentials handle
175         IWindowsCredentialsHandle clientCredentials = null;
176         WindowsSecurityContextImpl clientContext = null;
177         // role will contain both Everyone and SID
178         this.filter.setRoleFormat("both");
179         try {
180             // client credentials handle
181             clientCredentials = WindowsCredentialsHandleImpl.getCurrent(securityPackage);
182             clientCredentials.initialize();
183             // initial client security context
184             clientContext = new WindowsSecurityContextImpl();
185             clientContext.setPrincipalName(WindowsAccountImpl.getCurrentUsername());
186             clientContext.setCredentialsHandle(clientCredentials);
187             clientContext.setSecurityPackage(securityPackage);
188             clientContext.initialize(null, null, WindowsAccountImpl.getCurrentUsername());
189             // filter chain
190             final SimpleFilterChain filterChain = new SimpleFilterChain();
191             // negotiate
192             boolean authenticated = false;
193             final SimpleHttpRequest request = new SimpleHttpRequest();
194             while (true) {
195                 final String clientToken = Base64.getEncoder().encodeToString(clientContext.getToken());
196                 request.addHeader("Authorization", securityPackage + " " + clientToken);
197 
198                 final SimpleHttpResponse response = new SimpleHttpResponse();
199                 this.filter.doFilter(request, response, filterChain);
200 
201                 final Subject subject = (Subject) request.getSession(false).getAttribute("javax.security.auth.subject");
202                 authenticated = subject != null && subject.getPrincipals().size() > 0;
203 
204                 if (authenticated) {
205                     assertThat(response.getHeaderNamesSize()).isNotNegative();
206                     break;
207                 }
208 
209                 Assertions.assertTrue(response.getHeader("WWW-Authenticate").startsWith(securityPackage + " "));
210                 Assertions.assertEquals("keep-alive", response.getHeader("Connection"));
211                 Assertions.assertEquals(2, response.getHeaderNamesSize());
212                 Assertions.assertEquals(401, response.getStatus());
213                 final String continueToken = response.getHeader("WWW-Authenticate")
214                         .substring(securityPackage.length() + 1);
215                 final byte[] continueTokenBytes = Base64.getDecoder().decode(continueToken);
216                 assertThat(continueTokenBytes).isNotEmpty();
217                 final ManagedSecBufferDesc continueTokenBuffer = new ManagedSecBufferDesc(Sspi.SECBUFFER_TOKEN,
218                         continueTokenBytes);
219                 clientContext.initialize(clientContext.getHandle(), continueTokenBuffer, "localhost");
220             }
221             Assertions.assertTrue(authenticated);
222             Assertions.assertTrue(filterChain.getRequest() instanceof NegotiateRequestWrapper);
223             Assertions.assertTrue(filterChain.getResponse() instanceof SimpleHttpResponse);
224             final NegotiateRequestWrapper wrappedRequest = (NegotiateRequestWrapper) filterChain.getRequest();
225             Assertions.assertEquals(NegotiateSecurityFilterTest.NEGOTIATE.toUpperCase(), wrappedRequest.getAuthType());
226             Assertions.assertEquals(Secur32Util.getUserNameEx(EXTENDED_NAME_FORMAT.NameSamCompatible),
227                     wrappedRequest.getRemoteUser());
228             Assertions.assertTrue(wrappedRequest.getUserPrincipal() instanceof WindowsPrincipal);
229             final String everyoneGroupName = Advapi32Util.getAccountBySid("S-1-1-0").name;
230             Assertions.assertTrue(wrappedRequest.isUserInRole(everyoneGroupName));
231             Assertions.assertTrue(wrappedRequest.isUserInRole("S-1-1-0"));
232         } finally {
233             if (clientContext != null) {
234                 clientContext.dispose();
235             }
236             if (clientCredentials != null) {
237                 clientCredentials.dispose();
238             }
239         }
240     }
241 
242     /**
243      * Test negotiate previous auth with windows principal.
244      *
245      * @throws IOException
246      *             Signals that an I/O exception has occurred.
247      * @throws ServletException
248      *             the servlet exception
249      */
250     @Test
251     void testNegotiatePreviousAuthWithWindowsPrincipal() throws IOException, ServletException {
252         final MockWindowsIdentity mockWindowsIdentity = new MockWindowsIdentity("user", new ArrayList<String>());
253         final SimpleHttpRequest request = new SimpleHttpRequest();
254         final WindowsPrincipal windowsPrincipal = new WindowsPrincipal(mockWindowsIdentity);
255         request.setUserPrincipal(windowsPrincipal);
256         final SimpleFilterChain filterChain = new SimpleFilterChain();
257         final SimpleHttpResponse response = new SimpleHttpResponse();
258         this.filter.doFilter(request, response, filterChain);
259         Assertions.assertTrue(filterChain.getRequest() instanceof NegotiateRequestWrapper);
260         final NegotiateRequestWrapper wrappedRequest = (NegotiateRequestWrapper) filterChain.getRequest();
261         Assertions.assertTrue(wrappedRequest.getUserPrincipal() instanceof WindowsPrincipal);
262         Assertions.assertEquals(windowsPrincipal, wrappedRequest.getUserPrincipal());
263     }
264 
265     /**
266      * Test challenge ntlmpost.
267      *
268      * @throws IOException
269      *             Signals that an I/O exception has occurred.
270      * @throws ServletException
271      *             the servlet exception
272      */
273     @Test
274     void testChallengeNTLMPOST() throws IOException, ServletException {
275         final MockWindowsIdentity mockWindowsIdentity = new MockWindowsIdentity("user", new ArrayList<String>());
276         final SimpleHttpRequest request = new SimpleHttpRequest();
277         final WindowsPrincipal windowsPrincipal = new WindowsPrincipal(mockWindowsIdentity);
278         request.setUserPrincipal(windowsPrincipal);
279         request.setMethod("POST");
280         request.setContentLength(0);
281         request.addHeader("Authorization", "NTLM TlRMTVNTUAABAAAABzIAAAYABgArAAAACwALACAAAABXT1JLU1RBVElPTkRPTUFJTg==");
282         final SimpleFilterChain filterChain = new SimpleFilterChain();
283         final SimpleHttpResponse response = new SimpleHttpResponse();
284         this.filter.doFilter(request, response, filterChain);
285         Assertions.assertEquals(401, response.getStatus());
286         final String[] wwwAuthenticates = response.getHeaderValues("WWW-Authenticate");
287         Assertions.assertEquals(1, wwwAuthenticates.length);
288         Assertions.assertTrue(wwwAuthenticates[0].startsWith("NTLM "));
289         Assertions.assertEquals(2, response.getHeaderNamesSize());
290         Assertions.assertEquals("keep-alive", response.getHeader("Connection"));
291         Assertions.assertEquals(401, response.getStatus());
292     }
293 
294     /**
295      * Test challenge ntlmput.
296      *
297      * @throws IOException
298      *             Signals that an I/O exception has occurred.
299      * @throws ServletException
300      *             the servlet exception
301      */
302     @Test
303     void testChallengeNTLMPUT() throws IOException, ServletException {
304         final MockWindowsIdentity mockWindowsIdentity = new MockWindowsIdentity("user", new ArrayList<String>());
305         final SimpleHttpRequest request = new SimpleHttpRequest();
306         final WindowsPrincipal windowsPrincipal = new WindowsPrincipal(mockWindowsIdentity);
307         request.setUserPrincipal(windowsPrincipal);
308         request.setMethod("PUT");
309         request.setContentLength(0);
310         request.addHeader("Authorization", "NTLM TlRMTVNTUAABAAAABzIAAAYABgArAAAACwALACAAAABXT1JLU1RBVElPTkRPTUFJTg==");
311         final SimpleFilterChain filterChain = new SimpleFilterChain();
312         final SimpleHttpResponse response = new SimpleHttpResponse();
313         this.filter.doFilter(request, response, filterChain);
314         Assertions.assertEquals(401, response.getStatus());
315         final String[] wwwAuthenticates = response.getHeaderValues("WWW-Authenticate");
316         Assertions.assertEquals(1, wwwAuthenticates.length);
317         Assertions.assertTrue(wwwAuthenticates[0].startsWith("NTLM "));
318         Assertions.assertEquals(2, response.getHeaderNamesSize());
319         Assertions.assertEquals("keep-alive", response.getHeader("Connection"));
320         Assertions.assertEquals(401, response.getStatus());
321     }
322 
323     /**
324      * Test init basic security filter provider.
325      *
326      * @throws ServletException
327      *             the servlet exception
328      */
329     @Test
330     void testInitBasicSecurityFilterProvider() throws ServletException {
331         final SimpleFilterConfig filterConfig = new SimpleFilterConfig();
332         filterConfig.setParameter("principalFormat", "sid");
333         filterConfig.setParameter("roleFormat", "none");
334         filterConfig.setParameter("allowGuestLogin", "true");
335         filterConfig.setParameter("securityFilterProviders", "waffle.servlet.spi.BasicSecurityFilterProvider");
336         filterConfig.setParameter("waffle.servlet.spi.BasicSecurityFilterProvider/realm", "DemoRealm");
337         filterConfig.setParameter("authProvider", MockWindowsAuthProvider.class.getName());
338         this.filter.init(filterConfig);
339         Assertions.assertEquals(PrincipalFormat.SID, this.filter.getPrincipalFormat());
340         Assertions.assertEquals(PrincipalFormat.NONE, this.filter.getRoleFormat());
341         Assertions.assertTrue(this.filter.isAllowGuestLogin());
342         Assertions.assertEquals(1, this.filter.getProviders().size());
343         Assertions.assertTrue(this.filter.getAuth() instanceof MockWindowsAuthProvider);
344     }
345 
346     /**
347      * Test init two security filter providers.
348      *
349      * @throws ServletException
350      *             the servlet exception
351      */
352     @Test
353     void testInitTwoSecurityFilterProviders() throws ServletException {
354         // make sure that providers can be specified separated by any kind of space
355         final SimpleFilterConfig filterConfig = new SimpleFilterConfig();
356         filterConfig.setParameter("securityFilterProviders", "waffle.servlet.spi.BasicSecurityFilterProvider\n"
357                 + "waffle.servlet.spi.NegotiateSecurityFilterProvider waffle.servlet.spi.BasicSecurityFilterProvider");
358         this.filter.init(filterConfig);
359         Assertions.assertEquals(3, this.filter.getProviders().size());
360     }
361 
362     /**
363      * Test init negotiate security filter provider.
364      *
365      * @throws ServletException
366      *             the servlet exception
367      */
368     @Test
369     void testInitNegotiateSecurityFilterProvider() throws ServletException {
370         final SimpleFilterConfig filterConfig = new SimpleFilterConfig();
371         filterConfig.setParameter("securityFilterProviders", "waffle.servlet.spi.NegotiateSecurityFilterProvider");
372         filterConfig.setParameter("waffle.servlet.spi.NegotiateSecurityFilterProvider/protocols",
373                 "NTLM\nNegotiate NTLM");
374         this.filter.init(filterConfig);
375         Assertions.assertEquals(PrincipalFormat.FQN, this.filter.getPrincipalFormat());
376         Assertions.assertEquals(PrincipalFormat.FQN, this.filter.getRoleFormat());
377         Assertions.assertTrue(this.filter.isAllowGuestLogin());
378         Assertions.assertEquals(1, this.filter.getProviders().size());
379     }
380 
381     /**
382      * Test init negotiate security filter provider invalid protocol.
383      */
384     @Test
385     void testInitNegotiateSecurityFilterProviderInvalidProtocol() {
386         final SimpleFilterConfig filterConfig = new SimpleFilterConfig();
387         filterConfig.setParameter("securityFilterProviders", "waffle.servlet.spi.NegotiateSecurityFilterProvider");
388         filterConfig.setParameter("waffle.servlet.spi.NegotiateSecurityFilterProvider/protocols", "INVALID");
389         try {
390             this.filter.init(filterConfig);
391             Assertions.fail("expected ServletException");
392         } catch (final ServletException e) {
393             Assertions.assertEquals("java.lang.RuntimeException: Unsupported protocol: INVALID", e.getMessage());
394         }
395     }
396 
397     /**
398      * Test init invalid parameter.
399      */
400     @Test
401     void testInitInvalidParameter() {
402         try {
403             final SimpleFilterConfig filterConfig = new SimpleFilterConfig();
404             filterConfig.setParameter("invalidParameter", "random");
405             this.filter.init(filterConfig);
406             Assertions.fail("expected ServletException");
407         } catch (final ServletException e) {
408             Assertions.assertEquals("Invalid parameter: invalidParameter", e.getMessage());
409         }
410     }
411 
412     /**
413      * Test init invalid class in parameter.
414      */
415     @Test
416     void testInitInvalidClassInParameter() {
417         try {
418             final SimpleFilterConfig filterConfig = new SimpleFilterConfig();
419             filterConfig.setParameter("invalidClass/invalidParameter", "random");
420             this.filter.init(filterConfig);
421             Assertions.fail("expected ServletException");
422         } catch (final ServletException e) {
423             Assertions.assertEquals("java.lang.ClassNotFoundException: invalidClass", e.getMessage());
424         }
425     }
426 }