Required Client Certificate asp.net core web.api - c#

I want to implement client certificate authentication in my web api. I followed the MSDN documentation and tried other examples on the web. Unfortunately I can't get it to work.
The page can be reached with the certificate mode "noCertificate". But when I implement the following line I get this error.
opt.ClientCertificateMode = ClientCertificateMode.RequireCertificate;
Here is my code. Maybe someone can spot my mistake. Actually, I would expect the browser to open the certificate selection window.
Programm.cs
using Microsoft.AspNetCore.Authentication.Certificate;
using Microsoft.AspNetCore.Server.Kestrel.Https;
using System.Security.Cryptography.X509Certificates;
using TestClientCert.Validator;
var builder = WebApplication.CreateBuilder(args);
builder.WebHost.UseKestrel(options =>
{
options.ConfigureHttpsDefaults(opt =>
{
opt.ClientCertificateMode = ClientCertificateMode.RequireCertificate;
});
});
// Add services to the container.
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddTransient<MyCertificateValidationService>();
builder.Services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme)
.AddCertificate(options =>
{
options.RevocationMode = X509RevocationMode.NoCheck;
options.AllowedCertificateTypes = CertificateTypes.All;
options.Events = new CertificateAuthenticationEvents
{
OnCertificateValidated = context =>
{
var validationService = context.HttpContext.RequestServices.GetService<MyCertificateValidationService>();
if (validationService.ValidateCertificate(context.ClientCertificate))
{
context.Success();
}
else
{
context.Fail("invalid cert");
}
return Task.CompletedTask;
},
OnAuthenticationFailed = context =>
{
context.Fail("invalid cert");
return Task.CompletedTask;
}
};
});
builder.Services.AddAuthorization();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
MyCertificateValidationService.cs
using System.Security.Cryptography.X509Certificates;
namespace TestClientCert.Validator
{
public class MyCertificateValidationService
{
public bool ValidateCertificate(X509Certificate2 clientCertificate)
{
string[] allowedThumbprints = { "B30D884E44EC218513CF2A5CA246F0AFA1DD8E9B", "6ECB2E563B9129C72215EE00686CAA95FBC5BEC6" };
if (allowedThumbprints.Contains(clientCertificate.Thumbprint))
{
return true;
}
return false;
}
}
}
HomeController.cs
using Microsoft.AspNetCore.Mvc;
namespace TestClientCert.Controllers
{
[ApiController]
[Route("[controller]")]
public class HomeController : Controller
{
[HttpGet]
public string Get() => "Welcome to Narnia";
}
}

