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}