diff --git a/client/pom.xml b/client/pom.xml
index 23997b6a78..63ea6e173b 100644
--- a/client/pom.xml
+++ b/client/pom.xml
@@ -73,5 +73,10 @@
reactive-streams-examples
test
+
+ org.apache.kerby
+ kerb-simplekdc
+ test
+
diff --git a/client/src/main/java/org/asynchttpclient/Dsl.java b/client/src/main/java/org/asynchttpclient/Dsl.java
index 914b734b77..cdb30ed165 100644
--- a/client/src/main/java/org/asynchttpclient/Dsl.java
+++ b/client/src/main/java/org/asynchttpclient/Dsl.java
@@ -99,7 +99,11 @@ public static Realm.Builder realm(Realm prototype) {
.setNtlmDomain(prototype.getNtlmDomain())
.setNtlmHost(prototype.getNtlmHost())
.setUseAbsoluteURI(prototype.isUseAbsoluteURI())
- .setOmitQuery(prototype.isOmitQuery());
+ .setOmitQuery(prototype.isOmitQuery())
+ .setServicePrincipalName(prototype.getServicePrincipalName())
+ .setUseCanonicalHostname(prototype.isUseCanonicalHostname())
+ .setCustomLoginConfig(prototype.getCustomLoginConfig())
+ .setLoginContextName(prototype.getLoginContextName());
}
public static Realm.Builder realm(AuthScheme scheme, String principal, String password) {
diff --git a/client/src/main/java/org/asynchttpclient/Realm.java b/client/src/main/java/org/asynchttpclient/Realm.java
index 9b9bdf798e..c6324fd0b4 100644
--- a/client/src/main/java/org/asynchttpclient/Realm.java
+++ b/client/src/main/java/org/asynchttpclient/Realm.java
@@ -23,6 +23,7 @@
import java.nio.charset.Charset;
import java.security.MessageDigest;
+import java.util.Map;
import java.util.concurrent.ThreadLocalRandom;
import static java.nio.charset.StandardCharsets.*;
@@ -60,6 +61,10 @@ public class Realm {
private final String ntlmDomain;
private final boolean useAbsoluteURI;
private final boolean omitQuery;
+ private final Map customLoginConfig;
+ private final String servicePrincipalName;
+ private final boolean useCanonicalHostname;
+ private final String loginContextName;
private Realm(AuthScheme scheme,
String principal,
@@ -78,11 +83,15 @@ private Realm(AuthScheme scheme,
String ntlmDomain,
String ntlmHost,
boolean useAbsoluteURI,
- boolean omitQuery) {
+ boolean omitQuery,
+ String servicePrincipalName,
+ boolean useCanonicalHostname,
+ Map customLoginConfig,
+ String loginContextName) {
this.scheme = assertNotNull(scheme, "scheme");
- this.principal = assertNotNull(principal, "principal");
- this.password = assertNotNull(password, "password");
+ this.principal = principal;
+ this.password = password;
this.realmName = realmName;
this.nonce = nonce;
this.algorithm = algorithm;
@@ -98,6 +107,10 @@ private Realm(AuthScheme scheme,
this.ntlmHost = ntlmHost;
this.useAbsoluteURI = useAbsoluteURI;
this.omitQuery = omitQuery;
+ this.servicePrincipalName = servicePrincipalName;
+ this.useCanonicalHostname = useCanonicalHostname;
+ this.customLoginConfig = customLoginConfig;
+ this.loginContextName = loginContextName;
}
public String getPrincipal() {
@@ -187,12 +200,48 @@ public boolean isOmitQuery() {
return omitQuery;
}
+ public Map getCustomLoginConfig() {
+ return customLoginConfig;
+ }
+
+ public String getServicePrincipalName() {
+ return servicePrincipalName;
+ }
+
+ public boolean isUseCanonicalHostname() {
+ return useCanonicalHostname;
+ }
+
+ public String getLoginContextName() {
+ return loginContextName;
+ }
+
@Override
public String toString() {
- return "Realm{" + "principal='" + principal + '\'' + ", scheme=" + scheme + ", realmName='" + realmName + '\''
- + ", nonce='" + nonce + '\'' + ", algorithm='" + algorithm + '\'' + ", response='" + response + '\''
- + ", qop='" + qop + '\'' + ", nc='" + nc + '\'' + ", cnonce='" + cnonce + '\'' + ", uri='" + uri + '\''
- + ", useAbsoluteURI='" + useAbsoluteURI + '\'' + ", omitQuery='" + omitQuery + '\'' + '}';
+ return "Realm{" +
+ "principal='" + principal + '\'' +
+ ", password='" + password + '\'' +
+ ", scheme=" + scheme +
+ ", realmName='" + realmName + '\'' +
+ ", nonce='" + nonce + '\'' +
+ ", algorithm='" + algorithm + '\'' +
+ ", response='" + response + '\'' +
+ ", opaque='" + opaque + '\'' +
+ ", qop='" + qop + '\'' +
+ ", nc='" + nc + '\'' +
+ ", cnonce='" + cnonce + '\'' +
+ ", uri=" + uri +
+ ", usePreemptiveAuth=" + usePreemptiveAuth +
+ ", charset=" + charset +
+ ", ntlmHost='" + ntlmHost + '\'' +
+ ", ntlmDomain='" + ntlmDomain + '\'' +
+ ", useAbsoluteURI=" + useAbsoluteURI +
+ ", omitQuery=" + omitQuery +
+ ", customLoginConfig=" + customLoginConfig +
+ ", servicePrincipalName='" + servicePrincipalName + '\'' +
+ ", useCanonicalHostname=" + useCanonicalHostname +
+ ", loginContextName='" + loginContextName + '\'' +
+ '}';
}
public enum AuthScheme {
@@ -223,6 +272,18 @@ public static class Builder {
private String ntlmHost = "localhost";
private boolean useAbsoluteURI = false;
private boolean omitQuery;
+ /**
+ * Kerberos/Spnego properties
+ */
+ private Map customLoginConfig;
+ private String servicePrincipalName;
+ private boolean useCanonicalHostname;
+ private String loginContextName;
+
+ public Builder() {
+ this.principal = null;
+ this.password = null;
+ }
public Builder(String principal, String password) {
this.principal = principal;
@@ -311,6 +372,26 @@ public Builder setCharset(Charset charset) {
return this;
}
+ public Builder setCustomLoginConfig(Map customLoginConfig) {
+ this.customLoginConfig = customLoginConfig;
+ return this;
+ }
+
+ public Builder setServicePrincipalName(String servicePrincipalName) {
+ this.servicePrincipalName = servicePrincipalName;
+ return this;
+ }
+
+ public Builder setUseCanonicalHostname(boolean useCanonicalHostname) {
+ this.useCanonicalHostname = useCanonicalHostname;
+ return this;
+ }
+
+ public Builder setLoginContextName(String loginContextName) {
+ this.loginContextName = loginContextName;
+ return this;
+ }
+
private String parseRawQop(String rawQop) {
String[] rawServerSupportedQops = rawQop.split(",");
String[] serverSupportedQops = new String[rawServerSupportedQops.length];
@@ -501,7 +582,11 @@ public Realm build() {
ntlmDomain,
ntlmHost,
useAbsoluteURI,
- omitQuery);
+ omitQuery,
+ servicePrincipalName,
+ useCanonicalHostname,
+ customLoginConfig,
+ loginContextName);
}
}
}
diff --git a/client/src/main/java/org/asynchttpclient/netty/handler/intercept/ProxyUnauthorized407Interceptor.java b/client/src/main/java/org/asynchttpclient/netty/handler/intercept/ProxyUnauthorized407Interceptor.java
index 0812083ad5..02ee195622 100644
--- a/client/src/main/java/org/asynchttpclient/netty/handler/intercept/ProxyUnauthorized407Interceptor.java
+++ b/client/src/main/java/org/asynchttpclient/netty/handler/intercept/ProxyUnauthorized407Interceptor.java
@@ -140,7 +140,7 @@ public boolean exitAfterHandling407(Channel channel,
return false;
}
try {
- kerberosProxyChallenge(proxyServer, requestHeaders);
+ kerberosProxyChallenge(proxyRealm, proxyServer, requestHeaders);
} catch (SpnegoEngineException e) {
// FIXME
@@ -184,10 +184,17 @@ public boolean exitAfterHandling407(Channel channel,
return true;
}
- private void kerberosProxyChallenge(ProxyServer proxyServer,
+ private void kerberosProxyChallenge(Realm proxyRealm,
+ ProxyServer proxyServer,
HttpHeaders headers) throws SpnegoEngineException {
- String challengeHeader = SpnegoEngine.instance().generateToken(proxyServer.getHost());
+ String challengeHeader = SpnegoEngine.instance(proxyRealm.getPrincipal(),
+ proxyRealm.getPassword(),
+ proxyRealm.getServicePrincipalName(),
+ proxyRealm.getRealmName(),
+ proxyRealm.isUseCanonicalHostname(),
+ proxyRealm.getCustomLoginConfig(),
+ proxyRealm.getLoginContextName()).generateToken(proxyServer.getHost());
headers.set(PROXY_AUTHORIZATION, NEGOTIATE + " " + challengeHeader);
}
diff --git a/client/src/main/java/org/asynchttpclient/netty/handler/intercept/Unauthorized401Interceptor.java b/client/src/main/java/org/asynchttpclient/netty/handler/intercept/Unauthorized401Interceptor.java
index e63daece58..30ba1bc3d6 100644
--- a/client/src/main/java/org/asynchttpclient/netty/handler/intercept/Unauthorized401Interceptor.java
+++ b/client/src/main/java/org/asynchttpclient/netty/handler/intercept/Unauthorized401Interceptor.java
@@ -139,7 +139,7 @@ public boolean exitAfterHandling401(final Channel channel,
return false;
}
try {
- kerberosChallenge(request, requestHeaders);
+ kerberosChallenge(realm, request, requestHeaders);
} catch (SpnegoEngineException e) {
// FIXME
@@ -200,12 +200,19 @@ private void ntlmChallenge(String authenticateHeader,
}
}
- private void kerberosChallenge(Request request,
+ private void kerberosChallenge(Realm realm,
+ Request request,
HttpHeaders headers) throws SpnegoEngineException {
Uri uri = request.getUri();
String host = withDefault(request.getVirtualHost(), uri.getHost());
- String challengeHeader = SpnegoEngine.instance().generateToken(host);
+ String challengeHeader = SpnegoEngine.instance(realm.getPrincipal(),
+ realm.getPassword(),
+ realm.getServicePrincipalName(),
+ realm.getRealmName(),
+ realm.isUseCanonicalHostname(),
+ realm.getCustomLoginConfig(),
+ realm.getLoginContextName()).generateToken(host);
headers.set(AUTHORIZATION, NEGOTIATE + " " + challengeHeader);
}
}
diff --git a/client/src/main/java/org/asynchttpclient/spnego/NamePasswordCallbackHandler.java b/client/src/main/java/org/asynchttpclient/spnego/NamePasswordCallbackHandler.java
new file mode 100644
index 0000000000..ba79f9883a
--- /dev/null
+++ b/client/src/main/java/org/asynchttpclient/spnego/NamePasswordCallbackHandler.java
@@ -0,0 +1,82 @@
+package org.asynchttpclient.spnego;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.security.auth.callback.Callback;
+import javax.security.auth.callback.CallbackHandler;
+import javax.security.auth.callback.NameCallback;
+import javax.security.auth.callback.PasswordCallback;
+import javax.security.auth.callback.UnsupportedCallbackException;
+import java.io.IOException;
+import java.lang.reflect.Method;
+
+public class NamePasswordCallbackHandler implements CallbackHandler {
+ private final Logger log = LoggerFactory.getLogger(getClass());
+ private static final String PASSWORD_CALLBACK_NAME = "setObject";
+ private static final Class>[] PASSWORD_CALLBACK_TYPES =
+ new Class>[] {Object.class, char[].class, String.class};
+
+ private String username;
+ private String password;
+
+ private String passwordCallbackName;
+
+ public NamePasswordCallbackHandler(String username, String password) {
+ this(username, password, null);
+ }
+
+ public NamePasswordCallbackHandler(String username, String password, String passwordCallbackName) {
+ this.username = username;
+ this.password = password;
+ this.passwordCallbackName = passwordCallbackName;
+ }
+
+ public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
+ for (int i = 0; i < callbacks.length; i++) {
+ Callback callback = callbacks[i];
+ if (handleCallback(callback)) {
+ continue;
+ } else if (callback instanceof NameCallback) {
+ ((NameCallback) callback).setName(username);
+ } else if (callback instanceof PasswordCallback) {
+ PasswordCallback pwCallback = (PasswordCallback) callback;
+ pwCallback.setPassword(password.toCharArray());
+ } else if (!invokePasswordCallback(callback)) {
+ String errorMsg = "Unsupported callback type " + callbacks[i].getClass().getName();
+ log.info(errorMsg);
+ throw new UnsupportedCallbackException(callbacks[i], errorMsg);
+ }
+ }
+ }
+
+ protected boolean handleCallback(Callback callback) {
+ return false;
+ }
+
+ /*
+ * This method is called from the handle(Callback[]) method when the specified callback
+ * did not match any of the known callback classes. It looks for the callback method
+ * having the specified method name with one of the suppported parameter types.
+ * If found, it invokes the callback method on the object and returns true.
+ * If not, it returns false.
+ */
+ private boolean invokePasswordCallback(Callback callback) {
+ String cbname = passwordCallbackName == null
+ ? PASSWORD_CALLBACK_NAME : passwordCallbackName;
+ for (Class> arg : PASSWORD_CALLBACK_TYPES) {
+ try {
+ Method method = callback.getClass().getMethod(cbname, arg);
+ Object args[] = new Object[] {
+ arg == String.class ? password : password.toCharArray()
+ };
+ method.invoke(callback, args);
+ return true;
+ } catch (Exception e) {
+ // ignore and continue
+ log.debug(e.toString());
+ }
+ }
+ return false;
+ }
+}
\ No newline at end of file
diff --git a/client/src/main/java/org/asynchttpclient/spnego/SpnegoEngine.java b/client/src/main/java/org/asynchttpclient/spnego/SpnegoEngine.java
index 3326dca931..7f887965ec 100644
--- a/client/src/main/java/org/asynchttpclient/spnego/SpnegoEngine.java
+++ b/client/src/main/java/org/asynchttpclient/spnego/SpnegoEngine.java
@@ -38,6 +38,7 @@
package org.asynchttpclient.spnego;
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;
@@ -45,8 +46,19 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import javax.security.auth.Subject;
+import javax.security.auth.callback.CallbackHandler;
+import javax.security.auth.login.AppConfigurationEntry;
+import javax.security.auth.login.Configuration;
+import javax.security.auth.login.LoginContext;
+import javax.security.auth.login.LoginException;
import java.io.IOException;
+import java.net.InetAddress;
+import java.security.PrivilegedActionException;
+import java.security.PrivilegedExceptionAction;
import java.util.Base64;
+import java.util.HashMap;
+import java.util.Map;
/**
* SPNEGO (Simple and Protected GSSAPI Negotiation Mechanism) authentication scheme.
@@ -57,31 +69,87 @@ 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 static SpnegoEngine instance;
+ private static Map instances = new HashMap<>();
private final Logger log = LoggerFactory.getLogger(getClass());
private final SpnegoTokenGenerator spnegoGenerator;
+ private final String username;
+ private final String password;
+ private final String servicePrincipalName;
+ private final String realmName;
+ private final boolean useCanonicalHostname;
+ private final String loginContextName;
+ private final Map customLoginConfig;
- public SpnegoEngine(final SpnegoTokenGenerator spnegoGenerator) {
+ public SpnegoEngine(final String username,
+ final String password,
+ final String servicePrincipalName,
+ final String realmName,
+ final boolean useCanonicalHostname,
+ final Map customLoginConfig,
+ final String loginContextName,
+ final SpnegoTokenGenerator spnegoGenerator) {
+ this.username = username;
+ this.password = password;
+ this.servicePrincipalName = servicePrincipalName;
+ this.realmName = realmName;
+ this.useCanonicalHostname = useCanonicalHostname;
+ this.customLoginConfig = customLoginConfig;
this.spnegoGenerator = spnegoGenerator;
+ this.loginContextName = loginContextName;
}
public SpnegoEngine() {
- this(null);
+ this(null,
+ null,
+ null,
+ null,
+ true,
+ null,
+ null,
+ null);
}
- public static SpnegoEngine instance() {
- if (instance == null)
- instance = new SpnegoEngine();
- return instance;
+ public static SpnegoEngine instance(final String username,
+ final String password,
+ final String servicePrincipalName,
+ final String realmName,
+ final boolean useCanonicalHostname,
+ final Map customLoginConfig,
+ final String loginContextName) {
+ String key = "";
+ if (customLoginConfig != null && !customLoginConfig.isEmpty()) {
+ StringBuilder customLoginConfigKeyValues = new StringBuilder();
+ for (String loginConfigKey : customLoginConfig.keySet()) {
+ customLoginConfigKeyValues.append(loginConfigKey).append("=")
+ .append(customLoginConfig.get(loginConfigKey));
+ }
+ key = customLoginConfigKeyValues.toString();
+ }
+ if (username != null) {
+ key += username;
+ }
+ if (loginContextName != null) {
+ key += loginContextName;
+ }
+ if (!instances.containsKey(key)) {
+ instances.put(key, new SpnegoEngine(username,
+ password,
+ servicePrincipalName,
+ realmName,
+ useCanonicalHostname,
+ customLoginConfig,
+ loginContextName,
+ null));
+ }
+ return instances.get(key);
}
- public String generateToken(String server) throws SpnegoEngineException {
+ public String generateToken(String host) throws SpnegoEngineException {
GSSContext gssContext = null;
byte[] token = null; // base64 decoded challenge
Oid negotiationOid;
try {
- log.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 described
* here...
@@ -99,11 +167,30 @@ public String generateToken(String server) throws SpnegoEngineException {
negotiationOid = new Oid(SPNEGO_OID);
boolean tryKerberos = false;
+ String spn = getCompleteServicePrincipalName(host);
try {
GSSManager manager = GSSManager.getInstance();
- GSSName serverName = manager.createName("HTTP@" + server, GSSName.NT_HOSTBASED_SERVICE);
- gssContext = manager.createContext(serverName.canonicalize(negotiationOid), negotiationOid, null,
- GSSContext.DEFAULT_LIFETIME);
+ GSSName serverName = manager.createName(spn, GSSName.NT_HOSTBASED_SERVICE);
+ GSSCredential myCred = null;
+ if (username != null || loginContextName != null || (customLoginConfig != null && !customLoginConfig.isEmpty())) {
+ String contextName = loginContextName;
+ if (contextName == null) {
+ contextName = "";
+ }
+ LoginContext loginContext = new LoginContext(contextName,
+ null,
+ getUsernamePasswordHandler(),
+ getLoginConfiguration());
+ loginContext.login();
+ final Oid negotiationOidFinal = negotiationOid;
+ final PrivilegedExceptionAction action = () -> manager.createCredential(null,
+ GSSCredential.INDEFINITE_LIFETIME, negotiationOidFinal, GSSCredential.INITIATE_AND_ACCEPT);
+ myCred = Subject.doAs(loginContext.getSubject(), action);
+ }
+ gssContext = manager.createContext(useCanonicalHostname ? serverName.canonicalize(negotiationOid) : serverName,
+ negotiationOid,
+ myCred,
+ GSSContext.DEFAULT_LIFETIME);
gssContext.requestMutualAuth(true);
gssContext.requestCredDeleg(true);
} catch (GSSException ex) {
@@ -123,7 +210,7 @@ public String generateToken(String server) throws SpnegoEngineException {
log.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);
+ GSSName serverName = manager.createName(spn, GSSName.NT_HOSTBASED_SERVICE);
gssContext = manager.createContext(serverName.canonicalize(negotiationOid), negotiationOid, null,
GSSContext.DEFAULT_LIFETIME);
gssContext.requestMutualAuth(true);
@@ -164,8 +251,59 @@ public String generateToken(String server) throws SpnegoEngineException {
throw new SpnegoEngineException(gsse.getMessage(), gsse);
// other error
throw new SpnegoEngineException(gsse.getMessage());
- } catch (IOException ex) {
+ } catch (IOException | LoginException | PrivilegedActionException ex) {
throw new SpnegoEngineException(ex.getMessage());
}
}
+
+ protected String getCompleteServicePrincipalName(String host) {
+ String name;
+ if (servicePrincipalName == null) {
+ if (useCanonicalHostname) {
+ host = getCanonicalHostname(host);
+ }
+ name = "HTTP/" + host;
+ } else {
+ name = servicePrincipalName;
+ }
+ if (realmName != null) {
+ name += "@" + realmName;
+ }
+ log.debug("Service Principal Name is {}", name);
+ return name;
+ }
+
+ private String getCanonicalHostname(String hostname) {
+ String canonicalHostname = hostname;
+ try {
+ InetAddress in = InetAddress.getByName(hostname);
+ canonicalHostname = in.getCanonicalHostName();
+ log.debug("Resolved hostname={} to canonicalHostname={}", hostname, canonicalHostname);
+ } catch (Exception e) {
+ log.warn("Unable to resolve canonical hostname", e);
+ }
+ return canonicalHostname;
+ }
+
+ public CallbackHandler getUsernamePasswordHandler() {
+ if (username == null) {
+ return null;
+ }
+ return new NamePasswordCallbackHandler(username, password);
+ }
+
+ public Configuration getLoginConfiguration() {
+ if (customLoginConfig != null && !customLoginConfig.isEmpty()) {
+ return new Configuration() {
+ @Override
+ public AppConfigurationEntry[] getAppConfigurationEntry(String name) {
+ return new AppConfigurationEntry[] {
+ new AppConfigurationEntry("com.sun.security.auth.module.Krb5LoginModule",
+ AppConfigurationEntry.LoginModuleControlFlag.REQUIRED,
+ customLoginConfig)};
+ }
+ };
+ }
+ return null;
+ }
}
diff --git a/client/src/main/java/org/asynchttpclient/util/AuthenticatorUtils.java b/client/src/main/java/org/asynchttpclient/util/AuthenticatorUtils.java
index 59754e22a8..00d69af7d2 100644
--- a/client/src/main/java/org/asynchttpclient/util/AuthenticatorUtils.java
+++ b/client/src/main/java/org/asynchttpclient/util/AuthenticatorUtils.java
@@ -175,7 +175,14 @@ else if (request.getVirtualHost() != null)
host = request.getUri().getHost();
try {
- authorizationHeader = NEGOTIATE + " " + SpnegoEngine.instance().generateToken(host);
+ authorizationHeader = NEGOTIATE + " " + SpnegoEngine.instance(
+ realm.getPrincipal(),
+ realm.getPassword(),
+ realm.getServicePrincipalName(),
+ realm.getRealmName(),
+ realm.isUseCanonicalHostname(),
+ realm.getCustomLoginConfig(),
+ realm.getLoginContextName()).generateToken(host);
} catch (SpnegoEngineException e) {
throw new RuntimeException(e);
}
diff --git a/client/src/test/java/org/asynchttpclient/spnego/SpnegoEngineTest.java b/client/src/test/java/org/asynchttpclient/spnego/SpnegoEngineTest.java
new file mode 100644
index 0000000000..bd8fbf34ea
--- /dev/null
+++ b/client/src/test/java/org/asynchttpclient/spnego/SpnegoEngineTest.java
@@ -0,0 +1,125 @@
+package org.asynchttpclient.spnego;
+
+import org.apache.commons.io.FileUtils;
+import org.apache.kerby.kerberos.kerb.server.SimpleKdcServer;
+import org.asynchttpclient.AbstractBasicTest;
+import org.testng.Assert;
+import org.testng.annotations.AfterClass;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+import java.io.File;
+import java.util.HashMap;
+import java.util.Map;
+
+public class SpnegoEngineTest extends AbstractBasicTest {
+ private static SimpleKdcServer kerbyServer;
+
+ private static String basedir;
+ private static String alice;
+ private static String bob;
+ private static File aliceKeytab;
+ private static File bobKeytab;
+ private static File loginConfig;
+
+ @BeforeClass
+ public static void startServers() throws Exception {
+ basedir = System.getProperty("basedir");
+ if (basedir == null) {
+ basedir = new File(".").getCanonicalPath();
+ }
+
+ // System.setProperty("sun.security.krb5.debug", "true");
+ System.setProperty("java.security.krb5.conf",
+ new File(basedir + File.separator + "target" + File.separator + "krb5.conf").getCanonicalPath());
+ loginConfig = new File(basedir + File.separator + "target" + File.separator + "kerberos.jaas");
+ System.setProperty("java.security.auth.login.config", loginConfig.getCanonicalPath());
+
+ kerbyServer = new SimpleKdcServer();
+
+ kerbyServer.setKdcRealm("service.ws.apache.org");
+ kerbyServer.setAllowUdp(false);
+ kerbyServer.setWorkDir(new File(basedir, "target"));
+
+ //kerbyServer.setInnerKdcImpl(new NettyKdcServerImpl(kerbyServer.getKdcSetting()));
+
+ kerbyServer.init();
+
+ // Create principals
+ alice = "alice@service.ws.apache.org";
+ bob = "bob/service.ws.apache.org@service.ws.apache.org";
+
+ kerbyServer.createPrincipal(alice, "alice");
+ kerbyServer.createPrincipal(bob, "bob");
+
+ aliceKeytab = new File(basedir + File.separator + "target" + File.separator + "alice.keytab");
+ bobKeytab = new File(basedir + File.separator + "target" + File.separator + "bob.keytab");
+ kerbyServer.exportPrincipal(alice, aliceKeytab);
+ kerbyServer.exportPrincipal(bob, bobKeytab);
+
+ kerbyServer.start();
+
+ FileUtils.copyInputStreamToFile(SpnegoEngine.class.getResourceAsStream("/kerberos.jaas"), loginConfig);
+ }
+
+ @Test
+ public void testSpnegoGenerateTokenWithUsernamePassword() throws Exception {
+ SpnegoEngine spnegoEngine = new SpnegoEngine("alice",
+ "alice",
+ "bob",
+ "service.ws.apache.org",
+ false,
+ null,
+ "alice",
+ null);
+ String token = spnegoEngine.generateToken("localhost");
+ Assert.assertNotNull(token);
+ Assert.assertTrue(token.startsWith("YII"));
+ }
+
+ @Test(expectedExceptions = SpnegoEngineException.class)
+ public void testSpnegoGenerateTokenWithUsernamePasswordFail() throws Exception {
+ SpnegoEngine spnegoEngine = new SpnegoEngine("alice",
+ "wrong password",
+ "bob",
+ "service.ws.apache.org",
+ false,
+ null,
+ "alice",
+ null);
+ spnegoEngine.generateToken("localhost");
+ }
+
+ @Test
+ public void testSpnegoGenerateTokenWithCustomLoginConfig() throws Exception {
+ Map loginConfig = new HashMap<>();
+ loginConfig.put("useKeyTab", "true");
+ loginConfig.put("storeKey", "true");
+ loginConfig.put("refreshKrb5Config", "true");
+ loginConfig.put("keyTab", aliceKeytab.getCanonicalPath());
+ loginConfig.put("principal", alice);
+ loginConfig.put("debug", String.valueOf(true));
+ SpnegoEngine spnegoEngine = new SpnegoEngine(null,
+ null,
+ "bob",
+ "service.ws.apache.org",
+ false,
+ loginConfig,
+ null,
+ null);
+
+ String token = spnegoEngine.generateToken("localhost");
+ Assert.assertNotNull(token);
+ Assert.assertTrue(token.startsWith("YII"));
+ }
+
+ @AfterClass
+ public static void cleanup() throws Exception {
+ if (kerbyServer != null) {
+ kerbyServer.stop();
+ }
+ FileUtils.deleteQuietly(aliceKeytab);
+ FileUtils.deleteQuietly(bobKeytab);
+ FileUtils.deleteQuietly(loginConfig);
+ }
+}
diff --git a/client/src/test/resources/kerberos.jaas b/client/src/test/resources/kerberos.jaas
new file mode 100644
index 0000000000..cd5b316bf1
--- /dev/null
+++ b/client/src/test/resources/kerberos.jaas
@@ -0,0 +1,8 @@
+
+alice {
+ com.sun.security.auth.module.Krb5LoginModule required refreshKrb5Config=true useKeyTab=false principal="alice";
+};
+
+bob {
+ com.sun.security.auth.module.Krb5LoginModule required refreshKrb5Config=true useKeyTab=false storeKey=true principal="bob/service.ws.apache.org";
+};
diff --git a/pom.xml b/pom.xml
index 32e97bda23..c13def1e98 100644
--- a/pom.xml
+++ b/pom.xml
@@ -292,6 +292,12 @@
rxjava
${rxjava2.version}
+
+ org.apache.kerby
+ kerb-simplekdc
+ ${kerby.version}
+ test
+
@@ -418,5 +424,6 @@
1.2.2
2.19.0
2.0.0.0
+ 1.1.1