WebApi interface security authentication - HTTP digest authentication

       Digest access authentication is a protocol-specified method used by web servers to negotiate authentication information with web browsers. It applies a hash function to the password before sending it, which is more secure than sending plaintext with HTTP basic authentication. Technically, digest authentication is the application of the MD5 cryptographic hash function using a random number to prevent cryptanalysis. It uses HTTP protocol.

 

1. The basic process of digest authentication:

 

1. Client request (no authentication)

GET /dir/index.html HTTP/1.0
Host: localhost

 

2. Server response

The server returns a 401 unauthenticated status and returns WWW-Authenticate information, including the values ​​of the authentication methods Digest, realm, qop, nonce, and opaque . in:

Digest : Authentication method;

realm : Realm, the realm parameter is mandatory and must be present in all interrogations. Its purpose is to identify the secrets in SIP messages. In practical SIP applications, it is usually set to the domain name that the SIP proxy server is responsible for;

qop : The quality of protection, this parameter specifies which protection scheme the server supports, and the client can choose one from the list. A value of "auth" means that only authentication is performed, and "auth-int" means that there is some integrity protection in addition to authentication. For a more detailed description, please refer to RFC2617;

nonce : is a string of random values, which will be used in the following requests. When the lifetime expires, the server will refresh and generate a new nonce value;

opaque : An opaque (no outsiders know its meaning) data string, sent to the user during interrogation.

 

HTTP/1.0 401 Unauthorized
Server: HTTPd/0.9
Date: Sun, 10 Apr 2005 20:26:47 GMT
WWW-Authenticate: Digest realm="[email protected]",
                        qop="auth,auth-int",
                        nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093",
                        opaque="5ccc069c403ebaf9f0171e9517f40e41"

 

3. Client request (username "Mufasa", password "Circle Of Life")

After the client receives the request and returns, it performs HASH operation and returns the Authorization parameter

Among them: realm, nonce, qop are generated by the server;

uri : the URI the client wants to access;

nc : "current" counter, which is a hexadecimal value, that is, the number of requests sent by the client (including the current request), which all use the "current" value in the current request. For example, for a given "nonce" value, the client will send "nc=00000001" in the first request of the response. The purpose of this indicator is for the server to keep a copy of this counter in order to detect duplicate requests. If the same value is seen twice, the request is a duplicate;

cnonce : This is an opaque string value provided by the client and used by both client and server to avoid clear text. This allows both parties to verify the identity of the other and provides some protection for the integrity of the message;

response : This is a string calculated by the user agent software to prove that the user knows the password.

Response calculation process:
HA1=MD5(A1)=MD5(username:realm:password)
If the qop value is "auth" or not specified, then HA2 is
HA2=MD5(A2)=MD5(method:digestURI)
If the qop value is "auth-int", then HA2 is
HA2=MD5(A2)=MD5(method:digestURI:MD5(entityBody))
If the qop value is "auth" or "auth-int", the response is calculated as follows:
response=MD5(HA1:nonce:nonceCount:clientNonce:qop:HA2)
If qop is not specified, the response is calculated as follows:
response=MD5(HA1:nonce:HA2)

 

 Request header:

GET /dir/index.html HTTP/1.0
Host: localhost
Authorization: Digest username="Mufasa",
                     realm="[email protected]",
                     nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093",
                     uri="/dir/index.html",
                     qap = auth,
                     nc=00000001,
                     cnonce="0a4f113b",
                     response="6629fae49393a05397450978507c4ef1",
                     opaque="5ccc069c403ebaf9f0171e9517f40e41"

 

4. Server response

When the server receives the digest response, it also recalculates the value of each parameter in the response, and compares the parameter value provided by the client with the password stored on the server. If the calculated result is the same as the received client response value, the client has proven that it knows the password, and the client's authentication is passed.

 

HTTP/1.0 200 OK

 

2. Server-side verification

To write a custom message handler, you need to derive it from System.Net.Http.DelegatingHandler and override the SendAsync method.

 

public class AuthenticationHandler : DelegatingHandler
{
    protected async override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        try
        {
            HttpRequestHeaders headers = request.Headers;
            if (headers.Authorization != null)
            {
                Header header = new Header(request.Headers.Authorization.Parameter, request.Method.Method);

                if (Nonce.IsValid(header.Nonce, header.NounceCounter))
                {
                    // Just assuming password is same as username for the purpose of illustration
                    string password = header.UserName;

                    string ha1 = String.Format("{0}:{1}:{2}", header.UserName, header.Realm, password).ToMD5Hash();

                    string ha2 = String.Format("{0}:{1}", header.Method, header.Uri).ToMD5Hash();

                    string computedResponse = String.Format("{0}:{1}:{2}:{3}:{4}:{5}",
                                        ha1, header.Nonce, header.NounceCounter,header.Cnonce, "auth", ha2).ToMD5Hash();

                    if (String.CompareOrdinal(header.Response, computedResponse) == 0)
                    {
                        // digest computed matches the value sent by client in the response field.
                        // Looks like an authentic client! Create a principal.
                        var claims = new List<Claim>
                        {
                                        new Claim(ClaimTypes.Name, header.UserName),
                                        new Claim(ClaimTypes.AuthenticationMethod, AuthenticationMethods.Password)
                        };

                        ClaimsPrincipal principal = new ClaimsPrincipal(new[] { new ClaimsIdentity(claims, "Digest") });

                        Thread.CurrentPrincipal = principal;

                        if (HttpContext.Current != null)
                            HttpContext.Current.User = principal;
                    }
                }
            }

            HttpResponseMessage response = await base.SendAsync(request, cancellationToken);

            if (response.StatusCode == HttpStatusCode.Unauthorized)
            {
                response.Headers.WwwAuthenticate.Add(new AuthenticationHeaderValue("Digest",Header.UnauthorizedResponseHeader.ToString()));
            }

            return response;
        }
        catch (Exception)
        {
            var response = request.CreateResponse(HttpStatusCode.Unauthorized);
            response.Headers.WwwAuthenticate.Add(new AuthenticationHeaderValue("Digest",Header.UnauthorizedResponseHeader.ToString()));

            return response;
        }
    }
}
 

 Header class

 

