commons-dev mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From jsde...@apache.org
Subject cvs commit: jakarta-commons/httpclient/src/java/org/apache/commons/httpclient Authenticator.java
Date Tue, 16 Jul 2002 03:46:54 GMT
jsdever     2002/07/15 20:46:53

  Modified:    httpclient/src/java/org/apache/commons/httpclient
                        Authenticator.java
  Log:
  Add Digest authentication to the Authenticator class.
  
  NOTE: This patch breaks the testDigestAuthenticationScheme unit test.  Awaiting a followup
patch to add some unit tests to support this patch.
  
  Bugzilla bug: http://nagoya.apache.org/bugzilla/show_bug.cgi?id=10843
  Bug status remains open pending the submission of unit tests and completion of adequate
functional testing.
  
  Submited by: Davanum Srinivas
  
  Revision  Changes    Path
  1.16      +293 -5    jakarta-commons/httpclient/src/java/org/apache/commons/httpclient/Authenticator.java
  
  Index: Authenticator.java
  ===================================================================
  RCS file: /home/cvs/jakarta-commons/httpclient/src/java/org/apache/commons/httpclient/Authenticator.java,v
  retrieving revision 1.15
  retrieving revision 1.16
  diff -u -r1.15 -r1.16
  --- Authenticator.java	14 Jul 2002 02:14:59 -0000	1.15
  +++ Authenticator.java	16 Jul 2002 03:46:53 -0000	1.16
  @@ -65,6 +65,9 @@
   import org.apache.commons.httpclient.log.Log;
   import org.apache.commons.httpclient.log.LogSource;
   
  +import java.util.Hashtable;
  +import java.security.MessageDigest;
  +
   /**
    * <p>Utility methods for HTTP authorization and authentication.</p>
    * <p>
  @@ -208,7 +211,30 @@
                   return false;
               }
           } else if ("digest".equalsIgnoreCase(authScheme)) {
  -            throw new UnsupportedOperationException("Digest authentication is not supported.");
  +            // FIXME: Note that this won't work if there
  +            //        is more than one realm within
  +            //        the challenge
  +            // FIXME: We could probably make it a bit
  +            //        more flexible in parsing as well.
  +
  +            // parse the realm from the authentication challenge
  +            if(challenge.length() < space + 1) {
  +                throw new HttpException("Unable to parse authentication challenge \"" +
challenge + "\", expected realm");
  +            }
  +            String realmstr = challenge.substring(space+1,challenge.length());
  +            realmstr.trim();
  +            if(realmstr.length()<"realm=\"\"".length()) {
  +                throw new HttpException("Unable to parse authentication challenge \"" +
challenge + "\", expected realm");
  +            }
  +            String realm = realmstr.substring("realm=\"".length(),realmstr.length()-1);
  +            log.debug("Parsed realm \"" + realm + "\" from challenge \"" + challenge +
"\".");
  +            Header header = Authenticator.digest(realm, method, state, respHeader);
  +            if(null != header) {
  +                method.addRequestHeader(header);
  +                return true;
  +            } else {
  +                return false;
  +            }
           } else {
               throw new UnsupportedOperationException("Authentication type \"" + authScheme
+ "\" is not recognized.");
           }
  @@ -266,4 +292,266 @@
           return "Basic " + new String(base64.encode(authString.getBytes()));
       }
   
  +    /**
  +     * Create a Digest <tt>Authorization</tt> header for the given
  +     * <i>realm</i> and <i>state</i> to the given <i>method</i>.
  +     *
  +     * @param realm the basic realm to authenticate to
  +     * @param state a {@link HttpState} object providing {@link Credentials}
  +     * @param respHeader the header's name to store the authentication response
  +     * in. PROXY_AUTH_RESP will force the proxy credentials to be used.
  +     *
  +     * @return a digest <tt>Authorization</tt> header
  +     *
  +     * @throws HttpException when no matching credentials are available
  +     */
  +    static Header digest(String realm, HttpMethod method, HttpState state, String respHeader)
