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.apache;
25  
26  import static org.assertj.core.api.Assertions.assertThat;
27  
28  import com.sun.jna.platform.win32.Sspi;
29  import com.sun.jna.platform.win32.SspiUtil.ManagedSecBufferDesc;
30  
31  import java.util.Base64;
32  import java.util.Collections;
33  
34  import javax.servlet.ServletException;
35  
36  import mockit.Expectations;
37  import mockit.Mocked;
38  
39  import org.apache.catalina.Context;
40  import org.apache.catalina.Engine;
41  import org.apache.catalina.LifecycleException;
42  import org.apache.catalina.realm.GenericPrincipal;
43  import org.junit.jupiter.api.AfterEach;
44  import org.junit.jupiter.api.Assertions;
45  import org.junit.jupiter.api.BeforeEach;
46  import org.junit.jupiter.api.Test;
47  
48  import waffle.apache.catalina.SimpleHttpRequest;
49  import waffle.apache.catalina.SimpleHttpResponse;
50  import waffle.mock.MockWindowsAuthProvider;
51  import waffle.windows.auth.IWindowsCredentialsHandle;
52  import waffle.windows.auth.IWindowsIdentity;
53  import waffle.windows.auth.PrincipalFormat;
54  import waffle.windows.auth.impl.WindowsAccountImpl;
55  import waffle.windows.auth.impl.WindowsCredentialsHandleImpl;
56  import waffle.windows.auth.impl.WindowsSecurityContextImpl;
57  
58  /**
59   * Waffle Tomcat Mixed Authenticator Test.
60   */
61  class MixedAuthenticatorTest {
62  
63      /** The authenticator. */
64      MixedAuthenticator authenticator;
65  
66      /** The context. */
67      @Mocked
68      Context context;
69  
70      /** The engine. */
71      @Mocked
72      Engine engine;
73  
74      /**
75       * Sets the up.
76       *
77       * @throws LifecycleException
78       *             the lifecycle exception
79       */
80      @BeforeEach
81      void setUp() throws LifecycleException {
82          this.authenticator = new MixedAuthenticator();
83          this.authenticator.setContainer(this.context);
84          Assertions.assertNotNull(new Expectations() {
85              {
86                  MixedAuthenticatorTest.this.context.getParent();
87                  this.result = MixedAuthenticatorTest.this.engine;
88                  MixedAuthenticatorTest.this.context.getParent();
89                  this.result = null;
90              }
91          });
92          this.authenticator.start();
93      }
94  
95      /**
96       * Tear down.
97       *
98       * @throws LifecycleException
99       *             the lifecycle exception
100      */
101     @AfterEach
102     void tearDown() throws LifecycleException {
103         this.authenticator.stop();
104     }
105 
106     /**
107      * Test challenge get.
108      */
109     @Test
110     void testChallengeGET() {
111         final SimpleHttpRequest request = new SimpleHttpRequest();
112         request.setMethod("GET");
113         request.setQueryString("j_negotiate_check");
114         final SimpleHttpResponse response = new SimpleHttpResponse();
115         this.authenticator.authenticate(request, response);
116         final String[] wwwAuthenticates = response.getHeaderValues("WWW-Authenticate");
117         Assertions.assertNotNull(wwwAuthenticates);
118         Assertions.assertEquals(2, wwwAuthenticates.length);
119         Assertions.assertEquals("Negotiate", wwwAuthenticates[0]);
120         Assertions.assertEquals("NTLM", wwwAuthenticates[1]);
121         Assertions.assertEquals("close", response.getHeader("Connection"));
122         Assertions.assertEquals(2, response.getHeaderNames().size());
123         Assertions.assertEquals(401, response.getStatus());
124     }
125 
126     /**
127      * Test challenge post.
128      */
129     @Test
130     void testChallengePOST() {
131         final String securityPackage = "Negotiate";
132         IWindowsCredentialsHandle clientCredentials = null;
133         WindowsSecurityContextImpl clientContext = null;
134         try {
135             // client credentials handle
136             clientCredentials = WindowsCredentialsHandleImpl.getCurrent(securityPackage);
137             clientCredentials.initialize();
138             // initial client security context
139             clientContext = new WindowsSecurityContextImpl();
140             clientContext.setPrincipalName(WindowsAccountImpl.getCurrentUsername());
141             clientContext.setCredentialsHandle(clientCredentials);
142             clientContext.setSecurityPackage(securityPackage);
143             clientContext.initialize(null, null, WindowsAccountImpl.getCurrentUsername());
144             final SimpleHttpRequest request = new SimpleHttpRequest();
145             request.setQueryString("j_negotiate_check");
146             request.setMethod("POST");
147             request.setContentLength(0);
148             final String clientToken = Base64.getEncoder().encodeToString(clientContext.getToken());
149             request.addHeader("Authorization", securityPackage + " " + clientToken);
150             final SimpleHttpResponse response = new SimpleHttpResponse();
151             this.authenticator.authenticate(request, response);
152             Assertions.assertTrue(response.getHeader("WWW-Authenticate").startsWith(securityPackage + " "));
153             Assertions.assertEquals("keep-alive", response.getHeader("Connection"));
154             Assertions.assertEquals(2, response.getHeaderNames().size());
155             Assertions.assertEquals(401, response.getStatus());
156         } finally {
157             if (clientContext != null) {
158                 clientContext.dispose();
159             }
160             if (clientCredentials != null) {
161                 clientCredentials.dispose();
162             }
163         }
164     }
165 
166     /**
167      * Test get.
168      */
169     @Test
170     void testGet() {
171         final SimpleHttpRequest request = new SimpleHttpRequest();
172         final SimpleHttpResponse response = new SimpleHttpResponse();
173         Assertions.assertFalse(this.authenticator.authenticate(request, response));
174     }
175 
176     /**
177      * Test get info.
178      */
179     @Test
180     void testGetInfo() {
181         assertThat(this.authenticator.getInfo()).isNotEmpty();
182     }
183 
184     /**
185      * Test negotiate.
186      */
187     @Test
188     void testNegotiate() {
189         final String securityPackage = "Negotiate";
190         IWindowsCredentialsHandle clientCredentials = null;
191         WindowsSecurityContextImpl clientContext = null;
192         try {
193             // client credentials handle
194             clientCredentials = WindowsCredentialsHandleImpl.getCurrent(securityPackage);
195             clientCredentials.initialize();
196             // initial client security context
197             clientContext = new WindowsSecurityContextImpl();
198             clientContext.setPrincipalName(WindowsAccountImpl.getCurrentUsername());
199             clientContext.setCredentialsHandle(clientCredentials);
200             clientContext.setSecurityPackage(securityPackage);
201             clientContext.initialize(null, null, WindowsAccountImpl.getCurrentUsername());
202             // negotiate
203             boolean authenticated = false;
204             final SimpleHttpRequest request = new SimpleHttpRequest();
205             request.setQueryString("j_negotiate_check");
206             String clientToken;
207             while (true) {
208                 clientToken = Base64.getEncoder().encodeToString(clientContext.getToken());
209                 request.addHeader("Authorization", securityPackage + " " + clientToken);
210 
211                 final SimpleHttpResponse response = new SimpleHttpResponse();
212                 authenticated = this.authenticator.authenticate(request, response);
213 
214                 if (authenticated) {
215                     assertThat(response.getHeaderNames().size()).isNotNegative();
216                     break;
217                 }
218 
219                 Assertions.assertTrue(response.getHeader("WWW-Authenticate").startsWith(securityPackage + " "));
220                 Assertions.assertEquals("keep-alive", response.getHeader("Connection"));
221                 Assertions.assertEquals(2, response.getHeaderNames().size());
222                 Assertions.assertEquals(401, response.getStatus());
223                 final String continueToken = response.getHeader("WWW-Authenticate")
224                         .substring(securityPackage.length() + 1);
225                 final byte[] continueTokenBytes = Base64.getDecoder().decode(continueToken);
226                 assertThat(continueTokenBytes).isNotEmpty();
227                 final ManagedSecBufferDesc continueTokenBuffer = new ManagedSecBufferDesc(Sspi.SECBUFFER_TOKEN,
228                         continueTokenBytes);
229                 clientContext.initialize(clientContext.getHandle(), continueTokenBuffer,
230                         WindowsAccountImpl.getCurrentUsername());
231             }
232             Assertions.assertTrue(authenticated);
233         } finally {
234             if (clientContext != null) {
235                 clientContext.dispose();
236             }
237             if (clientCredentials != null) {
238                 clientCredentials.dispose();
239             }
240         }
241     }
242 
243     /**
244      * Test post security check.
245      */
246     @Test
247     void testPostSecurityCheck() {
248         final SimpleHttpRequest request = new SimpleHttpRequest();
249         request.setQueryString("j_security_check");
250         request.addParameter("j_username", "username");
251         request.addParameter("j_password", "password");
252         final SimpleHttpResponse response = new SimpleHttpResponse();
253         Assertions.assertFalse(this.authenticator.authenticate(request, response));
254     }
255 
256     /**
257      * Test programmatic security BOTH.
258      *
259      * @param identity
260      *            the identity
261      *
262      * @throws ServletException
263      *             the servlet exception
264      */
265     @Test
266     void testProgrammaticSecurityBoth(@Mocked final IWindowsIdentity identity) throws ServletException {
267         this.authenticator.setAuth(new MockWindowsAuthProvider());
268         final SimpleHttpRequest request = new SimpleHttpRequest();
269         request.getMappingData().context = (Context) this.authenticator.getContainer();
270 
271         request.login(WindowsAccountImpl.getCurrentUsername(), "");
272 
273         Assertions.assertNotNull(new Expectations() {
274             {
275                 identity.getFqn();
276                 this.result = "fqn";
277                 identity.getSidString();
278                 this.result = "S-1234";
279             }
280         });
281         request.setUserPrincipal(new GenericWindowsPrincipal(identity, PrincipalFormat.BOTH, PrincipalFormat.BOTH));
282 
283         Assertions.assertTrue(request.getUserPrincipal() instanceof GenericWindowsPrincipal);
284         final GenericWindowsPrincipal windowsPrincipal = (GenericWindowsPrincipal) request.getUserPrincipal();
285         Assertions.assertTrue(windowsPrincipal.getSidString().startsWith("S-"));
286     }
287 
288     /**
289      * Test programmatic security SID.
290      *
291      * @param identity
292      *            the identity
293      *
294      * @throws ServletException
295      *             the servlet exception
296      */
297     @Test
298     void testProgrammaticSecuritySID(@Mocked final IWindowsIdentity identity) throws ServletException {
299         this.authenticator.setAuth(new MockWindowsAuthProvider());
300         final SimpleHttpRequest request = new SimpleHttpRequest();
301         request.getMappingData().context = (Context) this.authenticator.getContainer();
302 
303         request.login(WindowsAccountImpl.getCurrentUsername(), "");
304 
305         Assertions.assertNotNull(new Expectations() {
306             {
307                 identity.getSidString();
308                 this.result = "S-1234";
309             }
310         });
311         request.setUserPrincipal(new GenericWindowsPrincipal(identity, PrincipalFormat.SID, PrincipalFormat.SID));
312 
313         Assertions.assertTrue(request.getUserPrincipal() instanceof GenericWindowsPrincipal);
314         final GenericWindowsPrincipal windowsPrincipal = (GenericWindowsPrincipal) request.getUserPrincipal();
315         Assertions.assertTrue(windowsPrincipal.getSidString().startsWith("S-"));
316     }
317 
318     /**
319      * Test programmatic security NONE.
320      *
321      * @param identity
322      *            the identity
323      *
324      * @throws ServletException
325      *             the servlet exception
326      */
327     @Test
328     void testProgrammaticSecurityNone(@Mocked final IWindowsIdentity identity) throws ServletException {
329         this.authenticator.setAuth(new MockWindowsAuthProvider());
330         final SimpleHttpRequest request = new SimpleHttpRequest();
331         request.getMappingData().context = (Context) this.authenticator.getContainer();
332 
333         request.login(WindowsAccountImpl.getCurrentUsername(), "");
334 
335         request.setUserPrincipal(new GenericWindowsPrincipal(identity, PrincipalFormat.NONE, PrincipalFormat.NONE));
336 
337         Assertions.assertTrue(request.getUserPrincipal() instanceof GenericWindowsPrincipal);
338         final GenericWindowsPrincipal windowsPrincipal = (GenericWindowsPrincipal) request.getUserPrincipal();
339         Assertions.assertNull(windowsPrincipal.getSidString());
340     }
341 
342     /**
343      * Test security check parameters.
344      */
345     @Test
346     void testSecurityCheckParameters() {
347         this.authenticator.setAuth(new MockWindowsAuthProvider());
348         final SimpleHttpRequest request = new SimpleHttpRequest();
349         request.addParameter("j_security_check", "");
350         request.addParameter("j_username", WindowsAccountImpl.getCurrentUsername());
351         request.addParameter("j_password", "");
352         final SimpleHttpResponse response = new SimpleHttpResponse();
353         Assertions.assertTrue(this.authenticator.authenticate(request, response));
354     }
355 
356     /**
357      * Test security check query string.
358      */
359     @Test
360     void testSecurityCheckQueryString() {
361         this.authenticator.setAuth(new MockWindowsAuthProvider());
362         final SimpleHttpRequest request = new SimpleHttpRequest();
363         request.setQueryString("j_security_check");
364         request.addParameter("j_username", WindowsAccountImpl.getCurrentUsername());
365         request.addParameter("j_password", "");
366         final SimpleHttpResponse response = new SimpleHttpResponse();
367         Assertions.assertTrue(this.authenticator.authenticate(request, response));
368     }
369 
370     @Test
371     void testCustomPrincipal() throws LifecycleException {
372         final GenericPrincipal genericPrincipal = new GenericPrincipal("my-principal", "my-password",
373                 Collections.emptyList());
374         final MixedAuthenticator customAuthenticator = new MixedAuthenticator() {
375             @Override
376             protected GenericPrincipal createPrincipal(final IWindowsIdentity windowsIdentity) {
377                 return genericPrincipal;
378             }
379         };
380         try {
381             customAuthenticator.setContainer(this.context);
382             customAuthenticator.setAlwaysUseSession(true);
383             customAuthenticator.start();
384 
385             customAuthenticator.setAuth(new MockWindowsAuthProvider());
386             final SimpleHttpRequest request = new SimpleHttpRequest();
387             request.addParameter("j_security_check", "");
388             request.addParameter("j_username", WindowsAccountImpl.getCurrentUsername());
389             request.addParameter("j_password", "");
390             final SimpleHttpResponse response = new SimpleHttpResponse();
391             Assertions.assertTrue(customAuthenticator.authenticate(request, response));
392 
393             Assertions.assertEquals(genericPrincipal, request.getUserPrincipal());
394         } finally {
395             customAuthenticator.stop();
396         }
397 
398     }
399 
400 }