Skip to content

Commit ac90c8b

Browse files
committed
Digest auth isn't working, close AsyncHttpClient#847
1 parent 3ab0b18 commit ac90c8b

File tree

3 files changed

+137
-80
lines changed

3 files changed

+137
-80
lines changed

api/src/main/java/org/asynchttpclient/Realm.java

Lines changed: 116 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import java.util.concurrent.ThreadLocalRandom;
2727

2828
import org.asynchttpclient.uri.Uri;
29+
import org.asynchttpclient.util.AuthenticatorUtils;
2930
import org.asynchttpclient.util.StringUtils;
3031

3132
/**
@@ -34,6 +35,7 @@
3435
public class Realm {
3536

3637
private static final String DEFAULT_NC = "00000001";
38+
private static final String EMPTY_ENTITY_MD5 = "d41d8cd98f00b204e9800998ecf8427e";
3739

3840
private final String principal;
3941
private final String password;
@@ -251,24 +253,24 @@ public static class RealmBuilder {
251253
// This code is already Apache licenced.
252254
//
253255

254-
private String principal = "";
255-
private String password = "";
256+
private String principal;
257+
private String password;
256258
private AuthScheme scheme = AuthScheme.NONE;
257-
private String realmName = "";
258-
private String nonce = "";
259-
private String algorithm = "MD5";
260-
private String response = "";
261-
private String opaque = "";
262-
private String qop = "auth";
259+
private String realmName;
260+
private String nonce;
261+
private String algorithm;
262+
private String response;
263+
private String opaque;
264+
private String qop;
263265
private String nc = DEFAULT_NC;
264-
private String cnonce = "";
266+
private String cnonce;
265267
private Uri uri;
266268
private String methodName = "GET";
267269
private boolean usePreemptive;
268270
private String ntlmDomain = System.getProperty("http.auth.ntlm.domain", "");
269271
private Charset charset = UTF_8;
270272
private String ntlmHost = "localhost";
271-
private boolean useAbsoluteURI = true;
273+
private boolean useAbsoluteURI = false;
272274
private boolean omitQuery;
273275
private boolean targetProxy;
274276

@@ -378,7 +380,9 @@ public String getQop() {
378380
}
379381

380382
public RealmBuilder setQop(String qop) {
381-
this.qop = qop;
383+
if (isNonEmpty(qop)) {
384+
this.qop = qop;
385+
}
382386
return this;
383387
}
384388

@@ -444,6 +448,26 @@ public RealmBuilder setTargetProxy(boolean targetProxy) {
444448
this.targetProxy = targetProxy;
445449
return this;
446450
}
451+
private String parseRawQop(String rawQop) {
452+
String[] rawServerSupportedQops = rawQop.split(",");
453+
String[] serverSupportedQops = new String[rawServerSupportedQops.length];
454+
for (int i = 0; i < rawServerSupportedQops.length; i++) {
455+
serverSupportedQops[i] = rawServerSupportedQops[i].trim();
456+
}
457+
458+
// prefer auth over auth-int
459+
for (String rawServerSupportedQop: serverSupportedQops) {
460+
if (rawServerSupportedQop.equals("auth"))
461+
return rawServerSupportedQop;
462+
}
463+
464+
for (String rawServerSupportedQop: serverSupportedQops) {
465+
if (rawServerSupportedQop.equals("auth-int"))
466+
return rawServerSupportedQop;
467+
}
468+
469+
return null;
470+
}
447471

448472
public RealmBuilder parseWWWAuthenticateHeader(String headerLine) {
449473
setRealmName(match(headerLine, "realm"));
@@ -453,7 +477,12 @@ public RealmBuilder parseWWWAuthenticateHeader(String headerLine) {
453477
setAlgorithm(algorithm);
454478
}
455479
setOpaque(match(headerLine, "opaque"));
456-
setQop(match(headerLine, "qop"));
480+
481+
String rawQop = match(headerLine, "qop");
482+
if (rawQop != null) {
483+
setQop(parseRawQop(rawQop));
484+
}
485+
457486
if (isNonEmpty(getNonce())) {
458487
setScheme(AuthScheme.DIGEST);
459488
} else {
@@ -466,6 +495,10 @@ public RealmBuilder parseProxyAuthenticateHeader(String headerLine) {
466495
setRealmName(match(headerLine, "realm"));
467496
setNonce(match(headerLine, "nonce"));
468497
setOpaque(match(headerLine, "opaque"));
498+
String algorithm = match(headerLine, "algorithm");
499+
if (isNonEmpty(algorithm)) {
500+
setAlgorithm(algorithm);
501+
}
469502
setQop(match(headerLine, "qop"));
470503
if (isNonEmpty(getNonce())) {
471504
setScheme(AuthScheme.DIGEST);
@@ -477,25 +510,24 @@ public RealmBuilder parseProxyAuthenticateHeader(String headerLine) {
477510
}
478511

479512
public RealmBuilder clone(Realm clone) {
480-
setRealmName(clone.getRealmName());
481-
setAlgorithm(clone.getAlgorithm());
482-
setMethodName(clone.getMethodName());
483-
setNc(clone.getNc());
484-
setNonce(clone.getNonce());
485-
setPassword(clone.getPassword());
486-
setPrincipal(clone.getPrincipal());
487-
setCharset(clone.getCharset());
488-
setOpaque(clone.getOpaque());
489-
setQop(clone.getQop());
490-
setScheme(clone.getScheme());
491-
setUri(clone.getUri());
492-
setUsePreemptiveAuth(clone.getUsePreemptiveAuth());
493-
setNtlmDomain(clone.getNtlmDomain());
494-
setNtlmHost(clone.getNtlmHost());
495-
setUseAbsoluteURI(clone.isUseAbsoluteURI());
496-
setOmitQuery(clone.isOmitQuery());
497-
setTargetProxy(clone.isTargetProxy());
498-
return this;
513+
return setRealmName(clone.getRealmName())//
514+
.setAlgorithm(clone.getAlgorithm())//
515+
.setMethodName(clone.getMethodName())//
516+
.setNc(clone.getNc())//
517+
.setNonce(clone.getNonce())//
518+
.setPassword(clone.getPassword())//
519+
.setPrincipal(clone.getPrincipal())//
520+
.setCharset(clone.getCharset())//
521+
.setOpaque(clone.getOpaque())//
522+
.setQop(clone.getQop())//
523+
.setScheme(clone.getScheme())//
524+
.setUri(clone.getUri())//
525+
.setUsePreemptiveAuth(clone.getUsePreemptiveAuth())//
526+
.setNtlmDomain(clone.getNtlmDomain())//
527+
.setNtlmHost(clone.getNtlmHost())//
528+
.setUseAbsoluteURI(clone.isUseAbsoluteURI())//
529+
.setOmitQuery(clone.isOmitQuery())//
530+
.setTargetProxy(clone.isTargetProxy());
499531
}
500532

501533
private void newCnonce(MessageDigest md) {
@@ -510,12 +542,12 @@ private void newCnonce(MessageDigest md) {
510542
*/
511543
private String match(String headerLine, String token) {
512544
if (headerLine == null) {
513-
return "";
545+
return null;
514546
}
515547

516548
int match = headerLine.indexOf(token);
517549
if (match <= 0)
518-
return "";
550+
return null;
519551

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

537-
private void newResponse(MessageDigest md) {
538-
// BEWARE: compute first as it used the cached StringBuilder
539-
String url = uri.toUrl();
540-
541-
StringBuilder sb = StringUtils.stringBuilder();
542-
sb.append(principal)
543-
.append(":")
544-
.append(realmName)
545-
.append(":")
546-
.append(password);
569+
private byte[] md5FromRecycledStringBuilder(StringBuilder sb, MessageDigest md) {
547570
md.update(StringUtils.charSequence2ByteBuffer(sb, ISO_8859_1));
548571
sb.setLength(0);
549-
byte[] ha1 = md.digest();
572+
return md.digest();
573+
}
574+
575+
private byte[] secretDigest(StringBuilder sb, MessageDigest md) {
576+
577+
sb.append(principal).append(':').append(realmName).append(':').append(password);
578+
byte[] ha1 = md5FromRecycledStringBuilder(sb, md);
579+
580+
if (algorithm == null || algorithm.equals("MD5")) {
581+
return ha1;
582+
} else if ("MD5-sess".equals(algorithm)) {
583+
appendBase16(sb, ha1);
584+
sb.append(':').append(nonce).append(':').append(cnonce);
585+
return md5FromRecycledStringBuilder(sb, md);
586+
}
550587

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

556-
md.update(StringUtils.charSequence2ByteBuffer(sb, ISO_8859_1));
557-
sb.setLength(0);
558-
byte[] ha2 = md.digest();
591+
private byte[] dataDigest(StringBuilder sb, String digestUri, MessageDigest md) {
592+
593+
sb.append(methodName).append(':').append(digestUri);
594+
if ("auth-int".equals(qop)) {
595+
sb.append(':').append(EMPTY_ENTITY_MD5);
559596

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

563-
if (isNonEmpty(qop)) {
564-
//qop ="auth" or "auth-int"
565-
sb.append(nc)//
566-
.append(':')//
567-
.append(cnonce)//
568-
.append(':')//
569-
.append(qop)//
570-
.append(':');
601+
return md5FromRecycledStringBuilder(sb, md);
602+
}
603+
604+
private void appendDataBase(StringBuilder sb) {
605+
sb.append(':').append(nonce).append(':');
606+
if ("auth".equals(qop) || "auth-int".equals(qop)) {
607+
sb.append(nc).append(':').append(cnonce).append(':').append(qop).append(':');
571608
}
572-
573-
appendBase16(sb, ha2);
574-
md.update(StringUtils.charSequence2ByteBuffer(sb, ISO_8859_1));
575-
sb.setLength(0);
576-
byte[] digest = md.digest();
577-
578-
response = toHexString(digest);
609+
}
610+
611+
private void newResponse(MessageDigest md) {
612+
// BEWARE: compute first as it used the cached StringBuilder
613+
String digestUri = AuthenticatorUtils.computeRealmURI(uri, useAbsoluteURI, omitQuery);
614+
615+
StringBuilder sb = StringUtils.stringBuilder();
616+
617+
// WARNING: DON'T MOVE, BUFFER IS RECYCLED!!!!
618+
byte[] secretDigest = secretDigest(sb, md);
619+
byte[] dataDigest = dataDigest(sb, digestUri, md);
620+
621+
appendBase16(sb, secretDigest);
622+
appendDataBase(sb);
623+
appendBase16(sb, dataDigest);
624+
625+
byte[] responseDigest = md5FromRecycledStringBuilder(sb, md);
626+
response = toHexString(responseDigest);
579627
}
580628

581629
private static String toHexString(byte[] data) {

api/src/main/java/org/asynchttpclient/util/AuthenticatorUtils.java

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,16 @@ private static String computeBasicAuthentication(String principal, String passwo
3737
return "Basic " + Base64.encode(s.getBytes(charset));
3838
}
3939

40-
private static String computeRealmURI(Realm realm) {
41-
Uri uri = realm.getUri();
42-
if (realm.isUseAbsoluteURI()) {
43-
return realm.isOmitQuery() && MiscUtils.isNonEmpty(uri.getQuery()) ? uri.withNewQuery(null).toUrl() : uri.toUrl();
40+
public static String computeRealmURI(Realm realm) {
41+
return computeRealmURI(realm.getUri(), realm.isUseAbsoluteURI(), realm.isOmitQuery());
42+
}
43+
44+
public static String computeRealmURI(Uri uri, boolean useAbsoluteURI, boolean omitQuery) {
45+
if (useAbsoluteURI) {
46+
return omitQuery && MiscUtils.isNonEmpty(uri.getQuery()) ? uri.withNewQuery(null).toUrl() : uri.toUrl();
4447
} else {
4548
String path = getNonEmptyPath(uri);
46-
return realm.isOmitQuery() || !MiscUtils.isNonEmpty(uri.getQuery()) ? path : path + "?" + uri.getQuery();
49+
return omitQuery || !MiscUtils.isNonEmpty(uri.getQuery()) ? path : path + "?" + uri.getQuery();
4750
}
4851
}
4952

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

5963
append(builder, "response", realm.getResponse(), true);
60-
if (isNonEmpty(realm.getOpaque()))
64+
65+
if (realm.getOpaque() != null)
6166
append(builder, "opaque", realm.getOpaque(), true);
62-
append(builder, "qop", realm.getQop(), false);
63-
append(builder, "nc", realm.getNc(), false);
64-
append(builder, "cnonce", realm.getCnonce(), true);
67+
68+
if (realm.getQop() != null) {
69+
append(builder, "qop", realm.getQop(), false);
70+
// nc and cnonce only sent if server sent qop
71+
append(builder, "nc", realm.getNc(), false);
72+
append(builder, "cnonce", realm.getCnonce(), true);
73+
}
6574
builder.setLength(builder.length() - 2); // remove tailing ", "
6675

6776
// FIXME isn't there a more efficient way?

api/src/test/java/org/asynchttpclient/RealmTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ private void testOldDigest(String qop) {
7373
Realm orig = builder.build();
7474

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

7979
assertEquals(expectedResponse, orig.getResponse());
@@ -101,7 +101,7 @@ public void testStrongDigest() {
101101
String nc = orig.getNc();
102102
String cnonce = orig.getCnonce();
103103
String ha1 = getMd5(user + ":" + realm + ":" + pass);
104-
String ha2 = getMd5(method + ":" + uri);
104+
String ha2 = getMd5(method + ":" + uri.getPath());
105105
String expectedResponse = getMd5(ha1 + ":" + nonce + ":" + nc + ":" + cnonce + ":" + qop + ":" + ha2);
106106

107107
assertEquals(expectedResponse, orig.getResponse());

0 commit comments

Comments
 (0)