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