throws HttpException {
  +        log.debug("Authenticator.digest(String,HttpState)");
  +        boolean proxy = PROXY_AUTH_RESP.equals(respHeader);
  +        UsernamePasswordCredentials cred = null;
  +        try {
  +            cred = (UsernamePasswordCredentials) ( proxy ?
  +                    state.getProxyCredentials(realm) :
  +                    state.getCredentials(realm));
  +        } catch(ClassCastException e) {
  +            throw new HttpException("UsernamePasswordCredentials required for Digest authentication.");
  +        }
  +        if(null == cred) {
  +            if(log.isInfoEnabled()) {
  +                log.info("No credentials found for realm \"" + realm + "\", attempting
to use default credentials.");
  +            }
  +            try {
  +                cred = (UsernamePasswordCredentials)( proxy ?
  +                        state.getProxyCredentials(null) :
  +                        state.getCredentials(null));
  +            } catch(ClassCastException e) {
  +                throw new HttpException("UsernamePasswordCredentials required for Digest
authentication.");
  +            }
  +        }
  +        if(null == cred) {
  +            throw new HttpException("No credentials available for the Digest authentication
realm \"" + realm + "\"/");
  +        } else {
  +            method.addRequestHeader(new Header("cnonce","\""+createCnonce()+"\""));
  +            method.addRequestHeader(new Header("nc", "00000001"));
  +            Hashtable headers = getHTTPDigestCredentials(method);
  +            String digest = createDigest(cred.getUserName(), cred.getPassword(), headers);
  +            return new Header(respHeader, Authenticator.digest(cred, headers, digest));
  +        }
  +    }
  +
  +    /**
  +     * Return a Digest <tt>Authorization</tt> header value for the
  +     * given {@link UsernamePasswordCredentials}.
  +     */
  +    static String digest(UsernamePasswordCredentials cred, Hashtable headers, String digest)
throws HttpException {
  +        return "Digest " + createDigestHeader(cred.getUserName(), headers, digest);
  +    }
  +
  +    /**
  +     * @todo + Add createDigest() method
  +     * Creates an MD5 response digest.
  +     *
  +     * @param uname Username
  +     * @param pwd Password
  +     * @param dCreds Hashtable containing necessary header parameters to
  +     * construct the digest. It must/can contain: uri, realm, nonce, cnonce,
  +     * qop, nc.
  +     * @return The created digest as string. This will be the response tag's
  +     * value in the Authentication HTTP header.
  +     */
  +    private static String createDigest(String uname, String pwd, Hashtable dCreds) throws
HttpException {
  +        String digAlg = "MD5";
  +        String method = "POST";
  +
  +        // Collecting required tokens
  +        String uri = removeQuotes((String)dCreds.get("uri"));
  +        String realm = removeQuotes((String)dCreds.get("realm"));
  +        String nonce = removeQuotes((String)dCreds.get("nonce"));
  +        String nc = removeQuotes((String)dCreds.get("nc"));
  +        String cnonce = removeQuotes((String)dCreds.get("cnonce"));
  +        String qop = removeQuotes((String)dCreds.get("qop"));
  +        if (qop!=null) qop = "auth";
  +
  +        MessageDigest md5Helper;
  +        try {
  +          md5Helper = MessageDigest.getInstance(digAlg);
  +        } catch (Exception e) {
  +            System.out.println("ERROR! Unsupported algorithm in HTTP Digest authentication:
"+digAlg);
  +            HttpException he = new HttpException("Unsupported algorithm in HTTP Digest
authentication: "+digAlg);
  +            throw he;
  +        }
  +
  +        // Calculating digest according to rfc 2617
  +        String a2 = method + ":" + uri;
  +        String md5a2 = encode(md5Helper.digest(a2.getBytes()));
  +        String digestValue = uname + ":" + realm + ":" + pwd;
  +        String md5a1 = encode(md5Helper.digest(digestValue.getBytes()));
  +        String serverDigestValue;
  +        if (qop==null) serverDigestValue = md5a1 + ":" + nonce + ":" + md5a2;
  +        else serverDigestValue = md5a1 + ":" + nonce + ":" + nc + ":" +
  +                                   cnonce + ":" + qop + ":" + md5a2;
  +        String serverDigest = encode(md5Helper.digest(serverDigestValue.getBytes()));
  +        return serverDigest;
  +    }
  +
  +    /**
  +     * @todo + Add createCnonce() method
  +     * Creates a random cnonce value based on the current time.
  +     *
  +     * @return The cnonce value as String.
  +     * @throws AxisFault if MD5 algorithm is not supported.
  +     */
  +    private static String createCnonce() throws HttpException {
  +        String cnonce;
  +        String digAlg = "MD5";
  +        MessageDigest md5Helper;
  +        try {
  +          md5Helper = MessageDigest.getInstance(digAlg);
  +        } catch (Exception e) {
  +            System.out.println("ERROR! Unsupported algorithm in HTTP Digest authentication:
"+digAlg);
  +            HttpException he = new HttpException("Unsupported algorithm in HTTP Digest
authentication: "+digAlg);
  +            throw he;
  +        }
  +        cnonce = Long.toString(System.currentTimeMillis());
  +        cnonce = encode(md5Helper.digest(cnonce.getBytes()));
  +        return cnonce;
  +    }
  +
  +    /**
  +     * @todo + Add createDigestHeader() method
  +     * Creates the header information that must be specified after the "Digest"
  +     * string in the HTTP Authorization header (digest-response in RFC2617).
  +     *
  +     * @param uname Username
  +     * @param dCreds Hashtable containing header information (uri, realm,
  +     * nonce, nc, cnonce, opaque, qop).
  +     * @digest The response tag's value as String.
  +     * @return The digest-response as String.
  +     */
  +    private static String createDigestHeader(String uname, Hashtable dCreds, String digest)
{
  +        StringBuffer sb = new StringBuffer();
  +        String uri = removeQuotes((String)dCreds.get("uri"));
  +        String realm = removeQuotes((String)dCreds.get("realm"));
  +        String nonce = removeQuotes((String)dCreds.get("nonce"));
  +        String nc = removeQuotes((String)dCreds.get("nc"));
  +        String cnonce = removeQuotes((String)dCreds.get("cnonce"));
  +        String opaque = removeQuotes((String)dCreds.get("opaque"));
  +        String response = digest;
  +        String qop= removeQuotes((String)dCreds.get("qop"));
  +        if (qop!=null) qop = "auth";    //we only support auth
  +        String algorithm = "MD5";       //we only support MD5
  +
  +        sb.append("username=\""+uname+"\"")
  +        .append(", realm=\""+realm+"\"")
  +        .append(", nonce=\""+nonce+"\"")
  +        .append(", uri=\""+uri+"\"")
  +        .append((qop==null?"":", qop=\""+qop+"\""))
  +        .append(", algorithm=\""+algorithm+"\"")
  +        .append((qop==null?"":", nc="+nc))
  +        .append((qop==null?"":", cnonce=\""+cnonce+"\""))
  +        .append(", response=\""+response+"\"")
  +        .append(", opaque=\""+opaque+"\"");
  +        return sb.toString();
  +    }
  +
  +    /**
  +     * Takes a <CODE>String</CODE> and cuts its prefix until the first double
  +     * quotation mark and its suffix from the last double quotation mark (and cuts
  +     * also the quotation marks).
  +     *
  +     * @param str the <CODE>String</CODE> from which the prefix and suffix
is to
  +     *    be cut.
  +     *
  +     * @return the stumped <CODE>String</CODE> if the format of <CODE>str</CODE>
  +     * is <CODE>*"*"*</CODE>. Otherwise the return value is same as
  +     * <CODE>str</CODE>
  +     */
  +    private static String removeQuotes(String str) {
  +      if (str == null)
  +        return null;
  +
  +      int fqpos = str.indexOf("\"")+1;
  +      int lqpos = str.lastIndexOf("\"");
  +      if (fqpos > 0 && lqpos > fqpos)
  +        return str.substring(fqpos,lqpos);
  +      else
  +        return str;
  +    }
  +
  +    /**
  +     * @todo + Add encode() method
  +     * Encodes the 128 bit (16 bytes) MD5 digest into a 32 characters log
  +     * <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 = (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);
  +    }
  +
  +    /**
  +     * Hexa values used when creating 32 character long digest in HTTP Digest 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'};
  +
  +    /**
  +     * Processes the www-authenticate HTTP header received from the server that
  +     * requires Digest authentication.
  +     *
  +     * @param headers The HTTP headers.
  +     * @return The parameters from www-authenticate header as a Hashtable
  +     */
  +    private static Hashtable getHTTPDigestCredentials(HttpMethod method) {
  +        String authHeader = method.getResponseHeader("www-authenticate").getValue();
  +        Hashtable ht = new Hashtable(15);
  +        if (authHeader == null) return ht;
  +        authHeader = authHeader.substring(7).trim();
  +        int i = 0;
  +        int j = authHeader.indexOf(",");
  +        while(j >= 0) {
  +            processDigestToken(authHeader.substring(i,j),ht);
  +            i = j+1;
  +            j = authHeader.indexOf(",",i);
  +        }
  +
  +        if (i < authHeader.length())
  +            processDigestToken(authHeader.substring(i),ht);
  +        return ht;
  +    }
  +
  +    /**
  +     * @todo + Add processDigestToken() method
  +     * Takes an entry of <CODE>"xxx=yyy"</CODE> format, partitions into a key
and
  +     * a value (key will be the left side, value will be the right side of the
  +     * equal sign) and places that into a <CDE>Hashtable</CODE>.
  +     *
  +     * @param token the entry to be processed
  +     * @param ht the <CODE>java.util.Hashtable</CODE> into which the processed
  +     *    entry is placed (only if it has <CODE>"xxx=yyy"</CODE> format).
  +     */
  +    private static void processDigestToken(String token, Hashtable ht) {
  +      int eqpos = token.indexOf("=");
  +
  +      if (eqpos > 0 && eqpos < token.length()-1)
  +        ht.put(token.substring(0,eqpos).trim(),token.substring(eqpos+1).trim());
  +    }
   }
  
  
  

--
To unsubscribe, e-mail:   <mailto:commons-dev-unsubscribe@jakarta.apache.org>
For additional commands, e-mail: <mailto:commons-dev-help@jakarta.apache.org>


Mime
View raw message