Skip to content

Improve SpnegoEngine to allow more login configuration options #1582

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
5 changes: 5 additions & 0 deletions client/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -73,5 +73,10 @@
<artifactId>reactive-streams-examples</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.kerby</groupId>
<artifactId>kerb-simplekdc</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
6 changes: 5 additions & 1 deletion client/src/main/java/org/asynchttpclient/Dsl.java
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
101 changes: 93 additions & 8 deletions client/src/main/java/org/asynchttpclient/Realm.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.*;
Expand Down Expand Up @@ -60,6 +61,10 @@ public class Realm {
private final String ntlmDomain;
private final boolean useAbsoluteURI;
private final boolean omitQuery;
private final Map<String, String> customLoginConfig;
private final String servicePrincipalName;
private final boolean useCanonicalHostname;
private final String loginContextName;

private Realm(AuthScheme scheme,
String principal,
Expand All @@ -78,11 +83,15 @@ private Realm(AuthScheme scheme,
String ntlmDomain,
String ntlmHost,
boolean useAbsoluteURI,
boolean omitQuery) {
boolean omitQuery,
String servicePrincipalName,
boolean useCanonicalHostname,
Map<String, String> 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;
Expand All @@ -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() {
Expand Down Expand Up @@ -187,12 +200,48 @@ public boolean isOmitQuery() {
return omitQuery;
}

public Map<String, String> 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 {
Expand Down Expand Up @@ -223,6 +272,18 @@ public static class Builder {
private String ntlmHost = "localhost";
private boolean useAbsoluteURI = false;
private boolean omitQuery;
/**
* Kerberos/Spnego properties
*/
private Map<String, String> 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;
Expand Down Expand Up @@ -311,6 +372,26 @@ public Builder setCharset(Charset charset) {
return this;
}

public Builder setCustomLoginConfig(Map<String, String> 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];
Expand Down Expand Up @@ -501,7 +582,11 @@ public Realm build() {
ntlmDomain,
ntlmHost,
useAbsoluteURI,
omitQuery);
omitQuery,
servicePrincipalName,
useCanonicalHostname,
customLoginConfig,
loginContextName);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ public boolean exitAfterHandling407(Channel channel,
return false;
}
try {
kerberosProxyChallenge(proxyServer, requestHeaders);
kerberosProxyChallenge(proxyRealm, proxyServer, requestHeaders);

} catch (SpnegoEngineException e) {
// FIXME
Expand Down Expand Up @@ -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);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ public boolean exitAfterHandling401(final Channel channel,
return false;
}
try {
kerberosChallenge(request, requestHeaders);
kerberosChallenge(realm, request, requestHeaders);

} catch (SpnegoEngineException e) {
// FIXME
Expand Down Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Loading