From 25afa68faa879a169bf13af4e69df4c53bb55e7f Mon Sep 17 00:00:00 2001 From: Oleg Kalnichevski Date: Wed, 11 Apr 2007 12:55:12 +0000 Subject: [PATCH] Ported DigestScheme and related test cases from Commons HttpClient git-svn-id: https://svn.apache.org/repos/asf/jakarta/httpcomponents/httpclient/trunk@527479 13f79535-47bb-0310-9956-ffa450edef68 --- src/java/org/apache/http/auth/AuthScheme.java | 6 +- .../org/apache/http/auth/Credentials.java | 13 +- .../auth/UsernamePasswordCredentials.java | 2 +- .../apache/http/impl/auth/BasicScheme.java | 43 +- .../apache/http/impl/auth/DigestScheme.java | 464 ++++++++++++++++++ .../apache/http/impl/auth/RFC2617Scheme.java | 24 +- .../UnsupportedDigestAlgorithmException.java | 71 +++ .../http/impl/auth/TestAllAuthImpl.java | 1 + .../apache/http/impl/auth/TestBasicAuth.java | 33 +- .../apache/http/impl/auth/TestDigestAuth.java | 317 ++++++++++++ .../http/impl/auth/TestRFC2617Scheme.java | 4 +- 11 files changed, 940 insertions(+), 38 deletions(-) create mode 100644 src/java/org/apache/http/impl/auth/DigestScheme.java create mode 100644 src/java/org/apache/http/impl/auth/UnsupportedDigestAlgorithmException.java create mode 100644 src/test/org/apache/http/impl/auth/TestDigestAuth.java diff --git a/src/java/org/apache/http/auth/AuthScheme.java b/src/java/org/apache/http/auth/AuthScheme.java index bdf139592..5b8d76445 100644 --- a/src/java/org/apache/http/auth/AuthScheme.java +++ b/src/java/org/apache/http/auth/AuthScheme.java @@ -31,7 +31,7 @@ package org.apache.http.auth; import org.apache.http.Header; -import org.apache.http.HttpMessage; +import org.apache.http.HttpRequest; /** *

@@ -128,13 +128,13 @@ public interface AuthScheme { * Produces an authorization string for the given set of {@link Credentials}. * * @param credentials The set of credentials to be used for athentication - * @param message The request being authenticated + * @param request The request being authenticated * @throws AuthenticationException if authorization string cannot * be generated due to an authentication failure * * @return the authorization string */ - Header authenticate(Credentials credentials, HttpMessage message) + Header authenticate(Credentials credentials, HttpRequest request) throws AuthenticationException; } diff --git a/src/java/org/apache/http/auth/Credentials.java b/src/java/org/apache/http/auth/Credentials.java index 1792e8d7a..d07a567f3 100644 --- a/src/java/org/apache/http/auth/Credentials.java +++ b/src/java/org/apache/http/auth/Credentials.java @@ -31,7 +31,7 @@ package org.apache.http.auth; /** - *

Authentication credentials.

+ * User name and password based authentication credentials. * * @author Unascribed * @author Mike Bowler @@ -39,12 +39,9 @@ package org.apache.http.auth; * @version $Revision$ $Date$ */ public interface Credentials { - - /** Returns textual representation of the user credentials, which, for instance, - * could be sent in the {@link HTTPAuth#WWW_AUTH} header. - * - * @return user credentials as a string of text - */ - String toText(); + + String getPrincipalName(); + + String getPassword(); } diff --git a/src/java/org/apache/http/auth/UsernamePasswordCredentials.java b/src/java/org/apache/http/auth/UsernamePasswordCredentials.java index 359ac1e5d..3b1da7816 100644 --- a/src/java/org/apache/http/auth/UsernamePasswordCredentials.java +++ b/src/java/org/apache/http/auth/UsernamePasswordCredentials.java @@ -106,7 +106,7 @@ public class UsernamePasswordCredentials implements Credentials { * @return the userName * @see #setUserName(String) */ - public String getUserName() { + public String getPrincipalName() { return userName; } diff --git a/src/java/org/apache/http/impl/auth/BasicScheme.java b/src/java/org/apache/http/impl/auth/BasicScheme.java index 0b40c20f9..2d2c9a818 100644 --- a/src/java/org/apache/http/impl/auth/BasicScheme.java +++ b/src/java/org/apache/http/impl/auth/BasicScheme.java @@ -32,7 +32,7 @@ package org.apache.http.impl.auth; import org.apache.commons.codec.binary.Base64; import org.apache.http.Header; -import org.apache.http.HttpMessage; +import org.apache.http.HttpRequest; import org.apache.http.auth.AuthenticationException; import org.apache.http.auth.Credentials; import org.apache.http.auth.HTTPAuth; @@ -89,7 +89,7 @@ public class BasicScheme extends RFC2617Scheme { * @throws MalformedChallengeException is thrown if the authentication challenge * is malformed * - * @since 3.0 + * @since 4.0 */ public void processChallenge( final Header header) throws MalformedChallengeException { @@ -124,7 +124,7 @@ public class BasicScheme extends RFC2617Scheme { * Produces basic authorization header for the given set of {@link Credentials}. * * @param credentials The set of credentials to be used for athentication - * @param message The message being authenticated + * @param request The request being authenticated * @throws InvalidCredentialsException if authentication credentials * are not valid or not applicable for this authentication scheme * @throws AuthenticationException if authorization string cannot @@ -136,17 +136,17 @@ public class BasicScheme extends RFC2617Scheme { */ public Header authenticate( final Credentials credentials, - final HttpMessage message) throws AuthenticationException { + final HttpRequest request) throws AuthenticationException { if (credentials == null) { throw new IllegalArgumentException("Credentials may not be null"); } - if (message == null) { - throw new IllegalArgumentException("HTTP message may not be null"); + if (request == null) { + throw new IllegalArgumentException("HTTP request may not be null"); } - String charset = AuthParams.getCredentialCharset(message.getParams()); - return authenticate(credentials, charset); + String charset = AuthParams.getCredentialCharset(request.getParams()); + return authenticate(credentials, charset, isProxy()); } /** @@ -160,22 +160,33 @@ public class BasicScheme extends RFC2617Scheme { * * @since 4.0 */ - public static Header authenticate(final Credentials credentials, final String charset) { + public static Header authenticate( + final Credentials credentials, + final String charset, + boolean proxy) { if (credentials == null) { throw new IllegalArgumentException("Credentials may not be null"); } if (charset == null) { throw new IllegalArgumentException("charset may not be null"); } + CharArrayBuffer buffer = new CharArrayBuffer(32); - buffer.append(HTTPAuth.WWW_AUTH_RESP); - buffer.append(": "); - buffer.append("Basic "); + buffer.append(credentials.getPrincipalName()); + buffer.append(":"); + buffer.append((credentials.getPassword() == null) ? "null" : credentials.getPassword()); + + byte[] base64password = Base64.encodeBase64( + EncodingUtils.getBytes(buffer.toString(), charset)); - byte[] passwd = Base64.encodeBase64( - EncodingUtils.getBytes(credentials.toText(), charset)); - - buffer.append(passwd, 0, passwd.length); + buffer.clear(); + if (proxy) { + buffer.append(HTTPAuth.PROXY_AUTH_RESP); + } else { + buffer.append(HTTPAuth.WWW_AUTH_RESP); + } + buffer.append(": Basic "); + buffer.append(base64password, 0, base64password.length); return new BufferedHeader(buffer); } diff --git a/src/java/org/apache/http/impl/auth/DigestScheme.java b/src/java/org/apache/http/impl/auth/DigestScheme.java new file mode 100644 index 000000000..05ff6108b --- /dev/null +++ b/src/java/org/apache/http/impl/auth/DigestScheme.java @@ -0,0 +1,464 @@ +/* + * $HeadURL$ + * $Revision$ + * $Date$ + * + * ==================================================================== + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ + +package org.apache.http.impl.auth; + +import java.security.MessageDigest; +import java.util.ArrayList; +import java.util.List; +import java.util.StringTokenizer; + +import org.apache.http.Header; +import org.apache.http.HttpRequest; +import org.apache.http.auth.AuthenticationException; +import org.apache.http.auth.Credentials; +import org.apache.http.auth.HTTPAuth; +import org.apache.http.auth.MalformedChallengeException; +import org.apache.http.auth.params.AuthParams; +import org.apache.http.message.BasicNameValuePair; +import org.apache.http.message.BufferedHeader; +import org.apache.http.util.CharArrayBuffer; +import org.apache.http.util.EncodingUtils; + +/** + *

+ * Digest authentication scheme as defined in RFC 2617. + * Both MD5 (default) and MD5-sess are supported. + * Currently only qop=auth or no qop is supported. qop=auth-int + * is unsupported. If auth and auth-int are provided, auth is + * used. + *

+ *

+ * Credential charset is configured via the + * {@link org.apache.commons.httpclient.params.HttpMethodParams#CREDENTIAL_CHARSET credential + * charset} parameter. Since the digest username is included as clear text in the generated + * Authentication header, the charset of the username must be compatible with the + * {@link org.apache.commons.httpclient.params.HttpMethodParams#HTTP_ELEMENT_CHARSET http element + * charset}. + *

+ * + * @author Remy Maucherat + * @author Rodney Waldhoff + * @author Jeff Dever + * @author Ortwin Glueck + * @author Sean C. Sullivan + * @author Adrian Sutton + * @author Mike Bowler + * @author Oleg Kalnichevski + */ + +public class DigestScheme extends RFC2617Scheme { + + /** + * Hexa values used when creating 32 character long digest in HTTP DigestScheme + * in case of authentication. + * + * @see #encode(byte[]) + */ + private static final char[] HEXADECIMAL = { + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', + 'e', 'f' + }; + + /** Whether the digest authentication process is complete */ + private boolean complete; + + //TODO: supply a real nonce-count, currently a server will interprete a repeated request as a replay + private static final String NC = "00000001"; //nonce-count is always 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 cnonce; + + /** + * Default constructor for the digest authetication scheme. + * + * @since 3.0 + */ + public DigestScheme() { + super(); + this.complete = false; + } + + /** + * Processes the Digest challenge. + * + * @param challenge the challenge string + * + * @throws MalformedChallengeException is thrown if the authentication challenge + * is malformed + * + * @since 4.0 + */ + public void processChallenge( + final Header header) throws MalformedChallengeException { + super.processChallenge(header); + + if (getParameter("realm") == null) { + throw new MalformedChallengeException("missing realm in challange"); + } + 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"); + } + + cnonce = createCnonce(); + this.complete = true; + } + + /** + * Tests if the Digest authentication process has been completed. + * + * @return true if Digest authorization has been processed, + * false otherwise. + * + * @since 3.0 + */ + public boolean isComplete() { + String s = getParameter("stale"); + if ("true".equalsIgnoreCase(s)) { + return false; + } else { + return this.complete; + } + } + + /** + * Returns textual designation of the digest authentication scheme. + * + * @return digest + */ + public String getSchemeName() { + return "digest"; + } + + /** + * Returns false. Digest authentication scheme is request based. + * + * @return false. + * + * @since 3.0 + */ + public boolean isConnectionBased() { + return false; + } + + /** + * Produces a digest authorization string for the given set of + * {@link Credentials}, method name and URI. + * + * @param credentials A set of credentials to be used for athentication + * @param method The method being authenticated + * + * @throws InvalidCredentialsException if authentication credentials + * are not valid or not applicable for this authentication scheme + * @throws AuthenticationException if authorization string cannot + * be generated due to an authentication failure + * + * @return a digest authorization string + * + * @since 4.0 + */ + public Header authenticate( + final Credentials credentials, + final HttpRequest request) throws AuthenticationException { + + if (credentials == null) { + throw new IllegalArgumentException("Credentials may not be null"); + } + if (request == null) { + throw new IllegalArgumentException("HTTP request may not be null"); + } + + // Add method name and request-URI to the parameter map + getParameters().put("methodname", request.getRequestLine().getMethod()); + getParameters().put("uri", request.getRequestLine().getUri()); + String charset = getParameter("charset"); + if (charset == null) { + charset = AuthParams.getCredentialCharset(request.getParams()); + getParameters().put("charset", charset); + } + String digest = createDigest(credentials); + return createDigestHeader(credentials, digest); + } + + private static MessageDigest createMessageDigest( + final String digAlg) throws UnsupportedDigestAlgorithmException { + try { + return MessageDigest.getInstance(digAlg); + } catch (Exception e) { + throw new UnsupportedDigestAlgorithmException( + "Unsupported algorithm in HTTP Digest authentication: " + + digAlg); + } + } + + /** + * Creates an MD5 response digest. + * + * @param uname Username + * @param pwd Password + * @param charset The credential charset + * + * @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 + */ + private String createDigest(final Credentials credentials) throws AuthenticationException { + // Collecting required tokens + String uri = getParameter("uri"); + String realm = getParameter("realm"); + String nonce = getParameter("nonce"); + String method = getParameter("methodname"); + String algorithm = getParameter("algorithm"); + // If an algorithm is not specified, default to MD5. + if (algorithm == null) { + algorithm = "MD5"; + } + // If an charset is not specified, default to ISO-8859-1. + String charset = getParameter("charset"); + if (charset == null) { + charset = "ISO-8859-1"; + } + + if (qopVariant == QOP_AUTH_INT) { + throw new AuthenticationException( + "Unsupported qop in HTTP Digest authentication"); + } + + MessageDigest md5Helper = createMessageDigest("MD5"); + + String uname = credentials.getPrincipalName(); + String pwd = credentials.getPassword(); + + // 3.2.2.2: Calculating digest + CharArrayBuffer tmp = new CharArrayBuffer(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(); + //a1 is suitable for MD5 algorithm + if(algorithm.equals("MD5-sess")) { + // H( unq(username-value) ":" unq(realm-value) ":" passwd ) + // ":" unq(nonce-value) + // ":" unq(cnonce-value) + + String tmp2=encode(md5Helper.digest(EncodingUtils.getBytes(a1, charset))); + CharArrayBuffer tmp3 = new CharArrayBuffer(tmp2.length() + nonce.length() + cnonce.length() + 2); + tmp3.append(tmp2); + tmp3.append(':'); + tmp3.append(nonce); + tmp3.append(':'); + tmp3.append(cnonce); + a1 = tmp3.toString(); + } else if (!algorithm.equals("MD5")) { + throw new AuthenticationException("Unhandled algorithm " + algorithm + " requested"); + } + String md5a1 = encode(md5Helper.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) + } else { + a2 = method + ":" + uri; + } + String md5a2 = encode(md5Helper.digest(EncodingUtils.getAsciiBytes(a2))); + + // 3.2.2.1 + String serverDigestValue; + if (qopVariant == QOP_MISSING) { + CharArrayBuffer tmp2 = new CharArrayBuffer(md5a1.length() + nonce.length() + md5a2.length()); + tmp2.append(md5a1); + tmp2.append(':'); + tmp2.append(nonce); + tmp2.append(':'); + tmp2.append(md5a2); + serverDigestValue = tmp2.toString(); + } else { + String qopOption = getQopVariantString(); + CharArrayBuffer tmp2 = new CharArrayBuffer(md5a1.length() + nonce.length() + + NC.length() + cnonce.length() + qopOption.length() + md5a2.length() + 5); + tmp2.append(md5a1); + tmp2.append(':'); + tmp2.append(nonce); + tmp2.append(':'); + tmp2.append(NC); + tmp2.append(':'); + tmp2.append(cnonce); + tmp2.append(':'); + tmp2.append(qopOption); + tmp2.append(':'); + tmp2.append(md5a2); + serverDigestValue = tmp2.toString(); + } + + String serverDigest = + encode(md5Helper.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) throws AuthenticationException { + + CharArrayBuffer buffer = new CharArrayBuffer(128); + if (isProxy()) { + buffer.append(HTTPAuth.PROXY_AUTH_RESP); + } else { + buffer.append(HTTPAuth.WWW_AUTH_RESP); + } + 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"); + + List params = new ArrayList(20); + params.add(new BasicNameValuePair("username", credentials.getPrincipalName())); + params.add(new BasicNameValuePair("realm", realm)); + params.add(new BasicNameValuePair("nonce", nonce)); + params.add(new BasicNameValuePair("uri", uri)); + params.add(new BasicNameValuePair("response", response)); + + if (qopVariant != QOP_MISSING) { + params.add(new BasicNameValuePair("qop", getQopVariantString())); + params.add(new BasicNameValuePair("nc", NC)); + params.add(new BasicNameValuePair("cnonce", this.cnonce)); + } + if (algorithm != null) { + params.add(new BasicNameValuePair("algorithm", algorithm)); + } + if (opaque != null) { + params.add(new BasicNameValuePair("opaque", opaque)); + } + + for (int i = 0; i < params.size(); i++) { + BasicNameValuePair param = (BasicNameValuePair) params.get(i); + if (i > 0) { + buffer.append(", "); + } + boolean noQuotes = "nc".equals(param.getName()) || + "qop".equals(param.getName()); + BasicNameValuePair.format(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; + } + + /** + * Encodes the 128 bit (16 bytes) MD5 digest into a 32 characters long + * String according to RFC 2617. + * + * @param binaryData array containing the digest + * @return encoded MD5, or null if encoding failed + */ + private static String encode(byte[] binaryData) { + if (binaryData.length != 16) { + return null; + } + + char[] buffer = new char[32]; + for (int i = 0; i < 16; i++) { + int low = (int) (binaryData[i] & 0x0f); + int high = (int) ((binaryData[i] & 0xf0) >> 4); + buffer[i * 2] = HEXADECIMAL[high]; + buffer[(i * 2) + 1] = HEXADECIMAL[low]; + } + + return new String(buffer); + } + + + /** + * Creates a random cnonce value based on the current time. + * + * @return The cnonce value as String. + * @throws HttpClientError 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; + } +} diff --git a/src/java/org/apache/http/impl/auth/RFC2617Scheme.java b/src/java/org/apache/http/impl/auth/RFC2617Scheme.java index 17477a401..ce36373bc 100644 --- a/src/java/org/apache/http/impl/auth/RFC2617Scheme.java +++ b/src/java/org/apache/http/impl/auth/RFC2617Scheme.java @@ -59,6 +59,11 @@ public abstract class RFC2617Scheme implements AuthScheme { */ private Map params = null; + /** + * Flag whether authenticating against a proxy. + */ + private boolean proxy; + /** * Default constructor for RFC2617 compliant authetication schemes. * @@ -85,10 +90,14 @@ public abstract class RFC2617Scheme implements AuthScheme { throw new IllegalArgumentException("Header may not be null"); } String authheader = header.getName(); - if (!authheader.equalsIgnoreCase(HTTPAuth.WWW_AUTH) - && !authheader.equalsIgnoreCase(HTTPAuth.PROXY_AUTH)) { + if (authheader.equalsIgnoreCase(HTTPAuth.WWW_AUTH)) { + this.proxy = false; + } else if (authheader.equalsIgnoreCase(HTTPAuth.PROXY_AUTH)) { + this.proxy = true; + } else { throw new MalformedChallengeException("Unexpected header name: " + authheader); } + CharArrayBuffer buffer; int pos; if (header instanceof BufferedHeader) { @@ -161,5 +170,16 @@ public abstract class RFC2617Scheme implements AuthScheme { public String getRealm() { return getParameter("realm"); } + + /** + * Returns true if authenticating against a proxy, false + * otherwise. + * + * @return true if authenticating against a proxy, false + * otherwise + */ + public boolean isProxy() { + return this.proxy; + } } diff --git a/src/java/org/apache/http/impl/auth/UnsupportedDigestAlgorithmException.java b/src/java/org/apache/http/impl/auth/UnsupportedDigestAlgorithmException.java new file mode 100644 index 000000000..178d62e83 --- /dev/null +++ b/src/java/org/apache/http/impl/auth/UnsupportedDigestAlgorithmException.java @@ -0,0 +1,71 @@ +/* + * $HeadURL$ + * $Revision$ + * $Date$ + * + * ==================================================================== + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ + +package org.apache.http.impl.auth; + +/** + * Authentication credentials required to respond to a authentication + * challenge are invalid + * + * @author Oleg Kalnichevski + * + * @since 4.0 + */ +public class UnsupportedDigestAlgorithmException extends RuntimeException { + + private static final long serialVersionUID = 319558534317118022L; + + /** + * Creates a new UnsupportedAuthAlgoritmException with a null detail message. + */ + public UnsupportedDigestAlgorithmException() { + super(); + } + + /** + * Creates a new UnsupportedAuthAlgoritmException with the specified message. + * + * @param message the exception detail message + */ + public UnsupportedDigestAlgorithmException(String message) { + super(message); + } + + /** + * Creates a new UnsupportedAuthAlgoritmException with the specified detail message and cause. + * + * @param message the exception detail message + * @param cause the Throwable that caused this exception, or null + * if the cause is unavailable, unknown, or not a Throwable + */ + public UnsupportedDigestAlgorithmException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/test/org/apache/http/impl/auth/TestAllAuthImpl.java b/src/test/org/apache/http/impl/auth/TestAllAuthImpl.java index 67eaed315..340c004cf 100644 --- a/src/test/org/apache/http/impl/auth/TestAllAuthImpl.java +++ b/src/test/org/apache/http/impl/auth/TestAllAuthImpl.java @@ -45,6 +45,7 @@ public class TestAllAuthImpl extends TestCase { suite.addTest(TestRFC2617Scheme.suite()); suite.addTest(TestBasicAuth.suite()); + suite.addTest(TestDigestAuth.suite()); return suite; } diff --git a/src/test/org/apache/http/impl/auth/TestBasicAuth.java b/src/test/org/apache/http/impl/auth/TestBasicAuth.java index 7e7525a3e..0a4621db5 100644 --- a/src/test/org/apache/http/impl/auth/TestBasicAuth.java +++ b/src/test/org/apache/http/impl/auth/TestBasicAuth.java @@ -37,14 +37,13 @@ import junit.framework.TestSuite; import org.apache.commons.codec.binary.Base64; import org.apache.http.Header; -import org.apache.http.HttpMessage; -import org.apache.http.HttpVersion; +import org.apache.http.HttpRequest; import org.apache.http.auth.AuthScheme; import org.apache.http.auth.HTTPAuth; import org.apache.http.auth.MalformedChallengeException; import org.apache.http.auth.UsernamePasswordCredentials; import org.apache.http.message.BasicHeader; -import org.apache.http.message.BasicHttpResponse; +import org.apache.http.message.BasicHttpRequest; import org.apache.http.util.EncodingUtils; /** @@ -94,7 +93,7 @@ public class TestBasicAuth extends TestCase { } UsernamePasswordCredentials credentials = new UsernamePasswordCredentials("dh", buffer.toString()); - Header header = BasicScheme.authenticate(credentials, "ISO-8859-1"); + Header header = BasicScheme.authenticate(credentials, "ISO-8859-1", false); assertEquals("Basic ZGg65C32Lfw=", header.getValue()); } @@ -107,11 +106,33 @@ public class TestBasicAuth extends TestCase { BasicScheme authscheme = new BasicScheme(); authscheme.processChallenge(challenge); - HttpMessage message = new BasicHttpResponse(HttpVersion.HTTP_1_1, 200, "OK"); - Header authResponse = authscheme.authenticate(creds, message); + HttpRequest request = new BasicHttpRequest("GET", "/"); + Header authResponse = authscheme.authenticate(creds, request); String expected = "Basic " + EncodingUtils.getAsciiString( Base64.encodeBase64(EncodingUtils.getAsciiBytes("testuser:testpass"))); + assertEquals(HTTPAuth.WWW_AUTH_RESP, authResponse.getName()); + assertEquals(expected, authResponse.getValue()); + assertEquals("test", authscheme.getRealm()); + assertTrue(authscheme.isComplete()); + assertFalse(authscheme.isConnectionBased()); + } + + public void testBasicProxyAuthentication() throws Exception { + UsernamePasswordCredentials creds = + new UsernamePasswordCredentials("testuser", "testpass"); + + Header challenge = new BasicHeader(HTTPAuth.PROXY_AUTH, "Basic realm=\"test\""); + + BasicScheme authscheme = new BasicScheme(); + authscheme.processChallenge(challenge); + + HttpRequest request = new BasicHttpRequest("GET", "/"); + Header authResponse = authscheme.authenticate(creds, request); + + String expected = "Basic " + EncodingUtils.getAsciiString( + Base64.encodeBase64(EncodingUtils.getAsciiBytes("testuser:testpass"))); + assertEquals(HTTPAuth.PROXY_AUTH_RESP, authResponse.getName()); assertEquals(expected, authResponse.getValue()); assertEquals("test", authscheme.getRealm()); assertTrue(authscheme.isComplete()); diff --git a/src/test/org/apache/http/impl/auth/TestDigestAuth.java b/src/test/org/apache/http/impl/auth/TestDigestAuth.java new file mode 100644 index 000000000..e7362b053 --- /dev/null +++ b/src/test/org/apache/http/impl/auth/TestDigestAuth.java @@ -0,0 +1,317 @@ +/* + * $HeadURL$ + * $Revision$ + * $Date$ + * ==================================================================== + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ + +package org.apache.http.impl.auth; + +import java.util.HashMap; +import java.util.Map; + +import junit.framework.Test; +import junit.framework.TestCase; +import junit.framework.TestSuite; + +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.Credentials; +import org.apache.http.auth.HTTPAuth; +import org.apache.http.auth.MalformedChallengeException; +import org.apache.http.auth.UsernamePasswordCredentials; +import org.apache.http.message.BasicHeader; +import org.apache.http.message.BasicHeaderElement; +import org.apache.http.message.BasicHttpRequest; + +/** + * Test Methods for DigestScheme Authentication. + * + * @author Rodney Waldhoff + * @author Jeff Dever + * @author Oleg Kalnichevski + */ +public class TestDigestAuth extends TestCase { + + // ------------------------------------------------------------ Constructor + public TestDigestAuth(String testName) { + super(testName); + } + + // ------------------------------------------------------------------- Main + public static void main(String args[]) { + String[] testCaseName = { TestDigestAuth.class.getName() }; + junit.textui.TestRunner.main(testCaseName); + } + + // ------------------------------------------------------- TestCase Methods + + public static Test suite() { + return new TestSuite(TestDigestAuth.class); + } + + public void testDigestAuthenticationWithNoRealm() throws Exception { + Header authChallenge = new BasicHeader(HTTPAuth.WWW_AUTH, "Digest"); + try { + AuthScheme authscheme = new DigestScheme(); + authscheme.processChallenge(authChallenge); + fail("Should have thrown MalformedChallengeException"); + } catch(MalformedChallengeException e) { + // expected + } + } + + public void testDigestAuthenticationWithNoRealm2() throws Exception { + Header authChallenge = new BasicHeader(HTTPAuth.WWW_AUTH, "Digest "); + try { + AuthScheme authscheme = new DigestScheme(); + authscheme.processChallenge(authChallenge); + fail("Should have thrown MalformedChallengeException"); + } catch(MalformedChallengeException e) { + // expected + } + } + + public void testDigestAuthenticationWithDefaultCreds() throws Exception { + String challenge = "Digest realm=\"realm1\", nonce=\"f2a3f18799759d4f1a1c068b92b573cb\""; + Header authChallenge = new BasicHeader(HTTPAuth.WWW_AUTH, challenge); + HttpRequest request = new BasicHttpRequest("Simple", "/"); + Credentials cred = new UsernamePasswordCredentials("username","password"); + AuthScheme authscheme = new DigestScheme(); + authscheme.processChallenge(authChallenge); + Header authResponse = authscheme.authenticate(cred, request); + + Map table = parseAuthResponse(authResponse); + assertEquals("username", table.get("username")); + assertEquals("realm1", table.get("realm")); + assertEquals("/", table.get("uri")); + assertEquals("f2a3f18799759d4f1a1c068b92b573cb", table.get("nonce")); + assertEquals("e95a7ddf37c2eab009568b1ed134f89a", table.get("response")); + } + + public void testDigestAuthentication() throws Exception { + String challenge = "Digest realm=\"realm1\", nonce=\"f2a3f18799759d4f1a1c068b92b573cb\""; + Header authChallenge = new BasicHeader(HTTPAuth.WWW_AUTH, challenge); + HttpRequest request = new BasicHttpRequest("Simple", "/"); + Credentials cred = new UsernamePasswordCredentials("username","password"); + AuthScheme authscheme = new DigestScheme(); + authscheme.processChallenge(authChallenge); + Header authResponse = authscheme.authenticate(cred, request); + + Map table = parseAuthResponse(authResponse); + assertEquals("username", table.get("username")); + assertEquals("realm1", table.get("realm")); + assertEquals("/", table.get("uri")); + assertEquals("f2a3f18799759d4f1a1c068b92b573cb", table.get("nonce")); + assertEquals("e95a7ddf37c2eab009568b1ed134f89a", table.get("response")); + } + + public void testDigestAuthenticationWithQueryStringInDigestURI() throws Exception { + String challenge = "Digest realm=\"realm1\", nonce=\"f2a3f18799759d4f1a1c068b92b573cb\""; + Header authChallenge = new BasicHeader(HTTPAuth.WWW_AUTH, challenge); + HttpRequest request = new BasicHttpRequest("Simple", "/?param=value"); + Credentials cred = new UsernamePasswordCredentials("username","password"); + AuthScheme authscheme = new DigestScheme(); + authscheme.processChallenge(authChallenge); + Header authResponse = authscheme.authenticate(cred, request); + + Map table = parseAuthResponse(authResponse); + assertEquals("username", table.get("username")); + assertEquals("realm1", table.get("realm")); + assertEquals("/?param=value", table.get("uri")); + assertEquals("f2a3f18799759d4f1a1c068b92b573cb", table.get("nonce")); + assertEquals("a847f58f5fef0bc087bcb9c3eb30e042", table.get("response")); + } + + public void testDigestAuthenticationWithMultipleRealms() throws Exception { + String challenge1 = "Digest realm=\"realm1\", nonce=\"abcde\""; + String challenge2 = "Digest realm=\"realm2\", nonce=\"123546\""; + Credentials cred = new UsernamePasswordCredentials("username","password"); + Credentials cred2 = new UsernamePasswordCredentials("uname2","password2"); + + Header authChallenge = new BasicHeader(HTTPAuth.WWW_AUTH, challenge1); + HttpRequest request = new BasicHttpRequest("Simple", "/"); + AuthScheme authscheme = new DigestScheme(); + authscheme.processChallenge(authChallenge); + Header authResponse = authscheme.authenticate(cred, request); + + Map table = parseAuthResponse(authResponse); + assertEquals("username", table.get("username")); + assertEquals("realm1", table.get("realm")); + assertEquals("/", table.get("uri")); + assertEquals("abcde", table.get("nonce")); + assertEquals("786f500303eac1478f3c2865e676ed68", table.get("response")); + + authChallenge = new BasicHeader(HTTPAuth.WWW_AUTH, challenge2); + AuthScheme authscheme2 = new DigestScheme(); + authscheme2.processChallenge(authChallenge); + authResponse = authscheme2.authenticate(cred2, request); + + table = parseAuthResponse(authResponse); + assertEquals("uname2", table.get("username")); + assertEquals("realm2", table.get("realm")); + assertEquals("/", table.get("uri")); + assertEquals("123546", table.get("nonce")); + assertEquals("0283edd9ef06a38b378b3b74661391e9", table.get("response")); + } + + /** + * Test digest authentication using the MD5-sess algorithm. + */ + public void testDigestAuthenticationMD5Sess() 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=MD5-sess, " + + "qop=\"auth,auth-int\""; // we pass both but expect auth to be used + + Header authChallenge = new BasicHeader(HTTPAuth.WWW_AUTH, challenge); + + Credentials cred = new UsernamePasswordCredentials(username, password); + HttpRequest request = new BasicHttpRequest("Simple", "/"); + + AuthScheme authscheme = new DigestScheme(); + authscheme.processChallenge(authChallenge); + Header authResponse = authscheme.authenticate(cred, request); + String response = authResponse.getValue(); + + assertTrue(response.indexOf("nc=00000001") > 0); // test for quotes + assertTrue(response.indexOf("qop=auth") > 0); // test for quotes + + Map table = parseAuthResponse(authResponse); + assertEquals(username, table.get("username")); + assertEquals(realm, table.get("realm")); + assertEquals("MD5-sess", table.get("algorithm")); + assertEquals("/", table.get("uri")); + assertEquals(nonce, table.get("nonce")); + assertEquals(1, Integer.parseInt((String) table.get("nc"),16)); + assertTrue(null != table.get("cnonce")); + assertEquals("SomeString", table.get("opaque")); + assertEquals("auth", table.get("qop")); + //@TODO: add better check + assertTrue(null != table.get("response")); + } + + /** + * Test digest authentication using the MD5-sess algorithm. + */ + public void testDigestAuthenticationMD5SessNoQop() 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=MD5-sess"; + + Header authChallenge = new BasicHeader(HTTPAuth.WWW_AUTH, challenge); + + Credentials cred = new UsernamePasswordCredentials(username, password); + + HttpRequest request = new BasicHttpRequest("Simple", "/"); + + AuthScheme authscheme = new DigestScheme(); + authscheme.processChallenge(authChallenge); + Header authResponse = authscheme.authenticate(cred, request); + + Map table = parseAuthResponse(authResponse); + assertEquals(username, table.get("username")); + assertEquals(realm, table.get("realm")); + assertEquals("MD5-sess", table.get("algorithm")); + assertEquals("/", table.get("uri")); + assertEquals(nonce, table.get("nonce")); + assertTrue(null == table.get("nc")); + assertEquals("SomeString", table.get("opaque")); + assertTrue(null == table.get("qop")); + //@TODO: add better check + assertTrue(null != table.get("response")); + } + + /** + * Test digest authentication with invalud qop value + */ + public void testDigestAuthenticationMD5SessInvalidQop() throws Exception { + // Example using Digest auth with MD5-sess + + String realm="realm"; + String nonce="e273f1776275974f1a120d8b92c5b3cb"; + + String challenge="Digest realm=\"" + realm + "\", " + + "nonce=\"" + nonce + "\", " + + "opaque=\"SomeString\", " + + "stale=false, " + + "algorithm=MD5-sess, " + + "qop=\"jakarta\""; // jakarta is an invalid qop value + + Header authChallenge = new BasicHeader(HTTPAuth.WWW_AUTH, challenge); + + try { + AuthScheme authscheme = new DigestScheme(); + authscheme.processChallenge(authChallenge); + fail("MalformedChallengeException exception expected due to invalid qop value"); + } catch(MalformedChallengeException e) { + } + } + + public void testDigestAuthenticationWithStaleNonce() throws Exception { + String challenge = "Digest realm=\"realm1\", " + + "nonce=\"f2a3f18799759d4f1a1c068b92b573cb\", stale=\"true\""; + Header authChallenge = new BasicHeader(HTTPAuth.WWW_AUTH, challenge); + AuthScheme authscheme = new DigestScheme(); + authscheme.processChallenge(authChallenge); + + assertFalse(authscheme.isComplete()); + } + + private static Map parseAuthResponse(final Header authResponse) { + String s = authResponse.getValue(); + if (!s.startsWith("Digest ")) { + return null; + } + HeaderElement[] elements = BasicHeaderElement.parseAll(s.substring(7)); + Map map = new HashMap(elements.length); + for (int i = 0; i < elements.length; i++) { + HeaderElement element = elements[i]; + map.put(element.getName(), element.getValue()); + } + return map; + } + +} diff --git a/src/test/org/apache/http/impl/auth/TestRFC2617Scheme.java b/src/test/org/apache/http/impl/auth/TestRFC2617Scheme.java index 80eba64a5..0ac2e68d0 100644 --- a/src/test/org/apache/http/impl/auth/TestRFC2617Scheme.java +++ b/src/test/org/apache/http/impl/auth/TestRFC2617Scheme.java @@ -31,7 +31,7 @@ package org.apache.http.impl.auth; import org.apache.http.Header; -import org.apache.http.HttpMessage; +import org.apache.http.HttpRequest; import org.apache.http.auth.AuthenticationException; import org.apache.http.auth.Credentials; import org.apache.http.auth.HTTPAuth; @@ -63,7 +63,7 @@ public class TestRFC2617Scheme extends TestCase { public Header authenticate( final Credentials credentials, - final HttpMessage message) throws AuthenticationException { + final HttpRequest request) throws AuthenticationException { return null; }