Java tutorial
/** * Licensed 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. See accompanying LICENSE file. */ package org.apache.hadoop.security.authentication.server; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.Hashtable; import java.util.Properties; import javax.naming.Context; import javax.naming.NamingException; import javax.naming.directory.InitialDirContext; import javax.naming.ldap.InitialLdapContext; import javax.naming.ldap.LdapContext; import javax.naming.ldap.StartTlsRequest; import javax.naming.ldap.StartTlsResponse; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLSession; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.commons.codec.binary.Base64; import org.apache.hadoop.classification.InterfaceAudience; import org.apache.hadoop.classification.InterfaceStability; import org.apache.hadoop.security.authentication.client.AuthenticationException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; /** * The {@link LdapAuthenticationHandler} implements the BASIC authentication * mechanism for HTTP using LDAP back-end. * * The supported configuration properties are: * <ul> * <li>ldap.providerurl: The url of the LDAP server. It does not have a default * value.</li> * <li>ldap.basedn: the base distinguished name (DN) to be used with the LDAP * server. This value is appended to the provided user id for authentication * purpose. It does not have a default value.</li> * <li>ldap.binddomain: the LDAP bind domain value to be used with the LDAP * server. This property is optional and useful only in case of Active * Directory server. * <li>ldap.enablestarttls: A boolean value used to define if the LDAP server * supports 'StartTLS' extension.</li> * </ul> */ @InterfaceAudience.Private @InterfaceStability.Evolving public class LdapAuthenticationHandler implements AuthenticationHandler { private static Logger logger = LoggerFactory.getLogger(LdapAuthenticationHandler.class); /** * Constant that identifies the authentication mechanism. */ public static final String TYPE = "ldap"; /** * Constant that identifies the authentication mechanism to be used with the * LDAP server. */ public static final String SECURITY_AUTHENTICATION = "simple"; /** * Constant for the configuration property that indicates the url of the LDAP * server. */ public static final String PROVIDER_URL = TYPE + ".providerurl"; /** * Constant for the configuration property that indicates the base * distinguished name (DN) to be used with the LDAP server. This value is * appended to the provided user id for authentication purpose. */ public static final String BASE_DN = TYPE + ".basedn"; /** * Constant for the configuration property that indicates the LDAP bind * domain value to be used with the LDAP server. */ public static final String LDAP_BIND_DOMAIN = TYPE + ".binddomain"; /** * Constant for the configuration property that indicates the base * distinguished name (DN) to be used with the LDAP server. This value is * appended to the provided user id for authentication purpose. */ public static final String ENABLE_START_TLS = TYPE + ".enablestarttls"; private String ldapDomain; private String baseDN; private String providerUrl; private Boolean enableStartTls; private Boolean disableHostNameVerification; /** * Configure StartTLS LDAP extension for this handler. * * @param enableStartTls true If the StartTLS LDAP extension is to be enabled * false otherwise */ @VisibleForTesting public void setEnableStartTls(Boolean enableStartTls) { this.enableStartTls = enableStartTls; } /** * Configure the Host name verification for this handler. This method is * introduced only for unit testing and should never be used in production. * * @param disableHostNameVerification true to disable host-name verification * false otherwise */ @VisibleForTesting public void setDisableHostNameVerification(Boolean disableHostNameVerification) { this.disableHostNameVerification = disableHostNameVerification; } @Override public String getType() { return TYPE; } @Override public void init(Properties config) throws ServletException { this.baseDN = config.getProperty(BASE_DN); this.providerUrl = config.getProperty(PROVIDER_URL); this.ldapDomain = config.getProperty(LDAP_BIND_DOMAIN); this.enableStartTls = Boolean.valueOf(config.getProperty(ENABLE_START_TLS, "false")); Preconditions.checkNotNull(this.providerUrl, "The LDAP URI can not be null"); Preconditions.checkArgument((this.baseDN == null) ^ (this.ldapDomain == null), "Either LDAP base DN or LDAP domain value needs to be specified"); if (this.enableStartTls) { String tmp = this.providerUrl.toLowerCase(); Preconditions.checkArgument(!tmp.startsWith("ldaps"), "Can not use ldaps and StartTLS option at the same time"); } } @Override public void destroy() { } @Override public boolean managementOperation(AuthenticationToken token, HttpServletRequest request, HttpServletResponse response) throws IOException, AuthenticationException { return true; } @Override public AuthenticationToken authenticate(HttpServletRequest request, HttpServletResponse response) throws IOException, AuthenticationException { AuthenticationToken token = null; String authorization = request.getHeader(HttpConstants.AUTHORIZATION_HEADER); if (authorization == null || !AuthenticationHandlerUtil.matchAuthScheme(HttpConstants.BASIC, authorization)) { response.setHeader(WWW_AUTHENTICATE, HttpConstants.BASIC); response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); if (authorization == null) { logger.trace("Basic auth starting"); } else { logger.warn("'" + HttpConstants.AUTHORIZATION_HEADER + "' does not start with '" + HttpConstants.BASIC + "' : {}", authorization); } } else { authorization = authorization.substring(HttpConstants.BASIC.length()).trim(); final Base64 base64 = new Base64(0); // As per RFC7617, UTF-8 charset should be used for decoding. String[] credentials = new String(base64.decode(authorization), StandardCharsets.UTF_8).split(":", 2); if (credentials.length == 2) { token = authenticateUser(credentials[0], credentials[1]); response.setStatus(HttpServletResponse.SC_OK); } } return token; } private AuthenticationToken authenticateUser(String userName, String password) throws AuthenticationException { if (userName == null || userName.isEmpty()) { throw new AuthenticationException( "Error validating LDAP user:" + " a null or blank username has been provided"); } // If the domain is available in the config, then append it unless domain // is already part of the username. LDAP providers like Active Directory // use a fully qualified user name like foo@bar.com. if (!hasDomain(userName) && ldapDomain != null) { userName = userName + "@" + ldapDomain; } if (password == null || password.isEmpty() || password.getBytes(StandardCharsets.UTF_8)[0] == 0) { throw new AuthenticationException( "Error validating LDAP user:" + " a null or blank password has been provided"); } // setup the security principal String bindDN; if (baseDN == null) { bindDN = userName; } else { bindDN = "uid=" + userName + "," + baseDN; } if (this.enableStartTls) { authenticateWithTlsExtension(bindDN, password); } else { authenticateWithoutTlsExtension(bindDN, password); } return new AuthenticationToken(userName, userName, TYPE); } private void authenticateWithTlsExtension(String userDN, String password) throws AuthenticationException { LdapContext ctx = null; Hashtable<String, Object> env = new Hashtable<String, Object>(); env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); env.put(Context.PROVIDER_URL, providerUrl); try { // Create initial context ctx = new InitialLdapContext(env, null); // Establish TLS session StartTlsResponse tls = (StartTlsResponse) ctx.extendedOperation(new StartTlsRequest()); if (disableHostNameVerification) { tls.setHostnameVerifier(new HostnameVerifier() { @Override public boolean verify(String hostname, SSLSession session) { return true; } }); } tls.negotiate(); // Initialize security credentials & perform read operation for // verification. ctx.addToEnvironment(Context.SECURITY_AUTHENTICATION, SECURITY_AUTHENTICATION); ctx.addToEnvironment(Context.SECURITY_PRINCIPAL, userDN); ctx.addToEnvironment(Context.SECURITY_CREDENTIALS, password); ctx.lookup(userDN); logger.debug("Authentication successful for {}", userDN); } catch (NamingException | IOException ex) { throw new AuthenticationException("Error validating LDAP user", ex); } finally { if (ctx != null) { try { ctx.close(); } catch (NamingException e) { /* Ignore. */ } } } } private void authenticateWithoutTlsExtension(String userDN, String password) throws AuthenticationException { Hashtable<String, Object> env = new Hashtable<String, Object>(); env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); env.put(Context.PROVIDER_URL, providerUrl); env.put(Context.SECURITY_AUTHENTICATION, SECURITY_AUTHENTICATION); env.put(Context.SECURITY_PRINCIPAL, userDN); env.put(Context.SECURITY_CREDENTIALS, password); try { // Create initial context Context ctx = new InitialDirContext(env); ctx.close(); logger.debug("Authentication successful for {}", userDN); } catch (NamingException e) { throw new AuthenticationException("Error validating LDAP user", e); } } private static boolean hasDomain(String userName) { return (indexOfDomainMatch(userName) > 0); } /* * Get the index separating the user name from domain name (the user's name * up to the first '/' or '@'). * * @param userName full user name. * * @return index of domain match or -1 if not found */ private static int indexOfDomainMatch(String userName) { if (userName == null) { return -1; } int idx = userName.indexOf('/'); int idx2 = userName.indexOf('@'); int endIdx = Math.min(idx, idx2); // Use the earlier match. // Unless at least one of '/' or '@' was not found, in // which case, user the latter match. if (endIdx == -1) { endIdx = Math.max(idx, idx2); } return endIdx; } }