You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@hc.apache.org by ol...@apache.org on 2011/06/23 11:38:07 UTC
svn commit: r1138794 - in /httpcomponents/httpclient/branches/4.1.x: ./
httpclient/
httpclient/src/main/java/org/apache/http/impl/auth/DigestScheme.java
httpclient/src/test/java/org/apache/http/impl/auth/TestDigestScheme.java
Author: olegk
Date: Thu Jun 23 09:38:07 2011
New Revision: 1138794
URL: http://svn.apache.org/viewvc?rev=1138794&view=rev
Log:
HTTPCLIENT-1094: digest auth scheme refactoring (merged from trunk)
Modified:
httpcomponents/httpclient/branches/4.1.x/ (props changed)
httpcomponents/httpclient/branches/4.1.x/httpclient/ (props changed)
httpcomponents/httpclient/branches/4.1.x/httpclient/src/main/java/org/apache/http/impl/auth/DigestScheme.java
httpcomponents/httpclient/branches/4.1.x/httpclient/src/test/java/org/apache/http/impl/auth/TestDigestScheme.java
Propchange: httpcomponents/httpclient/branches/4.1.x/
------------------------------------------------------------------------------
--- svn:mergeinfo (original)
+++ svn:mergeinfo Thu Jun 23 09:38:07 2011
@@ -1,4 +1,4 @@
/httpcomponents/httpclient/branches/4.0.x:950681-950688
/httpcomponents/httpclient/branches/branch_4_1:755593-811107
/httpcomponents/httpclient/branches/notice-plugin-test:1024348-1031454
-/httpcomponents/httpclient/trunk:1087358,1091140-1091141,1091904,1104207-1104211,1132902-1132903,1133907
+/httpcomponents/httpclient/trunk:1087358,1091140-1091141,1091904,1104207-1104211,1132902-1132903,1133907,1138790-1138791
Propchange: httpcomponents/httpclient/branches/4.1.x/httpclient/
------------------------------------------------------------------------------
--- svn:mergeinfo (original)
+++ svn:mergeinfo Thu Jun 23 09:38:07 2011
@@ -1,4 +1,4 @@
/httpcomponents/httpclient/branches/4.0.x/httpclient:950681-950688
/httpcomponents/httpclient/branches/branch_4_1/httpclient:755593-811107
/httpcomponents/httpclient/branches/notice-plugin-test/httpclient:1024348-1031454
-/httpcomponents/httpclient/trunk/httpclient:1087358,1088000-1088004,1091140-1091141,1091904,1100531-1100533,1104207-1104211,1132902-1132903
+/httpcomponents/httpclient/trunk/httpclient:1087358,1088000-1088004,1091140-1091141,1091904,1100531-1100533,1104207-1104211,1132902-1132903,1138790-1138791
Modified: httpcomponents/httpclient/branches/4.1.x/httpclient/src/main/java/org/apache/http/impl/auth/DigestScheme.java
URL: http://svn.apache.org/viewvc/httpcomponents/httpclient/branches/4.1.x/httpclient/src/main/java/org/apache/http/impl/auth/DigestScheme.java?rev=1138794&r1=1138793&r2=1138794&view=diff
==============================================================================
--- httpcomponents/httpclient/branches/4.1.x/httpclient/src/main/java/org/apache/http/impl/auth/DigestScheme.java (original)
+++ httpcomponents/httpclient/branches/4.1.x/httpclient/src/main/java/org/apache/http/impl/auth/DigestScheme.java Thu Jun 23 09:38:07 2011
@@ -27,6 +27,7 @@
package org.apache.http.impl.auth;
import java.security.MessageDigest;
+import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Formatter;
import java.util.List;
@@ -90,15 +91,16 @@ public class DigestScheme extends RFC261
/** Whether the digest authentication process is complete */
private boolean complete;
+ private static final int QOP_UNKNOWN = -1;
private static final int QOP_MISSING = 0;
private static final int QOP_AUTH_INT = 1;
private static final int QOP_AUTH = 2;
- private int qopVariant = QOP_MISSING;
private String lastNonce;
private long nounceCount;
private String cnonce;
- private String nc;
+ private String a1;
+ private String a2;
/**
* Default constructor for the digest authetication scheme.
@@ -122,32 +124,10 @@ public class DigestScheme extends RFC261
super.processChallenge(header);
if (getParameter("realm") == null) {
- throw new MalformedChallengeException("missing realm in challange");
+ throw new MalformedChallengeException("missing realm in challenge");
}
if (getParameter("nonce") == null) {
- throw new MalformedChallengeException("missing nonce in challange");
- }
-
- boolean unsupportedQop = false;
- // qop parsing
- String qop = getParameter("qop");
- if (qop != null) {
- StringTokenizer tok = new StringTokenizer(qop,",");
- while (tok.hasMoreTokens()) {
- String variant = tok.nextToken().trim();
- if (variant.equals("auth")) {
- qopVariant = QOP_AUTH;
- break; //that's our favourite, because auth-int is unsupported
- } else if (variant.equals("auth-int")) {
- qopVariant = QOP_AUTH_INT;
- } else {
- unsupportedQop = true;
- }
- }
- }
-
- if (unsupportedQop && (qopVariant == QOP_MISSING)) {
- throw new MalformedChallengeException("None of the qop methods is supported");
+ throw new MalformedChallengeException("missing nonce in challenge");
}
this.complete = true;
}
@@ -189,23 +169,6 @@ public class DigestScheme extends RFC261
getParameters().put(name, value);
}
- private String getCnonce() {
- if (this.cnonce == null) {
- this.cnonce = createCnonce();
- }
- return this.cnonce;
- }
-
- private String getNc() {
- if (this.nc == null) {
- StringBuilder sb = new StringBuilder();
- Formatter formatter = new Formatter(sb, Locale.US);
- formatter.format("%08x", this.nounceCount);
- this.nc = sb.toString();
- }
- return this.nc;
- }
-
/**
* Produces a digest authorization string for the given set of
* {@link Credentials}, method name and URI.
@@ -239,8 +202,7 @@ public class DigestScheme extends RFC261
charset = AuthParams.getCredentialCharset(request.getParams());
getParameters().put("charset", charset);
}
- String digest = createDigest(credentials);
- return createDigestHeader(credentials, digest);
+ return createDigestHeader(credentials);
}
private static MessageDigest createMessageDigest(
@@ -255,17 +217,18 @@ public class DigestScheme extends RFC261
}
/**
- * Creates an MD5 response digest.
+ * Creates digest-response header as defined in RFC2617.
*
- * @return The created digest as string. This will be the response tag's
- * value in the Authentication HTTP header.
- * @throws AuthenticationException when MD5 is an unsupported algorithm
+ * @param credentials User credentials
+ *
+ * @return The digest-response as String.
*/
- private String createDigest(final Credentials credentials) throws AuthenticationException {
- // Collecting required tokens
+ private Header createDigestHeader(
+ final Credentials credentials) throws AuthenticationException {
String uri = getParameter("uri");
String realm = getParameter("realm");
String nonce = getParameter("nonce");
+ String opaque = getParameter("opaque");
String method = getParameter("methodname");
String algorithm = getParameter("algorithm");
if (uri == null) {
@@ -278,9 +241,25 @@ public class DigestScheme extends RFC261
throw new IllegalStateException("Nonce may not be null");
}
- // Reset
- this.cnonce = null;
- this.nc = null;
+ //TODO: add support for QOP_INT
+ int qop = QOP_UNKNOWN;
+ String qoplist = getParameter("qop");
+ if (qoplist != null) {
+ StringTokenizer tok = new StringTokenizer(qoplist, ",");
+ while (tok.hasMoreTokens()) {
+ String variant = tok.nextToken().trim();
+ if (variant.equals("auth")) {
+ qop = QOP_AUTH;
+ break;
+ }
+ }
+ } else {
+ qop = QOP_MISSING;
+ }
+
+ if (qop == QOP_UNKNOWN) {
+ throw new AuthenticationException("None of the qop methods is supported: " + qoplist);
+ }
// If an algorithm is not specified, default to MD5.
if (algorithm == null) {
@@ -292,117 +271,90 @@ public class DigestScheme extends RFC261
charset = "ISO-8859-1";
}
- if (qopVariant == QOP_AUTH_INT) {
- throw new AuthenticationException(
- "Unsupported qop in HTTP Digest authentication");
- }
-
String digAlg = algorithm;
if (digAlg.equalsIgnoreCase("MD5-sess")) {
digAlg = "MD5";
}
- if (nonce.equals(this.lastNonce)) {
- this.nounceCount++;
- } else {
- this.nounceCount = 1;
- this.lastNonce = nonce;
+ MessageDigest digester;
+ try {
+ digester = createMessageDigest(digAlg);
+ } catch (UnsupportedDigestAlgorithmException ex) {
+ throw new AuthenticationException("Unsuppported digest algorithm: " + digAlg);
}
- MessageDigest digester = createMessageDigest(digAlg);
-
String uname = credentials.getUserPrincipal().getName();
String pwd = credentials.getPassword();
- // 3.2.2.2: Calculating digest
- StringBuilder tmp = new StringBuilder(uname.length() + realm.length() + pwd.length() + 2);
- tmp.append(uname);
- tmp.append(':');
- tmp.append(realm);
- tmp.append(':');
- tmp.append(pwd);
- // unq(username-value) ":" unq(realm-value) ":" passwd
- String a1 = tmp.toString();
+ if (nonce.equals(this.lastNonce)) {
+ nounceCount++;
+ } else {
+ nounceCount = 1;
+ cnonce = null;
+ lastNonce = nonce;
+ }
+ StringBuilder sb = new StringBuilder(256);
+ Formatter formatter = new Formatter(sb, Locale.US);
+ formatter.format("%08x", nounceCount);
+ String nc = sb.toString();
+
+ if (cnonce == null) {
+ cnonce = createCnonce();
+ }
- //a1 is suitable for MD5 algorithm
+ a1 = null;
+ a2 = null;
+ // 3.2.2.2: Calculating digest
if (algorithm.equalsIgnoreCase("MD5-sess")) {
// H( unq(username-value) ":" unq(realm-value) ":" passwd )
// ":" unq(nonce-value)
// ":" unq(cnonce-value)
- algorithm = "MD5";
- String cnonce = getCnonce();
-
- String tmp2 = encode(digester.digest(EncodingUtils.getBytes(a1, charset)));
- StringBuilder tmp3 = new StringBuilder(
- tmp2.length() + nonce.length() + cnonce.length() + 2);
- tmp3.append(tmp2);
- tmp3.append(':');
- tmp3.append(nonce);
- tmp3.append(':');
- tmp3.append(cnonce);
- a1 = tmp3.toString();
+ // calculated one per session
+ sb.setLength(0);
+ sb.append(uname).append(':').append(realm).append(':').append(pwd);
+ String checksum = encode(digester.digest(EncodingUtils.getBytes(sb.toString(), charset)));
+ sb.setLength(0);
+ sb.append(checksum).append(':').append(nonce).append(':').append(cnonce);
+ a1 = sb.toString();
+ } else {
+ // unq(username-value) ":" unq(realm-value) ":" passwd
+ sb.setLength(0);
+ sb.append(uname).append(':').append(realm).append(':').append(pwd);
+ a1 = sb.toString();
}
+
String hasha1 = encode(digester.digest(EncodingUtils.getBytes(a1, charset)));
- String a2 = null;
- if (qopVariant == QOP_AUTH_INT) {
- // Unhandled qop auth-int
- //we do not have access to the entity-body or its hash
- //TODO: add Method ":" digest-uri-value ":" H(entity-body)
+ if (qop == QOP_AUTH) {
+ // Method ":" digest-uri-value
+ a2 = method + ':' + uri;
+ } else if (qop == QOP_AUTH_INT) {
+ // Method ":" digest-uri-value ":" H(entity-body)
+ //TODO: calculate entity hash if entity is repeatable
+ throw new AuthenticationException("qop-int method is not suppported");
} else {
a2 = method + ':' + uri;
}
- String hasha2 = encode(digester.digest(EncodingUtils.getAsciiBytes(a2)));
+
+ String hasha2 = encode(digester.digest(EncodingUtils.getBytes(a2, charset)));
// 3.2.2.1
- String serverDigestValue;
- if (qopVariant == QOP_MISSING) {
- StringBuilder tmp2 = new StringBuilder(
- hasha1.length() + nonce.length() + hasha1.length());
- tmp2.append(hasha1);
- tmp2.append(':');
- tmp2.append(nonce);
- tmp2.append(':');
- tmp2.append(hasha2);
- serverDigestValue = tmp2.toString();
+
+ String digestValue;
+ if (qop == QOP_MISSING) {
+ sb.setLength(0);
+ sb.append(hasha1).append(':').append(nonce).append(':').append(hasha2);
+ digestValue = sb.toString();
} else {
- String qopOption = getQopVariantString();
- String cnonce = getCnonce();
- String nc = getNc();
- StringBuilder tmp2 = new StringBuilder(hasha1.length() + nonce.length()
- + nc.length() + cnonce.length() + qopOption.length() + hasha2.length() + 5);
- tmp2.append(hasha1);
- tmp2.append(':');
- tmp2.append(nonce);
- tmp2.append(':');
- tmp2.append(nc);
- tmp2.append(':');
- tmp2.append(cnonce);
- tmp2.append(':');
- tmp2.append(qopOption);
- tmp2.append(':');
- tmp2.append(hasha2);
- serverDigestValue = tmp2.toString();
+ sb.setLength(0);
+ sb.append(hasha1).append(':').append(nonce).append(':').append(nc).append(':')
+ .append(cnonce).append(':').append(qop == QOP_AUTH_INT ? "auth-int" : "auth")
+ .append(':').append(hasha2);
+ digestValue = sb.toString();
}
- String serverDigest =
- encode(digester.digest(EncodingUtils.getAsciiBytes(serverDigestValue)));
-
- return serverDigest;
- }
-
- /**
- * Creates digest-response header as defined in RFC2617.
- *
- * @param credentials User credentials
- * @param digest The response tag's value as String.
- *
- * @return The digest-response as String.
- */
- private Header createDigestHeader(
- final Credentials credentials,
- final String digest) {
+ String digest = encode(digester.digest(EncodingUtils.getAsciiBytes(digestValue)));
CharArrayBuffer buffer = new CharArrayBuffer(128);
if (isProxy()) {
@@ -412,26 +364,17 @@ public class DigestScheme extends RFC261
}
buffer.append(": Digest ");
- String uri = getParameter("uri");
- String realm = getParameter("realm");
- String nonce = getParameter("nonce");
- String opaque = getParameter("opaque");
- String response = digest;
- String algorithm = getParameter("algorithm");
-
- String uname = credentials.getUserPrincipal().getName();
-
List<BasicNameValuePair> params = new ArrayList<BasicNameValuePair>(20);
params.add(new BasicNameValuePair("username", uname));
params.add(new BasicNameValuePair("realm", realm));
params.add(new BasicNameValuePair("nonce", nonce));
params.add(new BasicNameValuePair("uri", uri));
- params.add(new BasicNameValuePair("response", response));
+ params.add(new BasicNameValuePair("response", digest));
- if (qopVariant != QOP_MISSING) {
- params.add(new BasicNameValuePair("qop", getQopVariantString()));
- params.add(new BasicNameValuePair("nc", getNc()));
- params.add(new BasicNameValuePair("cnonce", getCnonce()));
+ if (qop != QOP_MISSING) {
+ params.add(new BasicNameValuePair("qop", qop == QOP_AUTH_INT ? "auth-int" : "auth"));
+ params.add(new BasicNameValuePair("nc", nc));
+ params.add(new BasicNameValuePair("cnonce", cnonce));
}
if (algorithm != null) {
params.add(new BasicNameValuePair("algorithm", algorithm));
@@ -445,22 +388,22 @@ public class DigestScheme extends RFC261
if (i > 0) {
buffer.append(", ");
}
- boolean noQuotes = "nc".equals(param.getName()) ||
- "qop".equals(param.getName());
- BasicHeaderValueFormatter.DEFAULT
- .formatNameValuePair(buffer, param, !noQuotes);
+ boolean noQuotes = "nc".equals(param.getName()) || "qop".equals(param.getName());
+ BasicHeaderValueFormatter.DEFAULT.formatNameValuePair(buffer, param, !noQuotes);
}
return new BufferedHeader(buffer);
}
- private String getQopVariantString() {
- String qopOption;
- if (qopVariant == QOP_AUTH_INT) {
- qopOption = "auth-int";
- } else {
- qopOption = "auth";
- }
- return qopOption;
+ String getCnonce() {
+ return cnonce;
+ }
+
+ String getA1() {
+ return a1;
+ }
+
+ String getA2() {
+ return a2;
}
/**
@@ -488,16 +431,12 @@ public class DigestScheme extends RFC261
* Creates a random cnonce value based on the current time.
*
* @return The cnonce value as String.
- * @throws UnsupportedDigestAlgorithmException if MD5 algorithm is not supported.
*/
public static String createCnonce() {
- String cnonce;
-
- MessageDigest md5Helper = createMessageDigest("MD5");
-
- cnonce = Long.toString(System.currentTimeMillis());
- cnonce = encode(md5Helper.digest(EncodingUtils.getAsciiBytes(cnonce)));
-
- return cnonce;
+ SecureRandom rnd = new SecureRandom();
+ byte[] tmp = new byte[8];
+ rnd.nextBytes(tmp);
+ return encode(tmp);
}
+
}
Modified: httpcomponents/httpclient/branches/4.1.x/httpclient/src/test/java/org/apache/http/impl/auth/TestDigestScheme.java
URL: http://svn.apache.org/viewvc/httpcomponents/httpclient/branches/4.1.x/httpclient/src/test/java/org/apache/http/impl/auth/TestDigestScheme.java?rev=1138794&r1=1138793&r2=1138794&view=diff
==============================================================================
--- httpcomponents/httpclient/branches/4.1.x/httpclient/src/test/java/org/apache/http/impl/auth/TestDigestScheme.java (original)
+++ httpcomponents/httpclient/branches/4.1.x/httpclient/src/test/java/org/apache/http/impl/auth/TestDigestScheme.java Thu Jun 23 09:38:07 2011
@@ -33,6 +33,7 @@ import org.apache.http.Header;
import org.apache.http.HeaderElement;
import org.apache.http.HttpRequest;
import org.apache.http.auth.AuthScheme;
+import org.apache.http.auth.AuthenticationException;
import org.apache.http.auth.Credentials;
import org.apache.http.auth.AUTH;
import org.apache.http.auth.MalformedChallengeException;
@@ -257,13 +258,15 @@ public class TestDigestScheme {
}
/**
- * Test digest authentication with invalud qop value
+ * Test digest authentication with unknown qop value
*/
- @Test(expected=MalformedChallengeException.class)
- public void testDigestAuthenticationMD5SessInvalidQop() throws Exception {
+ @Test(expected=AuthenticationException.class)
+ public void testDigestAuthenticationMD5SessUnknownQop() throws Exception {
// Example using Digest auth with MD5-sess
String realm="realm";
+ String username="username";
+ String password="password";
String nonce="e273f1776275974f1a120d8b92c5b3cb";
String challenge="Digest realm=\"" + realm + "\", "
@@ -271,12 +274,45 @@ public class TestDigestScheme {
+ "opaque=\"SomeString\", "
+ "stale=false, "
+ "algorithm=MD5-sess, "
- + "qop=\"jakarta\""; // jakarta is an invalid qop value
+ + "qop=\"stuff\"";
Header authChallenge = new BasicHeader(AUTH.WWW_AUTH, challenge);
- AuthScheme authscheme = new DigestScheme();
+ DigestScheme authscheme = new DigestScheme();
authscheme.processChallenge(authChallenge);
+
+ Credentials cred = new UsernamePasswordCredentials(username, password);
+ HttpRequest request = new BasicHttpRequest("Simple", "/");
+ authscheme.authenticate(cred, request);
+ }
+
+ /**
+ * Test digest authentication with unknown qop value
+ */
+ @Test(expected=AuthenticationException.class)
+ public void testDigestAuthenticationUnknownAlgo() throws Exception {
+ // Example using Digest auth with MD5-sess
+
+ String realm="realm";
+ String username="username";
+ String password="password";
+ String nonce="e273f1776275974f1a120d8b92c5b3cb";
+
+ String challenge="Digest realm=\"" + realm + "\", "
+ + "nonce=\"" + nonce + "\", "
+ + "opaque=\"SomeString\", "
+ + "stale=false, "
+ + "algorithm=stuff, "
+ + "qop=\"auth\"";
+
+ Header authChallenge = new BasicHeader(AUTH.WWW_AUTH, challenge);
+
+ DigestScheme authscheme = new DigestScheme();
+ authscheme.processChallenge(authChallenge);
+
+ Credentials cred = new UsernamePasswordCredentials(username, password);
+ HttpRequest request = new BasicHttpRequest("Simple", "/");
+ authscheme.authenticate(cred, request);
}
@Test
@@ -308,7 +344,7 @@ public class TestDigestScheme {
public void testDigestNouceCount() throws Exception {
String challenge1 = "Digest realm=\"realm1\", nonce=\"f2a3f18799759d4f1a1c068b92b573cb\", qop=auth";
Header authChallenge1 = new BasicHeader(AUTH.WWW_AUTH, challenge1);
- HttpRequest request = new BasicHttpRequest("Simple", "/");
+ HttpRequest request = new BasicHttpRequest("GET", "/");
Credentials cred = new UsernamePasswordCredentials("username","password");
DigestScheme authscheme = new DigestScheme();
authscheme.processChallenge(authChallenge1);
@@ -332,4 +368,56 @@ public class TestDigestScheme {
Assert.assertEquals("00000001", table4.get("nc"));
}
+ @Test
+ public void testDigestMD5SessA1AndCnonceConsistency() throws Exception {
+ String challenge1 = "Digest qop=\"auth\", algorithm=MD5-sess, nonce=\"1234567890abcdef\", " +
+ "charset=utf-8, realm=\"subnet.domain.com\"";
+ Header authChallenge1 = new BasicHeader(AUTH.WWW_AUTH, challenge1);
+ HttpRequest request = new BasicHttpRequest("GET", "/");
+ Credentials cred = new UsernamePasswordCredentials("username","password");
+ DigestScheme authscheme = new DigestScheme();
+ authscheme.processChallenge(authChallenge1);
+ Header authResponse1 = authscheme.authenticate(cred, request);
+ Map<String, String> table1 = parseAuthResponse(authResponse1);
+ Assert.assertEquals("00000001", table1.get("nc"));
+ String cnonce1 = authscheme.getCnonce();
+ String sessionKey1 = authscheme.getA1();
+
+ Header authResponse2 = authscheme.authenticate(cred, request);
+ Map<String, String> table2 = parseAuthResponse(authResponse2);
+ Assert.assertEquals("00000002", table2.get("nc"));
+ String cnonce2 = authscheme.getCnonce();
+ String sessionKey2 = authscheme.getA1();
+
+ Assert.assertEquals(cnonce1, cnonce2);
+ Assert.assertEquals(sessionKey1, sessionKey2);
+
+ String challenge2 = "Digest qop=\"auth\", algorithm=MD5-sess, nonce=\"1234567890abcdef\", " +
+ "charset=utf-8, realm=\"subnet.domain.com\"";
+ Header authChallenge2 = new BasicHeader(AUTH.WWW_AUTH, challenge2);
+ authscheme.processChallenge(authChallenge2);
+ Header authResponse3 = authscheme.authenticate(cred, request);
+ Map<String, String> table3 = parseAuthResponse(authResponse3);
+ Assert.assertEquals("00000003", table3.get("nc"));
+
+ String cnonce3 = authscheme.getCnonce();
+ String sessionKey3 = authscheme.getA1();
+
+ Assert.assertEquals(cnonce1, cnonce3);
+ Assert.assertEquals(sessionKey1, sessionKey3);
+
+ String challenge3 = "Digest qop=\"auth\", algorithm=MD5-sess, nonce=\"fedcba0987654321\", " +
+ "charset=utf-8, realm=\"subnet.domain.com\"";
+ Header authChallenge3 = new BasicHeader(AUTH.WWW_AUTH, challenge3);
+ authscheme.processChallenge(authChallenge3);
+ Header authResponse4 = authscheme.authenticate(cred, request);
+ Map<String, String> table4 = parseAuthResponse(authResponse4);
+ Assert.assertEquals("00000001", table4.get("nc"));
+
+ String cnonce4 = authscheme.getCnonce();
+ String sessionKey4 = authscheme.getA1();
+
+ Assert.assertFalse(cnonce1.equals(cnonce4));
+ Assert.assertFalse(sessionKey1.equals(sessionKey4));
+ }
}