public class Header
{
    public Header() { }

    public Header(string header, string method)
    {
        string keyValuePairs = header.Replace("\"", String.Empty);

        foreach (string keyValuePair in keyValuePairs.Split(','))
        {
            int index = keyValuePair.IndexOf("=", System.StringComparison.Ordinal);
            string key = keyValuePair.Substring(0, index);
            string value = keyValuePair.Substring(index + 1);

            switch (key)
            {
                case "username": this.UserName = value; break;
                case "realm": this.Realm = value; break;
                case "nonce": this.Nonce = value; break;
                case "uri": this.Uri = value; break;
                case "nc": this.NounceCounter = value; break;
                case "cnonce": this.Cnonce = value; break;
                case "response": this.Response = value; break;
                case "method": this.Method = value; break;
            }
        }

        if (String.IsNullOrEmpty(this.Method))
            this.Method = method;
    }

    public string Cnonce { get; private set; }
    public string Nonce { get; private set; }
    public string Realm { get; private set; }
    public string UserName { get; private set; }
    public string Uri { get; private set; }
    public string Response { get; private set; }
    public string Method { get; private set; }
    public string NounceCounter { get; private set; }

    // This property is used by the handler to generate a
    // nonce and get it ready to be packaged in the
    // WWW-Authenticate header, as part of 401 response
    public static Header UnauthorizedResponseHeader
    {
        get
        {
            return new Header()
            {
                Realm = "MyRealm",
                Nonce = WebApiDemo.Nonce.Generate()
            };
        }
    }

    public override string ToString()
    {
        StringBuilder header = new StringBuilder();
        header.AppendFormat("realm=\"{0}\"", Realm);
        header.AppendFormat(",nonce=\"{0}\"", Nonce);
        header.AppendFormat(",qop=\"{0}\"", "auth");
        return header.ToString();
    }
}
 nonce class

 

public class Nonce
{
    private static ConcurrentDictionary<string, Tuple<int, DateTime>>
    nonces = new ConcurrentDictionary<string, Tuple<int, DateTime>>();

    public static string Generate()
    {
        byte[] bytes = new byte[16];

        using (var rngProvider = new RNGCryptoServiceProvider())
        {
            rngProvider.GetBytes(bytes);
        }

        string nonce = bytes.ToMD5Hash();

        nonces.TryAdd(nonce, new Tuple<int, DateTime>(0, DateTime.Now.AddMinutes(10)));

        return nonce;
    }

    public static bool IsValid(string nonce, string nonceCount)
    {
        Tuple<int, DateTime> cachedNonce = null;
        //nonces.TryGetValue(nonce, out cachedNonce);
        nonces.TryRemove(nonce, out cachedNonce);//Each nonce is only allowed to be used once

        if (cachedNonce != null) // nonce is found
        {
            // nonce count is greater than the one in record
            if (Int32.Parse(nonceCount) > cachedNonce.Item1)
            {
                // nonce has not expired yet
                if (cachedNonce.Item2 > DateTime.Now)
                {
                    // update the dictionary to reflect the nonce count just received in this request
                    //nonces[nonce] = new Tuple<int, DateTime>(Int32.Parse(nonceCount), cachedNonce.Item2);

                    // Every thing looks ok - server nonce is fresh and nonce count seems to be
                    // incremented. Does not look like replay.
                    return true;
                }
                   
            }
        }

        return false;
    }
}
 If you need to use digest authentication, you can add Attribute [Authorize] to the code, such as:
[Authorize]
public class ProductsController : ApiController
 Finally, you need to register in Global.asax
GlobalConfiguration.Configuration.MessageHandlers.Add(
new AuthenticationHandler());
  3. Client call here mainly describes the use of WebClient call
public static string Request(string sUrl, string sMethod, string sEntity, string sContentType,
    out string sMessage)
{
    try
    {
        sMessage = "";
        using (System.Net.WebClient client = new System.Net.WebClient())
        {
            client.Credentials = CreateAuthenticateValue(sUrl);
            client.Headers = CreateHeader(sContentType);

            Uri url = new Uri (sUrl);
            byte[] bytes = Encoding.UTF8.GetBytes(sEntity);
            byte[] buffer;
            switch (sMethod.ToUpper())
            {
                case "GET":
                    buffer = client.DownloadData(url);
                    break;
                case "POST":
                    buffer = client.UploadData(url, "POST", bytes);
                    break;
                default:
                    buffer = client.UploadData(url, "POST", bytes);
                    break;
            }

            return Encoding.UTF8.GetString(buffer);
        }
    }
    catch (WebException ex)
    {
        sMessage = ex.Message;
        var rsp = ex.Response as HttpWebResponse;
        var httpStatusCode = rsp.StatusCode;
        var authenticate = rsp.Headers.Get("WWW-Authenticate");

        return "";
    }
    catch (Exception ex)
    {
        sMessage = ex.Message;
        return "";
    }
}
 The key code, add user authentication here, use NetworkCredential
private static CredentialCache CreateAuthenticateValue(string sUrl)
{
    CredentialCache credentialCache = new CredentialCache();
    credentialCache.Add(new Uri(sUrl), "Digest", new NetworkCredential("Lime", "Lime"));

    return credentialCache;
}
 So far the whole authentication is ok.

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=326401510&siteId=291194637