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.io.UnsupportedEncodingException;
24  import java.security.MessageDigest;
25  import java.security.NoSuchAlgorithmException;
26  import java.security.SecureRandom;
27  import java.util.Arrays;
28  import java.util.Collection;
29  import java.util.Collections;
30  import java.util.HashSet;
31  import java.util.Set;
32  
33  import javax.naming.Context;
34  import javax.naming.NamingException;
35  
36  import org.apache.commons.collections.map.LRUMap;
37  import org.apache.directory.server.core.authz.AciAuthorizationInterceptor;
38  import org.apache.directory.server.core.authz.DefaultAuthorizationInterceptor;
39  import org.apache.directory.server.core.collective.CollectiveAttributeInterceptor;
40  import org.apache.directory.server.core.entry.ServerEntry;
41  import org.apache.directory.server.core.entry.ServerStringValue;
42  import org.apache.directory.server.core.event.EventInterceptor;
43  import org.apache.directory.server.core.exception.ExceptionInterceptor;
44  import org.apache.directory.server.core.interceptor.context.BindOperationContext;
45  import org.apache.directory.server.core.interceptor.context.LookupOperationContext;
46  import org.apache.directory.server.core.normalization.NormalizationInterceptor;
47  import org.apache.directory.server.core.operational.OperationalAttributeInterceptor;
48  import org.apache.directory.server.core.schema.SchemaInterceptor;
49  import org.apache.directory.server.core.subtree.SubentryInterceptor;
50  import org.apache.directory.server.core.trigger.TriggerInterceptor;
51  import org.apache.directory.shared.ldap.constants.AuthenticationLevel;
52  import org.apache.directory.shared.ldap.constants.LdapSecurityConstants;
53  import org.apache.directory.shared.ldap.constants.SchemaConstants;
54  import org.apache.directory.shared.ldap.entry.EntryAttribute;
55  import org.apache.directory.shared.ldap.entry.Value;
56  import org.apache.directory.shared.ldap.exception.LdapAuthenticationException;
57  import org.apache.directory.shared.ldap.name.LdapDN;
58  import org.apache.directory.shared.ldap.util.ArrayUtils;
59  import org.apache.directory.shared.ldap.util.Base64;
60  import org.apache.directory.shared.ldap.util.StringTools;
61  import org.apache.directory.shared.ldap.util.UnixCrypt;
62  
63  import org.slf4j.Logger;
64  import org.slf4j.LoggerFactory;
65  
66  
67  /**
68   * A simple {@link Authenticator} that authenticates clear text passwords
69   * contained within the <code>userPassword</code> attribute in DIT. If the
70   * password is stored with a one-way encryption applied (e.g. SHA), the password
71   * is hashed the same way before comparison.
72   * 
73   * We use a cache to speedup authentication, where the DN/password are stored.
74   * 
75   * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
76   */
77  public class SimpleAuthenticator extends AbstractAuthenticator
78  {
79      private static final Logger LOG = LoggerFactory.getLogger( SimpleAuthenticator.class );
80      
81      /** A speedup for logger in debug mode */
82      private static final boolean IS_DEBUG = LOG.isDebugEnabled();
83  
84      /**
85       * A cache to store passwords. It's a speedup, we will be able to avoid backend lookups.
86       * 
87       * Note that the backend also use a cache mechanism, but for performance gain, it's good 
88       * to manage a cache here. The main problem is that when a user modify his password, we will
89       * have to update it at three different places :
90       * - in the backend,
91       * - in the partition cache,
92       * - in this cache.
93       * 
94       * The update of the backend and partition cache is already correctly handled, so we will
95       * just have to offer an access to refresh the local cache.
96       * 
97       * We need to be sure that frequently used passwords be always in cache, and not discarded.
98       * We will use a LRU cache for this purpose. 
99       */ 
100     private final LRUMap credentialCache;
101     
102     /** Declare a default for this cache. 100 entries seems to be enough */
103     private static final int DEFAULT_CACHE_SIZE = 100;
104     
105     /**
106      * Define the interceptors we should *not* go through when we will have to request the backend
107      * about a userPassword.
108      */
109     private static final Collection<String> USERLOOKUP_BYPASS;
110     
111     
112     static
113     {
114         Set<String> c = new HashSet<String>();
115         c.add( NormalizationInterceptor.class.getName() );
116         c.add( AuthenticationInterceptor.class.getName() );
117         c.add( AciAuthorizationInterceptor.class.getName() );
118         c.add( DefaultAuthorizationInterceptor.class.getName() );
119         c.add( ExceptionInterceptor.class.getName() );
120         c.add( OperationalAttributeInterceptor.class.getName() );
121         c.add( SchemaInterceptor.class.getName() );
122         c.add( SubentryInterceptor.class.getName() );
123         c.add( CollectiveAttributeInterceptor.class.getName() );
124         c.add( EventInterceptor.class.getName() );
125         c.add( TriggerInterceptor.class.getName() );
126         USERLOOKUP_BYPASS = Collections.unmodifiableCollection( c );
127     }
128 
129 
130     /**
131      * Creates a new instance.
132      * @see AbstractAuthenticator
133      */
134     public SimpleAuthenticator()
135     {
136         super( AuthenticationLevel.SIMPLE.toString() );
137         credentialCache = new LRUMap( DEFAULT_CACHE_SIZE );
138     }
139 
140     
141     /**
142      * Creates a new instance, with an initial cache size
143      * @param cacheSize the size of the credential cache
144      */
145     public SimpleAuthenticator( int cacheSize )
146     {
147         super( AuthenticationLevel.SIMPLE.toString() );
148 
149         credentialCache = new LRUMap( cacheSize > 0 ? cacheSize : DEFAULT_CACHE_SIZE );
150     }
151 
152     
153     /**
154      * A private class to store all informations about the existing
155      * password found in the cache or get from the backend.
156      * 
157      * This is necessary as we have to compute :
158      * - the used algorithm
159      * - the salt if any
160      * - the password itself.
161      * 
162      * If we have a on-way encrypted password, it is stored using this 
163      * format :
164      * {<algorithm>}<encrypted password>
165      * where the encrypted password format can be :
166      * - MD5/SHA : base64([<salt (8 bytes)>]<password>)
167      * - crypt : <salt (2 btytes)><password> 
168      * 
169      * Algorithm are currently MD5, SMD5, SHA, SSHA, CRYPT and empty
170      */
171     private class EncryptionMethod
172     {
173         private byte[] salt;
174         private LdapSecurityConstants algorithm;
175         
176         private EncryptionMethod( LdapSecurityConstants algorithm, byte[] salt )
177         {
178             this.algorithm = algorithm;
179             this.salt = salt;
180         }
181     }
182     
183     
184     /**
185      * Get the password either from cache or from backend.
186      * @param principalDN The DN from which we want the password
187      * @return A byte array which can be empty if the password was not found
188      * @throws NamingException If we have a problem during the lookup operation
189      */
190     private LdapPrincipal getStoredPassword( BindOperationContext opContext ) throws Exception
191     {
192         LdapPrincipal principal = null;
193         
194         synchronized( credentialCache )
195         {
196             principal = ( LdapPrincipal ) credentialCache.get( opContext.getDn().getNormName() );
197         }
198         
199         byte[] storedPassword;
200         
201         if ( principal == null )
202         {
203             // Not found in the cache
204             // Get the user password from the backend
205             storedPassword = lookupUserPassword( opContext );
206             
207             
208             // Deal with the special case where the user didn't enter a password
209             // We will compare the empty array with the credentials. Sometime,
210             // a user does not set a password. This is bad, but there is nothing
211             // we can do against that, except education ...
212             if ( storedPassword == null )
213             {
214                 storedPassword = ArrayUtils.EMPTY_BYTE_ARRAY;
215             }
216 
217             // Create the new principal before storing it in the cache
218             principal = new LdapPrincipal( opContext.getDn(), AuthenticationLevel.SIMPLE, storedPassword );
219             
220             // Now, update the local cache.
221             synchronized( credentialCache )
222             {
223                 credentialCache.put( opContext.getDn().getNormName(), principal );
224             }
225         }
226         
227         return principal;
228     }
229 
230 
231     /**
232      * Looks up <tt>userPassword</tt> attribute of the entry whose name is the
233      * value of {@link Context#SECURITY_PRINCIPAL} environment variable, and
234      * authenticates a user with the plain-text password.
235      * 
236      * We have at least 6 algorithms to encrypt the password :
237      * - SHA
238      * - SSHA (salted SHA)
239      * - MD5
240      * - SMD5 (slated MD5)
241      * - crypt (unix crypt)
242      * - plain text, ie no encryption.
243      * 
244      *  If we get an encrypted password, it is prefixed by the used algorithm, between
245      *  brackets : {SSHA}password ...
246      *  
247      *  If the password is using SSHA, SMD5 or crypt, some 'salt' is added to the password :
248      *  - length(password) - 20, starting at 21th position for SSHA
249      *  - length(password) - 16, starting at 16th position for SMD5
250      *  - length(password) - 2, starting at 3rd position for crypt
251      *  
252      *  For (S)SHA and (S)MD5, we have to transform the password from Base64 encoded text
253      *  to a byte[] before comparing the password with the stored one.
254      *  For crypt, we only have to remove the salt.
255      *  
256      *  At the end, we use the digest() method for (S)SHA and (S)MD5, the crypt() method for
257      *  the CRYPT algorithm and a straight comparison for PLAIN TEXT passwords.
258      *  
259      *  The stored password is always using the unsalted form, and is stored as a bytes array.
260      */
261     public LdapPrincipal authenticate( BindOperationContext opContext ) throws Exception
262     {
263         if ( IS_DEBUG )
264         {
265             LOG.debug( "Authenticating {}", opContext.getDn() );
266         }
267         
268         // ---- extract password from JNDI environment
269         byte[] credentials = opContext.getCredentials();
270         
271         LdapPrincipal principal = getStoredPassword( opContext );
272         
273         // Get the stored password, either from cache or from backend
274         byte[] storedPassword = principal.getUserPassword();
275         
276         // Short circuit for PLAIN TEXT passwords : we compare the byte array directly
277         // Are the passwords equal ?
278         if ( Arrays.equals( credentials, storedPassword ) )
279         {
280             if ( IS_DEBUG )
281             {
282                 LOG.debug( "{} Authenticated", opContext.getDn() );
283             }
284             
285             return principal;
286         }
287         
288         // Let's see if the stored password was encrypted
289         LdapSecurityConstants algorithm = findAlgorithm( storedPassword );
290         
291         if ( algorithm != null )
292         {
293             EncryptionMethod encryptionMethod = new EncryptionMethod( algorithm, null );
294             
295             // Let's get the encrypted part of the stored password
296             // We should just keep the password, excluding the algorithm
297             // and the salt, if any.
298             // But we should also get the algorithm and salt to
299             // be able to encrypt the submitted user password in the next step
300             byte[] encryptedStored = splitCredentials( storedPassword, encryptionMethod );
301             
302             // Reuse the slatedPassword informations to construct the encrypted
303             // password given by the user.
304             byte[] userPassword = encryptPassword( credentials, encryptionMethod );
305             
306             // Now, compare the two passwords.
307             if ( Arrays.equals( userPassword, encryptedStored ) )
308             {
309                 if ( IS_DEBUG )
310                 {
311                     LOG.debug( "{} Authenticated", opContext.getDn() );
312                 }
313 
314                 return principal;
315             }
316             else
317             {
318                 // Bad password ...
319                 String message = "Password not correct for user '" + opContext.getDn().getUpName() + "'";
320                 LOG.info( message );
321                 throw new LdapAuthenticationException(message);
322             }
323         }
324         else
325         {
326             // Bad password ...
327             String message = "Password not correct for user '" + opContext.getDn().getUpName() + "'";
328             LOG.info( message );
329             throw new LdapAuthenticationException(message);
330         }
331     }
332     
333     
334     private static void split( byte[] all, int offset, byte[] left, byte[] right )
335     {
336         System.arraycopy( all, offset, left, 0, left.length );
337         System.arraycopy( all, offset + left.length, right, 0, right.length );
338     }
339 
340     
341     /**
342      * Decopose the stored password in an algorithm, an eventual salt
343      * and the password itself.
344      * 
345      * If the algorithm is SHA, SSHA, MD5 or SMD5, the part following the algorithm
346      * is base64 encoded
347      * 
348      * @param encryptionMethod The structure to feed
349      * @return The password
350      * @param credentials the credentials to split
351      */
352     private byte[] splitCredentials( byte[] credentials, EncryptionMethod encryptionMethod )
353     {
354         int pos = encryptionMethod.algorithm.getName().length() + 2;
355         
356         switch ( encryptionMethod.algorithm )
357         {
358             case HASH_METHOD_MD5 :
359             case HASH_METHOD_SHA :
360                 try
361                 {
362                     // We just have the password just after the algorithm, base64 encoded.
363                     // Just decode the password and return it.
364                     return Base64.decode( new String( credentials, pos, credentials.length - pos, "UTF-8" ).toCharArray() );
365                 }
366                 catch ( UnsupportedEncodingException uee )
367                 {
368                     // do nothing 
369                     return credentials;
370                 }
371                 
372             case HASH_METHOD_SMD5 :
373             case HASH_METHOD_SSHA :
374                 try
375                 {
376                     // The password is associated with a salt. Decompose it 
377                     // in two parts, after having decoded the password.
378                     // The salt will be stored into the EncryptionMethod structure
379                     // The salt is at the end of the credentials, and is 8 bytes long
380                     byte[] passwordAndSalt = Base64.decode( new String( credentials, pos, credentials.length - pos, "UTF-8" ).
381                         toCharArray() );
382                     
383                     encryptionMethod.salt = new byte[8];
384                     byte[] password = new byte[passwordAndSalt.length - encryptionMethod.salt.length];
385                     split( passwordAndSalt, 0, password, encryptionMethod.salt );
386                     
387                     return password;
388                 }
389                 catch ( UnsupportedEncodingException uee )
390                 {
391                     // do nothing 
392                     return credentials;
393                 }
394                 
395             case HASH_METHOD_CRYPT :
396                 // The password is associated with a salt. Decompose it 
397                 // in two parts, storing the salt into the EncryptionMethod structure.
398                 // The salt comes first, not like for SSHA and SMD5, and is 2 bytes long
399                 encryptionMethod.salt = new byte[2];
400                 byte[] password = new byte[credentials.length - encryptionMethod.salt.length - pos];
401                 split( credentials, pos, encryptionMethod.salt, password );
402                 
403                 return password;
404                 
405             default :
406                 // unknown method
407                 return credentials;
408                 
409         }
410     }
411     
412     
413     /**
414      * Get the algorithm from the stored password. 
415      * It can be found on the beginning of the stored password, between 
416      * curly brackets.
417      * @param credentials the credentials of the user
418      * @return the name of the algorithm to use
419      * TODO use an enum for the algorithm
420      */
421     private LdapSecurityConstants findAlgorithm( byte[] credentials )
422     {
423         if ( ( credentials == null ) || ( credentials.length == 0 ) )
424         {
425             return null;
426         }
427         
428         if ( credentials[0] == '{' )
429         {
430             // get the algorithm
431             int pos = 1;
432             
433             while ( pos < credentials.length )
434             {
435                 if ( credentials[pos] == '}' )
436                 {
437                     break;
438                 }
439                 
440                 pos++;
441             }
442             
443             if ( pos < credentials.length )
444             {
445                 if ( pos == 1 )
446                 {
447                     // We don't have an algorithm : return the credentials as is
448                     return null;
449                 }
450                 
451                 String algorithm = new String( credentials, 1, pos - 1 ).toLowerCase();
452                 
453                 return LdapSecurityConstants.getAlgorithm( algorithm );
454             }
455             else
456             {
457                 // We don't have an algorithm
458                 return null;
459             }
460         }
461         else
462         {
463             // No '{algo}' part
464             return null;
465         }
466     }
467 
468     
469     /**
470      * Compute the hashed password given an algorithm, the credentials and 
471      * an optional salt.
472      *
473      * @param algorithm the algorithm to use
474      * @param password the credentials
475      * @param salt the optional salt
476      * @return the digested credentials
477      */
478     private static byte[] digest( LdapSecurityConstants algorithm, byte[] password, byte[] salt )
479     {
480         MessageDigest digest;
481 
482         try
483         {
484             digest = MessageDigest.getInstance( algorithm.getName() );
485         }
486         catch ( NoSuchAlgorithmException e1 )
487         {
488             return null;
489         }
490 
491         if ( salt != null )
492         {
493             digest.update( password );
494             digest.update( salt );
495             return digest.digest();
496         }
497         else
498         {
499             return digest.digest( password );
500         }
501     }
502 
503     
504     private byte[] encryptPassword( byte[] credentials, EncryptionMethod encryptionMethod )
505     {
506         byte[] salt = encryptionMethod.salt;
507         
508         switch ( encryptionMethod.algorithm )
509         {
510             case HASH_METHOD_SHA :
511             case HASH_METHOD_SSHA :
512                 return digest( LdapSecurityConstants.HASH_METHOD_SHA, credentials, salt );
513 
514             case HASH_METHOD_MD5 :
515             case HASH_METHOD_SMD5 :
516                 return digest( LdapSecurityConstants.HASH_METHOD_MD5, credentials, salt );
517 
518             case HASH_METHOD_CRYPT :
519                 if ( salt == null )
520                 {
521                     salt = new byte[2];
522                     SecureRandom sr = new SecureRandom();
523                     int i1 = sr.nextInt( 64 );
524                     int i2 = sr.nextInt( 64 );
525                 
526                     salt[0] = ( byte ) ( i1 < 12 ? ( i1 + '.' ) : i1 < 38 ? ( i1 + 'A' - 12 ) : ( i1 + 'a' - 38 ) );
527                     salt[1] = ( byte ) ( i2 < 12 ? ( i2 + '.' ) : i2 < 38 ? ( i2 + 'A' - 12 ) : ( i2 + 'a' - 38 ) );
528                 }
529 
530                 String saltWithCrypted = UnixCrypt.crypt( StringTools.utf8ToString( credentials ), 
531                     StringTools.utf8ToString( salt ) );
532                 String crypted = saltWithCrypted.substring( 2 );
533                 
534                 return StringTools.getBytesUtf8( crypted );
535                 
536             default :
537                 return credentials;
538         }
539     }
540 
541     
542     /**
543      * Local function which request the password from the backend
544      * @param principalDn the principal to lookup
545      * @return the credentials from the backend
546      * @throws NamingException if there are problems accessing backend
547      */
548     private byte[] lookupUserPassword( BindOperationContext opContext ) throws Exception
549     {
550         // ---- lookup the principal entry's userPassword attribute
551         ServerEntry userEntry;
552 
553         try
554         {
555             /*
556              * NOTE: at this point the BindOperationContext does not has a 
557              * null session since the user has not yet authenticated so we
558              * cannot use opContext.lookup() yet.  This is a very special
559              * case where we cannot rely on the opContext to perform a new
560              * sub operation.
561              */
562             LookupOperationContext lookupContext = 
563                 new LookupOperationContext( getDirectoryService().getAdminSession(), opContext.getDn() );
564             lookupContext.setByPassed( USERLOOKUP_BYPASS );
565             userEntry = getDirectoryService().getOperationManager().lookup( lookupContext );
566 
567             if ( userEntry == null )
568             {
569             	LdapDN dn = opContext.getDn();
570             	String upDn = ( dn == null ? "" : dn.getUpName() );
571             	
572                 throw new LdapAuthenticationException( "Failed to lookup user for authentication: " 
573                     + upDn );
574             }
575         }
576         catch ( Exception cause )
577         {
578             LOG.error( "Authentication error : " + cause.getMessage() );
579             LdapAuthenticationException e = new LdapAuthenticationException( cause.getMessage() );
580             e.setRootCause( e );
581             throw e;
582         }
583 
584         Value<?> userPassword;
585 
586         EntryAttribute userPasswordAttr = userEntry.get( SchemaConstants.USER_PASSWORD_AT );
587 
588         // ---- assert that credentials match
589         if ( userPasswordAttr == null )
590         {
591             return StringTools.EMPTY_BYTES;
592         }
593         else
594         {
595             userPassword = userPasswordAttr.get();
596 
597             if ( userPassword instanceof ServerStringValue )
598             {
599                 return StringTools.getBytesUtf8( (String)userPassword.get() );
600             }
601             else
602             {
603                 return (byte[])userPassword.get();
604             }
605         }
606     }
607 
608     
609     /**
610      * Get the algorithm of a password, which is stored in the form "{XYZ}...".
611      * The method returns null, if the argument is not in this form. It returns
612      * XYZ, if XYZ is an algorithm known to the MessageDigest class of
613      * java.security.
614      * 
615      * @param password a byte[]
616      * @return included message digest alorithm, if any
617      * @throws IllegalArgumentException if the algorithm cannot be identified
618      */
619     protected String getAlgorithmForHashedPassword( byte[] password ) throws IllegalArgumentException
620     {
621         String result = null;
622 
623         // Check if password arg is string or byte[]
624         String sPassword = StringTools.utf8ToString( password );
625         int rightParen = sPassword.indexOf( '}' );
626 
627         if ( ( sPassword.length() > 2 ) &&
628              ( sPassword.charAt( 0 ) == '{' ) &&
629              ( rightParen > -1 ) )
630         {
631             String algorithm = sPassword.substring( 1, rightParen );
632 
633             if ( LdapSecurityConstants.HASH_METHOD_CRYPT.getName().equalsIgnoreCase( algorithm ) )
634             {
635                 return algorithm;
636             }
637             
638             try
639             {
640                 MessageDigest.getInstance( algorithm );
641                 result = algorithm;
642             }
643             catch ( NoSuchAlgorithmException e )
644             {
645                 LOG.warn( "Unknown message digest algorithm in password: " + algorithm, e );
646             }
647         }
648 
649         return result;
650     }
651 
652 
653     /**
654      * Creates a digested password. For a given hash algorithm and a password
655      * value, the algorithm is applied to the password, and the result is Base64
656      * encoded. The method returns a String which looks like "{XYZ}bbbbbbb",
657      * whereas XYZ is the name of the algorithm, and bbbbbbb is the Base64
658      * encoded value of XYZ applied to the password.
659      * 
660      * @param algorithm
661      *            an algorithm which is supported by
662      *            java.security.MessageDigest, e.g. SHA
663      * @param password
664      *            password value, a byte[]
665      * 
666      * @return a digested password, which looks like
667      *         {SHA}LhkDrSoM6qr0fW6hzlfOJQW61tc=
668      * 
669      * @throws IllegalArgumentException
670      *             if password is neither a String nor a byte[], or algorithm is
671      *             not known to java.security.MessageDigest class
672      */
673     protected String createDigestedPassword( String algorithm, byte[] password ) throws IllegalArgumentException
674     {
675         // create message digest object
676         try
677         {
678             if ( LdapSecurityConstants.HASH_METHOD_CRYPT.getName().equalsIgnoreCase( algorithm ) )
679             {
680                 String saltWithCrypted = UnixCrypt.crypt( StringTools.utf8ToString( password ), "" );
681                 String crypted = saltWithCrypted.substring( 2 );
682                 return '{' + algorithm + '}' + Arrays.toString( StringTools.getBytesUtf8( crypted ) );
683             }
684             else
685             {
686                 MessageDigest digest = MessageDigest.getInstance( algorithm );
687                 
688                 // calculate hashed value of password
689                 byte[] fingerPrint = digest.digest( password );
690                 char[] encoded = Base64.encode( fingerPrint );
691 
692                 // create return result of form "{alg}bbbbbbb"
693                 return '{' + algorithm + '}' + new String( encoded );
694             }
695         }
696         catch ( NoSuchAlgorithmException nsae )
697         {
698             LOG.error( "Cannot create a digested password for algorithm '{}'", algorithm );
699             throw new IllegalArgumentException( nsae.getMessage() );
700         }
701     }
702 
703     
704     /**
705      * Remove the principal form the cache. This is used when the user changes
706      * his password.
707      */
708     public void invalidateCache( LdapDN bindDn )
709     {
710         synchronized( credentialCache )
711         {
712             credentialCache.remove( bindDn.getNormName() );
713         }
714     }
715 }