001/*
002 *  Licensed to the Apache Software Foundation (ASF) under one
003 *  or more contributor license agreements.  See the NOTICE file
004 *  distributed with this work for additional information
005 *  regarding copyright ownership.  The ASF licenses this file
006 *  to you under the Apache License, Version 2.0 (the
007 *  "License"); you may not use this file except in compliance
008 *  with the License.  You may obtain a copy of the License at
009 *
010 *    http://www.apache.org/licenses/LICENSE-2.0
011 *
012 *  Unless required by applicable law or agreed to in writing,
013 *  software distributed under the License is distributed on an
014 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 *  KIND, either express or implied.  See the License for the
016 *  specific language governing permissions and limitations
017 *  under the License.
018 *
019 */
020package org.apache.directory.server.core.authn;
021
022
023import java.net.SocketAddress;
024
025import javax.naming.Context;
026
027import org.apache.commons.collections4.map.LRUMap;
028import org.apache.directory.api.ldap.model.constants.AuthenticationLevel;
029import org.apache.directory.api.ldap.model.constants.SchemaConstants;
030import org.apache.directory.api.ldap.model.entry.Attribute;
031import org.apache.directory.api.ldap.model.entry.Entry;
032import org.apache.directory.api.ldap.model.entry.Value;
033import org.apache.directory.api.ldap.model.exception.LdapAuthenticationException;
034import org.apache.directory.api.ldap.model.exception.LdapException;
035import org.apache.directory.api.ldap.model.name.Dn;
036import org.apache.directory.api.ldap.model.password.PasswordUtil;
037import org.apache.directory.server.core.api.DirectoryService;
038import org.apache.directory.server.core.api.InterceptorEnum;
039import org.apache.directory.server.core.api.LdapPrincipal;
040import org.apache.directory.server.core.api.authn.ppolicy.PasswordPolicyConfiguration;
041import org.apache.directory.server.core.api.authn.ppolicy.PasswordPolicyException;
042import org.apache.directory.server.core.api.entry.ClonedServerEntry;
043import org.apache.directory.server.core.api.interceptor.context.BindOperationContext;
044import org.apache.directory.server.core.api.interceptor.context.LookupOperationContext;
045import org.apache.directory.server.i18n.I18n;
046import org.apache.mina.core.session.IoSession;
047
048
049/**
050 * A simple {@link Authenticator} that authenticates clear text passwords
051 * contained within the <code>userPassword</code> attribute in DIT. If the
052 * password is stored with a one-way encryption applied (e.g. SHA), the password
053 * is hashed the same way before comparison.
054 *
055 * We use a cache to speedup authentication, where the Dn/password are stored.
056 *
057 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
058 */
059public class SimpleAuthenticator extends AbstractAuthenticator
060{
061    /** A speedup for logger in debug mode */
062    private static final boolean IS_DEBUG = LOG.isDebugEnabled();
063
064    /**
065     * A cache to store passwords. It's a speedup, we will be able to avoid backend lookups.
066     *
067     * Note that the backend also use a cache mechanism, but for performance gain, it's good
068     * to manage a cache here. The main problem is that when a user modify his password, we will
069     * have to update it at three different places :
070     * - in the backend,
071     * - in the partition cache,
072     * - in this cache.
073     *
074     * The update of the backend and partition cache is already correctly handled, so we will
075     * just have to offer an access to refresh the local cache.
076     *
077     * We need to be sure that frequently used passwords be always in cache, and not discarded.
078     * We will use a LRU cache for this purpose.
079     */
080    private final LRUMap credentialCache;
081
082    /** Declare a default for this cache. 100 entries seems to be enough */
083    private static final int DEFAULT_CACHE_SIZE = 100;
084
085
086    /**
087     * Creates a new instance.
088     */
089    public SimpleAuthenticator()
090    {
091        super( AuthenticationLevel.SIMPLE );
092        credentialCache = new LRUMap( DEFAULT_CACHE_SIZE );
093    }
094
095
096    /**
097     * Creates a new instance.
098     * @see AbstractAuthenticator
099     *
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                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            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}