/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.shiro.realm.ldap; import org.apache.shiro.util.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.naming.AuthenticationException; import javax.naming.Context; import javax.naming.NamingException; import javax.naming.ldap.InitialLdapContext; import javax.naming.ldap.LdapContext; import java.util.HashMap; import java.util.Hashtable; import java.util.Map; /** * {@link LdapContextFactory} implementation using the default Sun/Oracle JNDI Ldap API, utilizing JNDI * environment properties and an {@link javax.naming.InitialContext}. * <h2>Configuration</h2> * This class basically wraps a default template JNDI environment properties Map. This properties map is the base * configuration template used to acquire JNDI {@link LdapContext} connections at runtime. The * {@link #getLdapContext(Object, Object)} method implementation merges this default template with other properties * accessible at runtime only (for example per-method principals and credentials). The constructed runtime map is the * one used to acquire the {@link LdapContext}. * <p/> * The template can be configured directly via the {@link #getEnvironment()}/{@link #setEnvironment(java.util.Map)} * properties directly if necessary, but it is usually more convenient to use the supporting wrapper get/set methods * for various environment properties. These wrapper methods interact with the environment * template on your behalf, leaving your configuration cleaner and easier to understand. * <p/> * For example, consider the following two identical configurations: * <pre> * [main] * ldapRealm = org.apache.shiro.realm.ldap.DefaultLdapRealm * ldapRealm.contextFactory.url = ldap://localhost:389 * ldapRealm.contextFactory.authenticationMechanism = DIGEST-MD5 * </pre> * and * <pre> * [main] * ldapRealm = org.apache.shiro.realm.ldap.DefaultLdapRealm * ldapRealm.contextFactory.environment[java.naming.provider.url] = ldap://localhost:389 * ldapRealm.contextFactory.environment[java.naming.security.authentication] = DIGEST-MD5 * </pre> * As you can see, the 2nd configuration block is a little more difficult to read and also requires knowledge * of the underlying JNDI Context property keys. The first is easier to read and understand. * <p/> * Note that occasionally it will be necessary to use the latter configuration style to set environment properties * where no corresponding wrapper method exists. In this case, the hybrid approach is still a little easier to read. * For example: * <pre> * [main] * ldapRealm = org.apache.shiro.realm.ldap.DefaultLdapRealm * ldapRealm.contextFactory.url = ldap://localhost:389 * ldapRealm.contextFactory.authenticationMechanism = DIGEST-MD5 * ldapRealm.contextFactory.environment[some.other.obscure.jndi.key] = some value * </pre> * * @since 1.1 */ public class JndiLdapContextFactory implements LdapContextFactory { /*------------------------------------------- | C O N S T A N T S | ===========================================*/ /** * The Sun LDAP property used to enable connection pooling. This is used in the default implementation * to enable LDAP connection pooling. */ protected static final String SUN_CONNECTION_POOLING_PROPERTY = "com.sun.jndi.ldap.connect.pool"; protected static final String DEFAULT_CONTEXT_FACTORY_CLASS_NAME = "com.sun.jndi.ldap.LdapCtxFactory"; protected static final String SIMPLE_AUTHENTICATION_MECHANISM_NAME = "simple"; protected static final String DEFAULT_REFERRAL = "follow"; private static final Logger log = LoggerFactory.getLogger(JndiLdapContextFactory.class); /*------------------------------------------- | I N S T A N C E V A R I A B L E S | ============================================*/ private Map<String, Object> environment; private boolean poolingEnabled; private String systemPassword; private String systemUsername; /*------------------------------------------- | C O N S T R U C T O R S | ===========================================*/ /** * Default no-argument constructor that initializes the backing {@link #getEnvironment() environment template} with * the {@link #setContextFactoryClassName(String) contextFactoryClassName} equal to * {@code com.sun.jndi.ldap.LdapCtxFactory} (the Sun/Oracle default) and the default * {@link #setReferral(String) referral} behavior to {@code follow}. */ public JndiLdapContextFactory() { this.environment = new HashMap<String, Object>(); setContextFactoryClassName(DEFAULT_CONTEXT_FACTORY_CLASS_NAME); setReferral(DEFAULT_REFERRAL); poolingEnabled = true; } /*------------------------------------------- | A C C E S S O R S / M O D I F I E R S | ===========================================*/ /** * Sets the type of LDAP authentication mechanism to use when connecting to the LDAP server. * This is a wrapper method for setting the JNDI {@link #getEnvironment() environment template}'s * {@link Context#SECURITY_AUTHENTICATION} property. * <p/> * "none" (i.e. anonymous) and "simple" authentications are supported automatically and don't need to be configured * via this property. However, if you require a different mechanism, such as a SASL or External mechanism, you * must configure that explicitly via this property. See the * <a href="http://download-llnw.oracle.com/javase/tutorial/jndi/ldap/auth_mechs.html">JNDI LDAP * Authentication Mechanisms</a> for more information. * * @param authenticationMechanism the type of LDAP authentication to perform. * @see <a href="http://download-llnw.oracle.com/javase/tutorial/jndi/ldap/auth_mechs.html"> * http://download-llnw.oracle.com/javase/tutorial/jndi/ldap/auth_mechs.html</a> */ public void setAuthenticationMechanism(String authenticationMechanism) { setEnvironmentProperty(Context.SECURITY_AUTHENTICATION, authenticationMechanism); } /** * Returns the type of LDAP authentication mechanism to use when connecting to the LDAP server. * This is a wrapper method for getting the JNDI {@link #getEnvironment() environment template}'s * {@link Context#SECURITY_AUTHENTICATION} property. * <p/> * If this property remains un-configured (i.e. {@code null} indicating the * {@link #setAuthenticationMechanism(String)} method wasn't used), this indicates that the default JNDI * "none" (anonymous) and "simple" authentications are supported automatically. Any non-null value returned * represents an explicitly configured mechanism (e.g. a SASL or external mechanism). See the * <a href="http://download-llnw.oracle.com/javase/tutorial/jndi/ldap/auth_mechs.html">JNDI LDAP * Authentication Mechanisms</a> for more information. * * @return the type of LDAP authentication mechanism to use when connecting to the LDAP server. * @see <a href="http://download-llnw.oracle.com/javase/tutorial/jndi/ldap/auth_mechs.html"> * http://download-llnw.oracle.com/javase/tutorial/jndi/ldap/auth_mechs.html</a> */ public String getAuthenticationMechanism() { return (String) getEnvironmentProperty(Context.SECURITY_AUTHENTICATION); } /** * The name of the ContextFactory class to use. This defaults to the SUN LDAP JNDI implementation * but can be overridden to use custom LDAP factories. * <p/> * This is a wrapper method for setting the JNDI environment's {@link Context#INITIAL_CONTEXT_FACTORY} property. * * @param contextFactoryClassName the context factory that should be used. */ public void setContextFactoryClassName(String contextFactoryClassName) { setEnvironmentProperty(Context.INITIAL_CONTEXT_FACTORY, contextFactoryClassName); } /** * Sets the name of the ContextFactory class to use. This defaults to the SUN LDAP JNDI implementation * but can be overridden to use custom LDAP factories. * <p/> * This is a wrapper method for getting the JNDI environment's {@link Context#INITIAL_CONTEXT_FACTORY} property. * * @return the name of the ContextFactory class to use. */ public String getContextFactoryClassName() { return (String) getEnvironmentProperty(Context.INITIAL_CONTEXT_FACTORY); } /** * Returns the base JNDI environment template to use when acquiring an LDAP connection (an {@link LdapContext}). * This property is the base configuration template to use for all connections. This template is then * merged with appropriate runtime values as necessary in the * {@link #getLdapContext(Object, Object)} implementation. The merged environment instance is what is used to * acquire the {@link LdapContext} at runtime. * <p/> * Most other get/set methods in this class act as thin proxy wrappers that interact with this property. The * benefit of using them is you have an easier-to-use configuration mechanism compared to setting map properties * based on JNDI context keys. * * @return the base JNDI environment template to use when acquiring an LDAP connection (an {@link LdapContext}) */ public Map getEnvironment() { return this.environment; } /** * Sets the base JNDI environment template to use when acquiring LDAP connections. It is typically more common * to use the other get/set methods in this class to set individual environment settings rather than use * this method, but it is available for advanced users that want full control over the base JNDI environment * settings. * <p/> * Note that this template only represents the base/default environment settings. It is then merged with * appropriate runtime values as necessary in the {@link #getLdapContext(Object, Object)} implementation. * The merged environment instance is what is used to acquire the connection ({@link LdapContext}) at runtime. * * @param env the base JNDI environment template to use when acquiring LDAP connections. */ @SuppressWarnings({"unchecked"}) public void setEnvironment(Map env) { this.environment = env; } /** * Returns the environment property value bound under the specified key. * * @param name the name of the environment property * @return the property value or {@code null} if the value has not been set. */ private Object getEnvironmentProperty(String name) { return this.environment.get(name); } /** * Will apply the value to the environment attribute if and only if the value is not null or empty. If it is * null or empty, the corresponding environment attribute will be removed. * * @param name the environment property key * @param value the environment property value. A null/empty value will trigger removal. */ private void setEnvironmentProperty(String name, String value) { if (StringUtils.hasText(value)) { this.environment.put(name, value); } else { this.environment.remove(name); } } /** * Returns whether or not connection pooling should be used when possible and appropriate. This property is NOT * backed by the {@link #getEnvironment() environment template} like most other properties in this class. It * is a flag to indicate that pooling is preferred. The default value is {@code true}. * <p/> * However, pooling will only actually be enabled if this property is {@code true} <em>and</em> the connection * being created is for the {@link #getSystemUsername() systemUsername} user. Connection pooling is not used for * general authentication attempts by application end-users because the probability of re-use for that same * user-specific connection after an authentication attempt is extremely low. * <p/> * If this attribute is {@code true} and it has been determined that the connection is being made with the * {@link #getSystemUsername() systemUsername}, the * {@link #getLdapContext(Object, Object)} implementation will set the Sun/Oracle-specific * {@code com.sun.jndi.ldap.connect.pool} environment property to "{@code true}". This means setting * this property is only likely to work if using the Sun/Oracle default context factory class (i.e. not using * a custom {@link #getContextFactoryClassName() contextFactoryClassName}). * * @return whether or not connection pooling should be used when possible and appropriate */ public boolean isPoolingEnabled() { return poolingEnabled; } /** * Sets whether or not connection pooling should be used when possible and appropriate. This property is NOT * a wrapper to the {@link #getEnvironment() environment template} like most other properties in this class. It * is a flag to indicate that pooling is preferred. The default value is {@code true}. * <p/> * However, pooling will only actually be enabled if this property is {@code true} <em>and</em> the connection * being created is for the {@link #getSystemUsername() systemUsername} user. Connection pooling is not used for * general authentication attempts by application end-users because the probability of re-use for that same * user-specific connection after an authentication attempt is extremely low. * <p/> * If this attribute is {@code true} and it has been determined that the connection is being made with the * {@link #getSystemUsername() systemUsername}, the * {@link #getLdapContext(Object, Object)} implementation will set the Sun/Oracle-specific * {@code com.sun.jndi.ldap.connect.pool} environment property to "{@code true}". This means setting * this property is only likely to work if using the Sun/Oracle default context factory class (i.e. not using * a custom {@link #getContextFactoryClassName() contextFactoryClassName}). * * @param poolingEnabled whether or not connection pooling should be used when possible and appropriate */ public void setPoolingEnabled(boolean poolingEnabled) { this.poolingEnabled = poolingEnabled; } /** * Sets the LDAP referral behavior when creating a connection. Defaults to {@code follow}. See the Sun/Oracle LDAP * <a href="http://java.sun.com/products/jndi/tutorial/ldap/referral/jndi.html">referral documentation</a> for more. * * @param referral the referral property. * @see <a href="http://java.sun.com/products/jndi/tutorial/ldap/referral/jndi.html">Referrals in JNDI</a> */ public void setReferral(String referral) { setEnvironmentProperty(Context.REFERRAL, referral); } /** * Returns the LDAP referral behavior when creating a connection. Defaults to {@code follow}. * See the Sun/Oracle LDAP * <a href="http://java.sun.com/products/jndi/tutorial/ldap/referral/jndi.html">referral documentation</a> for more. * * @return the LDAP referral behavior when creating a connection. * @see <a href="http://java.sun.com/products/jndi/tutorial/ldap/referral/jndi.html">Referrals in JNDI</a> */ public String getReferral() { return (String) getEnvironmentProperty(Context.REFERRAL); } /** * The LDAP url to connect to. (e.g. ldap://<ldapDirectoryHostname>:<port>). This must be configured. * * @param url the LDAP url to connect to. (e.g. ldap://<ldapDirectoryHostname>:<port>) */ public void setUrl(String url) { setEnvironmentProperty(Context.PROVIDER_URL, url); } /** * Returns the LDAP url to connect to. (e.g. ldap://<ldapDirectoryHostname>:<port>). * This must be configured. * * @return the LDAP url to connect to. (e.g. ldap://<ldapDirectoryHostname>:<port>) */ public String getUrl() { return (String) getEnvironmentProperty(Context.PROVIDER_URL); } /** * Sets the password of the {@link #setSystemUsername(String) systemUsername} that will be used when creating an * LDAP connection used for authorization queries. * <p/> * Note that setting this property is not required if the calling LDAP Realm does not perform authorization * checks. * * @param systemPassword the password of the {@link #setSystemUsername(String) systemUsername} that will be used * when creating an LDAP connection used for authorization queries. */ public void setSystemPassword(String systemPassword) { this.systemPassword = systemPassword; } /** * Returns the password of the {@link #setSystemUsername(String) systemUsername} that will be used when creating an * LDAP connection used for authorization queries. * <p/> * Note that setting this property is not required if the calling LDAP Realm does not perform authorization * checks. * * @return the password of the {@link #setSystemUsername(String) systemUsername} that will be used when creating an * LDAP connection used for authorization queries. */ public String getSystemPassword() { return this.systemPassword; } /** * Sets the system username that will be used when creating an LDAP connection used for authorization queries. * The user must have the ability to query for authorization data for any application user. * <p/> * Note that setting this property is not required if the calling LDAP Realm does not perform authorization * checks. * * @param systemUsername the system username that will be used when creating an LDAP connection used for * authorization queries. */ public void setSystemUsername(String systemUsername) { this.systemUsername = systemUsername; } /** * Returns the system username that will be used when creating an LDAP connection used for authorization queries. * The user must have the ability to query for authorization data for any application user. * <p/> * Note that setting this property is not required if the calling LDAP Realm does not perform authorization * checks. * * @return the system username that will be used when creating an LDAP connection used for authorization queries. */ public String getSystemUsername() { return systemUsername; } /*-------------------------------------------- | M E T H O D S | ============================================*/ /** * This implementation delegates to {@link #getLdapContext(Object, Object)} using the * {@link #getSystemUsername() systemUsername} and {@link #getSystemPassword() systemPassword} properties as * arguments. * * @return the system LdapContext * @throws NamingException if there is a problem connecting to the LDAP directory */ public LdapContext getSystemLdapContext() throws NamingException { return getLdapContext((Object)getSystemUsername(), getSystemPassword()); } /** * Deprecated - use {@link #getLdapContext(Object, Object)} instead. This will be removed before Apache Shiro 2.0. * * @param username the username to use when creating the connection. * @param password the password to use when creating the connection. * @return a {@code LdapContext} bound using the given username and password. * @throws javax.naming.NamingException if there is an error creating the context. * @deprecated the {@link #getLdapContext(Object, Object)} method should be used in all cases to ensure more than * String principals and credentials can be used. Shiro no longer calls this method - it will be * removed before the 2.0 release. */ @Deprecated public LdapContext getLdapContext(String username, String password) throws NamingException { return getLdapContext((Object) username, password); } /** * Returns {@code true} if LDAP connection pooling should be used when acquiring a connection based on the specified * account principal, {@code false} otherwise. * <p/> * This implementation returns {@code true} only if {@link #isPoolingEnabled()} and the principal equals the * {@link #getSystemUsername()}. The reasoning behind this is that connection pooling is not desirable for * general authentication attempts by application end-users because the probability of re-use for that same * user-specific connection after an authentication attempt is extremely low. * * @param principal the principal under which the connection will be made * @return {@code true} if LDAP connection pooling should be used when acquiring a connection based on the specified * account principal, {@code false} otherwise. */ protected boolean isPoolingConnections(Object principal) { return isPoolingEnabled() && principal != null && principal.equals(getSystemUsername()); } /** * This implementation returns an LdapContext based on the configured JNDI/LDAP environment configuration. * The environnmet (Map) used at runtime is created by merging the default/configured * {@link #getEnvironment() environment template} with some runtime values as necessary (e.g. a principal and * credential available at runtime only). * <p/> * After the merged Map instance is created, the LdapContext connection is * {@link #createLdapContext(java.util.Hashtable) created} and returned. * * @param principal the principal to use when acquiring a connection to the LDAP directory * @param credentials the credentials (password, X.509 certificate, etc) to use when acquiring a connection to the * LDAP directory * @return the acquired {@code LdapContext} connection bound using the specified principal and credentials. * @throws NamingException * @throws IllegalStateException */ public LdapContext getLdapContext(Object principal, Object credentials) throws NamingException, IllegalStateException { String url = getUrl(); if (url == null) { throw new IllegalStateException("An LDAP URL must be specified of the form ldap://<hostname>:<port>"); } //copy the environment template into the runtime instance that will be further edited based on //the method arguments and other class attributes. Hashtable<String, Object> env = new Hashtable<String, Object>(this.environment); Object authcMech = getAuthenticationMechanism(); if (authcMech == null && (principal != null || credentials != null)) { //authenticationMechanism has not been set, but either a principal and/or credentials were //supplied, indicating that at least a 'simple' authentication attempt is indeed occurring - the Shiro //end-user just didn't configure it explicitly. So we set it to be 'simple' here as a convenience; //the Sun provider implementation already does this same logic, but by repeating that logic here, we ensure //this convenience exists regardless of provider implementation): env.put(Context.SECURITY_AUTHENTICATION, SIMPLE_AUTHENTICATION_MECHANISM_NAME); } if (principal != null) { env.put(Context.SECURITY_PRINCIPAL, principal); } if (credentials != null) { env.put(Context.SECURITY_CREDENTIALS, credentials); } boolean pooling = isPoolingConnections(principal); if (pooling) { env.put(SUN_CONNECTION_POOLING_PROPERTY, "true"); } if (log.isDebugEnabled()) { log.debug("Initializing LDAP context using URL [{}] and principal [{}] with pooling {}", new Object[]{url, principal, (pooling ? "enabled" : "disabled")}); } // validate the config before creating the context validateAuthenticationInfo(env); return createLdapContext(env); } /** * Creates and returns a new {@link javax.naming.ldap.InitialLdapContext} instance. This method exists primarily * to support testing where a mock LdapContext can be returned instead of actually creating a connection, but * subclasses are free to provide a different implementation if necessary. * * @param env the JNDI environment settings used to create the LDAP connection * @return an LdapConnection * @throws NamingException if a problem occurs creating the connection */ protected LdapContext createLdapContext(Hashtable env) throws NamingException { return new InitialLdapContext(env, null); } /** * Validates the configuration in the JNDI <code>environment</code> settings and throws an exception if a problem * exists. * <p/> * This implementation will throw a {@link AuthenticationException} if the authentication mechanism is set to * 'simple', the principal is non-empty, and the credentials are empty (as per * <a href="http://tools.ietf.org/html/rfc4513#section-5.1.2">rfc4513 section-5.1.2</a>). * * @param environment the JNDI environment settings to be validated * @throws AuthenticationException if a configuration problem is detected */ protected void validateAuthenticationInfo(Hashtable<String, Object> environment) throws AuthenticationException { // validate when using Simple auth both principal and credentials are set if(SIMPLE_AUTHENTICATION_MECHANISM_NAME.equals(environment.get(Context.SECURITY_AUTHENTICATION))) { // only validate credentials if we have a non-empty principal if( environment.get(Context.SECURITY_PRINCIPAL) != null && StringUtils.hasText( String.valueOf( environment.get(Context.SECURITY_PRINCIPAL) ))) { Object credentials = environment.get(Context.SECURITY_CREDENTIALS); // from the FAQ, we need to check for empty credentials: // http://docs.oracle.com/javase/tutorial/jndi/ldap/faq.html if( credentials == null || (credentials instanceof byte[] && ((byte[])credentials).length <= 0) || // empty byte[] (credentials instanceof char[] && ((char[])credentials).length <= 0) || // empty char[] (String.class.isInstance(credentials) && !StringUtils.hasText(String.valueOf(credentials)))) { throw new javax.naming.AuthenticationException("LDAP Simple authentication requires both a " + "principal and credentials."); } } } } }