View Javadoc
1   /*
2    * Copyright 2014 the original author or authors.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *      http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  package org.bremersee.spring.security.ldaptive.authentication;
18  
19  import static java.util.Objects.isNull;
20  import static java.util.Objects.nonNull;
21  import static java.util.Objects.requireNonNullElseGet;
22  import static org.springframework.util.ObjectUtils.isEmpty;
23  
24  import java.util.Collection;
25  import java.util.Optional;
26  import java.util.stream.Stream;
27  import lombok.AccessLevel;
28  import lombok.Getter;
29  import lombok.Setter;
30  import org.apache.commons.logging.Log;
31  import org.apache.commons.logging.LogFactory;
32  import org.bremersee.ldaptive.DefaultLdaptiveErrorHandler;
33  import org.bremersee.ldaptive.LdaptiveTemplate;
34  import org.bremersee.spring.security.core.EmailToUsernameResolver;
35  import org.bremersee.spring.security.ldaptive.authentication.provider.NoAccountControlEvaluator;
36  import org.bremersee.spring.security.ldaptive.userdetails.LdaptiveRememberMeTokenProvider;
37  import org.bremersee.spring.security.ldaptive.userdetails.LdaptiveUserDetails;
38  import org.bremersee.spring.security.ldaptive.userdetails.LdaptiveUserDetailsService;
39  import org.jspecify.annotations.NonNull;
40  import org.ldaptive.BindOperation;
41  import org.ldaptive.BindResponse;
42  import org.ldaptive.CompareRequest;
43  import org.ldaptive.ConnectionConfig;
44  import org.ldaptive.ConnectionFactory;
45  import org.ldaptive.DefaultConnectionFactory;
46  import org.ldaptive.LdapException;
47  import org.ldaptive.SimpleBindRequest;
48  import org.ldaptive.SingleConnectionFactory;
49  import org.springframework.context.MessageSource;
50  import org.springframework.context.MessageSourceAware;
51  import org.springframework.context.support.MessageSourceAccessor;
52  import org.springframework.core.convert.converter.Converter;
53  import org.springframework.security.authentication.AccountExpiredException;
54  import org.springframework.security.authentication.AuthenticationManager;
55  import org.springframework.security.authentication.AuthenticationProvider;
56  import org.springframework.security.authentication.BadCredentialsException;
57  import org.springframework.security.authentication.CredentialsExpiredException;
58  import org.springframework.security.authentication.DisabledException;
59  import org.springframework.security.authentication.LockedException;
60  import org.springframework.security.authentication.RememberMeAuthenticationToken;
61  import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
62  import org.springframework.security.core.Authentication;
63  import org.springframework.security.core.AuthenticationException;
64  import org.springframework.security.core.SpringSecurityMessageSource;
65  import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
66  import org.springframework.security.crypto.password.PasswordEncoder;
67  import org.springframework.util.Assert;
68  
69  /**
70   * The ldaptive authentication manager.
71   *
72   * @author Christian Bremer
73   */
74  public class LdaptiveAuthenticationManager
75      implements AuthenticationManager, AuthenticationProvider, MessageSourceAware {
76  
77    /**
78     * The Logger.
79     */
80    protected final Log logger = LogFactory.getLog(this.getClass());
81  
82    /**
83     * The authentication properties.
84     */
85    @Getter(AccessLevel.PROTECTED)
86    private final LdaptiveAuthenticationProperties authenticationProperties;
87  
88    /**
89     * The remember-me key.
90     */
91    @Getter(AccessLevel.PROTECTED)
92    private final String rememberMeKey;
93  
94    /**
95     * The application ldaptive template.
96     */
97    @Getter(AccessLevel.PROTECTED)
98    private final LdaptiveTemplate applicationLdaptiveTemplate;
99  
100   /**
101    * The email to username resolver.
102    */
103   @Getter(AccessLevel.PROTECTED)
104   private EmailToUsernameResolver emailToUsernameResolver;
105 
106   /**
107    * The password encoder.
108    */
109   @Getter(AccessLevel.PROTECTED)
110   @Setter
111   private PasswordEncoder passwordEncoder;
112 
113   /**
114    * The account control evaluator.
115    */
116   @Getter(AccessLevel.PROTECTED)
117   private AccountControlEvaluator accountControlEvaluator;
118 
119   /**
120    * The groups mapper.
121    */
122   @Getter(AccessLevel.PROTECTED)
123   @Setter
124   private GrantedAuthoritiesMapper grantedAuthoritiesMapper;
125 
126   /**
127    * The remember-me token provider.
128    */
129   @Getter(AccessLevel.PROTECTED)
130   @Setter
131   private LdaptiveRememberMeTokenProvider passwordProvider;
132 
133   /**
134    * The token converter.
135    */
136   @Getter(AccessLevel.PROTECTED)
137   @Setter
138   private Converter<LdaptiveUserDetails, LdaptiveAuthentication> tokenConverter;
139 
140   /**
141    * The message source.
142    */
143   @Getter(AccessLevel.PROTECTED)
144   private MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
145 
146   /**
147    * Instantiates a new ldaptive authentication manager.
148    *
149    * @param connectionConfig the connection config
150    * @param authenticationProperties the authentication properties
151    * @param rememberMeKey the remember me key
152    */
153   public LdaptiveAuthenticationManager(
154       ConnectionConfig connectionConfig,
155       LdaptiveAuthenticationProperties authenticationProperties,
156       String rememberMeKey) {
157     this(new DefaultConnectionFactory(connectionConfig), authenticationProperties, rememberMeKey);
158   }
159 
160   /**
161    * Instantiates a new ldaptive authentication manager.
162    *
163    * @param connectionFactory the connection factory
164    * @param authenticationProperties the authentication properties
165    * @param rememberMeKey the remember me key
166    */
167   public LdaptiveAuthenticationManager(
168       ConnectionFactory connectionFactory,
169       LdaptiveAuthenticationProperties authenticationProperties,
170       String rememberMeKey) {
171     this(new LdaptiveTemplate(connectionFactory), authenticationProperties, rememberMeKey);
172   }
173 
174   /**
175    * Instantiates a new ldaptive authentication manager.
176    *
177    * @param applicationLdaptiveTemplate the application ldaptive template
178    * @param authenticationProperties the authentication properties
179    * @param rememberMeKey the remember me key
180    */
181   public LdaptiveAuthenticationManager(
182       LdaptiveTemplate applicationLdaptiveTemplate,
183       LdaptiveAuthenticationProperties authenticationProperties,
184       String rememberMeKey) {
185 
186     this.applicationLdaptiveTemplate = applicationLdaptiveTemplate;
187     Assert.notNull(getApplicationLdaptiveTemplate(), "Application ldaptive template is required.");
188     this.authenticationProperties = authenticationProperties;
189     Assert.notNull(getAuthenticationProperties(), "Authentication properties are required.");
190     this.rememberMeKey = rememberMeKey;
191 
192     // emailToUsernameResolver
193     setEmailToUsernameResolver(new EmailToUsernameResolverByLdapAttribute(
194         getAuthenticationProperties(), getApplicationLdaptiveTemplate()));
195 
196     // accountControlEvaluator
197     if (isNull(getAuthenticationProperties().getAccountControlEvaluator())) {
198       setAccountControlEvaluator(new NoAccountControlEvaluator());
199     } else {
200       setAccountControlEvaluator(getAuthenticationProperties().getAccountControlEvaluator().get());
201     }
202   }
203 
204   /**
205    * Sets email to username resolver.
206    *
207    * @param emailToUsernameResolver the email to username resolver
208    */
209   public void setEmailToUsernameResolver(
210       EmailToUsernameResolver emailToUsernameResolver) {
211     if (nonNull(emailToUsernameResolver)) {
212       this.emailToUsernameResolver = emailToUsernameResolver;
213     }
214   }
215 
216   /**
217    * Sets account control evaluator.
218    *
219    * @param accountControlEvaluator the account control evaluator
220    */
221   public void setAccountControlEvaluator(
222       AccountControlEvaluator accountControlEvaluator) {
223     if (nonNull(accountControlEvaluator)) {
224       this.accountControlEvaluator = accountControlEvaluator;
225     }
226   }
227 
228   @Override
229   public void setMessageSource(@NonNull MessageSource messageSource) {
230     this.messages = new MessageSourceAccessor(messageSource);
231   }
232 
233   /**
234    * Init.
235    */
236   public void init() {
237     if (!isSimpleBindAuthentication() && isNull(getPasswordEncoder())) {
238       throw new IllegalStateException(String.format("A password attribute is set (%s) but no "
239               + "password encoder is present. Either delete the password attribute to perform a "
240               + "bind to authenticate or set a password encoder.",
241           getAuthenticationProperties().getPasswordAttribute()));
242     }
243   }
244 
245   @Override
246   public boolean supports(@NonNull Class<?> authentication) {
247     return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication)
248         || isRememberMeAuthentication(authentication);
249   }
250 
251   private boolean isRememberMeAuthentication(Class<?> authentication) {
252     return !isEmpty(getRememberMeKey())
253         && RememberMeAuthenticationToken.class.isAssignableFrom(authentication);
254   }
255 
256   /**
257    * Remember me key matches given authentication.
258    *
259    * @param authentication the authentication
260    * @return the boolean
261    */
262   protected boolean rememberMeKeyMatches(RememberMeAuthenticationToken authentication) {
263     return Optional.ofNullable(getRememberMeKey())
264         .filter(key -> key.hashCode() == authentication.getKeyHash())
265         .isPresent();
266   }
267 
268   @NonNull
269   @Override
270   public Authentication authenticate(Authentication authentication)
271       throws AuthenticationException {
272 
273     if (!supports(authentication.getClass())) {
274       logger.debug(String.format("Authentication [%s] is not supported.",
275           authentication.getClass().getName()));
276       //noinspection DataFlowIssue
277       return null;
278     }
279     if (authentication instanceof RememberMeAuthenticationToken rma) {
280       if (!rememberMeKeyMatches(rma)) {
281         throw new BadCredentialsException(getMessages().getMessage(
282             "RememberMeAuthenticationProvider.incorrectKey",
283             "The presented RememberMeAuthenticationToken does not contain the expected key"));
284       }
285       return rma;
286     }
287 
288     String name = getName(authentication);
289     logger.debug("Authenticating user '" + name + "' ...");
290     String password = Optional.ofNullable(authentication.getCredentials())
291         .map(String::valueOf)
292         .orElseThrow(() -> new BadCredentialsException("Password is required."));
293     String username = getEmailToUsernameResolver()
294         .getUsernameByEmail(name)
295         .orElse(name);
296     if (isRefusedUsername(username)) {
297       throw new DisabledException(String
298           .format("Username '%s' is refused by configuration.", username));
299     }
300 
301     LdaptiveUserDetails userDetails = getUserDetailsService().loadUserByUsername(username);
302     checkPassword(userDetails, password);
303     checkAccountControl(userDetails);
304 
305     if (nonNull(getTokenConverter())) {
306       return getTokenConverter().convert(userDetails);
307     }
308     return new LdaptiveAuthenticationToken(userDetails);
309   }
310 
311   /**
312    * Determines whether the username is refused by configuration.
313    *
314    * @param username the username
315    * @return {@code true} if the username is refused, otherwise {@code false}
316    */
317   protected boolean isRefusedUsername(String username) {
318     if (isEmpty(username)) {
319       return true;
320     }
321     return Stream.ofNullable(getAuthenticationProperties().getRefusedUsernames())
322         .flatMap(Collection::stream)
323         .filter(refusedUsername -> !isEmpty(refusedUsername))
324         .anyMatch(refusedUsername -> refusedUsername.equalsIgnoreCase(username));
325   }
326 
327   /**
328    * Gets name.
329    *
330    * @param authentication the authentication
331    * @return the name
332    */
333   protected String getName(Authentication authentication) {
334     Object principal = authentication.getPrincipal();
335     if (principal instanceof LdaptiveUserDetails ldaptiveUserDetails) {
336       return requireNonNullElseGet(ldaptiveUserDetails.getDn(), authentication::getName);
337     }
338     return authentication.getName();
339   }
340 
341   /**
342    * Gets user details service.
343    *
344    * @return the user details service
345    */
346   public LdaptiveUserDetailsService getUserDetailsService() {
347     LdaptiveUserDetailsService userDetailsService = new LdaptiveUserDetailsService(
348         getAuthenticationProperties(), getApplicationLdaptiveTemplate());
349     userDetailsService.setAccountControlEvaluator(getAccountControlEvaluator());
350     userDetailsService.setGrantedAuthoritiesMapper(getGrantedAuthoritiesMapper());
351     userDetailsService.setRememberMeTokenProvider(getPasswordProvider());
352     return userDetailsService;
353   }
354 
355   /**
356    * Determines whether to bind with username and password or to compare the passwords.
357    *
358    * @return the boolean
359    */
360   protected boolean isSimpleBindAuthentication() {
361     return isNull(getAuthenticationProperties().getPasswordAttribute())
362         || getAuthenticationProperties().getPasswordAttribute().isBlank();
363   }
364 
365   /**
366    * Check password.
367    *
368    * @param user the user
369    * @param password the password
370    */
371   protected void checkPassword(LdaptiveUserDetails user, String password) {
372     if (isSimpleBindAuthentication()) {
373       checkPasswordWithSimpleBind(user, password);
374     } else {
375       checkPasswordWithCompareRequest(user, password);
376     }
377   }
378 
379   /**
380    * Check password with compare request.
381    *
382    * @param user the user
383    * @param password the password
384    */
385   protected void checkPasswordWithCompareRequest(LdaptiveUserDetails user, String password) {
386     Assert.notNull(getPasswordEncoder(), "No password encoder is present.");
387     boolean matches = getApplicationLdaptiveTemplate().compare(CompareRequest.builder()
388         .dn(user.getDn())
389         .name(getAuthenticationProperties().getPasswordAttribute())
390         .value(getPasswordEncoder().encode(password))
391         .build());
392     if (!matches) {
393       throw new BadCredentialsException("Password doesn't match.");
394     }
395   }
396 
397   /**
398    * Check password with simple bind.
399    *
400    * @param user the user
401    * @param password the password
402    */
403   protected void checkPasswordWithSimpleBind(LdaptiveUserDetails user, String password) {
404     SingleConnectionFactory connectionFactory = getSingleConnectionFactory();
405     try {
406       connectionFactory.initialize();
407       BindOperation bind = getBindOperation(connectionFactory);
408       BindResponse response = bind.execute(SimpleBindRequest.builder()
409           .dn(user.getDn())
410           .password(password)
411           .build());
412       if (!response.isSuccess()) {
413         throw new BadCredentialsException("Password doesn't match.");
414       }
415 
416     } catch (LdapException ldapException) {
417       new DefaultLdaptiveErrorHandler().handleError(ldapException);
418     } finally {
419       connectionFactory.close();
420     }
421   }
422 
423   /**
424    * Gets single connection factory.
425    *
426    * @return the single connection factory
427    */
428   SingleConnectionFactory getSingleConnectionFactory() {
429     ConnectionConfig connectionConfig = ConnectionConfig
430         .copy(getApplicationLdaptiveTemplate().getConnectionFactory().getConnectionConfig());
431     connectionConfig.setConnectionInitializers();
432     return new SingleConnectionFactory(connectionConfig);
433   }
434 
435   /**
436    * Gets bind operation.
437    *
438    * @param cf the cf
439    * @return the bind operation
440    */
441   BindOperation getBindOperation(SingleConnectionFactory cf) {
442     return new BindOperation(cf);
443   }
444 
445   /**
446    * Check account control.
447    *
448    * @param user the user
449    */
450   protected void checkAccountControl(LdaptiveUserDetails user) {
451     if (!user.isEnabled()) {
452       throw new DisabledException("Account is disabled.");
453     }
454     if (!user.isAccountNonLocked()) {
455       throw new LockedException("Account is locked.");
456     }
457     if (!user.isAccountNonExpired()) {
458       throw new AccountExpiredException("Account is expired.");
459     }
460     if (!user.isCredentialsNonExpired()) {
461       throw new CredentialsExpiredException("Credentials are expired.");
462     }
463   }
464 
465 }