Skip to content

Commit

Permalink
Digest auth isn't working, close #847
Browse files Browse the repository at this point in the history
  • Loading branch information
slandelle committed Apr 1, 2015
1 parent 3ab0b18 commit ac90c8b
Show file tree
Hide file tree
Showing 3 changed files with 137 additions and 80 deletions.
184 changes: 116 additions & 68 deletions api/src/main/java/org/asynchttpclient/Realm.java
Expand Up @@ -26,6 +26,7 @@
import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.ThreadLocalRandom;


import org.asynchttpclient.uri.Uri; import org.asynchttpclient.uri.Uri;
import org.asynchttpclient.util.AuthenticatorUtils;
import org.asynchttpclient.util.StringUtils; import org.asynchttpclient.util.StringUtils;


/** /**
Expand All @@ -34,6 +35,7 @@
public class Realm { public class Realm {


private static final String DEFAULT_NC = "00000001"; private static final String DEFAULT_NC = "00000001";
private static final String EMPTY_ENTITY_MD5 = "d41d8cd98f00b204e9800998ecf8427e";


private final String principal; private final String principal;
private final String password; private final String password;
Expand Down Expand Up @@ -251,24 +253,24 @@ public static class RealmBuilder {
// This code is already Apache licenced. // This code is already Apache licenced.
// //


private String principal = ""; private String principal;
private String password = ""; private String password;
private AuthScheme scheme = AuthScheme.NONE; private AuthScheme scheme = AuthScheme.NONE;
private String realmName = ""; private String realmName;
private String nonce = ""; private String nonce;
private String algorithm = "MD5"; private String algorithm;
private String response = ""; private String response;
private String opaque = ""; private String opaque;
private String qop = "auth"; private String qop;
private String nc = DEFAULT_NC; private String nc = DEFAULT_NC;
private String cnonce = ""; private String cnonce;
private Uri uri; private Uri uri;
private String methodName = "GET"; private String methodName = "GET";
private boolean usePreemptive; private boolean usePreemptive;
private String ntlmDomain = System.getProperty("http.auth.ntlm.domain", ""); private String ntlmDomain = System.getProperty("http.auth.ntlm.domain", "");
private Charset charset = UTF_8; private Charset charset = UTF_8;
private String ntlmHost = "localhost"; private String ntlmHost = "localhost";
private boolean useAbsoluteURI = true; private boolean useAbsoluteURI = false;
private boolean omitQuery; private boolean omitQuery;
private boolean targetProxy; private boolean targetProxy;


Expand Down Expand Up @@ -378,7 +380,9 @@ public String getQop() {
} }


public RealmBuilder setQop(String qop) { public RealmBuilder setQop(String qop) {
this.qop = qop; if (isNonEmpty(qop)) {
this.qop = qop;
}
return this; return this;
} }


Expand Down Expand Up @@ -444,6 +448,26 @@ public RealmBuilder setTargetProxy(boolean targetProxy) {
this.targetProxy = targetProxy; this.targetProxy = targetProxy;
return this; return this;
} }
private String parseRawQop(String rawQop) {
String[] rawServerSupportedQops = rawQop.split(",");
String[] serverSupportedQops = new String[rawServerSupportedQops.length];
for (int i = 0; i < rawServerSupportedQops.length; i++) {
serverSupportedQops[i] = rawServerSupportedQops[i].trim();
}

// prefer auth over auth-int
for (String rawServerSupportedQop: serverSupportedQops) {
if (rawServerSupportedQop.equals("auth"))
return rawServerSupportedQop;
}

for (String rawServerSupportedQop: serverSupportedQops) {
if (rawServerSupportedQop.equals("auth-int"))
return rawServerSupportedQop;
}

return null;
}


public RealmBuilder parseWWWAuthenticateHeader(String headerLine) { public RealmBuilder parseWWWAuthenticateHeader(String headerLine) {
setRealmName(match(headerLine, "realm")); setRealmName(match(headerLine, "realm"));
Expand All @@ -453,7 +477,12 @@ public RealmBuilder parseWWWAuthenticateHeader(String headerLine) {
setAlgorithm(algorithm); setAlgorithm(algorithm);
} }
setOpaque(match(headerLine, "opaque")); setOpaque(match(headerLine, "opaque"));
setQop(match(headerLine, "qop"));
String rawQop = match(headerLine, "qop");
if (rawQop != null) {
setQop(parseRawQop(rawQop));
}

if (isNonEmpty(getNonce())) { if (isNonEmpty(getNonce())) {
setScheme(AuthScheme.DIGEST); setScheme(AuthScheme.DIGEST);
} else { } else {
Expand All @@ -466,6 +495,10 @@ public RealmBuilder parseProxyAuthenticateHeader(String headerLine) {
setRealmName(match(headerLine, "realm")); setRealmName(match(headerLine, "realm"));
setNonce(match(headerLine, "nonce")); setNonce(match(headerLine, "nonce"));
setOpaque(match(headerLine, "opaque")); setOpaque(match(headerLine, "opaque"));
String algorithm = match(headerLine, "algorithm");
if (isNonEmpty(algorithm)) {
setAlgorithm(algorithm);
}
setQop(match(headerLine, "qop")); setQop(match(headerLine, "qop"));
if (isNonEmpty(getNonce())) { if (isNonEmpty(getNonce())) {
setScheme(AuthScheme.DIGEST); setScheme(AuthScheme.DIGEST);
Expand All @@ -477,25 +510,24 @@ public RealmBuilder parseProxyAuthenticateHeader(String headerLine) {
} }


public RealmBuilder clone(Realm clone) { public RealmBuilder clone(Realm clone) {
setRealmName(clone.getRealmName()); return setRealmName(clone.getRealmName())//
setAlgorithm(clone.getAlgorithm()); .setAlgorithm(clone.getAlgorithm())//
setMethodName(clone.getMethodName()); .setMethodName(clone.getMethodName())//
setNc(clone.getNc()); .setNc(clone.getNc())//
setNonce(clone.getNonce()); .setNonce(clone.getNonce())//
setPassword(clone.getPassword()); .setPassword(clone.getPassword())//
setPrincipal(clone.getPrincipal()); .setPrincipal(clone.getPrincipal())//
setCharset(clone.getCharset()); .setCharset(clone.getCharset())//
setOpaque(clone.getOpaque()); .setOpaque(clone.getOpaque())//
setQop(clone.getQop()); .setQop(clone.getQop())//
setScheme(clone.getScheme()); .setScheme(clone.getScheme())//
setUri(clone.getUri()); .setUri(clone.getUri())//
setUsePreemptiveAuth(clone.getUsePreemptiveAuth()); .setUsePreemptiveAuth(clone.getUsePreemptiveAuth())//
setNtlmDomain(clone.getNtlmDomain()); .setNtlmDomain(clone.getNtlmDomain())//
setNtlmHost(clone.getNtlmHost()); .setNtlmHost(clone.getNtlmHost())//
setUseAbsoluteURI(clone.isUseAbsoluteURI()); .setUseAbsoluteURI(clone.isUseAbsoluteURI())//
setOmitQuery(clone.isOmitQuery()); .setOmitQuery(clone.isOmitQuery())//
setTargetProxy(clone.isTargetProxy()); .setTargetProxy(clone.isTargetProxy());
return this;
} }


private void newCnonce(MessageDigest md) { private void newCnonce(MessageDigest md) {
Expand All @@ -510,12 +542,12 @@ private void newCnonce(MessageDigest md) {
*/ */
private String match(String headerLine, String token) { private String match(String headerLine, String token) {
if (headerLine == null) { if (headerLine == null) {
return ""; return null;
} }


int match = headerLine.indexOf(token); int match = headerLine.indexOf(token);
if (match <= 0) if (match <= 0)
return ""; return null;


// = to skip // = to skip
match += token.length() + 1; match += token.length() + 1;
Expand All @@ -534,48 +566,64 @@ public RealmBuilder setCharset(Charset charset) {
return this; return this;
} }


private void newResponse(MessageDigest md) { private byte[] md5FromRecycledStringBuilder(StringBuilder sb, MessageDigest md) {
// BEWARE: compute first as it used the cached StringBuilder
String url = uri.toUrl();

StringBuilder sb = StringUtils.stringBuilder();
sb.append(principal)
.append(":")
.append(realmName)
.append(":")
.append(password);
md.update(StringUtils.charSequence2ByteBuffer(sb, ISO_8859_1)); md.update(StringUtils.charSequence2ByteBuffer(sb, ISO_8859_1));
sb.setLength(0); sb.setLength(0);
byte[] ha1 = md.digest(); return md.digest();
}

private byte[] secretDigest(StringBuilder sb, MessageDigest md) {

sb.append(principal).append(':').append(realmName).append(':').append(password);
byte[] ha1 = md5FromRecycledStringBuilder(sb, md);

if (algorithm == null || algorithm.equals("MD5")) {
return ha1;
} else if ("MD5-sess".equals(algorithm)) {
appendBase16(sb, ha1);
sb.append(':').append(nonce).append(':').append(cnonce);
return md5FromRecycledStringBuilder(sb, md);
}


//HA2 if qop is auth-int is methodName:url:md5(entityBody) throw new UnsupportedOperationException("Digest algorithm not supported: " + algorithm);
sb.append(methodName) }
.append(':')
.append(url);


md.update(StringUtils.charSequence2ByteBuffer(sb, ISO_8859_1)); private byte[] dataDigest(StringBuilder sb, String digestUri, MessageDigest md) {
sb.setLength(0);
byte[] ha2 = md.digest(); sb.append(methodName).append(':').append(digestUri);
if ("auth-int".equals(qop)) {
sb.append(':').append(EMPTY_ENTITY_MD5);


appendBase16(sb, ha1); } else if (qop != null && !qop.equals("auth")) {
sb.append(':').append(nonce).append(':'); throw new UnsupportedOperationException("Digest qop not supported: " + qop);
}


if (isNonEmpty(qop)) { return md5FromRecycledStringBuilder(sb, md);
//qop ="auth" or "auth-int" }
sb.append(nc)//
.append(':')// private void appendDataBase(StringBuilder sb) {
.append(cnonce)// sb.append(':').append(nonce).append(':');
.append(':')// if ("auth".equals(qop) || "auth-int".equals(qop)) {
.append(qop)// sb.append(nc).append(':').append(cnonce).append(':').append(qop).append(':');
.append(':');
} }

}
appendBase16(sb, ha2);
md.update(StringUtils.charSequence2ByteBuffer(sb, ISO_8859_1)); private void newResponse(MessageDigest md) {
sb.setLength(0); // BEWARE: compute first as it used the cached StringBuilder
byte[] digest = md.digest(); String digestUri = AuthenticatorUtils.computeRealmURI(uri, useAbsoluteURI, omitQuery);


response = toHexString(digest); StringBuilder sb = StringUtils.stringBuilder();

// WARNING: DON'T MOVE, BUFFER IS RECYCLED!!!!
byte[] secretDigest = secretDigest(sb, md);
byte[] dataDigest = dataDigest(sb, digestUri, md);

appendBase16(sb, secretDigest);
appendDataBase(sb);
appendBase16(sb, dataDigest);

byte[] responseDigest = md5FromRecycledStringBuilder(sb, md);
response = toHexString(responseDigest);
} }


