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 */ 019package org.apache.shiro.realm.ldap; 020 021import org.apache.shiro.lang.util.StringUtils; 022import org.slf4j.Logger; 023import org.slf4j.LoggerFactory; 024 025import javax.naming.AuthenticationException; 026import javax.naming.Context; 027import javax.naming.NamingException; 028import javax.naming.ldap.InitialLdapContext; 029import javax.naming.ldap.LdapContext; 030import java.util.HashMap; 031import java.util.Hashtable; 032import java.util.Map; 033 034/** 035 * {@link LdapContextFactory} implementation using the default Sun/Oracle JNDI Ldap API, utilizing JNDI 036 * environment properties and an {@link javax.naming.InitialContext}. 037 * <h2>Configuration</h2> 038 * This class basically wraps a default template JNDI environment properties Map. This properties map is the base 039 * configuration template used to acquire JNDI {@link LdapContext} connections at runtime. The 040 * {@link #getLdapContext(Object, Object)} method implementation merges this default template with other properties 041 * accessible at runtime only (for example per-method principals and credentials). The constructed runtime map is the 042 * one used to acquire the {@link LdapContext}. 043 * <p/> 044 * The template can be configured directly via the {@link #getEnvironment()}/{@link #setEnvironment(java.util.Map)} 045 * properties directly if necessary, but it is usually more convenient to use the supporting wrapper get/set methods 046 * for various environment properties. These wrapper methods interact with the environment 047 * template on your behalf, leaving your configuration cleaner and easier to understand. 048 * <p/> 049 * For example, consider the following two identical configurations: 050 * <pre> 051 * [main] 052 * ldapRealm = org.apache.shiro.realm.ldap.DefaultLdapRealm 053 * ldapRealm.contextFactory.url = ldap://localhost:389 054 * ldapRealm.contextFactory.authenticationMechanism = DIGEST-MD5 055 * </pre> 056 * and 057 * <pre> 058 * [main] 059 * ldapRealm = org.apache.shiro.realm.ldap.DefaultLdapRealm 060 * ldapRealm.contextFactory.environment[java.naming.provider.url] = ldap://localhost:389 061 * ldapRealm.contextFactory.environment[java.naming.security.authentication] = DIGEST-MD5 062 * </pre> 063 * As you can see, the 2nd configuration block is a little more difficult to read and also requires knowledge 064 * of the underlying JNDI Context property keys. The first is easier to read and understand. 065 * <p/> 066 * Note that occasionally it will be necessary to use the latter configuration style to set environment properties 067 * where no corresponding wrapper method exists. In this case, the hybrid approach is still a little easier to read. 068 * For example: 069 * <pre> 070 * [main] 071 * ldapRealm = org.apache.shiro.realm.ldap.DefaultLdapRealm 072 * ldapRealm.contextFactory.url = ldap://localhost:389 073 * ldapRealm.contextFactory.authenticationMechanism = DIGEST-MD5 074 * ldapRealm.contextFactory.environment[some.other.obscure.jndi.key] = some value 075 * </pre> 076 * 077 * @since 1.1 078 */ 079public class JndiLdapContextFactory implements LdapContextFactory { 080 081 /*------------------------------------------- 082 | C O N S T A N T S | 083 ===========================================*/ 084 /** 085 * The Sun LDAP property used to enable connection pooling. This is used in the default implementation 086 * to enable LDAP connection pooling. 087 */ 088 protected static final String SUN_CONNECTION_POOLING_PROPERTY = "com.sun.jndi.ldap.connect.pool"; 089 protected static final String DEFAULT_CONTEXT_FACTORY_CLASS_NAME = "com.sun.jndi.ldap.LdapCtxFactory"; 090 protected static final String SIMPLE_AUTHENTICATION_MECHANISM_NAME = "simple"; 091 protected static final String DEFAULT_REFERRAL = "follow"; 092 093 private static final Logger LOGGER = LoggerFactory.getLogger(JndiLdapContextFactory.class); 094 095 /*------------------------------------------- 096 | I N S T A N C E V A R I A B L E S | 097 ============================================*/ 098 private Map<String, Object> environment; 099 private boolean poolingEnabled; 100 private String systemPassword; 101 private String systemUsername; 102 103 /*------------------------------------------- 104 | C O N S T R U C T O R S | 105 ===========================================*/ 106 107 /** 108 * Default no-argument constructor that initializes the backing {@link #getEnvironment() environment template} with 109 * the {@link #setContextFactoryClassName(String) contextFactoryClassName} equal to 110 * {@code com.sun.jndi.ldap.LdapCtxFactory} (the Sun/Oracle default) and the default 111 * {@link #setReferral(String) referral} behavior to {@code follow}. 112 */ 113 public JndiLdapContextFactory() { 114 this.environment = new HashMap<String, Object>(); 115 setContextFactoryClassName(DEFAULT_CONTEXT_FACTORY_CLASS_NAME); 116 setReferral(DEFAULT_REFERRAL); 117 poolingEnabled = true; 118 } 119 120 /*------------------------------------------- 121 | A C C E S S O R S / M O D I F I E R S | 122 ===========================================*/ 123 124 /** 125 * Sets the type of LDAP authentication mechanism to use when connecting to the LDAP server. 126 * This is a wrapper method for setting the JNDI {@link #getEnvironment() environment template}'s 127 * {@link Context#SECURITY_AUTHENTICATION} property. 128 * <p/> 129 * "none" (i.e. anonymous) and "simple" authentications are supported automatically and don't need to be configured 130 * via this property. However, if you require a different mechanism, such as a SASL or External mechanism, you 131 * must configure that explicitly via this property. See the 132 * <a href="http://download-llnw.oracle.com/javase/tutorial/jndi/ldap/auth_mechs.html">JNDI LDAP 133 * Authentication Mechanisms</a> for more information. 134 * 135 * @param authenticationMechanism the type of LDAP authentication to perform. 136 * @see <a href="http://download-llnw.oracle.com/javase/tutorial/jndi/ldap/auth_mechs.html"> 137 * http://download-llnw.oracle.com/javase/tutorial/jndi/ldap/auth_mechs.html</a> 138 */ 139 public void setAuthenticationMechanism(String authenticationMechanism) { 140 setEnvironmentProperty(Context.SECURITY_AUTHENTICATION, authenticationMechanism); 141 } 142 143 /** 144 * Returns the type of LDAP authentication mechanism to use when connecting to the LDAP server. 145 * This is a wrapper method for getting the JNDI {@link #getEnvironment() environment template}'s 146 * {@link Context#SECURITY_AUTHENTICATION} property. 147 * <p/> 148 * If this property remains un-configured (i.e. {@code null} indicating the 149 * {@link #setAuthenticationMechanism(String)} method wasn't used), this indicates that the default JNDI 150 * "none" (anonymous) and "simple" authentications are supported automatically. Any non-null value returned 151 * represents an explicitly configured mechanism (e.g. a SASL or external mechanism). See the 152 * <a href="http://download-llnw.oracle.com/javase/tutorial/jndi/ldap/auth_mechs.html">JNDI LDAP 153 * Authentication Mechanisms</a> for more information. 154 * 155 * @return the type of LDAP authentication mechanism to use when connecting to the LDAP server. 156 * @see <a href="http://download-llnw.oracle.com/javase/tutorial/jndi/ldap/auth_mechs.html"> 157 * http://download-llnw.oracle.com/javase/tutorial/jndi/ldap/auth_mechs.html</a> 158 */ 159 public String getAuthenticationMechanism() { 160 return (String) getEnvironmentProperty(Context.SECURITY_AUTHENTICATION); 161 } 162 163 /** 164 * The name of the ContextFactory class to use. This defaults to the SUN LDAP JNDI implementation 165 * but can be overridden to use custom LDAP factories. 166 * <p/> 167 * This is a wrapper method for setting the JNDI environment's {@link Context#INITIAL_CONTEXT_FACTORY} property. 168 * 169 * @param contextFactoryClassName the context factory that should be used. 170 */ 171 public void setContextFactoryClassName(String contextFactoryClassName) { 172 setEnvironmentProperty(Context.INITIAL_CONTEXT_FACTORY, contextFactoryClassName); 173 } 174 175 /** 176 * Sets the name of the ContextFactory class to use. This defaults to the SUN LDAP JNDI implementation 177 * but can be overridden to use custom LDAP factories. 178 * <p/> 179 * This is a wrapper method for getting the JNDI environment's {@link Context#INITIAL_CONTEXT_FACTORY} property. 180 * 181 * @return the name of the ContextFactory class to use. 182 */ 183 public String getContextFactoryClassName() { 184 return (String) getEnvironmentProperty(Context.INITIAL_CONTEXT_FACTORY); 185 } 186 187 /** 188 * Returns the base JNDI environment template to use when acquiring an LDAP connection (an {@link LdapContext}). 189 * This property is the base configuration template to use for all connections. This template is then 190 * merged with appropriate runtime values as necessary in the 191 * {@link #getLdapContext(Object, Object)} implementation. The merged environment instance is what is used to 192 * acquire the {@link LdapContext} at runtime. 193 * <p/> 194 * Most other get/set methods in this class act as thin proxy wrappers that interact with this property. The 195 * benefit of using them is you have an easier-to-use configuration mechanism compared to setting map properties 196 * based on JNDI context keys. 197 * 198 * @return the base JNDI environment template to use when acquiring an LDAP connection (an {@link LdapContext}) 199 */ 200 public Map getEnvironment() { 201 return this.environment; 202 } 203 204 /** 205 * Sets the base JNDI environment template to use when acquiring LDAP connections. It is typically more common 206 * to use the other get/set methods in this class to set individual environment settings rather than use 207 * this method, but it is available for advanced users that want full control over the base JNDI environment 208 * settings. 209 * <p/> 210 * Note that this template only represents the base/default environment settings. It is then merged with 211 * appropriate runtime values as necessary in the {@link #getLdapContext(Object, Object)} implementation. 212 * The merged environment instance is what is used to acquire the connection ({@link LdapContext}) at runtime. 213 * 214 * @param env the base JNDI environment template to use when acquiring LDAP connections. 215 */ 216 @SuppressWarnings({"unchecked"}) 217 public void setEnvironment(Map env) { 218 this.environment = env; 219 } 220 221 /** 222 * Returns the environment property value bound under the specified key. 223 * 224 * @param name the name of the environment property 225 * @return the property value or {@code null} if the value has not been set. 226 */ 227 private Object getEnvironmentProperty(String name) { 228 return this.environment.get(name); 229 } 230 231 /** 232 * Will apply the value to the environment attribute if and only if the value is not null or empty. If it is 233 * null or empty, the corresponding environment attribute will be removed. 234 * 235 * @param name the environment property key 236 * @param value the environment property value. A null/empty value will trigger removal. 237 */ 238 private void setEnvironmentProperty(String name, String value) { 239 if (StringUtils.hasText(value)) { 240 this.environment.put(name, value); 241 } else { 242 this.environment.remove(name); 243 } 244 } 245 246 /** 247 * Returns whether or not connection pooling should be used when possible and appropriate. This property is NOT 248 * backed by the {@link #getEnvironment() environment template} like most other properties in this class. It 249 * is a flag to indicate that pooling is preferred. The default value is {@code true}. 250 * <p/> 251 * However, pooling will only actually be enabled if this property is {@code true} <em>and</em> the connection 252 * being created is for the {@link #getSystemUsername() systemUsername} user. Connection pooling is not used for 253 * general authentication attempts by application end-users because the probability of re-use for that same 254 * user-specific connection after an authentication attempt is extremely low. 255 * <p/> 256 * If this attribute is {@code true} and it has been determined that the connection is being made with the 257 * {@link #getSystemUsername() systemUsername}, the 258 * {@link #getLdapContext(Object, Object)} implementation will set the Sun/Oracle-specific 259 * {@code com.sun.jndi.ldap.connect.pool} environment property to "{@code true}". This means setting 260 * this property is only likely to work if using the Sun/Oracle default context factory class (i.e. not using 261 * a custom {@link #getContextFactoryClassName() contextFactoryClassName}). 262 * 263 * @return whether or not connection pooling should be used when possible and appropriate 264 */ 265 public boolean isPoolingEnabled() { 266 return poolingEnabled; 267 } 268 269 /** 270 * Sets whether or not connection pooling should be used when possible and appropriate. This property is NOT 271 * a wrapper to the {@link #getEnvironment() environment template} like most other properties in this class. It 272 * is a flag to indicate that pooling is preferred. The default value is {@code true}. 273 * <p/> 274 * However, pooling will only actually be enabled if this property is {@code true} <em>and</em> the connection 275 * being created is for the {@link #getSystemUsername() systemUsername} user. Connection pooling is not used for 276 * general authentication attempts by application end-users because the probability of re-use for that same 277 * user-specific connection after an authentication attempt is extremely low. 278 * <p/> 279 * If this attribute is {@code true} and it has been determined that the connection is being made with the 280 * {@link #getSystemUsername() systemUsername}, the 281 * {@link #getLdapContext(Object, Object)} implementation will set the Sun/Oracle-specific 282 * {@code com.sun.jndi.ldap.connect.pool} environment property to "{@code true}". This means setting 283 * this property is only likely to work if using the Sun/Oracle default context factory class (i.e. not using 284 * a custom {@link #getContextFactoryClassName() contextFactoryClassName}). 285 * 286 * @param poolingEnabled whether or not connection pooling should be used when possible and appropriate 287 */ 288 public void setPoolingEnabled(boolean poolingEnabled) { 289 this.poolingEnabled = poolingEnabled; 290 } 291 292 /** 293 * Sets the LDAP referral behavior when creating a connection. Defaults to {@code follow}. See the Sun/Oracle LDAP 294 * <a href="http://java.sun.com/products/jndi/tutorial/ldap/referral/jndi.html">referral documentation</a> for more. 295 * 296 * @param referral the referral property. 297 * @see <a href="http://java.sun.com/products/jndi/tutorial/ldap/referral/jndi.html">Referrals in JNDI</a> 298 */ 299 public void setReferral(String referral) { 300 setEnvironmentProperty(Context.REFERRAL, referral); 301 } 302 303 /** 304 * Returns the LDAP referral behavior when creating a connection. Defaults to {@code follow}. 305 * See the Sun/Oracle LDAP 306 * <a href="http://java.sun.com/products/jndi/tutorial/ldap/referral/jndi.html">referral documentation</a> for more. 307 * 308 * @return the LDAP referral behavior when creating a connection. 309 * @see <a href="http://java.sun.com/products/jndi/tutorial/ldap/referral/jndi.html">Referrals in JNDI</a> 310 */ 311 public String getReferral() { 312 return (String) getEnvironmentProperty(Context.REFERRAL); 313 } 314 315 /** 316 * The LDAP url to connect to. (e.g. ldap://<ldapDirectoryHostname>:<port>). This must be configured. 317 * 318 * @param url the LDAP url to connect to. (e.g. ldap://<ldapDirectoryHostname>:<port>) 319 */ 320 public void setUrl(String url) { 321 setEnvironmentProperty(Context.PROVIDER_URL, url); 322 } 323 324 /** 325 * Returns the LDAP url to connect to. (e.g. ldap://<ldapDirectoryHostname>:<port>). 326 * This must be configured. 327 * 328 * @return the LDAP url to connect to. (e.g. ldap://<ldapDirectoryHostname>:<port>) 329 */ 330 public String getUrl() { 331 return (String) getEnvironmentProperty(Context.PROVIDER_URL); 332 } 333 334 /** 335 * Sets the password of the {@link #setSystemUsername(String) systemUsername} that will be used when creating an 336 * LDAP connection used for authorization queries. 337 * <p/> 338 * Note that setting this property is not required if the calling LDAP Realm does not perform authorization 339 * checks. 340 * 341 * @param systemPassword the password of the {@link #setSystemUsername(String) systemUsername} that will be used 342 * when creating an LDAP connection used for authorization queries. 343 */ 344 public void setSystemPassword(String systemPassword) { 345 this.systemPassword = systemPassword; 346 } 347 348 /** 349 * Returns the password of the {@link #setSystemUsername(String) systemUsername} that will be used when creating an 350 * LDAP connection used for authorization queries. 351 * <p/> 352 * Note that setting this property is not required if the calling LDAP Realm does not perform authorization 353 * checks. 354 * 355 * @return the password of the {@link #setSystemUsername(String) systemUsername} that will be used when creating an 356 * LDAP connection used for authorization queries. 357 */ 358 public String getSystemPassword() { 359 return this.systemPassword; 360 } 361 362 /** 363 * Sets the system username that will be used when creating an LDAP connection used for authorization queries. 364 * The user must have the ability to query for authorization data for any application user. 365 * <p/> 366 * Note that setting this property is not required if the calling LDAP Realm does not perform authorization 367 * checks. 368 * 369 * @param systemUsername the system username that will be used when creating an LDAP connection used for 370 * authorization queries. 371 */ 372 public void setSystemUsername(String systemUsername) { 373 this.systemUsername = systemUsername; 374 } 375 376 /** 377 * Returns the system username that will be used when creating an LDAP connection used for authorization queries. 378 * The user must have the ability to query for authorization data for any application user. 379 * <p/> 380 * Note that setting this property is not required if the calling LDAP Realm does not perform authorization 381 * checks. 382 * 383 * @return the system username that will be used when creating an LDAP connection used for authorization queries. 384 */ 385 public String getSystemUsername() { 386 return systemUsername; 387 } 388 389 /*-------------------------------------------- 390 | M E T H O D S | 391 ============================================*/ 392 393 /** 394 * This implementation delegates to {@link #getLdapContext(Object, Object)} using the 395 * {@link #getSystemUsername() systemUsername} and {@link #getSystemPassword() systemPassword} properties as 396 * arguments. 397 * 398 * @return the system LdapContext 399 * @throws NamingException if there is a problem connecting to the LDAP directory 400 */ 401 public LdapContext getSystemLdapContext() throws NamingException { 402 return getLdapContext(getSystemUsername(), getSystemPassword()); 403 } 404 405 /** 406 * Returns {@code true} if LDAP connection pooling should be used when acquiring a connection based on the specified 407 * account principal, {@code false} otherwise. 408 * <p/> 409 * This implementation returns {@code true} only if {@link #isPoolingEnabled()} and the principal equals the 410 * {@link #getSystemUsername()}. The reasoning behind this is that connection pooling is not desirable for 411 * general authentication attempts by application end-users because the probability of re-use for that same 412 * user-specific connection after an authentication attempt is extremely low. 413 * 414 * @param principal the principal under which the connection will be made 415 * @return {@code true} if LDAP connection pooling should be used when acquiring a connection based on the specified 416 * account principal, {@code false} otherwise. 417 */ 418 protected boolean isPoolingConnections(Object principal) { 419 return isPoolingEnabled() && principal != null && principal.equals(getSystemUsername()); 420 } 421 422 /** 423 * This implementation returns an LdapContext based on the configured JNDI/LDAP environment configuration. 424 * The environment (Map) used at runtime is created by merging the default/configured 425 * {@link #getEnvironment() environment template} with some runtime values as necessary (e.g. a principal and 426 * credential available at runtime only). 427 * <p/> 428 * After the merged Map instance is created, the LdapContext connection is 429 * {@link #createLdapContext(java.util.Hashtable) created} and returned. 430 * 431 * @param principal the principal to use when acquiring a connection to the LDAP directory 432 * @param credentials the credentials (password, X.509 certificate, etc.) to use when acquiring a connection to the 433 * LDAP directory 434 * @return the acquired {@code LdapContext} connection bound using the specified principal and credentials. 435 * @throws NamingException 436 * @throws IllegalStateException 437 */ 438 public LdapContext getLdapContext(Object principal, Object credentials) throws NamingException, 439 IllegalStateException { 440 441 String url = getUrl(); 442 if (url == null) { 443 throw new IllegalStateException("An LDAP URL must be specified of the form ldap://<hostname>:<port>"); 444 } 445 446 //copy the environment template into the runtime instance that will be further edited based on 447 //the method arguments and other class attributes. 448 Hashtable<String, Object> env = new Hashtable<String, Object>(this.environment); 449 450 Object authcMech = getAuthenticationMechanism(); 451 if (authcMech == null && (principal != null || credentials != null)) { 452 //authenticationMechanism has not been set, but either a principal and/or credentials were 453 //supplied, indicating that at least a 'simple' authentication attempt is indeed occurring - the Shiro 454 //end-user just didn't configure it explicitly. So we set it to be 'simple' here as a convenience; 455 //the Sun provider implementation already does this same logic, but by repeating that logic here, we ensure 456 //this convenience exists regardless of provider implementation): 457 env.put(Context.SECURITY_AUTHENTICATION, SIMPLE_AUTHENTICATION_MECHANISM_NAME); 458 } 459 if (principal != null) { 460 env.put(Context.SECURITY_PRINCIPAL, principal); 461 } 462 if (credentials != null) { 463 env.put(Context.SECURITY_CREDENTIALS, credentials); 464 } 465 466 boolean pooling = isPoolingConnections(principal); 467 if (pooling) { 468 env.put(SUN_CONNECTION_POOLING_PROPERTY, "true"); 469 } 470 471 if (LOGGER.isDebugEnabled()) { 472 LOGGER.debug("Initializing LDAP context using URL [{}] and principal [{}] with pooling {}", 473 new Object[] {url, principal, (pooling ? "enabled" : "disabled")}); 474 } 475 476 // validate the config before creating the context 477 validateAuthenticationInfo(env); 478 479 return createLdapContext(env); 480 } 481 482 /** 483 * Creates and returns a new {@link javax.naming.ldap.InitialLdapContext} instance. This method exists primarily 484 * to support testing where a mock LdapContext can be returned instead of actually creating a connection, but 485 * subclasses are free to provide a different implementation if necessary. 486 * 487 * @param env the JNDI environment settings used to create the LDAP connection 488 * @return an LdapConnection 489 * @throws NamingException if a problem occurs creating the connection 490 */ 491 protected LdapContext createLdapContext(Hashtable env) throws NamingException { 492 return new InitialLdapContext(env, null); 493 } 494 495 496 /** 497 * Validates the configuration in the JNDI <code>environment</code> settings and throws an exception if a problem 498 * exists. 499 * <p/> 500 * This implementation will throw a {@link AuthenticationException} if the authentication mechanism is set to 501 * 'simple', the principal is non-empty, and the credentials are empty (as per 502 * <a href="http://tools.ietf.org/html/rfc4513#section-5.1.2">rfc4513 section-5.1.2</a>). 503 * 504 * @param environment the JNDI environment settings to be validated 505 * @throws AuthenticationException if a configuration problem is detected 506 */ 507 @SuppressWarnings({"checkstyle:BooleanExpressionComplexity"}) 508 protected void validateAuthenticationInfo(Hashtable<String, Object> environment) 509 throws AuthenticationException { 510 // validate when using Simple auth both principal and credentials are set 511 if (SIMPLE_AUTHENTICATION_MECHANISM_NAME.equals(environment.get(Context.SECURITY_AUTHENTICATION))) { 512 513 // only validate credentials if we have a non-empty principal 514 if (environment.get(Context.SECURITY_PRINCIPAL) != null 515 && StringUtils.hasText(String.valueOf(environment.get(Context.SECURITY_PRINCIPAL)))) { 516 517 Object credentials = environment.get(Context.SECURITY_CREDENTIALS); 518 519 // from the FAQ, we need to check for empty credentials: 520 // http://docs.oracle.com/javase/tutorial/jndi/ldap/faq.html 521 if (credentials == null 522 || (credentials instanceof byte[] && ((byte[]) credentials).length <= 0) 523 || (credentials instanceof char[] && ((char[]) credentials).length <= 0) 524 || (String.class.isInstance(credentials) && !StringUtils.hasText(String.valueOf(credentials)))) { 525 526 throw new javax.naming.AuthenticationException("LDAP Simple authentication requires both a " 527 + "principal and credentials."); 528 } 529 } 530 } 531 } 532 533}