Skip to content

Commit ccd0916

Browse files
committed
Improve SpnegoEngine to allow more login configuration options (AsyncHttpClient#1582)
* add the ability to pass in a {principal name, keytab} combination to async http client. * fix issue where spnego principal/keytab was no longer optional * specify the login config as a map to allow all the values custom not just a couple of them. * 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 * no need to detect null on loginContext * add a SpnegoEngine unit test. * Delete kerberos.jaas * Update pom.xml * Provide more granularity to be more aligned with other http clients: * Login context name * Username/password auth option * remove useless comment * add login context name and username into the instance key * cxf.kerby.version -> kerby.version # Conflicts: # pom.xml
1 parent da6c526 commit ccd0916

File tree

10 files changed

+498
-30
lines changed

10 files changed

+498
-30
lines changed

Diff for: client/pom.xml

+5
Original file line numberDiff line numberDiff line change
@@ -73,5 +73,10 @@
7373
<artifactId>reactive-streams-examples</artifactId>
7474
<scope>test</scope>
7575
</dependency>
76+
<dependency>
77+
<groupId>org.apache.kerby</groupId>
78+
<artifactId>kerb-simplekdc</artifactId>
79+
<scope>test</scope>
80+
</dependency>
7681
</dependencies>
7782
</project>

Diff for: client/src/main/java/org/asynchttpclient/Dsl.java

+5-1
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,11 @@ public static Realm.Builder realm(Realm prototype) {
9999
.setNtlmDomain(prototype.getNtlmDomain())
100100
.setNtlmHost(prototype.getNtlmHost())
101101
.setUseAbsoluteURI(prototype.isUseAbsoluteURI())
102-
.setOmitQuery(prototype.isOmitQuery());
102+
.setOmitQuery(prototype.isOmitQuery())
103+
.setServicePrincipalName(prototype.getServicePrincipalName())
104+
.setUseCanonicalHostname(prototype.isUseCanonicalHostname())
105+
.setCustomLoginConfig(prototype.getCustomLoginConfig())
106+
.setLoginContextName(prototype.getLoginContextName());
103107
}
104108

105109
public static Realm.Builder realm(AuthScheme scheme, String principal, String password) {

Diff for: client/src/main/java/org/asynchttpclient/Realm.java

+93-8
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323

2424
import java.nio.charset.Charset;
2525
import java.security.MessageDigest;
26+
import java.util.Map;
2627
import java.util.concurrent.ThreadLocalRandom;
2728

2829
import static java.nio.charset.StandardCharsets.*;
@@ -60,6 +61,10 @@ public class Realm {
6061
private final String ntlmDomain;
6162
private final boolean useAbsoluteURI;
6263
private final boolean omitQuery;
64+
private final Map<String, String> customLoginConfig;
65+
private final String servicePrincipalName;
66+
private final boolean useCanonicalHostname;
67+
private final String loginContextName;
6368

6469
private Realm(AuthScheme scheme,
6570
String principal,
@@ -78,11 +83,15 @@ private Realm(AuthScheme scheme,
7883
String ntlmDomain,
7984
String ntlmHost,
8085
boolean useAbsoluteURI,
81-
boolean omitQuery) {
86+
boolean omitQuery,
87+
String servicePrincipalName,
88+
boolean useCanonicalHostname,
89+
Map<String, String> customLoginConfig,
90+
String loginContextName) {
8291

8392
this.scheme = assertNotNull(scheme, "scheme");
84-
this.principal = assertNotNull(principal, "principal");
85-
this.password = assertNotNull(password, "password");
93+
this.principal = principal;
94+
this.password = password;
8695
this.realmName = realmName;
8796
this.nonce = nonce;
8897
this.algorithm = algorithm;
@@ -98,6 +107,10 @@ private Realm(AuthScheme scheme,
98107
this.ntlmHost = ntlmHost;
99108
this.useAbsoluteURI = useAbsoluteURI;
100109
this.omitQuery = omitQuery;
110+
this.servicePrincipalName = servicePrincipalName;
111+
this.useCanonicalHostname = useCanonicalHostname;
112+
this.customLoginConfig = customLoginConfig;
113+
this.loginContextName = loginContextName;
101114
}
102115

103116
public String getPrincipal() {
@@ -187,12 +200,48 @@ public boolean isOmitQuery() {
187200
return omitQuery;
188201
}
189202

203+
public Map<String, String> getCustomLoginConfig() {
204+
return customLoginConfig;
205+
}
206+
207+
public String getServicePrincipalName() {
208+
return servicePrincipalName;
209+
}
210+
211+
public boolean isUseCanonicalHostname() {
212+
return useCanonicalHostname;
213+
}
214+
215+
public String getLoginContextName() {
216+
return loginContextName;
217+
}
218+
190219
@Override
191220
public String toString() {
192-
return "Realm{" + "principal='" + principal + '\'' + ", scheme=" + scheme + ", realmName='" + realmName + '\''
193-
+ ", nonce='" + nonce + '\'' + ", algorithm='" + algorithm + '\'' + ", response='" + response + '\''
194-
+ ", qop='" + qop + '\'' + ", nc='" + nc + '\'' + ", cnonce='" + cnonce + '\'' + ", uri='" + uri + '\''
195-
+ ", useAbsoluteURI='" + useAbsoluteURI + '\'' + ", omitQuery='" + omitQuery + '\'' + '}';
221+
return "Realm{" +
222+
"principal='" + principal + '\'' +
223+
", password='" + password + '\'' +
224+
", scheme=" + scheme +
225+
", realmName='" + realmName + '\'' +
226+
", nonce='" + nonce + '\'' +
227+
", algorithm='" + algorithm + '\'' +
228+
", response='" + response + '\'' +
229+
", opaque='" + opaque + '\'' +
230+
", qop='" + qop + '\'' +
231+
", nc='" + nc + '\'' +
232+
", cnonce='" + cnonce + '\'' +
233+
", uri=" + uri +
234+
", usePreemptiveAuth=" + usePreemptiveAuth +
235+
", charset=" + charset +
236+
", ntlmHost='" + ntlmHost + '\'' +
237+
", ntlmDomain='" + ntlmDomain + '\'' +
238+
", useAbsoluteURI=" + useAbsoluteURI +
239+
", omitQuery=" + omitQuery +
240+
", customLoginConfig=" + customLoginConfig +
241+
", servicePrincipalName='" + servicePrincipalName + '\'' +
242+
", useCanonicalHostname=" + useCanonicalHostname +
243+
", loginContextName='" + loginContextName + '\'' +
244+
'}';
196245
}
197246

198247
public enum AuthScheme {
@@ -223,6 +272,18 @@ public static class Builder {
223272
private String ntlmHost = "localhost";
224273
private boolean useAbsoluteURI = false;
225274
private boolean omitQuery;
275+
/**
276+
* Kerberos/Spnego properties
277+
*/
278+
private Map<String, String> customLoginConfig;
279+
private String servicePrincipalName;
280+
private boolean useCanonicalHostname;
281+
private String loginContextName;
282+
283+
public Builder() {
284+
this.principal = null;
285+
this.password = null;
286+
}
226287

227288
public Builder(String principal, String password) {
228289
this.principal = principal;
@@ -311,6 +372,26 @@ public Builder setCharset(Charset charset) {
311372
return this;
312373
}
313374

375+
public Builder setCustomLoginConfig(Map<String, String> customLoginConfig) {
376+
this.customLoginConfig = customLoginConfig;
377+
return this;
378+
}
379+
380+
public Builder setServicePrincipalName(String servicePrincipalName) {
381+
this.servicePrincipalName = servicePrincipalName;
382+
return this;
383+
}
384+
385+
public Builder setUseCanonicalHostname(boolean useCanonicalHostname) {
386+
this.useCanonicalHostname = useCanonicalHostname;
387+
return this;
388+
}
389+
390+
public Builder setLoginContextName(String loginContextName) {
391+
this.loginContextName = loginContextName;
392+
return this;
393+
}
394+
314395
private String parseRawQop(String rawQop) {
315396
String[] rawServerSupportedQops = rawQop.split(",");
316397
String[] serverSupportedQops = new String[rawServerSupportedQops.length];
@@ -501,7 +582,11 @@ public Realm build() {
501582
ntlmDomain,
502583
ntlmHost,
503584
useAbsoluteURI,
504-
omitQuery);
585+
omitQuery,
586+
servicePrincipalName,
587+
useCanonicalHostname,
588+
customLoginConfig,
589+
loginContextName);
505590
}
506591
}
507592
}

Diff for: client/src/main/java/org/asynchttpclient/netty/handler/intercept/ProxyUnauthorized407Interceptor.java

+10-3
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ public boolean exitAfterHandling407(Channel channel,
140140
return false;
141141
}
142142
try {
143-
kerberosProxyChallenge(proxyServer, requestHeaders);
143+
kerberosProxyChallenge(proxyRealm, proxyServer, requestHeaders);
144144

145145
} catch (SpnegoEngineException e) {
146146
// FIXME
@@ -184,10 +184,17 @@ public boolean exitAfterHandling407(Channel channel,
184184
return true;
185185
}
186186

187-
private void kerberosProxyChallenge(ProxyServer proxyServer,
187+
private void kerberosProxyChallenge(Realm proxyRealm,
188+
ProxyServer proxyServer,
188189
HttpHeaders headers) throws SpnegoEngineException {
189190

190-
String challengeHeader = SpnegoEngine.instance().generateToken(proxyServer.getHost());
191+
String challengeHeader = SpnegoEngine.instance(proxyRealm.getPrincipal(),
192+
proxyRealm.getPassword(),
193+
proxyRealm.getServicePrincipalName(),
194+
proxyRealm.getRealmName(),
195+
proxyRealm.isUseCanonicalHostname(),
196+
proxyRealm.getCustomLoginConfig(),
197+
proxyRealm.getLoginContextName()).generateToken(proxyServer.getHost());
191198
headers.set(PROXY_AUTHORIZATION, NEGOTIATE + " " + challengeHeader);
192199
}
193200

Diff for: client/src/main/java/org/asynchttpclient/netty/handler/intercept/Unauthorized401Interceptor.java

+10-3
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ public boolean exitAfterHandling401(final Channel channel,
139139
return false;
140140
}
141141
try {
142-
kerberosChallenge(request, requestHeaders);
142+
kerberosChallenge(realm, request, requestHeaders);
143143

144144
} catch (SpnegoEngineException e) {
145145
// FIXME
@@ -200,12 +200,19 @@ private void ntlmChallenge(String authenticateHeader,
200200
}
201201
}
202202

203-
private void kerberosChallenge(Request request,
203+
private void kerberosChallenge(Realm realm,
204+
Request request,
204205
HttpHeaders headers) throws SpnegoEngineException {
205206

206207
Uri uri = request.getUri();
207208
String host = withDefault(request.getVirtualHost(), uri.getHost());
208-
String challengeHeader = SpnegoEngine.instance().generateToken(host);
209+
String challengeHeader = SpnegoEngine.instance(realm.getPrincipal(),
210+
realm.getPassword(),
211+
realm.getServicePrincipalName(),
212+
realm.getRealmName(),
213+
realm.isUseCanonicalHostname(),
214+
realm.getCustomLoginConfig(),
215+
realm.getLoginContextName()).generateToken(host);
209216
headers.set(AUTHORIZATION, NEGOTIATE + " " + challengeHeader);
210217
}
211218
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package org.asynchttpclient.spnego;
2+
3+
import org.slf4j.Logger;
4+
import org.slf4j.LoggerFactory;
5+
6+
import javax.security.auth.callback.Callback;
7+
import javax.security.auth.callback.CallbackHandler;
8+
import javax.security.auth.callback.NameCallback;
9+
import javax.security.auth.callback.PasswordCallback;
10+
import javax.security.auth.callback.UnsupportedCallbackException;
11+
import java.io.IOException;
12+
import java.lang.reflect.Method;
13+
14+
public class NamePasswordCallbackHandler implements CallbackHandler {
15+
private final Logger log = LoggerFactory.getLogger(getClass());
16+
private static final String PASSWORD_CALLBACK_NAME = "setObject";
17+
private static final Class<?>[] PASSWORD_CALLBACK_TYPES =
18+
new Class<?>[] {Object.class, char[].class, String.class};
19+
20+
private String username;
21+
private String password;
22+
23+
private String passwordCallbackName;
24+
25+
public NamePasswordCallbackHandler(String username, String password) {
26+
this(username, password, null);
27+
}
28+
29+
public NamePasswordCallbackHandler(String username, String password, String passwordCallbackName) {
30+
this.username = username;
31+
this.password = password;
32+
this.passwordCallbackName = passwordCallbackName;
33+
}
34+
35+
public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
36+
for (int i = 0; i < callbacks.length; i++) {
37+
Callback callback = callbacks[i];
38+
if (handleCallback(callback)) {
39+
continue;
40+
} else if (callback instanceof NameCallback) {
41+
((NameCallback) callback).setName(username);
42+
} else if (callback instanceof PasswordCallback) {
43+
PasswordCallback pwCallback = (PasswordCallback) callback;
44+
pwCallback.setPassword(password.toCharArray());
45+
} else if (!invokePasswordCallback(callback)) {
46+
String errorMsg = "Unsupported callback type " + callbacks[i].getClass().getName();
47+
log.info(errorMsg);
48+
throw new UnsupportedCallbackException(callbacks[i], errorMsg);
49+
}
50+
}
51+
}
52+
53+
protected boolean handleCallback(Callback callback) {
54+
return false;
55+
}
56+
57+
/*
58+
* This method is called from the handle(Callback[]) method when the specified callback
59+
* did not match any of the known callback classes. It looks for the callback method
60+
* having the specified method name with one of the suppported parameter types.
61+
* If found, it invokes the callback method on the object and returns true.
62+
* If not, it returns false.
63+
*/
64+
private boolean invokePasswordCallback(Callback callback) {
65+
String cbname = passwordCallbackName == null
66+
? PASSWORD_CALLBACK_NAME : passwordCallbackName;
67+
for (Class<?> arg : PASSWORD_CALLBACK_TYPES) {
68+
try {
69+
Method method = callback.getClass().getMethod(cbname, arg);
70+
Object args[] = new Object[] {
71+
arg == String.class ? password : password.toCharArray()
72+
};
73+
method.invoke(callback, args);
74+
return true;
75+
} catch (Exception e) {
76+
// ignore and continue
77+
log.debug(e.toString());
78+
}
79+
}
80+
return false;
81+
}
82+
}

0 commit comments

Comments
 (0)