如何使用客户端证书在 Web API 中进行身份验证和授权 [英] How to use a client certificate to authenticate and authorize in a Web API

查看:31
本文介绍了如何使用客户端证书在 Web API 中进行身份验证和授权的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在尝试使用客户端证书对使用 Web API 的设备进行身份验证和授权,并开发了一个简单的概念验证来解决潜在解决方案的问题.我遇到了 Web 应用程序未收到客户端证书的问题.许多人报告了这个问题,

这是发送带有客户端证书的请求的客户端的代码.这是一个控制台应用程序.

 私有静态异步任务 SendRequestUsingHttpClient(){WebRequestHandler handler = new WebRequestHandler();X509Certificate 证书 = GetCert("ClientCertificate.cer");handler.ClientCertificates.Add(certificate);handler.ServerCertificateValidationCallback = new RemoteCertificateValidationCallback(ValidateServerCertificate);handler.ClientCertificateOptions = ClientCertificateOption.Manual;使用 (var client = new HttpClient(handler)){client.BaseAddress = new Uri("https://localhost:44398/");client.DefaultRequestHeaders.Accept.Clear();client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));HttpResponseMessage response = await client.GetAsync("api/Secure/1");如果(响应.IsSuccessStatusCode){字符串内容 = 等待 response.Content.ReadAsStringAsync();Console.WriteLine("收到回复:{0}",content);}别的{Console.WriteLine("错误,收到状态码 {0}: {1}", response.StatusCode, response.ReasonPhrase);}}}公共静态布尔验证服务器证书(对象发送者,X509证书证书,X509链条链条,SslPolicyErrors sslPolicyErrors){Console.WriteLine("验证证书{0}", certificate.Issuer);如果(sslPolicyErrors == SslPolicyErrors.None)返回真;Console.WriteLine("证书错误:{0}", sslPolicyErrors);//不允许此客户端与未经身份验证的服务器通信.返回假;}

当我运行这个测试应用程序时,我得到一个 403 Forbidden 状态代码,原因短语是需要客户端证书",表明它正在进入我的 RequireHttpsAttribute 并且没有找到任何客户端证书.通过调试器运行它,我已经验证证书正在加载并添加到 WebRequestHandler.证书将导出到正在加载的 CER 文件中.带有私钥的完整证书位于 Web 应用程序服务器的本地计算机的个人和可信根存储中.对于此测试,客户端和 Web 应用程序在同一台机器上运行.

我可以使用 Fiddler 调用这个 Web API 方法,附加相同的客户端证书,它工作正常.使用 Fiddler 时,它通过了 RequireHttpsAttribute 中的测试并返回成功状态代码 200 并返回预期值.

有没有人遇到过同样的问题,即 HttpClient 没有在请求中发送客户端证书并找到了解决方案?

更新 1:

我还尝试从包含私钥的证书存储中获取证书.这是我检索它的方式:

 private static X509Certificate2 GetCert2(string hostname){X509Store myX509Store = new X509Store(StoreName.My, StoreLocation.LocalMachine);myX509Store.Open(OpenFlags.ReadWrite);X509Certificate2 myCertificate = myX509Store.Certificates.OfType().FirstOrDefault(cert => cert.GetNameInfo(X509NameType.SimpleName, false) == hostname);返回我的证书;}

我确认此证书已正确检索,并且已添加到客户端证书集合中.但是我得到了相同的结果,其中服务器代码不检索任何客户端证书.

为了完整起见,这里是用于从文件中检索证书的代码:

 private static X509Certificate GetCert(string filename){X509Certificate Cert = X509Certificate.CreateFromCertFile(filename);返回证书;}

您会注意到,当您从文件中获取证书时,它返回一个类型为 X509Certificate 的对象,而当您从证书存储中检索它时,它的类型为 X509Certificate2.X509CertificateCollection.Add 方法需要 X509Certificate 类型.

更新 2:我仍在试图弄清楚这一点,并尝试了许多不同的选择,但无济于事.

  • 我将 Web 应用程序更改为在主机名而不是本地主机上运行.
  • 我将 Web 应用程序设置为需要 SSL
  • 我确认证书是为客户端身份验证设置的,并且它位于受信任的根目录中
  • 除了在 Fiddler 中测试客户端证书之外,我还在 Chrome 中对其进行了验证.

在尝试这些选项的过程中,它开始起作用了.然后我开始撤回更改以查看是什么导致它起作用.它继续工作.然后我尝试从受信任的根中删除证书以验证这是必需的,并且它停止工作,现在即使我将证书放回受信任的根中,我也无法让它恢复工作.现在 Chrome 甚至不会提示我输入类似它使用的证书,它在 Chrome 中失败,但在 Fiddler 中仍然有效.一定是我遗漏了一些神奇的配置.

我还尝试在绑定中启用协商客户端证书",但 Chrome 仍然不会提示我输入客户端证书.这是使用netsh http show sslcert"的设置

 IP:port : 0.0.0.0:44398证书哈希:429e090db21e14344aa5d75d25074712f120f65f应用程序 ID:{4dc3e181-e14b-4a21-b022-59fc669b0914}证书商店名称 : MY验证客户端证书吊销:已禁用仅使用缓存的客户端证书验证吊销:已禁用使用情况检查:已启用撤销新鲜时间:0URL 检索超时:0Ctl 标识符:(空)Ctl 商店名称:(空)DS 映射器用法:已禁用协商客户端证书:已启用

这是我正在使用的客户端证书:

我对问题所在感到困惑.我正在为任何可以帮助我解决这个问题的人增加赏金.

解决方案

跟踪帮助我找到了问题所在(感谢 Fabian 的建议).我发现通过进一步的测试,我可以让客户端证书在另一台服务器(Windows Server 2012)上工作.我在我的开发机器(Window 7)上测试这个,所以我可以调试这个过程.因此,通过将跟踪与运行的 IIS 服务器和未运行的 IIS 服务器进行比较,我能够查明跟踪日志中的相关行.这是客户端证书工作的日志的一部分.这是发送前的设置

System.Net Information: 0 : [17444] InitializeSecurityContext(In-Buffers count=2, Out-Buffer length=0,返回的代码=CredentialsNeeded).System.Net 信息:0:[17444] SecureChannel#54718731 - 我们有用户提供的证书.服务器尚未指定任何颁发者,因此请尝试所有证书.System.Net 信息:0:[17444] SecureChannel#54718731 - 选择的证书:

这是客户端证书失败的机器上的跟踪日志的样子.

System.Net Information: 0 : [19616] InitializeSecurityContext(In-Buffers count=2, Out-Buffer length=0,返回的代码=CredentialsNeeded).System.Net 信息:0:[19616] SecureChannel#54718731 - 我们有用户提供的证书.服务器已指定 137 个发行者.寻找与任何发行者匹配的证书.System.Net 信息:0:[19616] SecureChannel#54718731 - 剩下 0 个客户端证书可供选择.System.Net 信息:0:[19616] 使用缓存的凭据句柄.

关注表明服务器指定了 137 个发行者的行,我发现了这个 与我的问题相似的问答.我的解决方案不是标记为答案的解决方案,因为我的证书位于受信任的根中.答案是它下面的,您可以在其中更新注册表.我刚刚将该值添加到注册表项中.

HKEY_LOCAL_MACHINESYSTEMCurrentControlSetControlSecurityProvidersSCHANNEL

值名称:SendTrustedIssuerList 值类型:REG_DWORD 值数据:0 (False)

将此值添加到注册表后,它开始在我的 Windows 7 机器上运行.这似乎是 Windows 7 的问题.

I am trying to use a client certificate to authenticate and authorize devices using a Web API and developed a simple proof of concept to work through issues with the potential solution. I am running into an issue where the client certificate is not being received by the web application. A number of people of reported this issue, including in this Q&A, but none of them have an answer. My hope is to provide more detail to revive this issue and hopefully get an answer for my issue. I am open to other solutions. The main requirement is that a standalone process written in C# can call a Web API and be authenticated using a client certificate.

The Web API in this POC is very simple and just returns a single value. It uses an attribute to validate that HTTPS is used and that a client certificate is present.

public class SecureController : ApiController
{
    [RequireHttps]
    public string Get(int id)
    {
        return "value";
    }

}

Here is the code for the RequireHttpsAttribute:

public class RequireHttpsAttribute : AuthorizationFilterAttribute 
{ 
    public override void OnAuthorization(HttpActionContext actionContext) 
    { 
        if (actionContext.Request.RequestUri.Scheme != Uri.UriSchemeHttps) 
        { 
            actionContext.Response = new HttpResponseMessage(System.Net.HttpStatusCode.Forbidden) 
            { 
                ReasonPhrase = "HTTPS Required" 
            }; 
        } 
        else 
        {
            var cert = actionContext.Request.GetClientCertificate();
            if (cert == null)
            {
                actionContext.Response = new HttpResponseMessage(System.Net.HttpStatusCode.Forbidden)
                {
                    ReasonPhrase = "Client Certificate Required"
                }; 

            }
            base.OnAuthorization(actionContext); 
        } 
    } 
}

In this POC I am just checking for the availability of the client certificate. Once this is working I can add checks for information in the certificate to validate against a list of certificates.

Here are the setting in IIS for SSL for this web application.

Here is the code for the client that sends the request with a client certificate. This is a console application.

    private static async Task SendRequestUsingHttpClient()
    {
        WebRequestHandler handler = new WebRequestHandler();
        X509Certificate certificate = GetCert("ClientCertificate.cer");
        handler.ClientCertificates.Add(certificate);
        handler.ServerCertificateValidationCallback = new RemoteCertificateValidationCallback(ValidateServerCertificate);
        handler.ClientCertificateOptions = ClientCertificateOption.Manual;
        using (var client = new HttpClient(handler))
        {
            client.BaseAddress = new Uri("https://localhost:44398/");
            client.DefaultRequestHeaders.Accept.Clear();
            client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

            HttpResponseMessage response = await client.GetAsync("api/Secure/1");
            if (response.IsSuccessStatusCode)
            {
                string content = await response.Content.ReadAsStringAsync();
                Console.WriteLine("Received response: {0}",content);
            }
            else
            {
                Console.WriteLine("Error, received status code {0}: {1}", response.StatusCode, response.ReasonPhrase);
            }
        }
    }

    public static bool ValidateServerCertificate(
      object sender,
      X509Certificate certificate,
      X509Chain chain,
      SslPolicyErrors sslPolicyErrors)
    {
        Console.WriteLine("Validating certificate {0}", certificate.Issuer);
        if (sslPolicyErrors == SslPolicyErrors.None)
            return true;

        Console.WriteLine("Certificate error: {0}", sslPolicyErrors);

        // Do not allow this client to communicate with unauthenticated servers.
        return false;
    }

When I run this test app I get back a status code of 403 Forbidden with a reason phrase of "Client Certificate Required" indicating that it is getting into my RequireHttpsAttribute and it is not finding any client certificates. Running this through a debugger I have verified that the certificate is getting loaded and added to the WebRequestHandler. The certificate is exported into a CER file that is being loaded. The full certificate with the private key is located on the Local Machine’s Personal and Trusted Root stores for the web application server. For this test the client and web application are being run on the same machine.

I can call this Web API method using Fiddler, attaching the same client certificate, and it works fine. When using Fiddler it passes the tests in RequireHttpsAttribute and returns a successful status code of 200 and returns the expected value.

Has anybody run into the same issue where HttpClient does not send a client certificate in the request and found a solution?

Update 1:

I also tried getting the certificate from the certificate store that includes the private key. Here is how I retrieved it:

    private static X509Certificate2 GetCert2(string hostname)
    {
        X509Store myX509Store = new X509Store(StoreName.My, StoreLocation.LocalMachine);
        myX509Store.Open(OpenFlags.ReadWrite);
        X509Certificate2 myCertificate = myX509Store.Certificates.OfType<X509Certificate2>().FirstOrDefault(cert => cert.GetNameInfo(X509NameType.SimpleName, false) == hostname);
        return myCertificate;
    }

I verified that this certificate was getting retrieved correctly and it was being added to the client certificate collection. But I got the same results where the server code does not retrieve any client certificates.

For completeness here is the code used to retrieve the certificate from a file:

    private static X509Certificate GetCert(string filename)
    {
        X509Certificate Cert = X509Certificate.CreateFromCertFile(filename);
        return Cert;

    }

You will notice that when you get the certificate from a file it returns an object of type X509Certificate and when you retrieve it from the certificate store it is of type X509Certificate2. The X509CertificateCollection.Add Method is expecting a type of X509Certificate.

Update 2: I am still trying to figure this out and have tried many different options but to no avail.

  • I changed the web application to run on a host name instead of local host.
  • I set the web application to require SSL
  • I verified that the certificate was set for Client Authentication and that it is in the trusted root
  • Besides testing the client certificate in Fiddler I also validated it in Chrome.

At one point during trying these options it started working. Then I started backing out changes to see what caused it to work. It continued to work. Then I tried removing the certificate from the trusted root to validate that this was required and it stopped working and now I cannot get it back to working even though I put the certificate back in the trusted root. Now Chrome will not even prompt me for a certificate like it used too and it fails in Chrome, but still works in Fiddler. There must be some magic configuration I am missing.

I also tried enabling "Negotiate Client Certificate" in the binding but Chrome still will not prompt me for a client certificate. Here is the settings using "netsh http show sslcert"

 IP:port                 : 0.0.0.0:44398
 Certificate Hash        : 429e090db21e14344aa5d75d25074712f120f65f
 Application ID          : {4dc3e181-e14b-4a21-b022-59fc669b0914}
 Certificate Store Name  : MY
 Verify Client Certificate Revocation    : Disabled
 Verify Revocation Using Cached Client Certificate Only    : Disabled
 Usage Check    : Enabled
 Revocation Freshness Time : 0
 URL Retrieval Timeout   : 0
 Ctl Identifier          : (null)
 Ctl Store Name          : (null)
 DS Mapper Usage    : Disabled
 Negotiate Client Certificate    : Enabled

Here is the client certificate I am using:

I am baffled as to what the issue is. I am adding a bounty for anyone that can help me figure this out.

解决方案

Tracing helped me find what the problem was (Thank you Fabian for that suggestion). I found with further testing that I could get the client certificate to work on another server (Windows Server 2012). I was testing this on my development machine (Window 7) so I could debug this process. So by comparing the trace to an IIS Server that worked and one that did not I was able to pinpoint the relevant lines in the trace log. Here is a portion of a log where the client certificate worked. This is the setup right before the send

System.Net Information: 0 : [17444] InitializeSecurityContext(In-Buffers count=2, Out-Buffer length=0, returned code=CredentialsNeeded).
System.Net Information: 0 : [17444] SecureChannel#54718731 - We have user-provided certificates. The server has not specified any issuers, so try all the certificates.
System.Net Information: 0 : [17444] SecureChannel#54718731 - Selected certificate:

Here is what the trace log looked like on the machine where the client certificate failed.

System.Net Information: 0 : [19616] InitializeSecurityContext(In-Buffers count=2, Out-Buffer length=0, returned code=CredentialsNeeded).
System.Net Information: 0 : [19616] SecureChannel#54718731 - We have user-provided certificates. The server has specified 137 issuer(s). Looking for certificates that match any of the issuers.
System.Net Information: 0 : [19616] SecureChannel#54718731 - Left with 0 client certificates to choose from.
System.Net Information: 0 : [19616] Using the cached credential handle.

Focusing on the line that indicated the server specified 137 issuers I found this Q&A that seemed similar to my issue. The solution for me was not the one marked as an answer since my certificate was in the trusted root. The answer is the one under it where you update the registry. I just added the value to the registry key.

HKEY_LOCAL_MACHINESYSTEMCurrentControlSetControlSecurityProvidersSCHANNEL

Value name: SendTrustedIssuerList Value type: REG_DWORD Value data: 0 (False)

After adding this value to the registry it started to work on my Windows 7 machine. This appears to be a Windows 7 issue.

这篇关于如何使用客户端证书在 Web API 中进行身份验证和授权的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