I would expect the browser to open the certificate selection window.
I test your code, you maybe need to use PowerShell Commands to create Certificates and add Root CA to CertMgr and add Child Certificate in Chrome Browser.
Step1 Use PowerShell Commands to create Certificates
Certificate Authentication requires 2 types of Certificates, these are:Certification Authority (CA) and Child Certificate.
1.Creating Certification Authority (CA) in PowerShell
First open the PowerShell as an adminstrator. Then run the following 3 commands one by one:
Command 1: Create Self Signed Certificate
New-SelfSignedCertificate -DnsName "localhost", "localhost" -CertStoreLocation "cert:\LocalMachine\My" -NotAfter (Get-Date).AddYears(20) -FriendlyName "Rlocalhost" -KeyUsageProperty All -KeyUsage CertSign, CRLSign, DigitalSignature
This command will create the self-signed Certificate Authority and provide it’s thumbprint. Keep this thumbprint safe as we will use it to create child certificate.
Command 2: Set the password for the Certificate(I am keeping the password 1234.)
$mypwd = ConvertTo-SecureString -String "1234" -Force -AsPlainText
Command 3: Export the Certificate in a PFX file
We will now use the password which we set earlier and use it along with the thumbprint to export the certificate in a .pfx file.
Run the below command but before that change the text "thumbprint" with the thumbprint which you got earlier.
Get-ChildItem -Path cert:\localMachine\my\"thumbprint" | Export-PfxCertificate -FilePath D:\root.pfx -Password $mypwd
The command will export the certificate authority file called root.pfx on the “D” drive.(Note: you can use your other drive too, ex: C:\root.pfx.)
2.Creating Child Certificate in PowerShell
Let us create Child Certificate from root CA. So run the following 4 commands
one by one. Note that:
In the first command change the text"ca thumbprint" with the thumbprint of root certificate which you got earlier.
After running the second command you will get the thumbprint of the child certificate. You have to change the text "thumbprint" in the fourth command with this thumbprint. These 4 commands are given below.
$rootcert = ( Get-ChildItem -Path cert:\LocalMachine\My\"ca thumbprint" )
New-SelfSignedCertificate -certstorelocation cert:\localmachine\my -dnsname "localhost" -Signer $rootcert -NotAfter (Get-Date).AddYears(20) -FriendlyName "Clocalhost"
$mypwd = ConvertTo-SecureString -String "1234" -Force -AsPlainText
Get-ChildItem -Path cert:\localMachine\my\"thumbprint" | Export-PfxCertificate -FilePath D:\child.pfx -Password $mypwd
The child certificate file called child.pfx will be created on the “D” drive.In the following image I have shown both the certificate files that are created on my D drive.
Step2 Add Root CA to CertMgr
Windows uses a utility called CertMgr to manage certificates. Search for “CertMgr” or “Manage Computer Certificates” in the windows search to open this utility. Next, right click on the “Certificates” folder which is under the “Trusted Root Certification Authorities” and select All Tasks ➤ Import and browse to the root.pfx file on the D drive.
Click on the Next button to reach the next screen where you are asked to enter the private key (password). Recall we used “1234” as the password.Continue the process and your root CA certificate will be added.
Step3 Add Child Certificate in Chrome Browser
Now we will add the child certificate to chrome browser. So go to settings and then click the Security section under “Privacy and security”. Here you will find Now we will add the child certificate to chrome browser. So go to settings and then click the Security section under “Privacy and security”. Here you will find Manage device Certificates, click on it to open a dialog window.
In this window make sure you are on the “Personal” tab and then click the Import button. Simply import the child.pfx file. As previously, enter the password 1234 when asked.
Now once again run your app on Visual Studio and open the url of the web api. This time chrome will ask you to select the child certificate.
result:

Related

Cant access certificates in X509Store from asp.net core 2.1 with thumbprints

I have strange problem when accessing X509Store from IIS. I can't look them up.
If I access both the rp cert and ca cert from powershell both are there,
dir cert: -Recurse | Where-Object { $_.Thumbprint -like "thumprintstring" }
I have checked that the thumbprints don't have a hidden char in the beginning of thumbprint
I have set that the certificates are exportable when I install them
I have for the moment set it accessable for everyone(its a certificate to a test server) in certficate
store
This is code I use
StoreLocation location = certificateConfig.UseCurrentUserStoreLocation ? StoreLocation.CurrentUser : StoreLocation.LocalMachine;
using (var clientCertStore = new X509Store(StoreName.My, location))
{
clientCertStore.Open(OpenFlags.ReadOnly);
//Search for the client cert
X509Certificate2 rpCert = GetCertByThumbprint(clientCertStore, certificateConfig.RpCertThumbprint);
if (rpCert == null)
{
throw new InvalidOperationException("No rp cert found for specified thumbprint #" + certificateConfig.RpCertThumbprint +"# "+location);
}
ClientCertificates.Add(rpCert);
}
<snip>
private X509Certificate2 GetCertByThumbprint(X509Store certStore, string thumbprint)
{
var certs = certStore.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, false);
return certs.Count > 0 ? certs[0] : null;
}
The rpcert is always null whatever i try.
Do I need another way to open up the store from IIS?
Any ideas or suggestions? What am I missing?
The problem was not what I expected. The config read from enviromentvariables that had been deleted so they didnt show in enviromentvariables and the server had not been restarted. And the deleted ones had most likely the bad character infront of the thumbprint.
Restarting iis doesn't solve this since the network service account doesnt reread these when already loggedon.
Follow up question: Is possible to relogin in network service account without restarting the server?

