View Javadoc
1   /*
2    * Copyright 2024-2026 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.userdetails;
18  
19  import static java.util.Objects.nonNull;
20  import static org.springframework.util.ObjectUtils.isEmpty;
21  
22  import java.util.Collection;
23  import java.util.Objects;
24  import java.util.Optional;
25  import java.util.Set;
26  import java.util.function.Predicate;
27  import java.util.regex.Pattern;
28  import java.util.stream.Collectors;
29  import java.util.stream.Stream;
30  import lombok.AccessLevel;
31  import lombok.Getter;
32  import org.apache.commons.logging.Log;
33  import org.apache.commons.logging.LogFactory;
34  import org.bremersee.ldaptive.LdaptiveAttribute;
35  import org.bremersee.ldaptive.LdaptiveTemplate;
36  import org.bremersee.spring.security.ldaptive.authentication.AccountControlEvaluator;
37  import org.bremersee.spring.security.ldaptive.authentication.LdaptiveAuthenticationProperties;
38  import org.bremersee.spring.security.ldaptive.authentication.provider.NoAccountControlEvaluator;
39  import org.ldaptive.FilterTemplate;
40  import org.ldaptive.LdapAttribute;
41  import org.ldaptive.LdapEntry;
42  import org.ldaptive.SearchRequest;
43  import org.ldaptive.dn.Dn;
44  import org.ldaptive.dn.NameValue;
45  import org.ldaptive.dn.RDn;
46  import org.springframework.security.core.GrantedAuthority;
47  import org.springframework.security.core.authority.SimpleGrantedAuthority;
48  import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
49  import org.springframework.security.core.authority.mapping.SimpleAuthorityMapper;
50  import org.springframework.security.core.userdetails.UserDetailsService;
51  import org.springframework.security.core.userdetails.UsernameNotFoundException;
52  import org.springframework.util.Assert;
53  
54  /**
55   * The ldaptive user details service.
56   *
57   * @author Christian Bremer
58   */
59  @Getter(AccessLevel.PROTECTED)
60  public class LdaptiveUserDetailsService implements UserDetailsService {
61  
62    /**
63     * The constant USERNAME_PLACEHOLDER.
64     */
65    protected static final String USERNAME_PLACEHOLDER = "${username}";
66  
67    /**
68     * The logger.
69     */
70    private final Log logger = LogFactory.getLog(this.getClass());
71  
72    /**
73     * The authentication properties.
74     */
75    private final LdaptiveAuthenticationProperties authenticationProperties;
76  
77    /**
78     * The ldaptive template.
79     */
80    private final LdaptiveTemplate ldaptiveTemplate;
81  
82    /**
83     * The groups mapper.
84     */
85    private GrantedAuthoritiesMapper grantedAuthoritiesMapper;
86  
87    /**
88     * The account control evaluator.
89     */
90    private AccountControlEvaluator accountControlEvaluator = new NoAccountControlEvaluator();
91  
92    /**
93     * The remember-me token provider.
94     */
95    private LdaptiveRememberMeTokenProvider rememberMeTokenProvider;
96  
97    /**
98     * Instantiates a ldaptive user details service.
99     *
100    * @param authenticationProperties the authentication properties
101    * @param ldaptiveTemplate the ldaptive template
102    */
103   public LdaptiveUserDetailsService(
104       LdaptiveAuthenticationProperties authenticationProperties,
105       LdaptiveTemplate ldaptiveTemplate) {
106 
107     this.authenticationProperties = authenticationProperties;
108     this.ldaptiveTemplate = ldaptiveTemplate;
109     Assert.notNull(getAuthenticationProperties(), "Authentication properties are required.");
110     Assert.notNull(getLdaptiveTemplate(), "Ldaptive template is required.");
111     setGrantedAuthoritiesMapper(new SimpleAuthorityMapper());
112     if (nonNull(authenticationProperties.getAccountControlEvaluator())) {
113       setAccountControlEvaluator(authenticationProperties.getAccountControlEvaluator().get());
114     }
115     if (isEmpty(getAuthenticationProperties().getPasswordLastSetAttribute())) {
116       setRememberMeTokenProvider(
117           new LdaptiveEvaluatedRememberMeTokenProvider(getAccountControlEvaluator()));
118     } else {
119       setRememberMeTokenProvider(
120           new LdaptivePwdLastSetRememberMeTokenProvider(getAccountControlEvaluator(),
121               getAuthenticationProperties().getPasswordLastSetAttribute()));
122     }
123   }
124 
125   /**
126    * Sets groups mapper.
127    *
128    * @param grantedAuthoritiesMapper the groups mapper
129    */
130   public void setGrantedAuthoritiesMapper(
131       GrantedAuthoritiesMapper grantedAuthoritiesMapper) {
132     if (nonNull(grantedAuthoritiesMapper)) {
133       this.grantedAuthoritiesMapper = grantedAuthoritiesMapper;
134     }
135   }
136 
137   /**
138    * Sets account control evaluator.
139    *
140    * @param accountControlEvaluator the account control evaluator
141    */
142   public void setAccountControlEvaluator(
143       AccountControlEvaluator accountControlEvaluator) {
144     if (nonNull(accountControlEvaluator)) {
145       this.accountControlEvaluator = accountControlEvaluator;
146     }
147   }
148 
149   /**
150    * Sets remember-me token provider.
151    *
152    * @param rememberMeTokenProvider the remember-me token provider
153    */
154   public void setRememberMeTokenProvider(LdaptiveRememberMeTokenProvider rememberMeTokenProvider) {
155     if (nonNull(rememberMeTokenProvider)) {
156       this.rememberMeTokenProvider = rememberMeTokenProvider;
157     }
158   }
159 
160   @Override
161   public LdaptiveUserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
162     logger.debug(String.format("Loading user '%s' ...", username));
163     LdapEntry ldapEntry = findUser(username)
164         .orElseThrow(() -> new UsernameNotFoundException(String
165             .format("User '%s' was  not found.", username)));
166     Collection<? extends GrantedAuthority> authorities = getAuthorities(ldapEntry);
167     var userDetailsBuilder = LdaptiveUserDetails.builder()
168         .dn(ldapEntry.getDn())
169         .username(LdaptiveAttribute.define(getAuthenticationProperties().getUsernameAttribute())
170             .getValue(ldapEntry)
171             .orElse(username))
172         .password(getRememberMeTokenProvider().getRememberMeToken(ldapEntry))
173         .accountNonExpired(getAccountControlEvaluator().isAccountNonExpired(ldapEntry))
174         .accountNonLocked(getAccountControlEvaluator().isAccountNonLocked(ldapEntry))
175         .credentialsNonExpired(getAccountControlEvaluator().isCredentialsNonExpired(ldapEntry))
176         .enabled(getAccountControlEvaluator().isEnabled(ldapEntry))
177         .authorities(authorities);
178     LdaptiveAttribute.define(getAuthenticationProperties().getFirstNameAttribute())
179         .getValue(ldapEntry)
180         .ifPresent(userDetailsBuilder::firstName);
181     LdaptiveAttribute.define(getAuthenticationProperties().getLastNameAttribute())
182         .getValue(ldapEntry)
183         .ifPresent(userDetailsBuilder::lastName);
184     LdaptiveAttribute.define(getAuthenticationProperties().getEmailAttribute())
185         .getValue(ldapEntry)
186         .ifPresent(userDetailsBuilder::email);
187     return userDetailsBuilder.build();
188   }
189 
190   /**
191    * Determines whether the given username is a distinguished name or not.
192    *
193    * @param username the username
194    * @return {@code true} if the username is a distinguished name, otherwise {@code false}
195    */
196   protected boolean isDn(String username) {
197     try {
198       Dn baseDn = new Dn(getAuthenticationProperties().getUserBaseDn());
199       return Optional.ofNullable(username)
200           .map(Dn::new)
201           .filter(baseDn::isAncestor)
202           .isPresent();
203 
204     } catch (RuntimeException e) {
205       return false;
206     }
207   }
208 
209   /**
210    * Find user.
211    *
212    * @param username the username
213    * @return the user
214    */
215   public Optional<LdapEntry> findUser(String username) {
216     SearchRequest searchRequest = isDn(username)
217         ? SearchRequest.objectScopeSearchRequest(username)
218         : SearchRequest.builder()
219             .dn(getAuthenticationProperties().getUserBaseDn())
220             .filter(FilterTemplate.builder()
221                 .filter(getAuthenticationProperties().getUserFindOneFilter())
222                 .parameters(username)
223                 .build())
224             .scope(getAuthenticationProperties().getUserFindOneSearchScope())
225             .sizeLimit(1)
226             .build();
227     return getLdaptiveTemplate().findOne(searchRequest);
228   }
229 
230   /**
231    * Gets authorities.
232    *
233    * @param user the user
234    * @return the authorities
235    */
236   public Collection<GrantedAuthority> getAuthorities(LdapEntry user) {
237 
238     return switch (getAuthenticationProperties().getGroupFetchStrategy()) {
239       case NONE -> Set.of();
240       case USER_CONTAINS_GROUPS -> getAuthoritiesByGroupsInUser(user);
241       case GROUP_CONTAINS_USERS -> getAuthoritiesByGroupsWithUser(user);
242     };
243   }
244 
245   /**
246    * Gets roles by groups in user.
247    *
248    * @param user the user
249    * @return the roles by groups in user
250    */
251   protected Collection<GrantedAuthority> getAuthoritiesByGroupsInUser(LdapEntry user) {
252     Collection<GrantedAuthority> authorities = Stream.ofNullable(user)
253         .map(entry -> entry.getAttribute(getAuthenticationProperties().getMemberAttribute()))
254         .filter(Objects::nonNull)
255         .map(LdapAttribute::getStringValues)
256         .filter(Objects::nonNull)
257         .flatMap(Collection::stream)
258         .flatMap(memberOfValue -> getGroupName(memberOfValue).stream())
259         .map(SimpleGrantedAuthority::new)
260         .collect(Collectors.toSet());
261     return Set.copyOf(getGrantedAuthoritiesMapper().mapAuthorities(authorities));
262   }
263 
264   private static Optional<String> getGroupName(String groupNameOrGroupDn) {
265     return getDn(groupNameOrGroupDn)
266         .map(Dn::getRDn)
267         .map(RDn::getNameValue)
268         .map(NameValue::getStringValue)
269         .or(() -> Optional.ofNullable(groupNameOrGroupDn))
270         .filter(Predicate.not(String::isBlank));
271   }
272 
273   private static Optional<Dn> getDn(String dn) {
274     if (isEmpty(dn)) {
275       return Optional.empty();
276     }
277     try {
278       return Optional.of(new Dn(dn));
279     } catch (RuntimeException e) {
280       return Optional.empty();
281     }
282   }
283 
284   /**
285    * Gets roles by groups with user.
286    *
287    * @param user the user
288    * @return the roles by groups with user
289    */
290   protected Collection<GrantedAuthority> getAuthoritiesByGroupsWithUser(LdapEntry user) {
291     Collection<GrantedAuthority> authorities = getLdaptiveTemplate()
292         .findAll(
293             SearchRequest.builder()
294                 .dn(getAuthenticationProperties().getGroupBaseDn())
295                 .filter(FilterTemplate.builder()
296                     .filter(getAuthorityFilter(user))
297                     .build())
298                 .scope(getAuthenticationProperties().getGroupSearchScope())
299                 .build())
300         .stream()
301         .map(this::getAuthorityName)
302         .map(SimpleGrantedAuthority::new)
303         .collect(Collectors.toSet());
304     return Set.copyOf(getGrantedAuthoritiesMapper().mapAuthorities(authorities));
305   }
306 
307   /**
308    * Gets group filter.
309    *
310    * @param user the user
311    * @return the group filter
312    */
313   protected String getAuthorityFilter(LdapEntry user) {
314     String groupObjectClass = getAuthenticationProperties().getGroupObjectClass();
315     String groupMemberAttribute = getAuthenticationProperties().getGroupMemberAttribute();
316     String groupMemberValue;
317     String groupMemberFormat = getAuthenticationProperties().getGroupMemberFormat();
318     if (isEmpty(groupMemberFormat)) {
319       groupMemberValue = user.getDn();
320     } else {
321       String username = user.getAttribute(getAuthenticationProperties().getUsernameAttribute())
322           .getStringValue();
323       groupMemberValue = groupMemberFormat
324           .replaceFirst(Pattern.quote(USERNAME_PLACEHOLDER), username);
325     }
326     return String.format("(&(objectClass=%s)(%s=%s))",
327         groupObjectClass, groupMemberAttribute, groupMemberValue);
328   }
329 
330   /**
331    * Gets group name.
332    *
333    * @param group the group
334    * @return the group name
335    */
336   protected String getAuthorityName(LdapEntry group) {
337     String groupIdAttribute = getAuthenticationProperties().getGroupIdAttribute();
338     String fallback = getGroupName(group.getDn()).orElse(null);
339     if (isEmpty(groupIdAttribute)) {
340       return fallback;
341     }
342     return Optional.ofNullable(group.getAttribute(groupIdAttribute))
343         .map(LdapAttribute::getStringValue)
344         .orElse(fallback);
345   }
346 
347 }