View Javadoc
1   /*
2    * Copyright 2021 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.security.core.userdetails;
18  
19  import java.util.Collections;
20  import lombok.AccessLevel;
21  import lombok.Getter;
22  import lombok.extern.slf4j.Slf4j;
23  import org.bremersee.data.ldaptive.LdaptiveOperations;
24  import org.ldaptive.CompareRequest;
25  import org.ldaptive.FilterTemplate;
26  import org.ldaptive.LdapEntry;
27  import org.ldaptive.SearchRequest;
28  import org.ldaptive.SearchScope;
29  import org.springframework.security.crypto.password.PasswordEncoder;
30  import org.springframework.util.StringUtils;
31  
32  /**
33   * The ldaptive password matcher.
34   *
35   * @author Christian Bremer
36   */
37  @Slf4j
38  public class LdaptivePasswordMatcher implements PasswordEncoder {
39  
40    @Getter(value = AccessLevel.PROTECTED)
41    private final LdaptiveOperations ldaptiveOperations;
42  
43    @Getter(value = AccessLevel.PROTECTED)
44    private final String userBaseDn;
45  
46    @Getter(value = AccessLevel.PROTECTED)
47    private final String userFindOneFilter;
48  
49    @Getter(value = AccessLevel.PROTECTED)
50    private SearchScope userFindOneSearchScope = SearchScope.ONELEVEL;
51  
52    @Getter(value = AccessLevel.PROTECTED)
53    private String userPasswordAttributeName = "userPassword";
54  
55    @Getter(value = AccessLevel.PROTECTED)
56    private PasswordEncoder delegate = new LdaptivePasswordEncoder();
57  
58    /**
59     * Instantiates a new ldaptive password matcher.
60     *
61     * @param ldaptiveOperations the ldaptive operations
62     * @param userBaseDn the user base dn
63     * @param userFindOneFilter the user find one filter
64     */
65    public LdaptivePasswordMatcher(
66        LdaptiveOperations ldaptiveOperations,
67        String userBaseDn,
68        String userFindOneFilter) {
69  
70      this.ldaptiveOperations = ldaptiveOperations;
71      this.userBaseDn = userBaseDn;
72      this.userFindOneFilter = userFindOneFilter;
73    }
74  
75    /**
76     * Sets user find one search scope.
77     *
78     * @param userFindOneSearchScope the user find one search scope
79     */
80    public void setUserFindOneSearchScope(SearchScope userFindOneSearchScope) {
81      if (userFindOneSearchScope != null) {
82        this.userFindOneSearchScope = userFindOneSearchScope;
83      }
84    }
85  
86    /**
87     * Sets user password attribute name.
88     *
89     * @param userPasswordAttributeName the user password attribute name
90     */
91    public void setUserPasswordAttributeName(String userPasswordAttributeName) {
92      if (StringUtils.hasText(userPasswordAttributeName)) {
93        this.userPasswordAttributeName = userPasswordAttributeName;
94      }
95    }
96  
97    /**
98     * Sets delegate.
99     *
100    * @param delegate the delegate
101    */
102   public void setDelegate(PasswordEncoder delegate) {
103     if (delegate != null) {
104       this.delegate = delegate;
105     }
106   }
107 
108   @Override
109   public String encode(CharSequence rawPassword) {
110     return getDelegate().encode(rawPassword);
111   }
112 
113   /**
114    * Checks whether the given raw password matches the value in the ldap store. Since the password attribute usually
115    * cannot be retrieved and cannot be stored in the user details, the comparison of the passwords is done by the ldap
116    * server. For this reason this password encoder implementation expects here the user name as second parameter instead
117    * of the encoded password from the user details.
118    *
119    * @param rawPassword the raw password
120    * @param userName the user name of the user
121    * @return {@code true} if the raw password matches the password in the ldap store, otherwise {@code false}
122    */
123   @Override
124   public boolean matches(CharSequence rawPassword, String userName) {
125     if (!StringUtils.hasText(userName)) {
126       log.warn("Ldaptive password matcher: password does not match because there is no user name.");
127       return false;
128     }
129     String raw = rawPassword != null ? rawPassword.toString() : "";
130     boolean result = getLdaptiveOperations()
131         .findOne(SearchRequest.builder()
132             .dn(getUserBaseDn())
133             .filter(FilterTemplate.builder()
134                 .filter(getUserFindOneFilter())
135                 .parameters(userName)
136                 .build())
137             .scope(getUserFindOneSearchScope())
138             .returnAttributes(Collections.emptyList())
139             .sizeLimit(1)
140             .build())
141         .map(LdapEntry::getDn)
142         .map(dn -> authenticate(dn, raw))
143         .orElse(false);
144     if (log.isDebugEnabled()) {
145       log.debug("Ldaptive password matcher: password matches for user ({})? {}", userName, result);
146     }
147     return result;
148   }
149 
150   private boolean authenticate(String dn, String rawPassword) {
151     return getLdaptiveOperations().compare(CompareRequest.builder()
152         .dn(dn)
153         .name(getUserPasswordAttributeName())
154         .value(encode(rawPassword))
155         .build());
156   }
157 
158 }