HADOOP-13565. KerberosAuthenticationHandler#authenticate should not rebuild SPN based on client request. Contributed by Xiaoyu Yao.
(cherry picked from commit 4c38f11cec
)
This commit is contained in:
parent
0c58241b22
commit
bd4e5bc501
|
@ -18,6 +18,7 @@ import org.apache.hadoop.security.authentication.client.KerberosAuthenticator;
|
||||||
import org.apache.commons.codec.binary.Base64;
|
import org.apache.commons.codec.binary.Base64;
|
||||||
import org.apache.hadoop.security.authentication.util.KerberosName;
|
import org.apache.hadoop.security.authentication.util.KerberosName;
|
||||||
import org.apache.hadoop.security.authentication.util.KerberosUtil;
|
import org.apache.hadoop.security.authentication.util.KerberosUtil;
|
||||||
|
import org.ietf.jgss.GSSException;
|
||||||
import org.ietf.jgss.GSSContext;
|
import org.ietf.jgss.GSSContext;
|
||||||
import org.ietf.jgss.GSSCredential;
|
import org.ietf.jgss.GSSCredential;
|
||||||
import org.ietf.jgss.GSSManager;
|
import org.ietf.jgss.GSSManager;
|
||||||
|
@ -48,25 +49,32 @@ import java.util.Properties;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import com.google.common.collect.HashMultimap;
|
||||||
|
|
||||||
import static org.apache.hadoop.util.PlatformName.IBM_JAVA;
|
import static org.apache.hadoop.util.PlatformName.IBM_JAVA;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The {@link KerberosAuthenticationHandler} implements the Kerberos SPNEGO authentication mechanism for HTTP.
|
* The {@link KerberosAuthenticationHandler} implements the Kerberos SPNEGO
|
||||||
|
* authentication mechanism for HTTP.
|
||||||
* <p>
|
* <p>
|
||||||
* The supported configuration properties are:
|
* The supported configuration properties are:
|
||||||
* <ul>
|
* <ul>
|
||||||
* <li>kerberos.principal: the Kerberos principal to used by the server. As stated by the Kerberos SPNEGO
|
* <li>kerberos.principal: the Kerberos principal to used by the server. As
|
||||||
* specification, it should be <code>HTTP/${HOSTNAME}@{REALM}</code>. The realm can be omitted from the
|
* stated by the Kerberos SPNEGO specification, it should be
|
||||||
* principal as the JDK GSS libraries will use the realm name of the configured default realm.
|
* <code>HTTP/${HOSTNAME}@{REALM}</code>. The realm can be omitted from the
|
||||||
|
* principal as the JDK GSS libraries will use the realm name of the configured
|
||||||
|
* default realm.
|
||||||
* It does not have a default value.</li>
|
* It does not have a default value.</li>
|
||||||
* <li>kerberos.keytab: the keytab file containing the credentials for the Kerberos principal.
|
* <li>kerberos.keytab: the keytab file containing the credentials for the
|
||||||
|
* Kerberos principal.
|
||||||
* It does not have a default value.</li>
|
* It does not have a default value.</li>
|
||||||
* <li>kerberos.name.rules: kerberos names rules to resolve principal names, see
|
* <li>kerberos.name.rules: kerberos names rules to resolve principal names, see
|
||||||
* {@link KerberosName#setRules(String)}</li>
|
* {@link KerberosName#setRules(String)}</li>
|
||||||
* </ul>
|
* </ul>
|
||||||
*/
|
*/
|
||||||
public class KerberosAuthenticationHandler implements AuthenticationHandler {
|
public class KerberosAuthenticationHandler implements AuthenticationHandler {
|
||||||
private static Logger LOG = LoggerFactory.getLogger(KerberosAuthenticationHandler.class);
|
private static final Logger LOG = LoggerFactory.getLogger(
|
||||||
|
KerberosAuthenticationHandler.class);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Kerberos context configuration for the JDK GSS library.
|
* Kerberos context configuration for the JDK GSS library.
|
||||||
|
@ -128,12 +136,14 @@ public class KerberosAuthenticationHandler implements AuthenticationHandler {
|
||||||
public static final String TYPE = "kerberos";
|
public static final String TYPE = "kerberos";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constant for the configuration property that indicates the kerberos principal.
|
* Constant for the configuration property that indicates the kerberos
|
||||||
|
* principal.
|
||||||
*/
|
*/
|
||||||
public static final String PRINCIPAL = TYPE + ".principal";
|
public static final String PRINCIPAL = TYPE + ".principal";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constant for the configuration property that indicates the keytab file path.
|
* Constant for the configuration property that indicates the keytab
|
||||||
|
* file path.
|
||||||
*/
|
*/
|
||||||
public static final String KEYTAB = TYPE + ".keytab";
|
public static final String KEYTAB = TYPE + ".keytab";
|
||||||
|
|
||||||
|
@ -148,6 +158,42 @@ public class KerberosAuthenticationHandler implements AuthenticationHandler {
|
||||||
private GSSManager gssManager;
|
private GSSManager gssManager;
|
||||||
private Subject serverSubject = new Subject();
|
private Subject serverSubject = new Subject();
|
||||||
private List<LoginContext> loginContexts = new ArrayList<LoginContext>();
|
private List<LoginContext> loginContexts = new ArrayList<LoginContext>();
|
||||||
|
/**
|
||||||
|
* HADOOP-10158 added support of running HTTP with multiple SPNs
|
||||||
|
* but implicit requirements is that they must come from the SAME local realm.
|
||||||
|
*
|
||||||
|
* This is a regression for use cases where HTTP service needs to run with
|
||||||
|
* with SPN from foreign realm, which is not supported after HADOOP-10158.
|
||||||
|
*
|
||||||
|
* HADOOP-13565 brings back support of SPNs from foreign realms
|
||||||
|
* without dependency on specific Kerberos domain_realm mapping mechanism.
|
||||||
|
*
|
||||||
|
* There are several reasons for not using native Kerberos domain_realm
|
||||||
|
* mapping:
|
||||||
|
* 1. As commented in KerberosUtil#getDomainRealm(), JDK's
|
||||||
|
* domain_realm mapping routines are private to the security.krb5
|
||||||
|
* package. As a result, KerberosUtil#getDomainRealm() always return local
|
||||||
|
* realm.
|
||||||
|
*
|
||||||
|
* 2. Server krb5.conf is not the only place that contains the domain_realm
|
||||||
|
* mapping in real deployment. Based on MIT KDC document here:
|
||||||
|
* https://web.mit.edu/kerberos/krb5-1.13/doc/admin/realm_config.html, the
|
||||||
|
* Kerberos domain_realm mapping can be implemented in one of the three
|
||||||
|
* mechanisms:
|
||||||
|
* 1) Server host-based krb5.conf on HTTP server
|
||||||
|
* 2) KDC-based krb5.conf on KDC server
|
||||||
|
* 3) DNS-based with TXT record with _kerberos prefix to the hostname.
|
||||||
|
*
|
||||||
|
* We choose to maintain domain_realm mapping based on HTTP principals
|
||||||
|
* from keytab. The mapping is built at login time with HTTP principals
|
||||||
|
* key-ed by server name and is used later to
|
||||||
|
* looked up SPNs based on server name from request for authentication.
|
||||||
|
* The multi-map implementation allows SPNs of same server from
|
||||||
|
* different realms.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
private HashMultimap<String, String> serverPrincipalMap =
|
||||||
|
HashMultimap.create();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a Kerberos SPNEGO authentication handler with the default
|
* Creates a Kerberos SPNEGO authentication handler with the default
|
||||||
|
@ -170,7 +216,8 @@ public class KerberosAuthenticationHandler implements AuthenticationHandler {
|
||||||
/**
|
/**
|
||||||
* Initializes the authentication handler instance.
|
* Initializes the authentication handler instance.
|
||||||
* <p>
|
* <p>
|
||||||
* It creates a Kerberos context using the principal and keytab specified in the configuration.
|
* It creates a Kerberos context using the principal and keytab specified in
|
||||||
|
* the configuration.
|
||||||
* <p>
|
* <p>
|
||||||
* This method is invoked by the {@link AuthenticationFilter#init} method.
|
* This method is invoked by the {@link AuthenticationFilter#init} method.
|
||||||
*
|
*
|
||||||
|
@ -225,10 +272,22 @@ public class KerberosAuthenticationHandler implements AuthenticationHandler {
|
||||||
throw new AuthenticationException(le);
|
throw new AuthenticationException(le);
|
||||||
}
|
}
|
||||||
loginContexts.add(loginContext);
|
loginContexts.add(loginContext);
|
||||||
|
KerberosName kerbName = new KerberosName(spnegoPrincipal);
|
||||||
|
if (kerbName.getHostName() != null
|
||||||
|
&& kerbName.getRealm() != null
|
||||||
|
&& kerbName.getServiceName() != null
|
||||||
|
&& kerbName.getServiceName().equals("HTTP")) {
|
||||||
|
LOG.trace("Map server: {} to principal: {}", kerbName.getHostName(),
|
||||||
|
spnegoPrincipal);
|
||||||
|
serverPrincipalMap.put(kerbName.getHostName(), spnegoPrincipal);
|
||||||
|
} else {
|
||||||
|
LOG.warn("HTTP principal: {} is invalid for SPNEGO!",
|
||||||
|
spnegoPrincipal);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
gssManager = Subject.doAs(serverSubject, new PrivilegedExceptionAction<GSSManager>() {
|
gssManager = Subject.doAs(serverSubject,
|
||||||
|
new PrivilegedExceptionAction<GSSManager>() {
|
||||||
@Override
|
@Override
|
||||||
public GSSManager run() throws Exception {
|
public GSSManager run() throws Exception {
|
||||||
return GSSManager.getInstance();
|
return GSSManager.getInstance();
|
||||||
|
@ -312,63 +371,116 @@ public class KerberosAuthenticationHandler implements AuthenticationHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* It enforces the the Kerberos SPNEGO authentication sequence returning an {@link AuthenticationToken} only
|
* It enforces the the Kerberos SPNEGO authentication sequence returning an
|
||||||
* after the Kerberos SPNEGO sequence has completed successfully.
|
* {@link AuthenticationToken} only after the Kerberos SPNEGO sequence has
|
||||||
|
* completed successfully.
|
||||||
*
|
*
|
||||||
* @param request the HTTP client request.
|
* @param request the HTTP client request.
|
||||||
* @param response the HTTP client response.
|
* @param response the HTTP client response.
|
||||||
*
|
*
|
||||||
* @return an authentication token if the Kerberos SPNEGO sequence is complete and valid,
|
* @return an authentication token if the Kerberos SPNEGO sequence is complete
|
||||||
* <code>null</code> if it is in progress (in this case the handler handles the response to the client).
|
* and valid, <code>null</code> if it is in progress (in this case the handler
|
||||||
|
* handles the response to the client).
|
||||||
*
|
*
|
||||||
* @throws IOException thrown if an IO error occurred.
|
* @throws IOException thrown if an IO error occurred.
|
||||||
* @throws AuthenticationException thrown if Kerberos SPNEGO sequence failed.
|
* @throws AuthenticationException thrown if Kerberos SPNEGO sequence failed.
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public AuthenticationToken authenticate(HttpServletRequest request, final HttpServletResponse response)
|
public AuthenticationToken authenticate(HttpServletRequest request,
|
||||||
|
final HttpServletResponse response)
|
||||||
throws IOException, AuthenticationException {
|
throws IOException, AuthenticationException {
|
||||||
AuthenticationToken token = null;
|
AuthenticationToken token = null;
|
||||||
String authorization = request.getHeader(KerberosAuthenticator.AUTHORIZATION);
|
String authorization = request.getHeader(
|
||||||
|
KerberosAuthenticator.AUTHORIZATION);
|
||||||
|
|
||||||
if (authorization == null || !authorization.startsWith(KerberosAuthenticator.NEGOTIATE)) {
|
if (authorization == null
|
||||||
|
|| !authorization.startsWith(KerberosAuthenticator.NEGOTIATE)) {
|
||||||
response.setHeader(WWW_AUTHENTICATE, KerberosAuthenticator.NEGOTIATE);
|
response.setHeader(WWW_AUTHENTICATE, KerberosAuthenticator.NEGOTIATE);
|
||||||
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
||||||
if (authorization == null) {
|
if (authorization == null) {
|
||||||
LOG.trace("SPNEGO starting");
|
LOG.trace("SPNEGO starting for url: {}", request.getRequestURL());
|
||||||
} else {
|
} else {
|
||||||
LOG.warn("'" + KerberosAuthenticator.AUTHORIZATION + "' does not start with '" +
|
LOG.warn("'" + KerberosAuthenticator.AUTHORIZATION +
|
||||||
|
"' does not start with '" +
|
||||||
KerberosAuthenticator.NEGOTIATE + "' : {}", authorization);
|
KerberosAuthenticator.NEGOTIATE + "' : {}", authorization);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
authorization = authorization.substring(KerberosAuthenticator.NEGOTIATE.length()).trim();
|
authorization = authorization.substring(
|
||||||
|
KerberosAuthenticator.NEGOTIATE.length()).trim();
|
||||||
final Base64 base64 = new Base64(0);
|
final Base64 base64 = new Base64(0);
|
||||||
final byte[] clientToken = base64.decode(authorization);
|
final byte[] clientToken = base64.decode(authorization);
|
||||||
final String serverName = InetAddress.getByName(request.getServerName())
|
final String serverName = InetAddress.getByName(request.getServerName())
|
||||||
.getCanonicalHostName();
|
.getCanonicalHostName();
|
||||||
try {
|
try {
|
||||||
token = Subject.doAs(serverSubject, new PrivilegedExceptionAction<AuthenticationToken>() {
|
token = Subject.doAs(serverSubject,
|
||||||
|
new PrivilegedExceptionAction<AuthenticationToken>() {
|
||||||
|
private Set<String> serverPrincipals =
|
||||||
|
serverPrincipalMap.get(serverName);
|
||||||
@Override
|
@Override
|
||||||
public AuthenticationToken run() throws Exception {
|
public AuthenticationToken run() throws Exception {
|
||||||
|
if (LOG.isTraceEnabled()) {
|
||||||
|
LOG.trace("SPNEGO with principals: {}",
|
||||||
|
serverPrincipals.toString());
|
||||||
|
}
|
||||||
AuthenticationToken token = null;
|
AuthenticationToken token = null;
|
||||||
|
Exception lastException = null;
|
||||||
|
for (String serverPrincipal : serverPrincipals) {
|
||||||
|
try {
|
||||||
|
token = runWithPrincipal(serverPrincipal, clientToken,
|
||||||
|
base64, response);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
lastException = ex;
|
||||||
|
LOG.trace("Auth {} failed with {}", serverPrincipal, ex);
|
||||||
|
} finally {
|
||||||
|
if (token != null) {
|
||||||
|
LOG.trace("Auth {} successfully", serverPrincipal);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (token != null) {
|
||||||
|
return token;
|
||||||
|
} else {
|
||||||
|
throw new AuthenticationException(lastException);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (PrivilegedActionException ex) {
|
||||||
|
if (ex.getException() instanceof IOException) {
|
||||||
|
throw (IOException) ex.getException();
|
||||||
|
} else {
|
||||||
|
throw new AuthenticationException(ex.getException());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
private AuthenticationToken runWithPrincipal(String serverPrincipal,
|
||||||
|
byte[] clientToken, Base64 base64, HttpServletResponse response) throws
|
||||||
|
IOException, AuthenticationException, ClassNotFoundException,
|
||||||
|
GSSException, IllegalAccessException, NoSuchFieldException {
|
||||||
GSSContext gssContext = null;
|
GSSContext gssContext = null;
|
||||||
GSSCredential gssCreds = null;
|
GSSCredential gssCreds = null;
|
||||||
|
AuthenticationToken token = null;
|
||||||
try {
|
try {
|
||||||
gssCreds = gssManager.createCredential(
|
LOG.trace("SPNEGO initiated with principal {}", serverPrincipal);
|
||||||
gssManager.createName(
|
gssCreds = this.gssManager.createCredential(
|
||||||
KerberosUtil.getServicePrincipal("HTTP", serverName),
|
this.gssManager.createName(serverPrincipal,
|
||||||
KerberosUtil.getOidInstance("NT_GSS_KRB5_PRINCIPAL")),
|
KerberosUtil.getOidInstance("NT_GSS_KRB5_PRINCIPAL")),
|
||||||
GSSCredential.INDEFINITE_LIFETIME,
|
GSSCredential.INDEFINITE_LIFETIME,
|
||||||
new Oid[]{
|
new Oid[]{
|
||||||
KerberosUtil.getOidInstance("GSS_SPNEGO_MECH_OID"),
|
KerberosUtil.getOidInstance("GSS_SPNEGO_MECH_OID"),
|
||||||
KerberosUtil.getOidInstance("GSS_KRB5_MECH_OID")},
|
KerberosUtil.getOidInstance("GSS_KRB5_MECH_OID")},
|
||||||
GSSCredential.ACCEPT_ONLY);
|
GSSCredential.ACCEPT_ONLY);
|
||||||
gssContext = gssManager.createContext(gssCreds);
|
gssContext = this.gssManager.createContext(gssCreds);
|
||||||
byte[] serverToken = gssContext.acceptSecContext(clientToken, 0, clientToken.length);
|
byte[] serverToken = gssContext.acceptSecContext(clientToken, 0,
|
||||||
|
clientToken.length);
|
||||||
if (serverToken != null && serverToken.length > 0) {
|
if (serverToken != null && serverToken.length > 0) {
|
||||||
String authenticate = base64.encodeToString(serverToken);
|
String authenticate = base64.encodeToString(serverToken);
|
||||||
response.setHeader(KerberosAuthenticator.WWW_AUTHENTICATE,
|
response.setHeader(KerberosAuthenticator.WWW_AUTHENTICATE,
|
||||||
KerberosAuthenticator.NEGOTIATE + " " + authenticate);
|
KerberosAuthenticator.NEGOTIATE + " " +
|
||||||
|
authenticate);
|
||||||
}
|
}
|
||||||
if (!gssContext.isEstablished()) {
|
if (!gssContext.isEstablished()) {
|
||||||
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
||||||
|
@ -391,17 +503,4 @@ public class KerberosAuthenticationHandler implements AuthenticationHandler {
|
||||||
}
|
}
|
||||||
return token;
|
return token;
|
||||||
}
|
}
|
||||||
});
|
|
||||||
} catch (PrivilegedActionException ex) {
|
|
||||||
if (ex.getException() instanceof IOException) {
|
|
||||||
throw (IOException) ex.getException();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
throw new AuthenticationException(ex.getException());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return token;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue