All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.helger.http.digestauth.HttpDigestAuth Maven / Gradle / Ivy

/**
 * Copyright (C) 2014-2020 Philip Helger (www.helger.com)
 * philip[at]helger[dot]com
 *
 * Licensed 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.
 */
package com.helger.http.digestauth;

import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;

import javax.annotation.CheckForSigned;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.helger.commons.ValueEnforcer;
import com.helger.commons.annotation.Nonempty;
import com.helger.commons.annotation.PresentForCodeCoverage;
import com.helger.commons.collection.impl.CommonsLinkedHashMap;
import com.helger.commons.collection.impl.ICommonsOrderedMap;
import com.helger.commons.http.EHttpMethod;
import com.helger.commons.string.StringHelper;
import com.helger.http.HttpStringHelper;
import com.helger.security.messagedigest.EMessageDigestAlgorithm;
import com.helger.security.messagedigest.MessageDigestValue;

/**
 * Handling for HTTP Digest Authentication
 *
 * @author Philip Helger
 */
@Immutable
public final class HttpDigestAuth
{
  public static final String HEADER_VALUE_PREFIX_DIGEST = "Digest";

  public static final String ALGORITHM_MD5 = "MD5";
  public static final String ALGORITHM_MD5_SESS = "MD5-sess";
  public static final String DEFAULT_ALGORITHM = ALGORITHM_MD5;

  public static final String QOP_AUTH = "auth";
  public static final String QOP_AUTH_INT = "auth-int";
  public static final String DEFAULT_QOP = QOP_AUTH;

  private static final Logger LOGGER = LoggerFactory.getLogger (HttpDigestAuth.class);
  private static final char SEPARATOR = ':';
  private static final Charset CHARSET = StandardCharsets.ISO_8859_1;

  @PresentForCodeCoverage
  private static final HttpDigestAuth s_aInstance = new HttpDigestAuth ();

  private HttpDigestAuth ()
  {}

  /**
   * Get the parameters of a Digest authentication string. It may be used for
   * both client and server handling.
   *
   * @param sAuthHeader
   *        The HTTP header value to be interpreted. May be null.
   * @return null if the passed value cannot be parsed as a HTTP
   *         Digest Authentication value, a {@link ICommonsOrderedMap} with all
   *         parameter name-value pairs in the order they are contained.
   */
  @Nullable
  public static ICommonsOrderedMap  getDigestAuthParams (@Nullable final String sAuthHeader)
  {
    final String sRealHeader = StringHelper.trim (sAuthHeader);
    if (StringHelper.hasNoText (sRealHeader))
      return null;

    if (!sRealHeader.startsWith (HEADER_VALUE_PREFIX_DIGEST))
    {
      if (LOGGER.isErrorEnabled ())
        LOGGER.error ("String does not start with '" + HEADER_VALUE_PREFIX_DIGEST + "'");
      return null;
    }

    final char [] aChars = sRealHeader.toCharArray ();
    int nIndex = HEADER_VALUE_PREFIX_DIGEST.length ();

    if (nIndex >= aChars.length || !HttpStringHelper.isLinearWhitespaceChar (aChars[nIndex]))
    {
      if (LOGGER.isErrorEnabled ())
        LOGGER.error ("No whitespace after '" + HEADER_VALUE_PREFIX_DIGEST + "'");
      return null;
    }
    nIndex++;

    final ICommonsOrderedMap  aParams = new CommonsLinkedHashMap <> ();
    while (true)
    {
      // Skip all spaces
      while (nIndex < aChars.length && HttpStringHelper.isLinearWhitespaceChar (aChars[nIndex]))
        nIndex++;

      // Find token name
      int nStartIndex = nIndex;
      while (nIndex < aChars.length && HttpStringHelper.isTokenChar (aChars[nIndex]))
        nIndex++;
      if (nStartIndex == nIndex)
      {
        if (LOGGER.isErrorEnabled ())
          LOGGER.error ("No token and no whitespace found for auth-param name: '" + aChars[nIndex] + "'");
        return null;
      }
      final String sToken = sRealHeader.substring (nStartIndex, nIndex);

      // Skip all spaces
      while (nIndex < aChars.length && HttpStringHelper.isLinearWhitespaceChar (aChars[nIndex]))
        nIndex++;

      if (nIndex >= aChars.length || aChars[nIndex] != '=')
      {
        if (LOGGER.isErrorEnabled ())
          LOGGER.error ("No separator char '=' found after '" + sToken + "'");
        return null;
      }
      nIndex++;

      // Skip all spaces
      while (nIndex < aChars.length && HttpStringHelper.isLinearWhitespaceChar (aChars[nIndex]))
        nIndex++;

      if (nIndex >= aChars.length)
      {
        if (LOGGER.isErrorEnabled ())
          LOGGER.error ("Found nothing after '=' of '" + sToken + "'");
        return null;
      }

      String sValue;
      if (aChars[nIndex] == HttpStringHelper.QUOTEDTEXT_BEGIN)
      {
        // Quoted string
        ++nIndex;
        nStartIndex = nIndex;
        while (nIndex < aChars.length && HttpStringHelper.isQuotedTextChar (aChars[nIndex]))
          nIndex++;
        if (nIndex >= aChars.length)
        {
          if (LOGGER.isErrorEnabled ())
            LOGGER.error ("Unexpected EOF in quoted text for '" + sToken + "'");
          return null;
        }
        if (aChars[nIndex] != HttpStringHelper.QUOTEDTEXT_END)
        {
          if (LOGGER.isErrorEnabled ())
            LOGGER.error ("Quoted string of token '" +
                             sToken +
                             "' is not terminated correctly: '" +
                             aChars[nIndex] +
                             "'");
          return null;
        }
        sValue = sRealHeader.substring (nStartIndex, nIndex);

        // Skip termination char
        nIndex++;
      }
      else
      {
        // Token
        nStartIndex = nIndex;
        while (nIndex < aChars.length && HttpStringHelper.isTokenChar (aChars[nIndex]))
          nIndex++;
        if (nStartIndex == nIndex)
        {
          if (LOGGER.isErrorEnabled ())
            LOGGER.error ("No token and no whitespace found for auth-param value of '" +
                             sToken +
                             "': '" +
                             aChars[nIndex] +
                             "'");
          return null;
        }
        sValue = sRealHeader.substring (nStartIndex, nIndex);
      }

      // Remember key/value pair
      aParams.put (sToken, sValue);

      // Skip all spaces
      while (nIndex < aChars.length && HttpStringHelper.isLinearWhitespaceChar (aChars[nIndex]))
        nIndex++;

      // Check if there are any additional parameters
      if (nIndex >= aChars.length)
      {
        // No more tokens - we're done
        break;
      }

      // If there is a comma, another parameter is expected
      if (aChars[nIndex] != ',')
      {
        if (LOGGER.isErrorEnabled ())
          LOGGER.error ("Illegal character after auth-param '" + sToken + "': '" + aChars[nIndex] + "'");
        return null;
      }
      ++nIndex;

      if (nIndex >= aChars.length)
      {
        if (LOGGER.isErrorEnabled ())
          LOGGER.error ("Found nothing after continuation of auth-param '" + sToken + "'");
        return null;
      }
    }

    return aParams;
  }

  /**
   * Get the Digest authentication credentials from the passed HTTP header
   * value.
   *
   * @param sAuthHeader
   *        The HTTP header value to be interpreted. May be null.
   * @return null if the passed value is not a correct HTTP Digest
   *         Authentication header value.
   */
  @Nullable
  public static DigestAuthClientCredentials getDigestAuthClientCredentials (@Nullable final String sAuthHeader)
  {
    final ICommonsOrderedMap  aParams = getDigestAuthParams (sAuthHeader);
    if (aParams == null)
      return null;

    final String sUserName = aParams.remove ("username");
    if (sUserName == null)
    {
      LOGGER.error ("Digest Auth does not container 'username'");
      return null;
    }
    final String sRealm = aParams.remove ("realm");
    if (sRealm == null)
    {
      LOGGER.error ("Digest Auth does not container 'realm'");
      return null;
    }
    final String sNonce = aParams.remove ("nonce");
    if (sNonce == null)
    {
      LOGGER.error ("Digest Auth does not container 'nonce'");
      return null;
    }
    final String sDigestURI = aParams.remove ("uri");
    if (sDigestURI == null)
    {
      LOGGER.error ("Digest Auth does not container 'uri'");
      return null;
    }
    final String sResponse = aParams.remove ("response");
    if (sResponse == null)
    {
      LOGGER.error ("Digest Auth does not container 'response'");
      return null;
    }
    final String sAlgorithm = aParams.remove ("algorithm");
    final String sCNonce = aParams.remove ("cnonce");
    final String sOpaque = aParams.remove ("opaque");
    final String sMessageQOP = aParams.remove ("qop");
    final String sNonceCount = aParams.remove ("nc");
    if (aParams.isNotEmpty ())
      if (LOGGER.isWarnEnabled ())
        LOGGER.warn ("Digest Auth contains unhandled parameters: " + aParams.toString ());

    return new DigestAuthClientCredentials (sUserName,
                                            sRealm,
                                            sNonce,
                                            sDigestURI,
                                            sResponse,
                                            sAlgorithm,
                                            sCNonce,
                                            sOpaque,
                                            sMessageQOP,
                                            sNonceCount);
  }

  @Nullable
  public static String getNonceCountString (@CheckForSigned final int nNonceCount)
  {
    return nNonceCount <= 0 ? null : StringHelper.getLeadingZero (StringHelper.getHexString (nNonceCount), 8);
  }

  @Nonnull
  private static String _md5 (@Nonnull final String s)
  {
    return MessageDigestValue.create (s.getBytes (CHARSET), EMessageDigestAlgorithm.MD5).getHexEncodedDigestString ();
  }

  /**
   * Create HTTP Digest auth credentials for a client
   *
   * @param eMethod
   *        The HTTP method of the request. May not be null.
   * @param sDigestURI
   *        The URI from Request-URI of the Request-Line; duplicated here
   *        because proxies are allowed to change the Request-Line in transit.
   *        May neither be null nor empty.
   * @param sUserName
   *        User name to use. May neither be null nor empty.
   * @param sPassword
   *        The user's password. May not be null.
   * @param sRealm
   *        The realm as provided by the server. May neither be
   *        null nor empty.
   * @param sServerNonce
   *        The nonce as supplied by the server. May neither be
   *        null nor empty.
   * @param sAlgorithm
   *        The algorithm as provided by the server. Currently only
   *        {@link #ALGORITHM_MD5} and {@link #ALGORITHM_MD5_SESS} is supported.
   *        If it is null than {@link #ALGORITHM_MD5} is used as
   *        default.
   * @param sClientNonce
   *        The client nonce to be used. Must be present if message QOP is
   *        specified or if algorithm is {@link #ALGORITHM_MD5_SESS}.
* This MUST be specified if a qop directive is sent, and MUST NOT be * specified if the server did not send a qop directive in the * WWW-Authenticate header field. The cnonce-value is an opaque quoted * string value provided by the client and used by both client and * server to avoid chosen plain text attacks, to provide mutual * authentication, and to provide some message integrity protection. * See the descriptions below of the calculation of the response- * digest and request-digest values. * @param sOpaque * The opaque value as supplied by the server. May be null * . * @param sMessageQOP * The message QOP. Currently only {@link #QOP_AUTH} is supported. If * null is passed, than {@link #QOP_AUTH} with backward * compatibility handling for RFC 2069 is applied.
* Indicates what "quality of protection" the client has applied to the * message. If present, its value MUST be one of the alternatives the * server indicated it supports in the WWW-Authenticate header. These * values affect the computation of the request-digest. Note that this * is a single token, not a quoted list of alternatives as in WWW- * Authenticate. This directive is optional in order to preserve * backward compatibility with a minimal implementation of RFC 2069 * [6], but SHOULD be used if the server indicated that qop is * supported by providing a qop directive in the WWW-Authenticate * header field. * @param nNonceCount * This MUST be specified if a qop directive is sent (see above), and * MUST NOT be specified if the server did not send a qop directive in * the WWW-Authenticate header field. The nc-value is the hexadecimal * count of the number of requests (including the current request) that * the client has sent with the nonce value in this request. For * example, in the first request sent in response to a given nonce * value, the client sends "nc=00000001". The purpose of this directive * is to allow the server to detect request replays by maintaining its * own copy of this count - if the same nc-value is seen twice, then * the request is a replay. * @return The created DigestAuthCredentials */ @Nonnull public static DigestAuthClientCredentials createDigestAuthClientCredentials (@Nonnull final EHttpMethod eMethod, @Nonnull @Nonempty final String sDigestURI, @Nonnull @Nonempty final String sUserName, @Nonnull final String sPassword, @Nonnull @Nonempty final String sRealm, @Nonnull @Nonempty final String sServerNonce, @Nullable final String sAlgorithm, @Nullable final String sClientNonce, @Nullable final String sOpaque, @Nullable final String sMessageQOP, @CheckForSigned final int nNonceCount) { ValueEnforcer.notNull (eMethod, "Method"); ValueEnforcer.notEmpty (sDigestURI, "DigestURI"); ValueEnforcer.notEmpty (sUserName, "UserName"); ValueEnforcer.notNull (sPassword, "Password"); ValueEnforcer.notEmpty (sRealm, "Realm"); ValueEnforcer.notEmpty (sServerNonce, "ServerNonce"); if (sMessageQOP != null && StringHelper.hasNoText (sClientNonce)) throw new IllegalArgumentException ("If a QOP is defined, client nonce must be set!"); if (sMessageQOP != null && nNonceCount <= 0) throw new IllegalArgumentException ("If a QOP is defined, nonce count must be positive!"); final String sRealAlgorithm = sAlgorithm == null ? DEFAULT_ALGORITHM : sAlgorithm; if (!sRealAlgorithm.equals (ALGORITHM_MD5) && !sRealAlgorithm.equals (ALGORITHM_MD5_SESS)) throw new IllegalArgumentException ("Currently only '" + ALGORITHM_MD5 + "' and '" + ALGORITHM_MD5_SESS + "' algorithms are supported!"); if (sMessageQOP != null && !sMessageQOP.equals (QOP_AUTH)) throw new IllegalArgumentException ("Currently only '" + QOP_AUTH + "' QOP is supported!"); // Nonce must always by 8 chars long final String sNonceCount = getNonceCountString (nNonceCount); // Create HA1 String sHA1 = _md5 (sUserName + SEPARATOR + sRealm + SEPARATOR + sPassword); if (sRealAlgorithm.equals (ALGORITHM_MD5_SESS)) { if (StringHelper.hasNoText (sClientNonce)) throw new IllegalArgumentException ("Algorithm requires client nonce!"); sHA1 = _md5 (sHA1 + SEPARATOR + sServerNonce + SEPARATOR + sClientNonce); } // Create HA2 // Method name must be upper-case! final String sHA2 = _md5 (eMethod.getName () + SEPARATOR + sDigestURI); // Create the request digest - result must be all lowercase hex chars! String sRequestDigest; if (sMessageQOP == null) { // RFC 2069 backwards compatibility sRequestDigest = _md5 (sHA1 + SEPARATOR + sServerNonce + SEPARATOR + sHA2); } else { sRequestDigest = _md5 (sHA1 + SEPARATOR + sServerNonce + SEPARATOR + sNonceCount + SEPARATOR + sClientNonce + SEPARATOR + sMessageQOP + SEPARATOR + sHA2); } return new DigestAuthClientCredentials (sUserName, sRealm, sServerNonce, sDigestURI, sRequestDigest, sAlgorithm, sClientNonce, sOpaque, sMessageQOP, sNonceCount); } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy