diff options
Diffstat (limited to 'src/org/apache/http/impl/auth/DigestScheme.java')
-rw-r--r-- | src/org/apache/http/impl/auth/DigestScheme.java | 484 |
1 files changed, 484 insertions, 0 deletions
diff --git a/src/org/apache/http/impl/auth/DigestScheme.java b/src/org/apache/http/impl/auth/DigestScheme.java new file mode 100644 index 0000000..803807b --- /dev/null +++ b/src/org/apache/http/impl/auth/DigestScheme.java @@ -0,0 +1,484 @@ +/* + * $HeadURL: http://svn.apache.org/repos/asf/httpcomponents/httpclient/trunk/module-client/src/main/java/org/apache/http/impl/auth/DigestScheme.java $ + * $Revision: 659595 $ + * $Date: 2008-05-23 09:47:14 -0700 (Fri, 23 May 2008) $ + * + * ==================================================================== + * + * 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 + * <http://www.apache.org/>. + * + */ + +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.AUTH; +import org.apache.http.auth.MalformedChallengeException; +import org.apache.http.auth.params.AuthParams; +import org.apache.http.message.BasicNameValuePair; +import org.apache.http.message.BasicHeaderValueFormatter; +import org.apache.http.message.BufferedHeader; +import org.apache.http.util.CharArrayBuffer; +import org.apache.http.util.EncodingUtils; + +/** + * <p> + * 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. + * </p> + * <p> + * Credential charset is configured via the + * {@link org.apache.http.auth.params.AuthPNames#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.http.params.CoreProtocolPNames#HTTP_ELEMENT_CHARSET + * http element charset}. + * </p> + * + * @author <a href="mailto:remm@apache.org">Remy Maucherat</a> + * @author Rodney Waldhoff + * @author <a href="mailto:jsdever@apache.org">Jeff Dever</a> + * @author Ortwin Glueck + * @author Sean C. Sullivan + * @author <a href="mailto:adrian@ephox.com">Adrian Sutton</a> + * @author <a href="mailto:mbowler@GargoyleSoftware.com">Mike Bowler</a> + * @author <a href="mailto:oleg at ural.ru">Oleg Kalnichevski</a> + * + * @since 4.0 + */ + +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. + */ + public DigestScheme() { + super(); + this.complete = false; + } + + /** + * Processes the Digest challenge. + * + * @param header the challenge header + * + * @throws MalformedChallengeException is thrown if the authentication challenge + * is malformed + */ + @Override + 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"); + } + // Reset cnonce + this.cnonce = null; + this.complete = true; + } + + /** + * Tests if the Digest authentication process has been completed. + * + * @return <tt>true</tt> if Digest authorization has been processed, + * <tt>false</tt> otherwise. + */ + 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 <code>digest</code> + */ + public String getSchemeName() { + return "digest"; + } + + /** + * Returns <tt>false</tt>. Digest authentication scheme is request based. + * + * @return <tt>false</tt>. + */ + public boolean isConnectionBased() { + return false; + } + + public void overrideParamter(final String name, final String value) { + getParameters().put(name, value); + } + + private String getCnonce() { + if (this.cnonce == null) { + this.cnonce = createCnonce(); + } + return this.cnonce; + } + + /** + * 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 request The request being authenticated + * + * @throws org.apache.http.auth.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 + */ + 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. + * + * @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 (uri == null) { + throw new IllegalStateException("URI may not be null"); + } + if (realm == null) { + throw new IllegalStateException("Realm may not be null"); + } + if (nonce == null) { + throw new IllegalStateException("Nonce may not be null"); + } + // 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.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(); + + //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 cnonce = getCnonce(); + + String tmp2=encode(md5Helper.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(); + } 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) { + StringBuilder tmp2 = new StringBuilder(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(); + String cnonce = getCnonce(); + + StringBuilder tmp2 = new StringBuilder(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(AUTH.PROXY_AUTH_RESP); + } else { + buffer.append(AUTH.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"); + + 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)); + + if (qopVariant != QOP_MISSING) { + params.add(new BasicNameValuePair("qop", getQopVariantString())); + params.add(new BasicNameValuePair("nc", NC)); + params.add(new BasicNameValuePair("cnonce", getCnonce())); + } + 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 = params.get(i); + if (i > 0) { + buffer.append(", "); + } + 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; + } + + /** + * Encodes the 128 bit (16 bytes) MD5 digest into a 32 characters long + * <CODE>String</CODE> according to RFC 2617. + * + * @param binaryData array containing the digest + * @return encoded MD5, or <CODE>null</CODE> 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 = (binaryData[i] & 0x0f); + int high = ((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 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; + } +} |