View Javadoc
1   package org.bremersee.spring.security.ldaptive.authentication;
2   
3   import static org.assertj.core.api.Assertions.assertThat;
4   import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
5   import static org.mockito.ArgumentMatchers.any;
6   import static org.mockito.ArgumentMatchers.anyString;
7   import static org.mockito.Mockito.doAnswer;
8   import static org.mockito.Mockito.doReturn;
9   import static org.mockito.Mockito.doThrow;
10  import static org.mockito.Mockito.mock;
11  import static org.mockito.Mockito.spy;
12  import static org.mockito.Mockito.verify;
13  
14  import java.util.ArrayList;
15  import java.util.List;
16  import java.util.Optional;
17  import org.assertj.core.api.Assertions;
18  import org.assertj.core.api.SoftAssertions;
19  import org.assertj.core.api.junit.jupiter.SoftAssertionsExtension;
20  import org.bremersee.ldaptive.DefaultLdaptiveErrorHandler;
21  import org.bremersee.ldaptive.LdaptiveException;
22  import org.bremersee.ldaptive.LdaptiveTemplate;
23  import org.bremersee.spring.security.ldaptive.authentication.provider.ActiveDirectoryTemplate;
24  import org.bremersee.spring.security.ldaptive.authentication.provider.OpenLdapTemplate;
25  import org.bremersee.spring.security.ldaptive.authentication.provider.UserContainsGroupsTemplate;
26  import org.bremersee.spring.security.ldaptive.userdetails.LdaptiveUser;
27  import org.bremersee.spring.security.ldaptive.userdetails.LdaptiveUserDetails;
28  import org.bremersee.spring.security.ldaptive.userdetails.LdaptiveUserDetailsService;
29  import org.junit.jupiter.api.Test;
30  import org.junit.jupiter.api.extension.ExtendWith;
31  import org.ldaptive.CompareRequest;
32  import org.ldaptive.ConnectionConfig;
33  import org.ldaptive.ConnectionFactory;
34  import org.ldaptive.DefaultConnectionFactory;
35  import org.ldaptive.LdapAttribute;
36  import org.ldaptive.LdapEntry;
37  import org.ldaptive.LdapException;
38  import org.ldaptive.ResultCode;
39  import org.mockito.ArgumentCaptor;
40  import org.springframework.security.authentication.AccountExpiredException;
41  import org.springframework.security.authentication.BadCredentialsException;
42  import org.springframework.security.authentication.CredentialsExpiredException;
43  import org.springframework.security.authentication.DisabledException;
44  import org.springframework.security.authentication.LockedException;
45  import org.springframework.security.authentication.RememberMeAuthenticationToken;
46  import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
47  import org.springframework.security.core.GrantedAuthority;
48  import org.springframework.security.core.authority.SimpleGrantedAuthority;
49  import org.springframework.security.core.userdetails.UsernameNotFoundException;
50  import org.springframework.security.crypto.factory.PasswordEncoderFactories;
51  import org.springframework.security.crypto.password.PasswordEncoder;
52  
53  /**
54   * The ldaptive authentication manager test.
55   */
56  @ExtendWith({SoftAssertionsExtension.class})
57  class LdaptiveAuthenticationManagerTest {
58  
59    private static final String USER_BASE_DN = "ou=people,dc=bremersee,dc=org";
60  
61    private static final String USER_DN = "uid=junit,ou=people,dc=bremersee,dc=org";
62  
63    private static final String REMEMBER_ME_KEY = "bremersee";
64  
65    /**
66     * Init.
67     */
68    @Test
69    void init() {
70      UserContainsGroupsTemplate properties = new UserContainsGroupsTemplate();
71      properties.setPasswordAttribute("userPassword");
72      LdaptiveAuthenticationManager target = new LdaptiveAuthenticationManager(
73          new ConnectionConfig("ldap://localhost:389"),
74          properties,
75          REMEMBER_ME_KEY);
76      assertThatExceptionOfType(IllegalStateException.class)
77          .isThrownBy(target::init);
78  
79      target.setPasswordEncoder(PasswordEncoderFactories.createDelegatingPasswordEncoder());
80      target.setEmailToUsernameResolver(null); // has no effect
81      target.setUsernameToBindDnConverter(null); // has no effect
82      target.setAccountControlEvaluator(null); // has no effect
83      target.init();
84    }
85  
86    /**
87     * Supports.
88     */
89    @Test
90    void supports() {
91      LdaptiveAuthenticationManager target = new LdaptiveAuthenticationManager(
92          mock(ConnectionFactory.class),
93          new OpenLdapTemplate(),
94          REMEMBER_ME_KEY);
95      target.init();
96      boolean actual = target.supports(UsernamePasswordAuthenticationToken.class);
97      assertThat(actual)
98          .isTrue();
99    }
100 
101   /**
102    * Is refused username.
103    *
104    * @param softly the softly
105    */
106   @Test
107   void isRefusedUsername(SoftAssertions softly) {
108     OpenLdapTemplate properties = new OpenLdapTemplate();
109     properties.setRefusedUsernames(List.of("junit"));
110     LdaptiveAuthenticationManager target = new LdaptiveAuthenticationManager(
111         mock(ConnectionFactory.class),
112         properties,
113         REMEMBER_ME_KEY);
114     target.init();
115     boolean actual = target.isRefusedUsername("junit");
116     softly.assertThat(actual).isTrue();actual = target.isRefusedUsername("");
117     softly.assertThat(actual).isTrue();
118     actual = target.isRefusedUsername("not-refused");
119     softly.assertThat(actual).isFalse();
120   }
121 
122   /**
123    * Gets ldaptive template.
124    *
125    * @param softly the softly
126    */
127   @Test
128   void getLdaptiveTemplate(SoftAssertions softly) {
129     ConnectionConfig connectionConfig = new ConnectionConfig("ldap://localhost:389");
130     ConnectionFactory connectionFactory = new DefaultConnectionFactory(connectionConfig);
131     LdaptiveTemplate ldaptiveTemplate = new LdaptiveTemplate(connectionFactory);
132     LdaptiveAuthenticationManager target = new LdaptiveAuthenticationManager(
133         ldaptiveTemplate,
134         new ActiveDirectoryTemplate(),
135         REMEMBER_ME_KEY);
136     target.setUsernameToBindDnConverter(username -> username);
137     target.init();
138     softly
139         .assertThat(target.getApplicationLdaptiveTemplate())
140         .isEqualTo(ldaptiveTemplate);
141     softly
142         .assertThat(target.getLdapTemplate("junit", "secret"))
143         .isNotNull();
144   }
145 
146   /**
147    * Gets user details service.
148    */
149   @Test
150   void getUserDetailsService() {
151     LdaptiveAuthenticationManager target = new LdaptiveAuthenticationManager(
152         mock(ConnectionFactory.class),
153         new OpenLdapTemplate(),
154         REMEMBER_ME_KEY);
155     assertThat(target.getUserDetailsService())
156         .isNotNull();
157   }
158 
159   /**
160    * Authenticate with application ldaptive template.
161    *
162    * @param softly the softly
163    */
164   @Test
165   void authenticateWithApplicationLdaptiveTemplate(SoftAssertions softly) {
166     ConnectionConfig connectionConfig = new ConnectionConfig("ldap://localhost:389");
167     ConnectionFactory connectionFactory = new DefaultConnectionFactory(connectionConfig);
168     LdaptiveTemplate ldaptiveTemplate = spy(new LdaptiveTemplate(connectionFactory));
169     UserContainsGroupsTemplate properties = new UserContainsGroupsTemplate();
170     properties.setUserBaseDn(USER_BASE_DN);
171     properties.setPasswordAttribute("userPassword");
172     properties.setAccountControlEvaluator(null);
173     LdaptiveAuthenticationManager target = spy(new LdaptiveAuthenticationManager(
174         ldaptiveTemplate, properties,
175         REMEMBER_ME_KEY));
176     target.setTokenConverter(LdaptiveAuthenticationToken::new);
177 
178     PasswordEncoder passwordEncoder = mock(PasswordEncoder.class);
179     doAnswer(invocationOnMock -> invocationOnMock.getArgument(0))
180         .when(passwordEncoder)
181         .encode(anyString());
182     target.setPasswordEncoder(passwordEncoder);
183 
184     target.init();
185 
186     LdapEntry user = createUser();
187     doReturn(Optional.of(user))
188         .when(ldaptiveTemplate)
189         .findOne(any());
190     ArgumentCaptor<CompareRequest> compareCaptor = ArgumentCaptor.forClass(CompareRequest.class);
191     doReturn(true)
192         .when(ldaptiveTemplate)
193         .compare(compareCaptor.capture());
194 
195     LdaptiveAuthentication actual = target
196         .authenticate(new UsernamePasswordAuthenticationToken("junit", "secret"));
197 
198     softly
199         .assertThat(compareCaptor.getValue())
200         .isNotNull();
201     verify(target)
202         .checkAccountControl(any());
203     assertActual(actual, softly);
204   }
205 
206   /**
207    * Authenticate with application ldaptive template and bad credentials.
208    */
209   @Test
210   void authenticateWithApplicationLdaptiveTemplateAndBadCredentials() {
211     ConnectionConfig connectionConfig = new ConnectionConfig("ldap://localhost:389");
212     ConnectionFactory connectionFactory = new DefaultConnectionFactory(connectionConfig);
213     LdaptiveTemplate ldaptiveTemplate = spy(new LdaptiveTemplate(connectionFactory));
214     UserContainsGroupsTemplate properties = new UserContainsGroupsTemplate();
215     properties.setUserBaseDn(USER_BASE_DN);
216     properties.setPasswordAttribute("userPassword");
217     LdaptiveAuthenticationManager target = new LdaptiveAuthenticationManager(
218         ldaptiveTemplate, properties,
219         REMEMBER_ME_KEY);
220 
221     PasswordEncoder passwordEncoder = mock(PasswordEncoder.class);
222     doAnswer(invocationOnMock -> invocationOnMock.getArgument(0))
223         .when(passwordEncoder)
224         .encode(anyString());
225     target.setPasswordEncoder(passwordEncoder);
226 
227     target.init();
228 
229     LdapEntry user = createUser();
230     doReturn(Optional.of(user))
231         .when(ldaptiveTemplate)
232         .findOne(any());
233     doReturn(false)
234         .when(ldaptiveTemplate)
235         .compare(any());
236 
237     var token = new UsernamePasswordAuthenticationToken("junit", "secret");
238     assertThatExceptionOfType(BadCredentialsException.class)
239         .isThrownBy(() -> target.authenticate(token));
240   }
241 
242   /**
243    * Authenticate with user ldaptive template.
244    *
245    * @param softly the softly
246    */
247   @Test
248   void authenticateWithUserLdaptiveTemplate(SoftAssertions softly) {
249     ConnectionConfig connectionConfig = new ConnectionConfig("ldap://localhost:389");
250     ConnectionFactory connectionFactory = new DefaultConnectionFactory(connectionConfig);
251     LdaptiveTemplate ldaptiveTemplate = spy(new LdaptiveTemplate(connectionFactory));
252     UserContainsGroupsTemplate properties = new UserContainsGroupsTemplate();
253     properties.setUserBaseDn(USER_BASE_DN);
254     LdaptiveAuthenticationManager target = spy(new LdaptiveAuthenticationManager(
255         ldaptiveTemplate, properties, REMEMBER_ME_KEY));
256 
257     target.init();
258 
259     doReturn(ldaptiveTemplate)
260         .when(target)
261         .getLdapTemplate("junit", "secret");
262 
263     LdapEntry user = createUser();
264     doReturn(Optional.of(user))
265         .when(ldaptiveTemplate)
266         .findOne(any());
267 
268     LdaptiveAuthentication actual = target
269         .authenticate(new UsernamePasswordAuthenticationToken("junit", "secret"));
270 
271     verify(target)
272         .checkAccountControl(any());
273     assertActual(actual, softly);
274   }
275 
276   /**
277    * Authenticate with user ldaptive template and username not found.
278    */
279   @Test
280   void authenticateWithUserLdaptiveTemplateAndUsernameNotFound() {
281     ConnectionConfig connectionConfig = new ConnectionConfig("ldap://localhost:389");
282     ConnectionFactory connectionFactory = new DefaultConnectionFactory(connectionConfig);
283     LdaptiveTemplate ldaptiveTemplate = spy(new LdaptiveTemplate(connectionFactory));
284     UserContainsGroupsTemplate properties = new UserContainsGroupsTemplate();
285     properties.setUserBaseDn(USER_BASE_DN);
286     LdaptiveAuthenticationManager target = spy(new LdaptiveAuthenticationManager(
287         ldaptiveTemplate, properties, REMEMBER_ME_KEY));
288 
289     target.init();
290 
291     doReturn(ldaptiveTemplate)
292         .when(target)
293         .getLdapTemplate("junit", "secret");
294     LdaptiveUserDetailsService userDetailsService = mock(LdaptiveUserDetailsService.class);
295     doThrow(new UsernameNotFoundException("junit not found"))
296         .when(userDetailsService)
297         .loadUserByUsername(anyString());
298     doReturn(userDetailsService)
299         .when(target)
300         .getUserDetailsService(any());
301 
302     var token = new UsernamePasswordAuthenticationToken("junit", "secret");
303     assertThatExceptionOfType(UsernameNotFoundException.class)
304         .isThrownBy(() -> target.authenticate(token));
305   }
306 
307   /**
308    * Authenticate with user ldaptive template and bad credentials with result code.
309    */
310   @Test
311   void authenticateWithUserLdaptiveTemplateAndBadCredentialsWithResultCode() {
312     ConnectionConfig connectionConfig = new ConnectionConfig("ldap://localhost:389");
313     ConnectionFactory connectionFactory = new DefaultConnectionFactory(connectionConfig);
314     LdaptiveTemplate ldaptiveTemplate = spy(new LdaptiveTemplate(connectionFactory));
315     UserContainsGroupsTemplate properties = new UserContainsGroupsTemplate();
316     properties.setUserBaseDn(USER_BASE_DN);
317     LdaptiveAuthenticationManager target = spy(new LdaptiveAuthenticationManager(
318         ldaptiveTemplate, properties, REMEMBER_ME_KEY));
319 
320     target.init();
321 
322     doReturn(ldaptiveTemplate)
323         .when(target)
324         .getLdapTemplate("junit", "secret");
325     LdaptiveException ldaptiveException = new DefaultLdaptiveErrorHandler()
326         .map(new LdapException(ResultCode.INVALID_CREDENTIALS, "Bad credentials"));
327     doThrow(ldaptiveException)
328         .when(ldaptiveTemplate)
329         .findOne(any());
330 
331     var token = new UsernamePasswordAuthenticationToken("junit", "secret");
332     assertThatExceptionOfType(BadCredentialsException.class)
333         .isThrownBy(() -> target.authenticate(token));
334   }
335 
336   /**
337    * Authenticate with user ldaptive template and bad credentials with message.
338    */
339   @Test
340   void authenticateWithUserLdaptiveTemplateAndBadCredentialsWithMessage() {
341     ConnectionConfig connectionConfig = new ConnectionConfig("ldap://localhost:389");
342     ConnectionFactory connectionFactory = new DefaultConnectionFactory(connectionConfig);
343     LdaptiveTemplate ldaptiveTemplate = spy(new LdaptiveTemplate(connectionFactory));
344     UserContainsGroupsTemplate properties = new UserContainsGroupsTemplate();
345     properties.setUserBaseDn(USER_BASE_DN);
346     LdaptiveAuthenticationManager target = spy(new LdaptiveAuthenticationManager(
347         ldaptiveTemplate, properties, REMEMBER_ME_KEY));
348 
349     target.init();
350 
351     doReturn(ldaptiveTemplate)
352         .when(target)
353         .getLdapTemplate("junit", "secret");
354     LdaptiveException ldaptiveException = new DefaultLdaptiveErrorHandler()
355         .map(new LdapException(ResultCode.CONNECT_ERROR,
356             "resultCode=" + ResultCode.INVALID_CREDENTIALS));
357     doThrow(ldaptiveException)
358         .when(ldaptiveTemplate)
359         .findOne(any());
360 
361     var token = new UsernamePasswordAuthenticationToken("junit", "secret");
362     assertThatExceptionOfType(BadCredentialsException.class)
363         .isThrownBy(() -> target.authenticate(token));
364   }
365 
366   /**
367    * Authenticate with user ldaptive template and refused username.
368    */
369   @Test
370   void authenticateWithUserLdaptiveTemplateAndRefusedUsername() {
371     ConnectionConfig connectionConfig = new ConnectionConfig("ldap://localhost:389");
372     ConnectionFactory connectionFactory = new DefaultConnectionFactory(connectionConfig);
373     LdaptiveTemplate ldaptiveTemplate = spy(new LdaptiveTemplate(connectionFactory));
374     ActiveDirectoryTemplate properties = new ActiveDirectoryTemplate();
375     properties.setUserBaseDn(USER_BASE_DN);
376     properties.setRefusedUsernames(List.of("refused"));
377     LdaptiveAuthenticationManager target = spy(new LdaptiveAuthenticationManager(
378         ldaptiveTemplate, properties, REMEMBER_ME_KEY));
379 
380     target.init();
381 
382     var token = new UsernamePasswordAuthenticationToken("refused", "secret");
383     assertThatExceptionOfType(DisabledException.class)
384         .isThrownBy(() -> target.authenticate(token));
385   }
386 
387   /**
388    * Check account control.
389    *
390    * @param softly the softly
391    */
392   @Test
393   void checkAccountControl(SoftAssertions softly) {
394     ConnectionConfig connectionConfig = new ConnectionConfig("ldap://localhost:389");
395     ConnectionFactory connectionFactory = new DefaultConnectionFactory(connectionConfig);
396     LdaptiveTemplate ldaptiveTemplate = new LdaptiveTemplate(connectionFactory);
397     UserContainsGroupsTemplate properties = new UserContainsGroupsTemplate();
398     properties.setUserBaseDn(USER_BASE_DN);
399     LdaptiveAuthenticationManager target = new LdaptiveAuthenticationManager(
400         ldaptiveTemplate, properties, REMEMBER_ME_KEY);
401 
402     LdapEntry user = createUser();
403 
404     LdaptiveUserDetails d0 = new LdaptiveUser(
405         user, "junit", null, null, null,
406         List.of(new SimpleGrantedAuthority("ROLE_tester")),
407         "secret",
408         false, true, true, true);
409     softly.assertThatExceptionOfType(AccountExpiredException.class)
410         .isThrownBy(() -> target.checkAccountControl(d0));
411 
412     LdaptiveUserDetails d1 = new LdaptiveUser(
413         user, "junit", null, null, null,
414         List.of(new SimpleGrantedAuthority("ROLE_tester")),
415         "secret",
416         true, false, true, true);
417     softly.assertThatExceptionOfType(LockedException.class)
418         .isThrownBy(() -> target.checkAccountControl(d1));
419 
420     LdaptiveUserDetails d2 = new LdaptiveUser(
421         user, "junit", null, null, null,
422         List.of(new SimpleGrantedAuthority("ROLE_tester")),
423         "secret",
424         true, true, false, true);
425     softly.assertThatExceptionOfType(CredentialsExpiredException.class)
426         .isThrownBy(() -> target.checkAccountControl(d2));
427 
428     LdaptiveUserDetails d3 = new LdaptiveUser(
429         user, "junit", null, null, null,
430         List.of(new SimpleGrantedAuthority("ROLE_tester")),
431         "secret",
432         true, true, true, false);
433     softly.assertThatExceptionOfType(DisabledException.class)
434         .isThrownBy(() -> target.checkAccountControl(d3));
435   }
436 
437   private void assertActual(LdaptiveAuthentication authentication, SoftAssertions softly) {
438     List<GrantedAuthority> actualAuthorities = new ArrayList<>(authentication.getAuthorities());
439     List<GrantedAuthority> expectedAuthorities = List.of(new SimpleGrantedAuthority("ROLE_tester"));
440     softly
441         .assertThat(actualAuthorities)
442         .containsExactlyInAnyOrderElementsOf(expectedAuthorities);
443     softly
444         .assertThat(authentication.getName())
445         .isEqualTo("junit");
446     softly
447         .assertThat(authentication.getPrincipal().getFirstName())
448         .isEqualTo("Test");
449     softly
450         .assertThat(authentication.getPrincipal().getLastName())
451         .isEqualTo("User");
452     softly
453         .assertThat(authentication.getPrincipal().getEmail())
454         .isEqualTo("junit@example.com");
455 
456     assertThat(authentication.getPrincipal())
457         .isInstanceOf(LdaptiveUserDetails.class);
458     LdaptiveUserDetails principal = authentication.getPrincipal();
459     softly
460         .assertThat(principal.getDn())
461         .isEqualTo(USER_DN);
462     softly
463         .assertThat(principal.getName())
464         .isEqualTo("junit");
465     softly
466         .assertThat(principal.getName())
467         .isEqualTo("junit");
468     softly
469         .assertThat(principal.getUsername())
470         .isEqualTo("junit");
471     softly
472         .assertThat(principal.getPassword())
473         .isNotEmpty();
474     softly
475         .assertThat(principal.isEnabled())
476         .isTrue();
477     softly
478         .assertThat(principal.isAccountNonLocked())
479         .isTrue();
480     softly
481         .assertThat(principal.isAccountNonExpired())
482         .isTrue();
483     softly
484         .assertThat(principal.isCredentialsNonExpired())
485         .isTrue();
486   }
487 
488   /**
489    * Remember me authentication throws bad credentials exception with invalid key.
490    */
491   @Test
492   void rememberMeAuthenticationThrowsBadCredentialsExceptionWithInvalidKey() {
493     LdaptiveAuthenticationManager target = new LdaptiveAuthenticationManager(
494         mock(ConnectionFactory.class),
495         new OpenLdapTemplate(),
496         REMEMBER_ME_KEY);
497 
498     RememberMeAuthenticationToken token = mock(RememberMeAuthenticationToken.class);
499     doReturn(0)
500         .when(token)
501         .getKeyHash();
502 
503     Assertions.assertThatExceptionOfType(BadCredentialsException.class)
504         .isThrownBy(() -> target.authenticate(token));
505   }
506 
507   private LdapEntry createUser() {
508     LdapEntry entry = new LdapEntry();
509     entry.setDn(USER_DN);
510     entry.addAttributes(LdapAttribute.builder().name("uid").values("junit").build());
511     entry.addAttributes(LdapAttribute.builder().name("givenName").values("Test").build());
512     entry.addAttributes(LdapAttribute.builder().name("sn").values("User").build());
513     entry.addAttributes(LdapAttribute.builder().name("mail").values("junit@example.com").build());
514     entry.addAttributes(LdapAttribute.builder()
515         .name("memberOf").values("cn=tester," + USER_BASE_DN).build());
516     return entry;
517   }
518 
519 }