问题
I have created CertificateTestController and ValuesController from this example How to use a client certificate to authenticate and authorize in a Web API. If you scroll down to "Update" from user Ogglas. I have taken his example and gotten "CertificateTestController" to work where I can grab the Certificate from my store and add it to the "handler". When I call "ValuesController", there is no cert being initialized by
X509Certificate2 cert = actionContext.Request.GetClientCertificate();
Here is the complete code that I have
ValuesController
{
[RequireSpecificCert]
public class ValuesController : ApiController
{
// GET api/values
public IHttpActionResult Get()
{
return Ok("It works!");
}
public class RequireSpecificCertAttribute : 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
{
X509Certificate2 cert = actionContext.Request.GetClientCertificate();
X509Certificate2 cert2 = actionContext.RequestContext.ClientCertificate;
if (cert == null)
{
actionContext.Response = new HttpResponseMessage(System.Net.HttpStatusCode.Forbidden)
{
ReasonPhrase = "Client Certificate Required"
};
}
else
{
X509Chain chain = new X509Chain();
//Needed because the error "The revocation function was unable to check revocation for the certificate" happened to me otherwise
chain.ChainPolicy = new X509ChainPolicy()
{
RevocationMode = X509RevocationMode.NoCheck,
};
try
{
var chainBuilt = chain.Build(cert);
Debug.WriteLine(string.Format("Chain building status: {0}", chainBuilt));
var validCert = CheckCertificate(chain, cert);
if (chainBuilt == false || validCert == false)
{
actionContext.Response = new HttpResponseMessage(System.Net.HttpStatusCode.Forbidden)
{
ReasonPhrase = "Client Certificate not valid"
};
foreach (X509ChainStatus chainStatus in chain.ChainStatus)
{
Debug.WriteLine(string.Format("Chain error: {0} {1}", chainStatus.Status, chainStatus.StatusInformation));
}
}
}
catch (Exception ex)
{
Debug.WriteLine(ex.ToString());
}
}
base.OnAuthorization(actionContext);
}
}
private bool CheckCertificate(X509Chain chain, X509Certificate2 cert)
{
var rootThumbprint = WebConfigurationManager.AppSettings["rootThumbprint"].ToUpper().Replace(" ", string.Empty);
var clientThumbprint = WebConfigurationManager.AppSettings["clientThumbprint"].ToUpper().Replace(" ", string.Empty);
//Check that the certificate have been issued by a specific Root Certificate
var validRoot = chain.ChainElements.Cast<X509ChainElement>().Any(x => x.Certificate.Thumbprint.Equals(rootThumbprint, StringComparison.InvariantCultureIgnoreCase));
//Check that the certificate thumbprint matches our expected thumbprint
var validCert = cert.Thumbprint.Equals(clientThumbprint, StringComparison.InvariantCultureIgnoreCase);
return validRoot && validCert;
}
}
calling above ValuesController with below CertificateTestController
{
[RoutePrefix("api/certificatetest")]
public class CertificateTestController : ApiController
{
public IHttpActionResult Get()
{
var handler = new WebRequestHandler();
handler.ClientCertificateOptions = ClientCertificateOption.Manual;
handler.ClientCertificates.Add(GetClientCert());
handler.UseProxy = false;
var client = new HttpClient(handler);
var result = client.GetAsync("https://localhost:44301//values").GetAwaiter().GetResult();
var resultString = result.Content.ReadAsStringAsync().GetAwaiter().GetResult();
return Ok(resultString);
}
private static X509Certificate GetClientCert()
{
X509Store store = null;
try
{
store = new X509Store(StoreName.My, StoreLocation.LocalMachine);
store.Open(OpenFlags.OpenExistingOnly | OpenFlags.ReadOnly);
var certificateSerialNumber = "2bc034466b6960d2fee84d86e6c2532a".ToUpper().Replace(" ", string.Empty);
var cert = store.Certificates.Cast<X509Certificate>().FirstOrDefault(x => x.GetSerialNumberString().Equals(certificateSerialNumber, StringComparison.InvariantCultureIgnoreCase));
return cert;
}
finally
{
store.Close();
}
}
}
}
Please help!
回答1:
Here are the issues/problems I answered along the way when I was trying to fix this issue.
Q1 .
Why wasn't my certificate getting to the client side (code)?
A1.
VS does a initial SSL negotiation that happens before the OnAuthorization(HttpActionContext actionContext) is even hit. At that point, server searches for a client certificate in the certificate store that has a private key installed to verify. If there is no private key then it fails. I found the issue by turning on the verbose. Please see below.
Here are configurations that need to be changes in order to get this working.
1. Web Config file changes
• SSL Negotiation
Below configuration tells visual studio that there is a ssl negotiation expected for “api/values” URL. This helps us limit when and where is certificate negotiation is expected. Location path property
<location path="api/values">
<system.webServer>
<security>
<access sslFlags="SslNegotiateCert" />
<authentication>
<iisClientCertificateMappingAuthentication enabled="true">
</iisClientCertificateMappingAuthentication>
</authentication>
</security>
</system.webServer>
</location>
• Certificate Verbose
Below diagnostics helps us during troubleshooting and spits out any issues that might be causing with certificates validation.
<system.diagnostics>
<trace autoflush="true" />
<sources>
<source name="System.Net">
<listeners>
<add name="System.Net"/>
</listeners>
</source>
<source name="System.Net.HttpListener">
<listeners>
<add name="System.Net"/>
</listeners>
</source>
<source name="System.Net.Sockets">
<listeners>
<add name="System.Net"/>
</listeners>
</source>
<source name="System.Net.Cache">
<listeners>
<add name="System.Net"/>
</listeners>
</source>
</sources>
<sharedListeners>
<add name="System.Net"
type="System.Diagnostics.TextWriterTraceListener"
initializeData="System.Net.trace.log"
traceOutputOptions = "ProcessId, DateTime"/>
</sharedListeners>
<switches>
<add name="System.Net" value="Verbose" />
<add name="System.Net.Sockets" value="Verbose" />
<add name="System.Net.Cache" value="Verbose" />
<add name="System.Net.HttpListener" value="Verbose" />
</switches>
</system.diagnostics>
• App settings
Change rootThumbprint value to whatever server certificates’s thumbprint is and clientThumprint would be whatever client certificate thumbprint is. ceritificateSerialNumber should be outgoing certificate serial number.
<appSettings>
<add key="webpages:Version" value="3.0.0.0" />
<add key="webpages:Enabled" value="false" />
<add key="ClientValidationEnabled" value="true" />
<add key="UnobtrusiveJavaScriptEnabled" value="true" />
<add key="rootThumbprint" value="change"/>
<add key="clientThumbprint" value="change"/>
<add key="certificateSerialNumber" value="change"/>
</appSettings>
2. Local Visual Studio and IIS express
1) Applicationhost.config
Path
Visual studio earlier than 2015 but after 2008
Location --> c:\Users\e#\Documents\IISExpress\Config
Visual Studio 2015 & 2017
{project_name}.vs\config\config.host.config
Changes
Make sure below properties are set to “Allow”
2) Enable SSL
Enable SSL in the visual studio properties window . this is important because URL changes from http to https.
3) WebApiConfig.cs
Create a class filters folder. Name it appropriately. Certificate validation needs to be the very first check before any of the other code is ran. This is where WebApiConfig.cs comes in handy. For example I called my class RequireHttpsAttribute. In order to run the check just put the below line in WebApiConfig.cs config.Filters.Add(new RequireHttpsAttribute());
4) CertificateTestController.cs
This is class acts as a client. This class is used to attach the cert with the request and send it out. There is one change in this class. This is was only tested locally on the computer. var result = client.GetAsync("https://localhost:44300//api//values").GetAwaiter().GetResult(); Change the URL. We are attaching the certificate based off of serial number below. Certificate can also be attached based off of Thumprint, Subject, Expiration/Revocation, Chain Validation etc.
var certificateSerialNumber = WebConfigurationManager.AppSettings["certificateSerialNumber"].ToUpper().Replace(" ", string.Empty);
5) ValueController.cs
This class acts as a server. This is where the certificate validation happens. There are two changes in “CheckCertificate” method/function that refers to the webApiConfig.cs. if you followed App Settings changes from above under WebApiConfig.cs then you are good to go.
3. Files
1) CertificateTestController.cs
2) ValuesController.cs
来源:https://stackoverflow.com/questions/54948695/no-certificate-in-onauthorizationhttpactioncontext-actioncontext