View Javadoc
1   /*
2    *  Licensed to the Apache Software Foundation (ASF) under one
3    *  or more contributor license agreements.  See the NOTICE file
4    *  distributed with this work for additional information
5    *  regarding copyright ownership.  The ASF licenses this file
6    *  to you under the Apache License, Version 2.0 (the
7    *  "License"); you may not use this file except in compliance
8    *  with the License.  You may obtain a copy of the License at
9    *
10   *    http://www.apache.org/licenses/LICENSE-2.0
11   *
12   *  Unless required by applicable law or agreed to in writing,
13   *  software distributed under the License is distributed on an
14   *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   *  KIND, either express or implied.  See the License for the
16   *  specific language governing permissions and limitations
17   *  under the License.
18   *
19   */
20  package org.apache.directory.server.core.authn;
21  
22  
23  import java.net.SocketAddress;
24  
25  import javax.naming.Context;
26  
27  import org.apache.commons.collections4.map.LRUMap;
28  import org.apache.directory.api.ldap.model.constants.AuthenticationLevel;
29  import org.apache.directory.api.ldap.model.constants.SchemaConstants;
30  import org.apache.directory.api.ldap.model.entry.Attribute;
31  import org.apache.directory.api.ldap.model.entry.Entry;
32  import org.apache.directory.api.ldap.model.entry.Value;
33  import org.apache.directory.api.ldap.model.exception.LdapAuthenticationException;
34  import org.apache.directory.api.ldap.model.exception.LdapException;
35  import org.apache.directory.api.ldap.model.name.Dn;
36  import org.apache.directory.api.ldap.model.password.PasswordUtil;
37  import org.apache.directory.server.core.api.DirectoryService;
38  import org.apache.directory.server.core.api.InterceptorEnum;
39  import org.apache.directory.server.core.api.LdapPrincipal;
40  import org.apache.directory.server.core.api.authn.ppolicy.PasswordPolicyConfiguration;
41  import org.apache.directory.server.core.api.authn.ppolicy.PasswordPolicyException;
42  import org.apache.directory.server.core.api.entry.ClonedServerEntry;
43  import org.apache.directory.server.core.api.interceptor.context.BindOperationContext;
44  import org.apache.directory.server.core.api.interceptor.context.LookupOperationContext;
45  import org.apache.directory.server.i18n.I18n;
46  import org.apache.mina.core.session.IoSession;
47  
48  
49  /**
50   * A simple {@link Authenticator} that authenticates clear text passwords
51   * contained within the <code>userPassword</code> attribute in DIT. If the
52   * password is stored with a one-way encryption applied (e.g. SHA), the password
53   * is hashed the same way before comparison.
54   *
55   * We use a cache to speedup authentication, where the Dn/password are stored.
56   *
57   * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
58   */
59  public class SimpleAuthenticator extends AbstractAuthenticator
60  {
61      /** A speedup for logger in debug mode */
62      private static final boolean IS_DEBUG = LOG.isDebugEnabled();
63  
64      /**
65       * A cache to store passwords. It's a speedup, we will be able to avoid backend lookups.
66       *
67       * Note that the backend also use a cache mechanism, but for performance gain, it's good
68       * to manage a cache here. The main problem is that when a user modify his password, we will
69       * have to update it at three different places :
70       * - in the backend,
71       * - in the partition cache,
72       * - in this cache.
73       *
74       * The update of the backend and partition cache is already correctly handled, so we will
75       * just have to offer an access to refresh the local cache.
76       *
77       * We need to be sure that frequently used passwords be always in cache, and not discarded.
78       * We will use a LRU cache for this purpose.
79       */
80      private final LRUMap credentialCache;
81  
82      /** Declare a default for this cache. 100 entries seems to be enough */
83      private static final int DEFAULT_CACHE_SIZE = 100;
84  
85  
86      /**
87       * Creates a new instance.
88       */
89      public SimpleAuthenticator()
90      {
91          super( AuthenticationLevel.SIMPLE );
92          credentialCache = new LRUMap( DEFAULT_CACHE_SIZE );
93      }
94  
95  
96      /**
97       * Creates a new instance.
98       * @see AbstractAuthenticator
99       *
100      * @param baseDn The base Dn
101      */
102     public SimpleAuthenticator( Dn baseDn )
103     {
104         super( AuthenticationLevel.SIMPLE, baseDn );
105         credentialCache = new LRUMap( DEFAULT_CACHE_SIZE );
106     }
107 
108 
109     /**
110      * Creates a new instance, with an initial cache size
111      * @param cacheSize the size of the credential cache
112      */
113     public SimpleAuthenticator( int cacheSize )
114     {
115         super( AuthenticationLevel.SIMPLE, Dn.ROOT_DSE );
116 
117         credentialCache = new LRUMap( cacheSize > 0 ? cacheSize : DEFAULT_CACHE_SIZE );
118     }
119 
120 
121     /**
122      * Creates a new instance, with an initial cache size
123      *
124      * @param cacheSize the size of the credential cache
125      * @param baseDn The base Dn
126      */
127     public SimpleAuthenticator( int cacheSize, Dn baseDn )
128     {
129         super( AuthenticationLevel.SIMPLE, baseDn );
130 
131         credentialCache = new LRUMap( cacheSize > 0 ? cacheSize : DEFAULT_CACHE_SIZE );
132     }
133 
134 
135     /**
136      * Get the password either from cache or from backend.
137      * @param principalDN The Dn from which we want the password
138      * @return A byte array which can be empty if the password was not found
139      * @throws Exception If we have a problem during the lookup operation
140      */
141     private LdapPrincipal getStoredPassword( BindOperationContext bindContext ) throws LdapException
142     {
143         LdapPrincipal principal = null;
144 
145         // use cache only if pwdpolicy is not enabled
146         if ( !getDirectoryService().isPwdPolicyEnabled() )
147         {
148             synchronized ( credentialCache )
149             {
150                 principal = ( LdapPrincipal ) credentialCache.get( bindContext.getDn() );
151             }
152         }
153 
154         byte[][] storedPasswords;
155 
156         if ( principal == null )
157         {
158             // Not found in the cache
159             // Get the user password from the backend
160             storedPasswords = lookupUserPassword( bindContext );
161 
162             // Deal with the special case where the user didn't enter a password
163             // We will compare the empty array with the credentials. Sometime,
164             // a user does not set a password. This is bad, but there is nothing
165             // we can do against that, except education ...
166             if ( storedPasswords == null )
167             {
168                 storedPasswords = new byte[][]
169                     {};
170             }
171 
172             // Create the new principal before storing it in the cache
173             principal = new LdapPrincipal( getDirectoryService().getSchemaManager(), bindContext.getDn(),
174                 AuthenticationLevel.SIMPLE );
175             principal.setUserPassword( storedPasswords );
176 
177             // Now, update the local cache ONLY if pwdpolicy is not enabled.
178             if ( !getDirectoryService().isPwdPolicyEnabled() )
179             {
180                 synchronized ( credentialCache )
181                 {
182                     credentialCache.put( bindContext.getDn().getNormName(), principal );
183                 }
184             }
185         }
186 
187         return principal;
188     }
189 
190 
191     /**
192      * <p>
193      * Looks up <tt>userPassword</tt> attribute of the entry whose name is the
194      * value of {@link Context#SECURITY_PRINCIPAL} environment variable, and
195      * authenticates a user with the plain-text password.
196      * </p>
197      */
198     @Override
199     public LdapPrincipal authenticate( BindOperationContext bindContext ) throws LdapException
200     {
201         if ( IS_DEBUG )
202         {
203             LOG.debug( "Authenticating {}", bindContext.getDn() );
204         }
205 
206         // ---- extract password from JNDI environment
207         byte[] credentials = bindContext.getCredentials();
208 
209         LdapPrincipal principal = getStoredPassword( bindContext );
210 
211         IoSession session = bindContext.getIoSession();
212 
213         if ( session != null )
214         {
215             SocketAddress clientAddress = session.getRemoteAddress();
216             principal.setClientAddress( clientAddress );
217             SocketAddress serverAddress = session.getServiceAddress();
218             principal.setServerAddress( serverAddress );
219         }
220 
221         // Get the stored password, either from cache or from backend
222         byte[][] storedPasswords = principal.getUserPasswords();
223 
224         PasswordPolicyException ppe = null;
225         try
226         {
227             checkPwdPolicy( bindContext.getEntry() );
228         }
229         catch ( PasswordPolicyException e )
230         {
231             ppe = e;
232         }
233 
234         // Now, compare the passwords.
235         for ( byte[] storedPassword : storedPasswords )
236         {
237             if ( PasswordUtil.compareCredentials( credentials, storedPassword ) )
238             {
239                 if ( ppe != null )
240                 {
241                     LOG.debug( "{} Authentication failed: {}", bindContext.getDn(), ppe.getMessage() );
242                     throw ppe;
243                 }
244 
245                 if ( IS_DEBUG )
246                 {
247                     LOG.debug( "{} Authenticated", bindContext.getDn() );
248                 }
249 
250                 return principal;
251             }
252         }
253 
254         // Bad password ...
255         String message = I18n.err( I18n.ERR_230, bindContext.getDn().getName() );
256         LOG.info( message );
257         throw new LdapAuthenticationException( message );
258     }
259 
260 
261     /**
262      * Local function which request the password from the backend
263      * @param bindContext the Bind operation context
264      * @return the credentials from the backend
265      * @throws Exception if there are problems accessing backend
266      */
267     private byte[][] lookupUserPassword( BindOperationContext bindContext ) throws LdapException
268     {
269         // ---- lookup the principal entry's userPassword attribute
270         Entry userEntry;
271 
272         try
273         {
274             /*
275              * NOTE: at this point the BindOperationContext does not has a
276              * null session since the user has not yet authenticated so we
277              * cannot use lookup() yet.  This is a very special
278              * case where we cannot rely on the bindContext to perform a new
279              * sub operation.
280              * We request all the attributes
281              */
282             userEntry = bindContext.getPrincipal();
283             
284             if ( userEntry == null )
285             {
286                 LookupOperationContexttor/context/LookupOperationContext.html#LookupOperationContext">LookupOperationContext lookupContext = new LookupOperationContext( getDirectoryService().getAdminSession(),
287                     bindContext.getDn(), SchemaConstants.ALL_USER_ATTRIBUTES, SchemaConstants.ALL_OPERATIONAL_ATTRIBUTES );
288     
289                 lookupContext.setPartition( bindContext.getPartition() );
290                 lookupContext.setTransaction( bindContext.getTransaction() );
291     
292                 userEntry = getDirectoryService().getPartitionNexus().lookup( lookupContext );
293             }
294 
295             if ( userEntry == null )
296             {
297                 Dn dn = bindContext.getDn();
298                 String upDn = dn == null ? "" : dn.getName();
299 
300                 throw new LdapAuthenticationException( I18n.err( I18n.ERR_231, upDn ) );
301             }
302         }
303         catch ( Exception cause )
304         {
305             LOG.error( I18n.err( I18n.ERR_6, cause.getLocalizedMessage() ) );
306             LdapAuthenticationException e = new LdapAuthenticationException( cause.getLocalizedMessage() );
307             e.initCause( cause );
308             throw e;
309         }
310 
311         DirectoryService directoryService = getDirectoryService();
312         String userPasswordAttribute = SchemaConstants.USER_PASSWORD_AT;
313 
314         if ( directoryService.isPwdPolicyEnabled() )
315         {
316             AuthenticationInterceptorserver/core/authn/AuthenticationInterceptor.html#AuthenticationInterceptor">AuthenticationInterceptor authenticationInterceptor = ( AuthenticationInterceptor ) directoryService
317                 .getInterceptor(
318                 InterceptorEnum.AUTHENTICATION_INTERCEPTOR.getName() );
319             PasswordPolicyConfiguration pPolicyConfig = authenticationInterceptor.getPwdPolicy( userEntry );
320             userPasswordAttribute = pPolicyConfig.getPwdAttribute();
321 
322         }
323 
324         Attribute userPasswordAttr = userEntry.get( userPasswordAttribute );
325 
326         bindContext.setEntry( new ClonedServerEntry( userEntry ) );
327 
328         // ---- assert that credentials match
329         if ( userPasswordAttr == null )
330         {
331             return new byte[][]
332                 {};
333         }
334         else
335         {
336             byte[][] userPasswords = new byte[userPasswordAttr.size()][];
337             int pos = 0;
338 
339             for ( Value userPassword : userPasswordAttr )
340             {
341                 userPasswords[pos] = userPassword.getBytes();
342                 pos++;
343             }
344 
345             return userPasswords;
346         }
347     }
348 
349 
350     /**
351      * Remove the principal form the cache. This is used when the user changes
352      * his password.
353      */
354     @Override
355     public void invalidateCache( Dn bindDn )
356     {
357         synchronized ( credentialCache )
358         {
359             credentialCache.remove( bindDn.getNormName() );
360         }
361     }
362 }