identity server4 getting "invalid_grant" error for the connect/token endpoint when deployed to production

I use Identityserver4 to implement OAUTH2 and the server supports ResourceOwnerPassword and code flow. I use AWS's EC2 to host the app for production.
The weird thing is even the app runs perfectly fine in my dev machine, after deployed to AWS, I keep getting this invalid_grant and I do not know what goes wrong.
Here is my code:
services.AddIdentityServer()
//.AddDeveloperSigningCredential()
.AddSigningCredential(Certificate.GetCertificate())
.AddInMemoryIdentityResources(Config.GetIdentityResources())
.AddInMemoryApiResources(Config.GetApiResources())
.AddInMemoryClients(Config.GetClients())
.AddTestUsers(Config.GetUsers());
new Client
{
ClientId = "client",
ClientName = "client",
ClientSecrets =
{
new Secret("secret".Sha256())
},
RequireClientSecret = false,
RedirectUris = new List<string>(new string[] { "https://www.getpostman.com/oauth2/callback", "http://localhost:8002", "http://192.168.1.5:8002","app.buyingagent:/oauthredirect"}),
AllowedGrantTypes = GrantTypes.Code,
//RequirePkce = true,
AllowedScopes = { "api" },
AllowOfflineAccess = true
}
public static X509Certificate2 GetCertificate()
{
using (var store = new X509Store(StoreName.Root, StoreLocation.LocalMachine))
{
store.Open(OpenFlags.OpenExistingOnly);
var certs = store.Certificates.Find(X509FindType.FindBySubjectName, "cert", false);
return certs.Count > 0 ? certs[0] : null;
}
}
I understand it is not a good practice to save the information in memory, but I just want to get the proof of concept first. For the x509 certificate which is passed to AddSigningCredential for signing token, I created a self-singing certificate in my local machine using makecert and then export it to the trusted store in AWS via RDP. (makecert does not seems avalible in AWS's command line)
I used this command:
makecert -pe -ss MY -$ individual -n "CN=cert" -len 2048 -r
The app runs find locally but in production I keep getting this "invalid_grant" error. (I use Postman to get token) I can visit the connect/authorize end point though(where I can enter client id and password)
The flow fails at connect/authorize end point.
The error message is like this:
POST http://{url}/connect/token
Request Headers:
undefined:undefined
Request Body:
grant_type:"authorization_code"
code:"7cb58d345975af02332f2b67cb71958ba0a48c391e34edabd0d9dd1500e3f24e"
redirect_uri:"https://www.getpostman.com/oauth2/callback"
client_id:"client"
Response Headers:
undefined:undefined
Response Body:
error:"invalid_grant"
invalid_grant
Error
I know the entered info(client id, redirect url) are all correct(all working fine locally) what could go wrong here once deployed to production? Is the certificate not trusted in production or I cannot use in-memory storage for the client and resource? I do not think it is due to the redirect_url because even if I use the password flow which does not even require a redirect_url it still fails in production.
Note: if remove this line AddSigningCredential(Certificate.GetCertificate())(pass no certificate to identityserver4) I would get this same "invalid_grant. So maybe the certificate imported from my dev machine to AWS is invalid?
After turning on the log,
the problem is keyset does not exist
The App has not permission to read the private key in the certificate. After adding the permission the problem is solved.
The initial invalid_grant is so misleading...
Had this problem when I, in my infinite wisdom, set accessToken expiry time equal to refreshToken expiry, and asked for refresh only when access expired.

Azure Function role like permissions to Stop Azure Virtual Machines

I'm hoping to manage some Azure resources using a scheduled C# Azure Function.
Currently in a command line application I've made, I've been using libraries 'Microsoft.IdentityModel.Clients.ActiveDirectory' for token authorization and 'Microsoft.Azure.Management.Compute' for client calls for resource management like so.
//... var credential generated my AD authentication and extending Microsoft.Rest.ServiceClientCredentials
using (var client = new ComputeManagementClient(credential)) {
client.SubscriptionId = "[SOME_SUBSCRIPTION_ID]";
client.VirtualMachines.BeginPowerOff("[RESOURCE_GROUP]", "[VM_NAME]");
}
Can my management client interact with Azure resources without providing a User Credential or Key-Secret like credential establishment?
My previous experience is related to AWS and admittedly it has confused my view of Azure Resource Management.
Older posts I've looked at are: Start and Stop Azure Virtual Machine
and
Is it possible to stop/start an Azure ARM Virtual from an Azure Function?
-EDIT 1-
I was hoping for something similar to run-time credentials in AWS resource clients for Lambda based on an assigned role with a variety of permissions. I will have a look at certificates though.
There are a few resources online on using C# to make REST API calls to start and stop a VM. Here's a link to such a document:
https://msftstack.wordpress.com/2016/01/03/how-to-call-the-azure-resource-manager-rest-api-from-c/
You could use the above as a reference to create C# Functions to start/stop your VM.
However, using C# to make these REST calls requires pre-packaging the HTTP request and post processing the HTTP response. If your use-case just calls for a start/stop VM, an easier approach would be use PowerShell in Azure Functions to call the Start-AzureRmVM and Stop-AzureRmVM cmdlets.
The following are steps on how to create HTTP-triggered PowerShell Functions to start and stop a VM:
Setup a service principal to obtain the username, password and tenant id. This initial setup may be considered tedious by some users, but since it's a one-time task, I feel that it is worth it to leverage running Azure PowerShell in Functions. There are many docs online, but here are some links to documents on how to setup your service principal:
i. http://blog.davidebbo.com/2014/12/azure-service-principal.html (I used this one)
ii. https://learn.microsoft.com/en-us/azure/azure-resource-manager/resource-group-create-service-principal-portal
Log into the Functions portal to access your Function app.
Click on Function app settings->Configure app settings and add the key-value pairs for the settings SP_USERNAME, SP_PASSWORD, and TENANTID (You may use other desired key names).
Create an HTTP-triggered PowerShell Function named, e.g. StartVm with the following content in its run.ps1 file.
$requestBody = Get-Content $req -Raw | ConvertFrom-Json
# Set Service Principal credentials
# SP_PASSWORD, SP_USERNAME, TENANTID are app settings
$secpasswd = ConvertTo-SecureString $env:SP_PASSWORD -AsPlainText -Force;
$mycreds = New-Object System.Management.Automation.PSCredential ($env:SP_USERNAME, $secpasswd)
Add-AzureRmAccount -ServicePrincipal -Tenant $env:TENANTID -Credential $mycreds;
$context = Get-AzureRmContext;
Set-AzureRmContext -Context $context;
# Start VM
Start-AzureRmVM -ResourceGroupName $requestBody.resourcegroup -Name $requestBody.vmname | Out-String
Click on the Save button.
Next, click on the Logs button to open the log viewer.
Click on the Test button to open the simple HTTP client. In the request body, provide the vmname and resourcegroup values for the VM, e.g.
{
"vmname": "testvm",
"resourcegroup": "testresourcegroup"
}
Click on the Run button and wait for a few seconds. It takes some time for the Start-AzureRmVM cmdlet to run to completion. When it does, you should see similar entries in the log viewer.
2016-11-30T07:11:26.479 Function started (Id=1e38ae2c-3cca-4e2f-a85d-f62c0d565c34)
2016-11-30T07:11:28.276 Microsoft.Azure.Commands.Profile.Models.PSAzureContext
2016-11-30T07:11:28.276 Microsoft.Azure.Commands.Profile.Models.PSAzureContext
2016-11-30T07:11:59.312 RequestId IsSuccessStatusCode StatusCode ReasonPhrase
--------- ------------------- ---------- ------------
True OK OK
2016-11-30T07:11:59.327 Function completed (Success, Id=1e38ae2c-3cca-4e2f-a85d-f62c0d565c34)
Repeat steps 4-8 to create the StopVm Function with the following content in its run.ps1 file. If the execution succeeds, the log output should be similar to the log entries for the StartVm Function.
$requestBody = Get-Content $req -Raw | ConvertFrom-Json
# Set Service Principal credentials
# SP_PASSWORD, SP_USERNAME, TENANTID are app settings
$secpasswd = ConvertTo-SecureString $env:SP_PASSWORD -AsPlainText -Force;
$mycreds = New-Object System.Management.Automation.PSCredential ($env:SP_USERNAME, $secpasswd)
Add-AzureRmAccount -ServicePrincipal -Tenant $env:TENANTID -Credential $mycreds;
$context = Get-AzureRmContext;
Set-AzureRmContext -Context $context;
# Stop VM
Stop-AzureRmVM -ResourceGroupName $requestBody.resourcegroup -Name $requestBody.vmname -Force | Out-String
When the StopVm Function execution succeeds, you may also add another GetVm Function with the following content in its run.ps1 file to verify that the VM has indeed been stopped.
$requestBody = Get-Content $req -Raw | ConvertFrom-Json
# Set Service Principal credentials
# SP_PASSWORD, SP_USERNAME, TENANTID are app settings
$secpasswd = ConvertTo-SecureString $env:SP_PASSWORD -AsPlainText -Force;
$mycreds = New-Object System.Management.Automation.PSCredential ($env:SP_USERNAME, $secpasswd)
Add-AzureRmAccount -ServicePrincipal -Tenant $env:TENANTID -Credential $mycreds;
$context = Get-AzureRmContext;
Set-AzureRmContext -Context $context;
# Get VM
Get-AzureRmVM -ResourceGroupName $requestBody.resourcegroup -Name $requestBody.vmname -Status | Out-String
The log entries for the GetVM Function on a stopped VM will would be similar to the following:
2016-11-30T07:53:59.956 Function started (Id=1841757f-bbb8-45cb-8777-80edb4e75ced)
2016-11-30T07:54:02.040 Microsoft.Azure.Commands.Profile.Models.PSAzureContext
2016-11-30T07:54:02.040 Microsoft.Azure.Commands.Profile.Models.PSAzureContext
2016-11-30T07:54:02.977 ResourceGroupName : testresourcegroup
Name : testvm
BootDiagnostics :
ConsoleScreenshotBlobUri : https://teststorage.blob.core.windows.net/boot
diagnostics-vmtest-[someguid]/testvm.[someguid].screenshot.bmp
Disks[0] :
Name : windowsvmosdisk
Statuses[0] :
Code : ProvisioningState/succeeded
Level : Info
DisplayStatus : Provisioning succeeded
Time : 11/30/2016 7:15:15 AM
Extensions[0] :
Name : BGInfo
VMAgent :
VmAgentVersion : Unknown
Statuses[0] :
Code : ProvisioningState/Unavailable
Level : Warning
DisplayStatus : Not Ready
Message : VM Agent is unresponsive.
Time : 11/30/2016 7:54:02 AM
Statuses[0] :
Code : ProvisioningState/succeeded
Level : Info
DisplayStatus : Provisioning succeeded
Time : 11/30/2016 7:15:15 AM
Statuses[1] :
Code : PowerState/deallocated
Level : Info
DisplayStatus : VM deallocated
2016-11-30T07:54:02.977 Function completed (Success, Id=1841757f-bbb8-45cb-8777-80edb4e75ced)
Note: FYI, while you may write a Function to create a VM by calling the New-AzureRmVM cmdlet, it will not run to completion in Azure Functions. VM creation in Azure Function's infrastructure seem to take ~9 mins to complete but a Function's execution is terminated at 5 minutes. You may write another script to poll the results separately. This limitation will be lifted when we start supporting custom configuration for maximum execution time in one of our upcoming releases.
--Update--
I just realized you were trying to create scheduled Functions. In that case, you can use Timer-triggered PowerShell Functions and hard-code the vmname and resourcegroup.
Well, I don't really understand how do you expect to authenticate without authenticating, I guess your only option would be certificates?
https://azure.microsoft.com/en-us/resources/samples/active-directory-dotnet-daemon-certificate-credential/

Reading all certificates from asp.net

I have a problem with reading certificates. I have a web service that has to get a certificate serial number using part of the subject. Everything works fine if I'm doing it from a form but when I try it from a web service it seems that it cannot find any certificate. I'm using this code to read all of the the certificates:
X509Store store = new X509Store();
store.Open(OpenFlags.ReadOnly);
if (args.Parameters["CertificateName"].ToString() != "")
{
foreach (X509Certificate2 mCert in store.Certificates)
{
if (mCert.Subject.Contains("OU=" + args.Parameters["CertificateName"].ToString()))
{
SerialNum = mCert.SerialNumber;
break;
}
}
if (SerialNum == String.Empty)
{
throw new Exception("Certificate not found with name: " + args.Parameters["CertificateName"].ToString() + " ;" + " OU=" + args.Parameters["CertificateName"]);
}
}
else
{
foreach (X509Certificate2 mCert in store.Certificates)
{
if (mCert.Subject.Contains("OU=Eua"))
{
SerialNum = mCert.SerialNumber;
break;
}
}
if (SerialNum == String.Empty)
{
throw new Exception("Haven't found default certificate ;");
}
}
store=null;
You are using the parameterless constructor for X509Store which according to the documentation will open the cert store for the current user. Well, the current user for your forms application is probably not the same as the current user for your web application, which most likely runs within an AppDomain configured to use a service account. So that means the web application won't be able to find it.
To fix this, you have two options
Option 1
First store your certificate in the machine store (not the user store). Then, in your code, open the store using a different constructor that lets you specify the store location, and specify that you want the machine store. Like this:
var store = new X509Store(StoreLocation.MachineStore);
Option 2
Maintain two copies of the certificate. Follow these steps:
Export the certificate from your current user's cert store
Start certificate manager using "RunAs" to impersonate the service account for the app domain, e.g. runas /user:MyDomain\MyServiceAccount "cmd /c start /B certmgr.msc". When prompted make sure you tell it you want to work with the current user's cert store, not the machine store.
Import the certificate there
Open the cert up and make sure its chain of trust is intact; if any intermediate or root certs are missing, you may have to import those as well.
Remember when this cert expires, you will have to replace both copies.

Digitally sign a Visual Studio 2012 VSIX extension

I am trying to sign a Visual Studio 2012 extension that is packaged as a VSIX file.
I have followed the instructions at http://www.jeff.wilcox.name/2010/03/vsixcodesigning/; however, I am interested in performing signing without specifying a pfx file and password.
For example, if I were to call 'signtool.exe', my command line would be:
"signtool.exe" sign /n MySubjectName /t 'http://timestamp.verisign.com/scripts/timstamp.dll' /d "MyDescription" MyPackage.vsix
I understand that this command does not work with VSIX files, though it does work for an MSI archive.
With this command, I do not need to specify a password or pfx file when calling signtool. The best installed certificate is selected, using the specified subject MySubjectName.
Following the code on Jeff's Blog, the signing step requires pfx file name and password to be defined to create the X509Certificate2 used in signing:
private static void SignAllParts(Package package, string pfx, string password, string timestamp){
var signatureManager = new PackageDigitalSignatureManager(package);
signatureManager.CertificateOption = CertificateEmbeddingOption.InSignaturePart;
/*...*/
signatureManager.Sign(toSign, new System.Security.Cryptography.X509Certificates.X509Certificate2(pfx, password));
}
Is there any API involving PackageDigitalSignatureManager that might let me find a X509Certificate based on MySubjectName so that I can sign against that?
I've solved this by iterating over the certificates found in the current user's store. I filter by the issuer name and take only valid certificates, then I loop over the matching certificates and return the first one which matches also the subject name:
public static X509Certificate2 Find(string issuer, string subject)
{
var certStore = new X509Store (StoreName.My, StoreLocation.CurrentUser);
certStore.Open (OpenFlags.ReadOnly);
var certCollection = certStore.Certificates.Find (X509FindType.FindByIssuerName, issuer, true);
foreach (var cert in certCollection)
{
if (cert.FriendlyName == subject)
{
return cert;
}
}
return null;
}

Categories