blob: f452317f931a1ba8473825b2d773705eb6fb1f60 [file] [log] [blame]
/**
* 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;
}
}