From 28ccdcbe06ee7250f33607c8f29e1be28e507d00 Mon Sep 17 00:00:00 2001 From: Nicholas DiPiazza Date: Thu, 4 Oct 2018 12:09:26 -0500 Subject: [PATCH 01/12] add the ability to pass in a {principal name, keytab} combination to async http client. --- .../main/java/org/asynchttpclient/Dsl.java | 4 +- .../main/java/org/asynchttpclient/Realm.java | 37 +++++++++++- .../ProxyUnauthorized407Interceptor.java | 7 ++- .../intercept/Unauthorized401Interceptor.java | 7 ++- .../asynchttpclient/spnego/SpnegoEngine.java | 56 +++++++++++++++++-- .../util/AuthenticatorUtils.java | 2 +- 6 files changed, 97 insertions(+), 16 deletions(-) diff --git a/client/src/main/java/org/asynchttpclient/Dsl.java b/client/src/main/java/org/asynchttpclient/Dsl.java index 914b734b77..51fa03d711 100644 --- a/client/src/main/java/org/asynchttpclient/Dsl.java +++ b/client/src/main/java/org/asynchttpclient/Dsl.java @@ -99,7 +99,9 @@ public static Realm.Builder realm(Realm prototype) { .setNtlmDomain(prototype.getNtlmDomain()) .setNtlmHost(prototype.getNtlmHost()) .setUseAbsoluteURI(prototype.isUseAbsoluteURI()) - .setOmitQuery(prototype.isOmitQuery()); + .setOmitQuery(prototype.isOmitQuery()) + .setSpnegoKeytabFilePath(prototype.getSpnegoKeytabFilePath()) + .setSpnegoPrincipal(prototype.getSpnegoPrincipal()); } 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..2109cd513e 100644 --- a/client/src/main/java/org/asynchttpclient/Realm.java +++ b/client/src/main/java/org/asynchttpclient/Realm.java @@ -60,6 +60,8 @@ public class Realm { private final String ntlmDomain; private final boolean useAbsoluteURI; private final boolean omitQuery; + private final String spnegoKeytabFilePath; + private final String spnegoPrincipal; private Realm(AuthScheme scheme, String principal, @@ -78,7 +80,9 @@ private Realm(AuthScheme scheme, String ntlmDomain, String ntlmHost, boolean useAbsoluteURI, - boolean omitQuery) { + boolean omitQuery, + String spnegoKeytabFilePath, + String spnegoPrincipal) { this.scheme = assertNotNull(scheme, "scheme"); this.principal = assertNotNull(principal, "principal"); @@ -98,6 +102,8 @@ private Realm(AuthScheme scheme, this.ntlmHost = ntlmHost; this.useAbsoluteURI = useAbsoluteURI; this.omitQuery = omitQuery; + this.spnegoKeytabFilePath = spnegoKeytabFilePath; + this.spnegoPrincipal = spnegoPrincipal; } public String getPrincipal() { @@ -187,6 +193,14 @@ public boolean isOmitQuery() { return omitQuery; } + public String getSpnegoKeytabFilePath() { + return spnegoKeytabFilePath; + } + + public String getSpnegoPrincipal() { + return spnegoPrincipal; + } + @Override public String toString() { return "Realm{" + "principal='" + principal + '\'' + ", scheme=" + scheme + ", realmName='" + realmName + '\'' @@ -207,6 +221,8 @@ public static class Builder { private final String principal; private final String password; private AuthScheme scheme; + private String spnegoKeytabFilePath; + private String spnegoPrincipal; private String realmName; private String nonce; private String algorithm; @@ -224,6 +240,11 @@ public static class Builder { private boolean useAbsoluteURI = false; private boolean omitQuery; + public Builder() { + this.principal = null; + this.password = null; + } + public Builder(String principal, String password) { this.principal = principal; this.password = password; @@ -311,6 +332,16 @@ public Builder setCharset(Charset charset) { return this; } + public Builder setSpnegoKeytabFilePath(String spnegoKeytabFilePath) { + this.spnegoKeytabFilePath = spnegoKeytabFilePath; + return this; + } + + public Builder setSpnegoPrincipal(String spnegoPrincipal) { + this.spnegoPrincipal = spnegoPrincipal; + return this; + } + private String parseRawQop(String rawQop) { String[] rawServerSupportedQops = rawQop.split(","); String[] serverSupportedQops = new String[rawServerSupportedQops.length]; @@ -501,7 +532,9 @@ public Realm build() { ntlmDomain, ntlmHost, useAbsoluteURI, - omitQuery); + omitQuery, + spnegoKeytabFilePath, + spnegoPrincipal); } } } 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..408ef4bbf1 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,11 @@ 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.getSpnegoKeytabFilePath(), proxyRealm.getSpnegoPrincipal()).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..5a6d9047a2 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,13 @@ 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.getSpnegoKeytabFilePath(), realm.getSpnegoPrincipal()).generateToken(host); headers.set(AUTHORIZATION, NEGOTIATE + " " + challengeHeader); } } diff --git a/client/src/main/java/org/asynchttpclient/spnego/SpnegoEngine.java b/client/src/main/java/org/asynchttpclient/spnego/SpnegoEngine.java index 3326dca931..f0f4ec12bb 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,17 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.security.auth.Subject; +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.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. @@ -60,18 +70,22 @@ public class SpnegoEngine { private static SpnegoEngine instance; private final Logger log = LoggerFactory.getLogger(getClass()); private final SpnegoTokenGenerator spnegoGenerator; + private final String spnegoPrincipal; + private final String spnegoKeytabFilePath; - public SpnegoEngine(final SpnegoTokenGenerator spnegoGenerator) { + public SpnegoEngine(final String spnegoPrincipal, final String spnegoKeytabFilePath, final SpnegoTokenGenerator spnegoGenerator) { this.spnegoGenerator = spnegoGenerator; + this.spnegoPrincipal = spnegoPrincipal; + this.spnegoKeytabFilePath = spnegoKeytabFilePath; } public SpnegoEngine() { - this(null); + this(null, null, null); } - public static SpnegoEngine instance() { + public static SpnegoEngine instance(final String spnegoKeytabFilePath, final String spnegoPrincipal) { if (instance == null) - instance = new SpnegoEngine(); + instance = new SpnegoEngine(spnegoPrincipal, spnegoKeytabFilePath, null); return instance; } @@ -102,8 +116,38 @@ public String generateToken(String server) throws SpnegoEngineException { 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); + LoginContext loginContext; + try { + loginContext = new LoginContext("", new Subject(), null, new Configuration() { + @Override + public AppConfigurationEntry[] getAppConfigurationEntry(String name) { + Map options = new HashMap<>(); + options.put("useKeyTab", "true"); + options.put("storeKey", "true"); + options.put("refreshKrb5Config", "true"); + options.put("keyTab", spnegoKeytabFilePath); + options.put("principal", spnegoPrincipal); + options.put("useTicketCache", "true"); + options.put("debug", String.valueOf(true)); + return new AppConfigurationEntry[]{ + new AppConfigurationEntry("com.sun.security.auth.module.Krb5LoginModule", + AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, + options)}; + } + }); + loginContext.login(); + } catch (LoginException e) { + throw new RuntimeException(e); + } + final Oid negotiationOidFinal = negotiationOid; + final PrivilegedExceptionAction action = () -> manager.createCredential(null, + GSSCredential.INDEFINITE_LIFETIME, negotiationOidFinal, GSSCredential.INITIATE_AND_ACCEPT); + try { + gssContext = manager.createContext(serverName.canonicalize(negotiationOid), negotiationOid, Subject.doAs(loginContext.getSubject(), action), + GSSContext.DEFAULT_LIFETIME); + } catch (PrivilegedActionException e) { + throw new RuntimeException(e); + } gssContext.requestMutualAuth(true); gssContext.requestCredDeleg(true); } catch (GSSException ex) { diff --git a/client/src/main/java/org/asynchttpclient/util/AuthenticatorUtils.java b/client/src/main/java/org/asynchttpclient/util/AuthenticatorUtils.java index 59754e22a8..370f54e854 100644 --- a/client/src/main/java/org/asynchttpclient/util/AuthenticatorUtils.java +++ b/client/src/main/java/org/asynchttpclient/util/AuthenticatorUtils.java @@ -175,7 +175,7 @@ else if (request.getVirtualHost() != null) host = request.getUri().getHost(); try { - authorizationHeader = NEGOTIATE + " " + SpnegoEngine.instance().generateToken(host); + authorizationHeader = NEGOTIATE + " " + SpnegoEngine.instance(realm.getSpnegoKeytabFilePath(), realm.getSpnegoPrincipal()).generateToken(host); } catch (SpnegoEngineException e) { throw new RuntimeException(e); } From a30d90af405987b6b02f5d58a61a69101b88d5d0 Mon Sep 17 00:00:00 2001 From: Nicholas DiPiazza Date: Thu, 4 Oct 2018 12:25:43 -0500 Subject: [PATCH 02/12] fix issue where spnego principal/keytab was no longer optional --- .../asynchttpclient/spnego/SpnegoEngine.java | 26 ++++++++----------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/client/src/main/java/org/asynchttpclient/spnego/SpnegoEngine.java b/client/src/main/java/org/asynchttpclient/spnego/SpnegoEngine.java index f0f4ec12bb..7dfd1bca6b 100644 --- a/client/src/main/java/org/asynchttpclient/spnego/SpnegoEngine.java +++ b/client/src/main/java/org/asynchttpclient/spnego/SpnegoEngine.java @@ -116,8 +116,10 @@ public String generateToken(String server) throws SpnegoEngineException { try { GSSManager manager = GSSManager.getInstance(); GSSName serverName = manager.createName("HTTP@" + server, GSSName.NT_HOSTBASED_SERVICE); - LoginContext loginContext; - try { + GSSCredential myCred = null; + if (spnegoKeytabFilePath != null && spnegoKeytabFilePath.trim().length() > 0 && + spnegoPrincipal != null && spnegoPrincipal.trim().length() > 0) { + LoginContext loginContext; loginContext = new LoginContext("", new Subject(), null, new Configuration() { @Override public AppConfigurationEntry[] getAppConfigurationEntry(String name) { @@ -129,25 +131,19 @@ public AppConfigurationEntry[] getAppConfigurationEntry(String name) { options.put("principal", spnegoPrincipal); options.put("useTicketCache", "true"); options.put("debug", String.valueOf(true)); - return new AppConfigurationEntry[]{ + return new AppConfigurationEntry[] { new AppConfigurationEntry("com.sun.security.auth.module.Krb5LoginModule", AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, options)}; } }); loginContext.login(); - } catch (LoginException e) { - throw new RuntimeException(e); - } - final Oid negotiationOidFinal = negotiationOid; - final PrivilegedExceptionAction action = () -> manager.createCredential(null, - GSSCredential.INDEFINITE_LIFETIME, negotiationOidFinal, GSSCredential.INITIATE_AND_ACCEPT); - try { - gssContext = manager.createContext(serverName.canonicalize(negotiationOid), negotiationOid, Subject.doAs(loginContext.getSubject(), action), - GSSContext.DEFAULT_LIFETIME); - } catch (PrivilegedActionException e) { - throw new RuntimeException(e); + final Oid negotiationOidFinal = negotiationOid; + final PrivilegedExceptionAction action = () -> manager.createCredential(null, + GSSCredential.INDEFINITE_LIFETIME, negotiationOidFinal, GSSCredential.INITIATE_AND_ACCEPT); + myCred = loginContext != null ? Subject.doAs(loginContext.getSubject(), action) : null; } + gssContext = manager.createContext(serverName.canonicalize(negotiationOid), negotiationOid, myCred, GSSContext.DEFAULT_LIFETIME); gssContext.requestMutualAuth(true); gssContext.requestCredDeleg(true); } catch (GSSException ex) { @@ -208,7 +204,7 @@ public AppConfigurationEntry[] getAppConfigurationEntry(String name) { 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()); } } From 1695e54d87b3b1cb01ed82d62f88ebb95ffbed9a Mon Sep 17 00:00:00 2001 From: Nicholas DiPiazza Date: Thu, 4 Oct 2018 12:42:25 -0500 Subject: [PATCH 03/12] specify the login config as a map to allow all the values custom not just a couple of them. --- .../main/java/org/asynchttpclient/Dsl.java | 3 +- .../main/java/org/asynchttpclient/Realm.java | 58 ++++++++++--------- .../ProxyUnauthorized407Interceptor.java | 2 +- .../intercept/Unauthorized401Interceptor.java | 2 +- .../asynchttpclient/spnego/SpnegoEngine.java | 27 +++------ .../util/AuthenticatorUtils.java | 2 +- 6 files changed, 43 insertions(+), 51 deletions(-) diff --git a/client/src/main/java/org/asynchttpclient/Dsl.java b/client/src/main/java/org/asynchttpclient/Dsl.java index 51fa03d711..e4891bf599 100644 --- a/client/src/main/java/org/asynchttpclient/Dsl.java +++ b/client/src/main/java/org/asynchttpclient/Dsl.java @@ -100,8 +100,7 @@ public static Realm.Builder realm(Realm prototype) { .setNtlmHost(prototype.getNtlmHost()) .setUseAbsoluteURI(prototype.isUseAbsoluteURI()) .setOmitQuery(prototype.isOmitQuery()) - .setSpnegoKeytabFilePath(prototype.getSpnegoKeytabFilePath()) - .setSpnegoPrincipal(prototype.getSpnegoPrincipal()); + .setCustomLoginConfig(prototype.getCustomLoginConfig()); } 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 2109cd513e..9b1cdea60e 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,8 +61,7 @@ public class Realm { private final String ntlmDomain; private final boolean useAbsoluteURI; private final boolean omitQuery; - private final String spnegoKeytabFilePath; - private final String spnegoPrincipal; + private final Map customLoginConfig; private Realm(AuthScheme scheme, String principal, @@ -81,8 +81,7 @@ private Realm(AuthScheme scheme, String ntlmHost, boolean useAbsoluteURI, boolean omitQuery, - String spnegoKeytabFilePath, - String spnegoPrincipal) { + Map customLoginConfig) { this.scheme = assertNotNull(scheme, "scheme"); this.principal = assertNotNull(principal, "principal"); @@ -102,8 +101,7 @@ private Realm(AuthScheme scheme, this.ntlmHost = ntlmHost; this.useAbsoluteURI = useAbsoluteURI; this.omitQuery = omitQuery; - this.spnegoKeytabFilePath = spnegoKeytabFilePath; - this.spnegoPrincipal = spnegoPrincipal; + this.customLoginConfig = customLoginConfig; } public String getPrincipal() { @@ -193,20 +191,33 @@ public boolean isOmitQuery() { return omitQuery; } - public String getSpnegoKeytabFilePath() { - return spnegoKeytabFilePath; - } - - public String getSpnegoPrincipal() { - return spnegoPrincipal; + public Map getCustomLoginConfig() { + return customLoginConfig; } @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 + + '}'; } public enum AuthScheme { @@ -221,8 +232,6 @@ public static class Builder { private final String principal; private final String password; private AuthScheme scheme; - private String spnegoKeytabFilePath; - private String spnegoPrincipal; private String realmName; private String nonce; private String algorithm; @@ -239,6 +248,7 @@ public static class Builder { private String ntlmHost = "localhost"; private boolean useAbsoluteURI = false; private boolean omitQuery; + private Map customLoginConfig; public Builder() { this.principal = null; @@ -332,13 +342,8 @@ public Builder setCharset(Charset charset) { return this; } - public Builder setSpnegoKeytabFilePath(String spnegoKeytabFilePath) { - this.spnegoKeytabFilePath = spnegoKeytabFilePath; - return this; - } - - public Builder setSpnegoPrincipal(String spnegoPrincipal) { - this.spnegoPrincipal = spnegoPrincipal; + public Builder setCustomLoginConfig(Map customLoginConfig) { + this.customLoginConfig = customLoginConfig; return this; } @@ -533,8 +538,7 @@ public Realm build() { ntlmHost, useAbsoluteURI, omitQuery, - spnegoKeytabFilePath, - spnegoPrincipal); + customLoginConfig); } } } 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 408ef4bbf1..77d5aba978 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 @@ -188,7 +188,7 @@ private void kerberosProxyChallenge(Realm proxyRealm, ProxyServer proxyServer, HttpHeaders headers) throws SpnegoEngineException { - String challengeHeader = SpnegoEngine.instance(proxyRealm.getSpnegoKeytabFilePath(), proxyRealm.getSpnegoPrincipal()).generateToken(proxyServer.getHost()); + String challengeHeader = SpnegoEngine.instance(proxyRealm.getCustomLoginConfig()).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 5a6d9047a2..1f4acb5539 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 @@ -206,7 +206,7 @@ private void kerberosChallenge(Realm realm, Uri uri = request.getUri(); String host = withDefault(request.getVirtualHost(), uri.getHost()); - String challengeHeader = SpnegoEngine.instance(realm.getSpnegoKeytabFilePath(), realm.getSpnegoPrincipal()).generateToken(host); + String challengeHeader = SpnegoEngine.instance(realm.getCustomLoginConfig()).generateToken(host); headers.set(AUTHORIZATION, NEGOTIATE + " " + challengeHeader); } } diff --git a/client/src/main/java/org/asynchttpclient/spnego/SpnegoEngine.java b/client/src/main/java/org/asynchttpclient/spnego/SpnegoEngine.java index 7dfd1bca6b..70e140099d 100644 --- a/client/src/main/java/org/asynchttpclient/spnego/SpnegoEngine.java +++ b/client/src/main/java/org/asynchttpclient/spnego/SpnegoEngine.java @@ -70,22 +70,20 @@ public class SpnegoEngine { private static SpnegoEngine instance; private final Logger log = LoggerFactory.getLogger(getClass()); private final SpnegoTokenGenerator spnegoGenerator; - private final String spnegoPrincipal; - private final String spnegoKeytabFilePath; + private final Map customLoginConfig; - public SpnegoEngine(final String spnegoPrincipal, final String spnegoKeytabFilePath, final SpnegoTokenGenerator spnegoGenerator) { + public SpnegoEngine(final Map customLoginConfig, final SpnegoTokenGenerator spnegoGenerator) { this.spnegoGenerator = spnegoGenerator; - this.spnegoPrincipal = spnegoPrincipal; - this.spnegoKeytabFilePath = spnegoKeytabFilePath; + this.customLoginConfig = customLoginConfig; } public SpnegoEngine() { - this(null, null, null); + this(null, null); } - public static SpnegoEngine instance(final String spnegoKeytabFilePath, final String spnegoPrincipal) { + public static SpnegoEngine instance(final Map customLoginConfig) { if (instance == null) - instance = new SpnegoEngine(spnegoPrincipal, spnegoKeytabFilePath, null); + instance = new SpnegoEngine(customLoginConfig, null); return instance; } @@ -117,24 +115,15 @@ public String generateToken(String server) throws SpnegoEngineException { GSSManager manager = GSSManager.getInstance(); GSSName serverName = manager.createName("HTTP@" + server, GSSName.NT_HOSTBASED_SERVICE); GSSCredential myCred = null; - if (spnegoKeytabFilePath != null && spnegoKeytabFilePath.trim().length() > 0 && - spnegoPrincipal != null && spnegoPrincipal.trim().length() > 0) { + if (customLoginConfig != null && !customLoginConfig.isEmpty()) { LoginContext loginContext; loginContext = new LoginContext("", new Subject(), null, new Configuration() { @Override public AppConfigurationEntry[] getAppConfigurationEntry(String name) { - Map options = new HashMap<>(); - options.put("useKeyTab", "true"); - options.put("storeKey", "true"); - options.put("refreshKrb5Config", "true"); - options.put("keyTab", spnegoKeytabFilePath); - options.put("principal", spnegoPrincipal); - options.put("useTicketCache", "true"); - options.put("debug", String.valueOf(true)); return new AppConfigurationEntry[] { new AppConfigurationEntry("com.sun.security.auth.module.Krb5LoginModule", AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, - options)}; + customLoginConfig)}; } }); loginContext.login(); diff --git a/client/src/main/java/org/asynchttpclient/util/AuthenticatorUtils.java b/client/src/main/java/org/asynchttpclient/util/AuthenticatorUtils.java index 370f54e854..6f4ce1a652 100644 --- a/client/src/main/java/org/asynchttpclient/util/AuthenticatorUtils.java +++ b/client/src/main/java/org/asynchttpclient/util/AuthenticatorUtils.java @@ -175,7 +175,7 @@ else if (request.getVirtualHost() != null) host = request.getUri().getHost(); try { - authorizationHeader = NEGOTIATE + " " + SpnegoEngine.instance(realm.getSpnegoKeytabFilePath(), realm.getSpnegoPrincipal()).generateToken(host); + authorizationHeader = NEGOTIATE + " " + SpnegoEngine.instance(realm.getCustomLoginConfig()).generateToken(host); } catch (SpnegoEngineException e) { throw new RuntimeException(e); } From d057aa8a966ec70ee85cd7430c0e86d39793b39a Mon Sep 17 00:00:00 2001 From: Nicholas DiPiazza Date: Thu, 4 Oct 2018 13:01:29 -0500 Subject: [PATCH 04/12] remove the principal/password assertion on not null add a map of spnego engines so you can support more than one spnego login confg per jvm --- .../main/java/org/asynchttpclient/Realm.java | 4 ++-- .../asynchttpclient/spnego/SpnegoEngine.java | 18 ++++++++++++++---- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/client/src/main/java/org/asynchttpclient/Realm.java b/client/src/main/java/org/asynchttpclient/Realm.java index 9b1cdea60e..5f74c36dcb 100644 --- a/client/src/main/java/org/asynchttpclient/Realm.java +++ b/client/src/main/java/org/asynchttpclient/Realm.java @@ -84,8 +84,8 @@ private Realm(AuthScheme scheme, Map customLoginConfig) { 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; diff --git a/client/src/main/java/org/asynchttpclient/spnego/SpnegoEngine.java b/client/src/main/java/org/asynchttpclient/spnego/SpnegoEngine.java index 70e140099d..2e49403bc7 100644 --- a/client/src/main/java/org/asynchttpclient/spnego/SpnegoEngine.java +++ b/client/src/main/java/org/asynchttpclient/spnego/SpnegoEngine.java @@ -67,7 +67,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 static SpnegoEngine instance; + private static Map instances = new HashMap<>(); private final Logger log = LoggerFactory.getLogger(getClass()); private final SpnegoTokenGenerator spnegoGenerator; private final Map customLoginConfig; @@ -82,9 +82,19 @@ public SpnegoEngine() { } public static SpnegoEngine instance(final Map customLoginConfig) { - if (instance == null) - instance = new SpnegoEngine(customLoginConfig, null); - return instance; + String key = ""; + if (customLoginConfig != null) { + StringBuilder customLoginConfigKeyValues = new StringBuilder(); + for (String loginConfigKey : customLoginConfig.keySet()) { + customLoginConfigKeyValues.append(loginConfigKey).append("=") + .append(customLoginConfig.get(loginConfigKey)); + } + key = customLoginConfigKeyValues.toString(); + } + if (!instances.containsKey(key)) { + instances.put(key, new SpnegoEngine(customLoginConfig, null)); + } + return instances.get(key); } public String generateToken(String server) throws SpnegoEngineException { From f60b144b8755ed3283ede6e6bb6b9c5a443caad4 Mon Sep 17 00:00:00 2001 From: Nicholas DiPiazza Date: Thu, 4 Oct 2018 13:04:20 -0500 Subject: [PATCH 05/12] no need to detect null on loginContext --- .../src/main/java/org/asynchttpclient/spnego/SpnegoEngine.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/main/java/org/asynchttpclient/spnego/SpnegoEngine.java b/client/src/main/java/org/asynchttpclient/spnego/SpnegoEngine.java index 2e49403bc7..ed2cb92f26 100644 --- a/client/src/main/java/org/asynchttpclient/spnego/SpnegoEngine.java +++ b/client/src/main/java/org/asynchttpclient/spnego/SpnegoEngine.java @@ -140,7 +140,7 @@ public AppConfigurationEntry[] getAppConfigurationEntry(String name) { final Oid negotiationOidFinal = negotiationOid; final PrivilegedExceptionAction action = () -> manager.createCredential(null, GSSCredential.INDEFINITE_LIFETIME, negotiationOidFinal, GSSCredential.INITIATE_AND_ACCEPT); - myCred = loginContext != null ? Subject.doAs(loginContext.getSubject(), action) : null; + myCred = Subject.doAs(loginContext.getSubject(), action); } gssContext = manager.createContext(serverName.canonicalize(negotiationOid), negotiationOid, myCred, GSSContext.DEFAULT_LIFETIME); gssContext.requestMutualAuth(true); From 6275c628da616b0fd2c12afaf8b936ad9ff4e00e Mon Sep 17 00:00:00 2001 From: Nicholas DiPiazza Date: Thu, 18 Oct 2018 01:07:42 -0500 Subject: [PATCH 06/12] add a SpnegoEngine unit test. --- client/pom.xml | 6 + .../main/java/org/asynchttpclient/Dsl.java | 2 + .../main/java/org/asynchttpclient/Realm.java | 73 ++++++++--- .../ProxyUnauthorized407Interceptor.java | 5 +- .../intercept/Unauthorized401Interceptor.java | 5 +- .../asynchttpclient/spnego/SpnegoEngine.java | 70 +++++++++-- .../util/AuthenticatorUtils.java | 5 +- .../spnego/SpnegoEngineTest.java | 116 ++++++++++++++++++ client/src/test/resources/kerberos.jaas | 8 ++ pom.xml | 7 ++ 10 files changed, 264 insertions(+), 33 deletions(-) create mode 100644 client/src/test/java/org/asynchttpclient/spnego/SpnegoEngineTest.java create mode 100644 client/src/test/resources/kerberos.jaas diff --git a/client/pom.xml b/client/pom.xml index 23997b6a78..b4c73610ec 100644 --- a/client/pom.xml +++ b/client/pom.xml @@ -73,5 +73,11 @@ reactive-streams-examples test + + org.apache.kerby + kerb-simplekdc + ${cxf.kerby.version} + test + diff --git a/client/src/main/java/org/asynchttpclient/Dsl.java b/client/src/main/java/org/asynchttpclient/Dsl.java index e4891bf599..cd38a9f2f5 100644 --- a/client/src/main/java/org/asynchttpclient/Dsl.java +++ b/client/src/main/java/org/asynchttpclient/Dsl.java @@ -100,6 +100,8 @@ public static Realm.Builder realm(Realm prototype) { .setNtlmHost(prototype.getNtlmHost()) .setUseAbsoluteURI(prototype.isUseAbsoluteURI()) .setOmitQuery(prototype.isOmitQuery()) + .setServicePrincipalName(prototype.getServicePrincipalName()) + .setUseCanonicalHostname(prototype.isUseCanonicalHostname()) .setCustomLoginConfig(prototype.getCustomLoginConfig()); } diff --git a/client/src/main/java/org/asynchttpclient/Realm.java b/client/src/main/java/org/asynchttpclient/Realm.java index 5f74c36dcb..0a567044a7 100644 --- a/client/src/main/java/org/asynchttpclient/Realm.java +++ b/client/src/main/java/org/asynchttpclient/Realm.java @@ -62,6 +62,8 @@ public class Realm { private final boolean useAbsoluteURI; private final boolean omitQuery; private final Map customLoginConfig; + private final String servicePrincipalName; + private final boolean useCanonicalHostname; private Realm(AuthScheme scheme, String principal, @@ -81,6 +83,8 @@ private Realm(AuthScheme scheme, String ntlmHost, boolean useAbsoluteURI, boolean omitQuery, + String servicePrincipalName, + boolean useCanonicalHostname, Map customLoginConfig) { this.scheme = assertNotNull(scheme, "scheme"); @@ -101,6 +105,8 @@ private Realm(AuthScheme scheme, this.ntlmHost = ntlmHost; this.useAbsoluteURI = useAbsoluteURI; this.omitQuery = omitQuery; + this.servicePrincipalName = servicePrincipalName; + this.useCanonicalHostname = useCanonicalHostname; this.customLoginConfig = customLoginConfig; } @@ -195,29 +201,39 @@ public Map getCustomLoginConfig() { return customLoginConfig; } + public String getServicePrincipalName() { + return servicePrincipalName; + } + + public boolean isUseCanonicalHostname() { + return useCanonicalHostname; + } + @Override public String toString() { 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 + - '}'; + "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 + + '}'; } public enum AuthScheme { @@ -248,7 +264,12 @@ 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; public Builder() { this.principal = null; @@ -347,6 +368,16 @@ public Builder setCustomLoginConfig(Map customLoginConfig) { return this; } + public Builder setServicePrincipalName(String servicePrincipalName) { + this.servicePrincipalName = servicePrincipalName; + return this; + } + + public Builder setUseCanonicalHostname(boolean useCanonicalHostname) { + this.useCanonicalHostname = useCanonicalHostname; + return this; + } + private String parseRawQop(String rawQop) { String[] rawServerSupportedQops = rawQop.split(","); String[] serverSupportedQops = new String[rawServerSupportedQops.length]; @@ -538,6 +569,8 @@ public Realm build() { ntlmHost, useAbsoluteURI, omitQuery, + servicePrincipalName, + useCanonicalHostname, customLoginConfig); } } 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 77d5aba978..1bb0df6b75 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 @@ -188,7 +188,10 @@ private void kerberosProxyChallenge(Realm proxyRealm, ProxyServer proxyServer, HttpHeaders headers) throws SpnegoEngineException { - String challengeHeader = SpnegoEngine.instance(proxyRealm.getCustomLoginConfig()).generateToken(proxyServer.getHost()); + String challengeHeader = SpnegoEngine.instance(proxyRealm.getServicePrincipalName(), + proxyRealm.getRealmName(), + proxyRealm.isUseCanonicalHostname(), + proxyRealm.getCustomLoginConfig()).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 1f4acb5539..6404b7b4b4 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 @@ -206,7 +206,10 @@ private void kerberosChallenge(Realm realm, Uri uri = request.getUri(); String host = withDefault(request.getVirtualHost(), uri.getHost()); - String challengeHeader = SpnegoEngine.instance(realm.getCustomLoginConfig()).generateToken(host); + String challengeHeader = SpnegoEngine.instance(realm.getServicePrincipalName(), + realm.getRealmName(), + realm.isUseCanonicalHostname(), + realm.getCustomLoginConfig()).generateToken(host); headers.set(AUTHORIZATION, NEGOTIATE + " " + challengeHeader); } } diff --git a/client/src/main/java/org/asynchttpclient/spnego/SpnegoEngine.java b/client/src/main/java/org/asynchttpclient/spnego/SpnegoEngine.java index ed2cb92f26..4783027544 100644 --- a/client/src/main/java/org/asynchttpclient/spnego/SpnegoEngine.java +++ b/client/src/main/java/org/asynchttpclient/spnego/SpnegoEngine.java @@ -52,6 +52,7 @@ 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; @@ -70,18 +71,31 @@ public class SpnegoEngine { private static Map instances = new HashMap<>(); private final Logger log = LoggerFactory.getLogger(getClass()); private final SpnegoTokenGenerator spnegoGenerator; + private final String realmName; + private final String servicePrincipalName; + private final boolean useCanonicalHostname; private final Map customLoginConfig; - public SpnegoEngine(final Map customLoginConfig, final SpnegoTokenGenerator spnegoGenerator) { - this.spnegoGenerator = spnegoGenerator; + public SpnegoEngine(final String servicePrincipalName, + final String realmName, + final boolean useCanonicalHostname, + final Map customLoginConfig, + final SpnegoTokenGenerator spnegoGenerator) { + this.useCanonicalHostname = useCanonicalHostname; + this.realmName = realmName; + this.servicePrincipalName = servicePrincipalName; this.customLoginConfig = customLoginConfig; + this.spnegoGenerator = spnegoGenerator; } public SpnegoEngine() { - this(null, null); + this(null, null, false, null, null); } - public static SpnegoEngine instance(final Map customLoginConfig) { + public static SpnegoEngine instance(final String servicePrincipalName, + final String realm, + final boolean useCanonicalHostname, + final Map customLoginConfig) { String key = ""; if (customLoginConfig != null) { StringBuilder customLoginConfigKeyValues = new StringBuilder(); @@ -92,18 +106,21 @@ public static SpnegoEngine instance(final Map customLoginConfig) key = customLoginConfigKeyValues.toString(); } if (!instances.containsKey(key)) { - instances.put(key, new SpnegoEngine(customLoginConfig, null)); + instances.put(key, new SpnegoEngine(servicePrincipalName, + realm, + useCanonicalHostname, + customLoginConfig, + 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... @@ -121,9 +138,10 @@ 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); + GSSName serverName = manager.createName(spn, GSSName.NT_HOSTBASED_SERVICE); GSSCredential myCred = null; if (customLoginConfig != null && !customLoginConfig.isEmpty()) { LoginContext loginContext; @@ -142,7 +160,10 @@ public AppConfigurationEntry[] getAppConfigurationEntry(String name) { GSSCredential.INDEFINITE_LIFETIME, negotiationOidFinal, GSSCredential.INITIATE_AND_ACCEPT); myCred = Subject.doAs(loginContext.getSubject(), action); } - gssContext = manager.createContext(serverName.canonicalize(negotiationOid), negotiationOid, myCred, GSSContext.DEFAULT_LIFETIME); + gssContext = manager.createContext(useCanonicalHostname ? serverName.canonicalize(negotiationOid) : serverName, + negotiationOid, + myCred, + GSSContext.DEFAULT_LIFETIME); gssContext.requestMutualAuth(true); gssContext.requestCredDeleg(true); } catch (GSSException ex) { @@ -162,7 +183,7 @@ public AppConfigurationEntry[] getAppConfigurationEntry(String name) { 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); @@ -207,4 +228,33 @@ public AppConfigurationEntry[] getAppConfigurationEntry(String name) { 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; + } } diff --git a/client/src/main/java/org/asynchttpclient/util/AuthenticatorUtils.java b/client/src/main/java/org/asynchttpclient/util/AuthenticatorUtils.java index 6f4ce1a652..3fe15c91fc 100644 --- a/client/src/main/java/org/asynchttpclient/util/AuthenticatorUtils.java +++ b/client/src/main/java/org/asynchttpclient/util/AuthenticatorUtils.java @@ -175,7 +175,10 @@ else if (request.getVirtualHost() != null) host = request.getUri().getHost(); try { - authorizationHeader = NEGOTIATE + " " + SpnegoEngine.instance(realm.getCustomLoginConfig()).generateToken(host); + authorizationHeader = NEGOTIATE + " " + SpnegoEngine.instance(realm.getServicePrincipalName(), + realm.getRealmName(), + realm.isUseCanonicalHostname(), + realm.getCustomLoginConfig()).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..6548b8cc15 --- /dev/null +++ b/client/src/test/java/org/asynchttpclient/spnego/SpnegoEngineTest.java @@ -0,0 +1,116 @@ +package org.asynchttpclient.spnego; + +import org.apache.commons.io.FileUtils; +import org.apache.kerby.kerberos.kerb.server.SimpleKdcServer; +import org.asynchttpclient.AbstractBasicTest; +import org.slf4j.LoggerFactory; +import org.testng.Assert; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import javax.security.auth.Subject; +import javax.security.auth.login.AppConfigurationEntry; +import javax.security.auth.login.Configuration; +import javax.security.auth.login.LoginContext; +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; + + @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()); + + 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(); + + Map loginConfig = new HashMap<>(); + loginConfig.put("useKeyTab", "true"); + loginConfig.put("refreshKrb5Config", "true"); + loginConfig.put("keyTab", bobKeytab.getCanonicalPath()); + loginConfig.put("principal", bob); + loginConfig.put("debug", String.valueOf(true)); + + LoginContext loginContext; + loginContext = new LoginContext("", new Subject(), null, new Configuration() { + @Override + public AppConfigurationEntry[] getAppConfigurationEntry(String name) { + return new AppConfigurationEntry[] { + new AppConfigurationEntry("com.sun.security.auth.module.Krb5LoginModule", + AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, + loginConfig)}; + } + }); + loginContext.login(); + + LoggerFactory.getLogger(SpnegoEngineTest.class).info("Able to log in as {} successfully. Kerby server is ready.", alice); + + loginContext.logout(); + } + + @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("bob", + "service.ws.apache.org", + false, + loginConfig, + 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); + } +} 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..8f50130de9 100644 --- a/pom.xml +++ b/pom.xml @@ -292,6 +292,12 @@ rxjava ${rxjava2.version} + + org.apache.kerby + kerb-simplekdc + ${cxf.kerby.version} + test + @@ -418,5 +424,6 @@ 1.2.2 2.19.0 2.0.0.0 + 1.1.1 From dcdc893b0798ea548879ae503a6d60328b1bd514 Mon Sep 17 00:00:00 2001 From: Nicholas DiPiazza Date: Thu, 18 Oct 2018 01:10:17 -0500 Subject: [PATCH 07/12] Delete kerberos.jaas --- client/src/test/resources/kerberos.jaas | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 client/src/test/resources/kerberos.jaas diff --git a/client/src/test/resources/kerberos.jaas b/client/src/test/resources/kerberos.jaas deleted file mode 100644 index cd5b316bf1..0000000000 --- a/client/src/test/resources/kerberos.jaas +++ /dev/null @@ -1,8 +0,0 @@ - -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"; -}; From 5f5f65163cac80df9545607cc2c48c7d12730d79 Mon Sep 17 00:00:00 2001 From: Nicholas DiPiazza Date: Thu, 18 Oct 2018 01:10:46 -0500 Subject: [PATCH 08/12] Update pom.xml --- client/pom.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/client/pom.xml b/client/pom.xml index b4c73610ec..63ea6e173b 100644 --- a/client/pom.xml +++ b/client/pom.xml @@ -76,7 +76,6 @@ org.apache.kerby kerb-simplekdc - ${cxf.kerby.version} test From 472118ee1d7dacc2c6de72fc5a4227279fd125fb Mon Sep 17 00:00:00 2001 From: Nicholas DiPiazza Date: Thu, 18 Oct 2018 01:46:35 -0500 Subject: [PATCH 09/12] Provide more granularity to be more aligned with other http clients: * Login context name * Username/password auth option --- .../main/java/org/asynchttpclient/Dsl.java | 3 +- .../main/java/org/asynchttpclient/Realm.java | 19 ++++- .../ProxyUnauthorized407Interceptor.java | 7 +- .../intercept/Unauthorized401Interceptor.java | 7 +- .../spnego/NamePasswordCallbackHandler.java | 82 ++++++++++++++++++ .../asynchttpclient/spnego/SpnegoEngine.java | 85 ++++++++++++++----- .../util/AuthenticatorUtils.java | 8 +- .../spnego/SpnegoEngineTest.java | 60 +++++++------ client/src/test/resources/kerberos.jaas | 8 ++ 9 files changed, 225 insertions(+), 54 deletions(-) create mode 100644 client/src/main/java/org/asynchttpclient/spnego/NamePasswordCallbackHandler.java create mode 100644 client/src/test/resources/kerberos.jaas diff --git a/client/src/main/java/org/asynchttpclient/Dsl.java b/client/src/main/java/org/asynchttpclient/Dsl.java index cd38a9f2f5..cdb30ed165 100644 --- a/client/src/main/java/org/asynchttpclient/Dsl.java +++ b/client/src/main/java/org/asynchttpclient/Dsl.java @@ -102,7 +102,8 @@ public static Realm.Builder realm(Realm prototype) { .setOmitQuery(prototype.isOmitQuery()) .setServicePrincipalName(prototype.getServicePrincipalName()) .setUseCanonicalHostname(prototype.isUseCanonicalHostname()) - .setCustomLoginConfig(prototype.getCustomLoginConfig()); + .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 0a567044a7..c6324fd0b4 100644 --- a/client/src/main/java/org/asynchttpclient/Realm.java +++ b/client/src/main/java/org/asynchttpclient/Realm.java @@ -64,6 +64,7 @@ public class Realm { private final Map customLoginConfig; private final String servicePrincipalName; private final boolean useCanonicalHostname; + private final String loginContextName; private Realm(AuthScheme scheme, String principal, @@ -85,7 +86,8 @@ private Realm(AuthScheme scheme, boolean omitQuery, String servicePrincipalName, boolean useCanonicalHostname, - Map customLoginConfig) { + Map customLoginConfig, + String loginContextName) { this.scheme = assertNotNull(scheme, "scheme"); this.principal = principal; @@ -108,6 +110,7 @@ private Realm(AuthScheme scheme, this.servicePrincipalName = servicePrincipalName; this.useCanonicalHostname = useCanonicalHostname; this.customLoginConfig = customLoginConfig; + this.loginContextName = loginContextName; } public String getPrincipal() { @@ -209,6 +212,10 @@ public boolean isUseCanonicalHostname() { return useCanonicalHostname; } + public String getLoginContextName() { + return loginContextName; + } + @Override public String toString() { return "Realm{" + @@ -233,6 +240,7 @@ public String toString() { ", customLoginConfig=" + customLoginConfig + ", servicePrincipalName='" + servicePrincipalName + '\'' + ", useCanonicalHostname=" + useCanonicalHostname + + ", loginContextName='" + loginContextName + '\'' + '}'; } @@ -270,6 +278,7 @@ public static class Builder { private Map customLoginConfig; private String servicePrincipalName; private boolean useCanonicalHostname; + private String loginContextName; public Builder() { this.principal = null; @@ -378,6 +387,11 @@ public Builder setUseCanonicalHostname(boolean 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]; @@ -571,7 +585,8 @@ public Realm build() { omitQuery, servicePrincipalName, useCanonicalHostname, - customLoginConfig); + 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 1bb0df6b75..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 @@ -188,10 +188,13 @@ private void kerberosProxyChallenge(Realm proxyRealm, ProxyServer proxyServer, HttpHeaders headers) throws SpnegoEngineException { - String challengeHeader = SpnegoEngine.instance(proxyRealm.getServicePrincipalName(), + String challengeHeader = SpnegoEngine.instance(proxyRealm.getPrincipal(), + proxyRealm.getPassword(), + proxyRealm.getServicePrincipalName(), proxyRealm.getRealmName(), proxyRealm.isUseCanonicalHostname(), - proxyRealm.getCustomLoginConfig()).generateToken(proxyServer.getHost()); + 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 6404b7b4b4..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 @@ -206,10 +206,13 @@ private void kerberosChallenge(Realm realm, Uri uri = request.getUri(); String host = withDefault(request.getVirtualHost(), uri.getHost()); - String challengeHeader = SpnegoEngine.instance(realm.getServicePrincipalName(), + String challengeHeader = SpnegoEngine.instance(realm.getPrincipal(), + realm.getPassword(), + realm.getServicePrincipalName(), realm.getRealmName(), realm.isUseCanonicalHostname(), - realm.getCustomLoginConfig()).generateToken(host); + 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 4783027544..fb8156add0 100644 --- a/client/src/main/java/org/asynchttpclient/spnego/SpnegoEngine.java +++ b/client/src/main/java/org/asynchttpclient/spnego/SpnegoEngine.java @@ -47,6 +47,7 @@ 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; @@ -71,31 +72,50 @@ public class SpnegoEngine { private static Map instances = new HashMap<>(); private final Logger log = LoggerFactory.getLogger(getClass()); private final SpnegoTokenGenerator spnegoGenerator; - private final String realmName; + 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 String servicePrincipalName, + 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.useCanonicalHostname = useCanonicalHostname; - this.realmName = realmName; + 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, null, false, null, null); + this(null, + null, + null, + null, + true, + null, + null, + null); } - public static SpnegoEngine instance(final String servicePrincipalName, - final String realm, + public static SpnegoEngine instance(final String username, + final String password, + final String servicePrincipalName, + final String realmName, final boolean useCanonicalHostname, - final Map customLoginConfig) { + final Map customLoginConfig, + final String loginContextName) { String key = ""; if (customLoginConfig != null) { StringBuilder customLoginConfigKeyValues = new StringBuilder(); @@ -106,10 +126,13 @@ public static SpnegoEngine instance(final String servicePrincipalName, key = customLoginConfigKeyValues.toString(); } if (!instances.containsKey(key)) { - instances.put(key, new SpnegoEngine(servicePrincipalName, - realm, + instances.put(key, new SpnegoEngine(username, + password, + servicePrincipalName, + realmName, useCanonicalHostname, customLoginConfig, + loginContextName, null)); } return instances.get(key); @@ -143,17 +166,15 @@ public String generateToken(String host) throws SpnegoEngineException { GSSManager manager = GSSManager.getInstance(); GSSName serverName = manager.createName(spn, GSSName.NT_HOSTBASED_SERVICE); GSSCredential myCred = null; - if (customLoginConfig != null && !customLoginConfig.isEmpty()) { - LoginContext loginContext; - loginContext = new LoginContext("", new Subject(), null, new Configuration() { - @Override - public AppConfigurationEntry[] getAppConfigurationEntry(String name) { - return new AppConfigurationEntry[] { - new AppConfigurationEntry("com.sun.security.auth.module.Krb5LoginModule", - AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, - customLoginConfig)}; - } - }); + 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, @@ -257,4 +278,26 @@ private String getCanonicalHostname(String hostname) { } 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 3fe15c91fc..00d69af7d2 100644 --- a/client/src/main/java/org/asynchttpclient/util/AuthenticatorUtils.java +++ b/client/src/main/java/org/asynchttpclient/util/AuthenticatorUtils.java @@ -175,10 +175,14 @@ else if (request.getVirtualHost() != null) host = request.getUri().getHost(); try { - authorizationHeader = NEGOTIATE + " " + SpnegoEngine.instance(realm.getServicePrincipalName(), + authorizationHeader = NEGOTIATE + " " + SpnegoEngine.instance( + realm.getPrincipal(), + realm.getPassword(), + realm.getServicePrincipalName(), realm.getRealmName(), realm.isUseCanonicalHostname(), - realm.getCustomLoginConfig()).generateToken(host); + 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 index 6548b8cc15..7fac21be1c 100644 --- a/client/src/test/java/org/asynchttpclient/spnego/SpnegoEngineTest.java +++ b/client/src/test/java/org/asynchttpclient/spnego/SpnegoEngineTest.java @@ -9,10 +9,6 @@ import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; -import javax.security.auth.Subject; -import javax.security.auth.login.AppConfigurationEntry; -import javax.security.auth.login.Configuration; -import javax.security.auth.login.LoginContext; import java.io.File; import java.util.HashMap; import java.util.Map; @@ -25,6 +21,7 @@ public class SpnegoEngineTest extends AbstractBasicTest { private static String bob; private static File aliceKeytab; private static File bobKeytab; + private static File loginConfig; @BeforeClass public static void startServers() throws Exception { @@ -36,6 +33,8 @@ public static void startServers() throws Exception { // 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(); @@ -61,28 +60,37 @@ public static void startServers() throws Exception { kerbyServer.start(); - Map loginConfig = new HashMap<>(); - loginConfig.put("useKeyTab", "true"); - loginConfig.put("refreshKrb5Config", "true"); - loginConfig.put("keyTab", bobKeytab.getCanonicalPath()); - loginConfig.put("principal", bob); - loginConfig.put("debug", String.valueOf(true)); - - LoginContext loginContext; - loginContext = new LoginContext("", new Subject(), null, new Configuration() { - @Override - public AppConfigurationEntry[] getAppConfigurationEntry(String name) { - return new AppConfigurationEntry[] { - new AppConfigurationEntry("com.sun.security.auth.module.Krb5LoginModule", - AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, - loginConfig)}; - } - }); - loginContext.login(); + FileUtils.copyInputStreamToFile(SpnegoEngine.class.getResourceAsStream("/kerberos.jaas"), loginConfig); LoggerFactory.getLogger(SpnegoEngineTest.class).info("Able to log in as {} successfully. Kerby server is ready.", alice); + } + + @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")); + } - loginContext.logout(); + @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 @@ -94,10 +102,13 @@ public void testSpnegoGenerateTokenWithCustomLoginConfig() throws Exception { loginConfig.put("keyTab", aliceKeytab.getCanonicalPath()); loginConfig.put("principal", alice); loginConfig.put("debug", String.valueOf(true)); - SpnegoEngine spnegoEngine = new SpnegoEngine("bob", + SpnegoEngine spnegoEngine = new SpnegoEngine(null, + null, + "bob", "service.ws.apache.org", false, loginConfig, + null, null); String token = spnegoEngine.generateToken("localhost"); @@ -112,5 +123,6 @@ public static void cleanup() throws Exception { } 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"; +}; From a3b030120aa4c0c76dc838f9408c8ab013db81a7 Mon Sep 17 00:00:00 2001 From: Nicholas DiPiazza Date: Thu, 18 Oct 2018 02:01:13 -0500 Subject: [PATCH 10/12] remove useless comment --- .../test/java/org/asynchttpclient/spnego/SpnegoEngineTest.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/client/src/test/java/org/asynchttpclient/spnego/SpnegoEngineTest.java b/client/src/test/java/org/asynchttpclient/spnego/SpnegoEngineTest.java index 7fac21be1c..bd8fbf34ea 100644 --- a/client/src/test/java/org/asynchttpclient/spnego/SpnegoEngineTest.java +++ b/client/src/test/java/org/asynchttpclient/spnego/SpnegoEngineTest.java @@ -3,7 +3,6 @@ import org.apache.commons.io.FileUtils; import org.apache.kerby.kerberos.kerb.server.SimpleKdcServer; import org.asynchttpclient.AbstractBasicTest; -import org.slf4j.LoggerFactory; import org.testng.Assert; import org.testng.annotations.AfterClass; import org.testng.annotations.BeforeClass; @@ -61,8 +60,6 @@ public static void startServers() throws Exception { kerbyServer.start(); FileUtils.copyInputStreamToFile(SpnegoEngine.class.getResourceAsStream("/kerberos.jaas"), loginConfig); - - LoggerFactory.getLogger(SpnegoEngineTest.class).info("Able to log in as {} successfully. Kerby server is ready.", alice); } @Test From b1d83f57123f4d7b144b6552a53427bdd295b1a9 Mon Sep 17 00:00:00 2001 From: Nicholas DiPiazza Date: Thu, 18 Oct 2018 02:13:33 -0500 Subject: [PATCH 11/12] add login context name and username into the instance key --- .../java/org/asynchttpclient/spnego/SpnegoEngine.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/client/src/main/java/org/asynchttpclient/spnego/SpnegoEngine.java b/client/src/main/java/org/asynchttpclient/spnego/SpnegoEngine.java index fb8156add0..7f887965ec 100644 --- a/client/src/main/java/org/asynchttpclient/spnego/SpnegoEngine.java +++ b/client/src/main/java/org/asynchttpclient/spnego/SpnegoEngine.java @@ -117,7 +117,7 @@ public static SpnegoEngine instance(final String username, final Map customLoginConfig, final String loginContextName) { String key = ""; - if (customLoginConfig != null) { + if (customLoginConfig != null && !customLoginConfig.isEmpty()) { StringBuilder customLoginConfigKeyValues = new StringBuilder(); for (String loginConfigKey : customLoginConfig.keySet()) { customLoginConfigKeyValues.append(loginConfigKey).append("=") @@ -125,6 +125,12 @@ public static SpnegoEngine instance(final String username, } key = customLoginConfigKeyValues.toString(); } + if (username != null) { + key += username; + } + if (loginContextName != null) { + key += loginContextName; + } if (!instances.containsKey(key)) { instances.put(key, new SpnegoEngine(username, password, From 67bf5795f478775de6611809b9ab5074c15b6e36 Mon Sep 17 00:00:00 2001 From: Nicholas DiPiazza Date: Thu, 18 Oct 2018 14:44:35 -0500 Subject: [PATCH 12/12] cxf.kerby.version -> kerby.version --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 8f50130de9..c13def1e98 100644 --- a/pom.xml +++ b/pom.xml @@ -295,7 +295,7 @@ org.apache.kerby kerb-simplekdc - ${cxf.kerby.version} + ${kerby.version} test @@ -424,6 +424,6 @@ 1.2.2 2.19.0 2.0.0.0 - 1.1.1 + 1.1.1