diff --git a/src/main/java/com/ning/http/client/spnego/CreateGssCredentialAction.java b/src/main/java/com/ning/http/client/spnego/CreateGssCredentialAction.java new file mode 100644 index 000000000..c1448abeb --- /dev/null +++ b/src/main/java/com/ning/http/client/spnego/CreateGssCredentialAction.java @@ -0,0 +1,24 @@ +package com.ning.http.client.spnego; + +import java.security.PrivilegedExceptionAction; + +import org.ietf.jgss.GSSCredential; +import org.ietf.jgss.GSSManager; +import org.ietf.jgss.Oid; + +class CreateGssCredentialAction implements PrivilegedExceptionAction { + + private final GSSManager gssManager; + private final Oid negotiationOid; + + CreateGssCredentialAction(GSSManager gssManager, Oid negotiationOid) { + this.gssManager = gssManager; + this.negotiationOid = negotiationOid; + } + + @Override + public GSSCredential run() throws Exception { + return gssManager + .createCredential(null, GSSCredential.DEFAULT_LIFETIME, negotiationOid, GSSCredential.INITIATE_AND_ACCEPT); + } +} diff --git a/src/main/java/com/ning/http/client/spnego/KeytabKerberosConfiguration.java b/src/main/java/com/ning/http/client/spnego/KeytabKerberosConfiguration.java new file mode 100644 index 000000000..ef1a2c74c --- /dev/null +++ b/src/main/java/com/ning/http/client/spnego/KeytabKerberosConfiguration.java @@ -0,0 +1,45 @@ +package com.ning.http.client.spnego; + +import java.util.HashMap; +import java.util.Map; + +import javax.security.auth.login.AppConfigurationEntry; +import javax.security.auth.login.Configuration; + +/** + * Kerberos' configuration that uses a keytab file. + */ +class KeytabKerberosConfiguration extends Configuration { + + private final String principal; + private final String keytabFileName; + + public KeytabKerberosConfiguration(String principal, String keytabFileName) { + this.principal = principal; + this.keytabFileName = keytabFileName; + } + + @Override + public AppConfigurationEntry[] getAppConfigurationEntry(String name) { + Map options = new HashMap<>(); + options.put("keyTab", keytabFileName); + options.put("principal", principal); + options.put("useKeyTab", "true"); + options.put("storeKey", "true"); + options.put("doNotPrompt", "true"); + options.put("useTicketCache", "true"); + options.put("renewTGT", "true"); + options.put("refreshKrb5Config", "true"); + options.put("isInitiator", "true"); + String ticketCache = System.getenv("KRB5CCNAME"); + if (ticketCache != null) { + options.put("ticketCache", ticketCache); + } + options.put("debug", "true"); + + return new AppConfigurationEntry[]{ + new AppConfigurationEntry("com.sun.security.auth.module.Krb5LoginModule", + AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, + options),}; + } +} diff --git a/src/main/java/com/ning/http/client/spnego/SpnegoEngine.java b/src/main/java/com/ning/http/client/spnego/SpnegoEngine.java index 8068c13ba..a1a9bf024 100644 --- a/src/main/java/com/ning/http/client/spnego/SpnegoEngine.java +++ b/src/main/java/com/ning/http/client/spnego/SpnegoEngine.java @@ -37,17 +37,28 @@ package com.ning.http.client.spnego; +import static javax.security.auth.Subject.doAs; +import static org.slf4j.LoggerFactory.getLogger; + import org.ietf.jgss.GSSContext; +import org.ietf.jgss.GSSCredential; import org.ietf.jgss.GSSException; import org.ietf.jgss.GSSManager; import org.ietf.jgss.GSSName; import org.ietf.jgss.Oid; import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import com.ning.http.util.Base64; import java.io.IOException; +import java.security.Principal; +import java.security.PrivilegedActionException; +import java.util.HashSet; +import java.util.Set; + +import javax.security.auth.Subject; +import javax.security.auth.kerberos.KerberosPrincipal; +import javax.security.auth.login.LoginContext; /** * SPNEGO (Simple and Protected GSSAPI Negotiation Mechanism) authentication @@ -60,7 +71,7 @@ public class SpnegoEngine { private static final String SPNEGO_OID = "1.3.6.1.5.5.2"; private static final String KERBEROS_OID = "1.2.840.113554.1.2.2"; - private final Logger log = LoggerFactory.getLogger(getClass()); + private static final Logger LOGGER = getLogger(SpnegoEngine.class); private final SpnegoTokenGenerator spnegoGenerator; @@ -74,10 +85,10 @@ public SpnegoEngine() { this(null); } - public String generateToken(String server) throws Throwable { + public String generateToken(String server, String clientPrincipal, String keytabFilename) throws Throwable { try { - log.debug("init {}", server); + LOGGER.debug("init {}", server); /* Using the SPNEGO OID is the correct method. * Kerberos v5 works for IIS but not JBoss. Unwrapping * the initial token when using SPNEGO OID looks like what is @@ -102,18 +113,19 @@ public String generateToken(String server) throws Throwable { boolean tryKerberos = false; try { GSSManager manager = GSSManager.getInstance(); + GSSCredential gssCredential = getGssCredential(clientPrincipal, keytabFilename, manager, negotiationOid); GSSName serverName = manager.createName("HTTP@" + server, GSSName.NT_HOSTBASED_SERVICE); gssContext = manager.createContext( - serverName.canonicalize(negotiationOid), negotiationOid, null, + serverName.canonicalize(negotiationOid), negotiationOid, gssCredential, GSSContext.DEFAULT_LIFETIME); gssContext.requestMutualAuth(true); gssContext.requestCredDeleg(true); } catch (GSSException ex) { - log.error("generateToken", ex); + LOGGER.error("generateToken", ex); // BAD MECH means we are likely to be using 1.5, fall back to Kerberos MECH. // Rethrow any other exception. if (ex.getMajor() == GSSException.BAD_MECH) { - log.debug("GSSException BAD_MECH, retry with Kerberos MECH"); + LOGGER.debug("GSSException BAD_MECH, retry with Kerberos MECH"); tryKerberos = true; } else { throw ex; @@ -122,12 +134,13 @@ public String generateToken(String server) throws Throwable { } if (tryKerberos) { /* Kerberos v5 GSS-API mechanism defined in RFC 1964.*/ - log.debug("Using Kerberos MECH {}", KERBEROS_OID); + LOGGER.debug("Using Kerberos MECH {}", KERBEROS_OID); negotiationOid = new Oid(KERBEROS_OID); GSSManager manager = GSSManager.getInstance(); GSSName serverName = manager.createName("HTTP@" + server, GSSName.NT_HOSTBASED_SERVICE); + GSSCredential gssCredential = getGssCredential(clientPrincipal, keytabFilename, manager, negotiationOid); gssContext = manager.createContext( - serverName.canonicalize(negotiationOid), negotiationOid, null, + serverName.canonicalize(negotiationOid), negotiationOid, gssCredential, GSSContext.DEFAULT_LIFETIME); gssContext.requestMutualAuth(true); gssContext.requestCredDeleg(true); @@ -154,11 +167,11 @@ public String generateToken(String server) throws Throwable { gssContext.dispose(); String tokenstr = new String(Base64.encode(token)); - log.debug("Sending response '{}' back to the server", tokenstr); + LOGGER.debug("Sending response '{}' back to the server", tokenstr); return tokenstr; } catch (GSSException gsse) { - log.error("generateToken", gsse); + LOGGER.error("generateToken", gsse); if (gsse.getMajor() == GSSException.DEFECTIVE_CREDENTIAL || gsse.getMajor() == GSSException.CREDENTIALS_EXPIRED) throw new Exception(gsse.getMessage(), gsse); @@ -174,4 +187,25 @@ public String generateToken(String server) throws Throwable { throw new Exception(ex.getMessage()); } } + + private static GSSCredential getGssCredential(String clientPrincipal, + String keytabFilename, + GSSManager manager, + final Oid negotiationOid) throws Exception { + LoginContext loginContext = null; + try { + Set principals = new HashSet<>(); + principals.add(new KerberosPrincipal(clientPrincipal)); + Subject subject = new Subject(false, principals, new HashSet<>(), new HashSet<>()); + loginContext = new LoginContext("", subject, null, new KeytabKerberosConfiguration(clientPrincipal, keytabFilename)); + loginContext.login(); + return doAs(subject, new CreateGssCredentialAction(manager, negotiationOid)); + } catch (PrivilegedActionException ex) { + throw ex.getException(); + } finally { + if (loginContext != null) { + loginContext.logout(); + } + } + } } diff --git a/src/main/java/com/ning/http/util/AuthenticatorUtils.java b/src/main/java/com/ning/http/util/AuthenticatorUtils.java index f837759f8..987e8583f 100644 --- a/src/main/java/com/ning/http/util/AuthenticatorUtils.java +++ b/src/main/java/com/ning/http/util/AuthenticatorUtils.java @@ -48,15 +48,26 @@ public static String perConnectionAuthorizationHeader(Request request, case KERBEROS: case SPNEGO: String host; - if (proxyServer != null) + String principal; + String keytabName; + if (proxyServer != null) { host = proxyServer.getHost(); - else if (request.getVirtualHost() != null) + principal = proxyServer.getPrincipal(); + keytabName = proxyServer.getPassword(); + } else if (request.getVirtualHost() != null) { host = request.getVirtualHost(); - else + // TODO: ?? + principal = request.getRealm().getPrincipal(); + keytabName = request.getRealm().getPassword(); + } else { host = uri.getHost(); + // TODO: ?? + principal = request.getRealm().getPrincipal(); + keytabName = request.getRealm().getPassword(); + } try { - authorizationHeader = "Negotiate " + SpnegoEngine.INSTANCE.generateToken(host); + authorizationHeader = "Negotiate " + SpnegoEngine.INSTANCE.generateToken(host, principal, keytabName); } catch (Throwable e) { throw new IOException(e); }