private static String toHexString(byte[] data) { private static String toHexString(byte[] data) {
Expand Down
29 changes: 19 additions & 10 deletions api/src/main/java/org/asynchttpclient/util/AuthenticatorUtils.java
Expand Up @@ -37,13 +37,16 @@ private static String computeBasicAuthentication(String principal, String passwo
return "Basic " + Base64.encode(s.getBytes(charset)); return "Basic " + Base64.encode(s.getBytes(charset));
} }


private static String computeRealmURI(Realm realm) { public static String computeRealmURI(Realm realm) {
Uri uri = realm.getUri(); return computeRealmURI(realm.getUri(), realm.isUseAbsoluteURI(), realm.isOmitQuery());
if (realm.isUseAbsoluteURI()) { }
return realm.isOmitQuery() && MiscUtils.isNonEmpty(uri.getQuery()) ? uri.withNewQuery(null).toUrl() : uri.toUrl();
public static String computeRealmURI(Uri uri, boolean useAbsoluteURI, boolean omitQuery) {
if (useAbsoluteURI) {
return omitQuery && MiscUtils.isNonEmpty(uri.getQuery()) ? uri.withNewQuery(null).toUrl() : uri.toUrl();
} else { } else {
String path = getNonEmptyPath(uri); String path = getNonEmptyPath(uri);
return realm.isOmitQuery() || !MiscUtils.isNonEmpty(uri.getQuery()) ? path : path + "?" + uri.getQuery(); return omitQuery || !MiscUtils.isNonEmpty(uri.getQuery()) ? path : path + "?" + uri.getQuery();
} }
} }


Expand All @@ -54,14 +57,20 @@ public static String computeDigestAuthentication(Realm realm) {
append(builder, "realm", realm.getRealmName(), true); append(builder, "realm", realm.getRealmName(), true);
append(builder, "nonce", realm.getNonce(), true); append(builder, "nonce", realm.getNonce(), true);
append(builder, "uri", computeRealmURI(realm), true); append(builder, "uri", computeRealmURI(realm), true);
append(builder, "algorithm", realm.getAlgorithm(), false); if (isNonEmpty(realm.getAlgorithm()))
append(builder, "algorithm", realm.getAlgorithm(), false);


append(builder, "response", realm.getResponse(), true); append(builder, "response", realm.getResponse(), true);
if (isNonEmpty(realm.getOpaque()))
if (realm.getOpaque() != null)
append(builder, "opaque", realm.getOpaque(), true); append(builder, "opaque", realm.getOpaque(), true);
append(builder, "qop", realm.getQop(), false);
append(builder, "nc", realm.getNc(), false); if (realm.getQop() != null) {
append(builder, "cnonce", realm.getCnonce(), true); append(builder, "qop", realm.getQop(), false);
// nc and cnonce only sent if server sent qop
append(builder, "nc", realm.getNc(), false);
append(builder, "cnonce", realm.getCnonce(), true);
}
builder.setLength(builder.length() - 2); // remove tailing ", " builder.setLength(builder.length() - 2); // remove tailing ", "


// FIXME isn't there a more efficient way? // FIXME isn't there a more efficient way?
Expand Down
4 changes: 2 additions & 2 deletions api/src/test/java/org/asynchttpclient/RealmTest.java
Expand Up @@ -73,7 +73,7 @@ private void testOldDigest(String qop) {
Realm orig = builder.build(); Realm orig = builder.build();


String ha1 = getMd5(user + ":" + realm + ":" + pass); String ha1 = getMd5(user + ":" + realm + ":" + pass);
String ha2 = getMd5(method + ":" + uri); String ha2 = getMd5(method + ":" + uri.getPath());
String expectedResponse = getMd5(ha1 + ":" + nonce + ":" + ha2); String expectedResponse = getMd5(ha1 + ":" + nonce + ":" + ha2);


assertEquals(expectedResponse, orig.getResponse()); assertEquals(expectedResponse, orig.getResponse());
Expand Down Expand Up @@ -101,7 +101,7 @@ public void testStrongDigest() {
String nc = orig.getNc(); String nc = orig.getNc();
String cnonce = orig.getCnonce(); String cnonce = orig.getCnonce();
String ha1 = getMd5(user + ":" + realm + ":" + pass); String ha1 = getMd5(user + ":" + realm + ":" + pass);
String ha2 = getMd5(method + ":" + uri); String ha2 = getMd5(method + ":" + uri.getPath());
String expectedResponse = getMd5(ha1 + ":" + nonce + ":" + nc + ":" + cnonce + ":" + qop + ":" + ha2); String expectedResponse = getMd5(ha1 + ":" + nonce + ":" + nc + ":" + cnonce + ":" + qop + ":" + ha2);


assertEquals(expectedResponse, orig.getResponse()); assertEquals(expectedResponse, orig.getResponse());
Expand Down

0 comments on commit ac90c8b

Please sign in to comment.