Azure Web App: System.Security.Cryptography.CryptographicException: Keyset does not exist - azure

My app encounters "System.Security.Cryptography.CryptographicException: Keyset does not exist" exception during the creation of a X509Certificate2.
Function to read cert from Azure Key Vault:
private string GetEncryptSecret(string certConfigName)
{
var kv = new KeyVaultClient(GetToken);
var sec = kv.GetSecretAsync(WebConfigurationManager.AppSettings[certConfigName]).Result;
return sec.Value;
}
How I create the new X509Certificate2 object:
public X509Certificate2 GetCertificate(CertificatesEnum certificate)
{
switch (certificate)
{
case CertificatesEnum.Accounts:
return new X509Certificate2(
Convert.FromBase64String(GetEncryptSecret(Constants.Magda.Certificates.Accounts)),
string.Empty, X509KeyStorageFlags.MachineKeySet);
}
}
After noticing that this was only working for the first 3-9 requests I've started an Azure Remote Debugging session and saw that the new certificate.Privatekey caused the "System.Security.Cryptography.CryptographicException: Keyset does not exist" exception.
Temporary fixed this by the implementation of a waiting loop because after a couple of retries the new certificate will be created without exception.
public X509Certificate2 GetCertificate(CertificatesEnum certificate)
{
switch (certificate)
{
case CertificatesEnum.Accounts:
var accountCertRawData = Convert.FromBase64String(GetEncryptSecret(Constants.Magda.Certificates.Accounts));
X509Certificate2 accountCert = null;
for (int i = 0; i < 20; i++)
{
try
{
accountCert= new X509Certificate2(accountCertRawData , string.Empty,
X509KeyStorageFlags.MachineKeySet);
//set variable to test exception
var accountCert = accountCert.PrivateKey;
break;
}
catch (System.Security.Cryptography.CryptographicException)
{
Thread.Sleep(1000 * i);
}
}
return accountCert;
}
}
Does anyone has a proper solution for this and can explain what is happening in the background on the Azure Web app?

Try removing WEBSITE_LOAD_USER_PROFILE application setting (if available) from the web application.

In Azure WebApp what seams to fix for me the issue with "some times" throws Keyset does not exist when calling GetPrivateKey() was not to use MachineKeySet for storage flags argument in constructor of X509Certificate2
new X509Certificate2(certContent, "", X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.Exportable)

Related

Blazor Server App with Azure AD authentication - Token expired and Custom AuthenticationStateProvider

I have built a Blazor Server App with Azure AD authentication. This server app access a web api written in net core and sends the JWT token to that api. Everything is working, data is gathered, page is displayed accordingly.
The problem is: after some time, when user interacts with some menu option in UI, nothing else is returned from webapi. After some tests I found out that the token has expired, then when it is sent to web api, it is not working. But the AuthenticationState remains same, like it is authenticated and valid irrespective the token is expired.
Thus, I have been trying some suggestions like : Client side Blazor authentication token expired on server side. Actually it is the closest solution I got.
But the problem is that, after implemented a CustomAuthenticationStateProvider class, even after injected it, the default AuthenticationStateProvider of the app remains like ServerAuthenticationStateProvider and not the CustomAuthenticationStateProvider I have implemented. This is part of my code:
public class CustomAuthenticationStateProvider : AuthenticationStateProvider
{
private readonly IConfiguration _configuration;
private readonly ITokenAcquisition _tokenAcquisition;
public CustomAuthenticationStateProvider(IConfiguration configuration, ITokenAcquisition tokenAcquisition)
{
_configuration = configuration;
_tokenAcquisition = tokenAcquisition;
}
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
var apiScope = _configuration["DownloadApiStream:Scope"];
var anonymousState = new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
string savedToken = string.Empty;
try
{
savedToken = await _tokenAcquisition.GetAccessTokenForUserAsync(new[] { apiScope });
}
catch (MsalUiRequiredException)
{
savedToken = string.Empty;
}
catch (Exception)
{
savedToken = string.Empty;
}
if (string.IsNullOrWhiteSpace(savedToken))
{
return anonymousState;
}
var claims = ParseClaimsFromJwt(savedToken).ToList();
var expiry = claims.Where(claim => claim.Type.Equals("exp")).FirstOrDefault();
if (expiry == null)
return anonymousState;
// The exp field is in Unix time
var datetime = DateTimeOffset.FromUnixTimeSeconds(long.Parse(expiry.Value));
if (datetime.UtcDateTime <= DateTime.UtcNow)
return anonymousState;
return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity(claims, "jwt")));
}
public void NotifyExpiredToken()
{
var anonymousUser = new ClaimsPrincipal(new ClaimsIdentity());
var authState = Task.FromResult(new AuthenticationState(anonymousUser));
NotifyAuthenticationStateChanged(authState);
}
private IEnumerable<Claim> ParseClaimsFromJwt(string jwt)
{
var claims = new List<Claim>();
var payload = jwt.Split('.')[1];
var jsonBytes = ParseBase64WithoutPadding(payload);
var keyValuePairs = JsonSerializer.Deserialize<Dictionary<string, object>>(jsonBytes);
keyValuePairs.TryGetValue(ClaimTypes.Role, out object roles);
if (roles != null)
{
if (roles.ToString().Trim().StartsWith("["))
{
var parsedRoles = JsonSerializer.Deserialize<string[]>(roles.ToString());
foreach (var parsedRole in parsedRoles)
{
claims.Add(new Claim(ClaimTypes.Role, parsedRole));
}
}
else
{
claims.Add(new Claim(ClaimTypes.Role, roles.ToString()));
}
keyValuePairs.Remove(ClaimTypes.Role);
}
claims.AddRange(keyValuePairs.Select(kvp => new Claim(kvp.Key, kvp.Value.ToString())));
return claims;
}
private byte[] ParseBase64WithoutPadding(string base64)
{
switch (base64.Length % 4)
{
case 2: base64 += "=="; break;
case 3: base64 += "="; break;
}
return Convert.FromBase64String(base64);
}
}
This is my Program.cs where I added the services :
builder.Services.AddScoped<CustomAuthenticationStateProvider>();
builder.Services.AddScoped<AuthenticationStateProvider>(provider => provider.GetRequiredService<CustomAuthenticationStateProvider>());
Here in the MainLayou.razor, I inject the service and try to use it :
#inject CustomAuthenticationStateProvider authenticationStateProvider;
protected async override Task OnInitializedAsync()
{
var authState = await authenticationStateProvider.GetAuthenticationStateAsync();
if (authState.User?.Identity == null || !authState.User.Identity.IsAuthenticated)
{
authenticationStateProvider.NotifyExpiredToken();
}
await base.OnInitializedAsync();
}
The problem comes up here, because the authenticationStateProvider is not an instance of the CustomAuthenticationStateProvider , but the instance of ServerAuthenticationStateProvider. It is like AuthenticationStateProvider was not replaced by the custom implementation, therefore I can't use the NotifyAuthenticationStateChanged and inform the CascadingAuthenticationState that it was changed.
If anyone has already been thru this or have any suggestion, it would be appreciated.
Actually I just wanna to change authentication state to not authenticated. So user will be pushed to login again using Azure AD.
Thanks

How to use Azure Key-vault to retrieve connection string then set to AzureWebJobsServiceBus for ServiceBusTrigger

If I set connection string of AzureWebJobsServiceBus in local.settings.json, then there is no error. However, I would like to use Azure Key-vault to prevent disclosing connection string.
Below is my error:
Microsoft.Azure.WebJobs.Host: Error indexing method 'MyAzureFunc.Run'. Microsoft.ServiceBus: The Service Bus connection string is not of the expected format. Either there are unexpected properties within the string or the format is incorrect. Please check the string before. trying again.
Here is my code:
public static class MyAzureFunc
{
private static readonly SettingsContext _settings;
static MyAzureFunc()
{
_settings = new SettingsContext(new Settings
{
BaseUrl = Environment.GetEnvironmentVariable("BaseUrl"),
ServiceBusConnectionString = Environment.GetEnvironmentVariable("ServiceBus"),
certThumbprint = Environment.GetEnvironmentVariable("CertThumbprint"),
keyVaultClientId = Environment.GetEnvironmentVariable("KeyVaultClientId"),
ServiceBusSecretUrl = Environment.GetEnvironmentVariable("ServiceBusSecretUrl")
});
Environment.SetEnvironmentVariable("AzureWebJobsServiceBus", _settings.ServiceBusConnectionString);
}
[FunctionName("Func")]
public static async Task Run([ServiceBusTrigger(ServiceBusContext.MyQueueName)] BrokeredMessage msg, TraceWriter log)
{
......
}
}
public SettingsContext(Settings settings)
{
new MapperConfiguration(cfg => cfg.CreateMap<Settings, SettingsContext>()).CreateMapper().Map(settings, this);
if (!string.IsNullOrEmpty(settings.certThumbprint) && !string.IsNullOrEmpty(settings.keyVaultClientId))
{
var cert = Helpers.GetCertificate(settings.certThumbprint);
var assertionCert = new ClientAssertionCertificate(settings.keyVaultClientId, cert);
KeyVaultClient = GetKeyVaultClient(assertionCert);
if (ServiceBusConnectionString == "nil" && !string.IsNullOrEmpty(settings.ServiceBusSecretUrl))
{
ServiceBusConnectionString = KeyVaultClient.GetSecretAsync(settings.ServiceBusSecretUrl).Result.Value;
}
}
}
private static KeyVaultClient GetKeyVaultClient(ClientAssertionCertificate assertionCert)
{
return new KeyVaultClient(new KeyVaultClient.AuthenticationCallback(async (string authority, string resource, string scope) =>
{
var context = new AuthenticationContext(authority, TokenCache.DefaultShared);
var result = await context.AcquireTokenAsync(resource, assertionCert);
return result.AccessToken;
}));
}
This is actually much simpler than what you are trying ;) See here. KeyVault is natively integrated with Azure Functions / App Services for secure storage of settings.
In your local.settings.json you use the connection string as is (in plain text). This file is never checked in.
In Azure you have an app setting with the same name, but instead of putting the plain text connection string, you put the reference to your KeyVault setting like this:
#Microsoft.KeyVault(SecretUri=https://myvault.vault.azure.net/secrets/mysecret/ec96f02080254f109c51a1f14cdb1931)
The only thing you need to do is to enable Managed Identity of your Function and give that identity read permissions in the KeyVault.

Windows Azure Active Directory Authentication Exception

I'm using Windows Azure Active Directory Authentication. This is used to secure a c# windows service that calls a c# Web API service in Azure.It has worked for quite some time but now I've started getting the following exception:
Microsoft.Owin.Security.OAuth.OAuthBearerAuthenticationMiddleware Error: 0 : Authentication failed
System.IdentityModel.Tokens.SecurityTokenSignatureKeyNotFoundException: IDX10500: Signature validation failed. Unable to resolve SecurityKeyIdentifier: 'SecurityKeyIdentifier
(
IsReadOnly = False,
Count = 2,
Clause[0] = X509ThumbprintKeyIdentifierClause(Hash = 0x61B44041161C13F9A8B56549287AF02C16DDFFDB),
Clause[1] = System.IdentityModel.Tokens.NamedKeySecurityKeyIdentifierClause
)
I have no idea what this means or how to fix it :(
Update
In answer to the comment about key rollover my web service is using the following code:
private void ConfigureAuth(IAppBuilder app)
{
app.UseWindowsAzureActiveDirectoryBearerAuthentication(
new WindowsAzureActiveDirectoryBearerAuthenticationOptions
{
TokenValidationParameters = new System.IdentityModel.Tokens.TokenValidationParameters
{
ValidAudience = ConfigurationManager.AppSettings["ida:Audience"]
},
Tenant = ConfigurationManager.AppSettings["ida:Tenant"]
});
}
which according to that link means it should be secured against this type of issue.
The start of the token including the kid looks like this:
token: '{"typ":"JWT","alg":"RS256","x5t":"YbRAQRYcE_motWVJKHrwLBbd_9s","kid":"YbRAQRYcE_motWVJKHrwLBbd_9s"}
Update 2
My code to acquire token in the windows service:
internal string GetAuthorizationToken()
{
string authority = String.Format(aadInstance, tenant);
var authContext = new AuthenticationContext(authority);
var authResult = AcquireToken(authContext);
return authResult == null ? null : authResult.CreateAuthorizationHeader();
}
/// <summary>
/// Acquires the token.
/// </summary>
/// <param name="authContext">The authentication context.</param>
/// <returns>Authentication Result</returns>
private AuthenticationResult AcquireToken(AuthenticationContext authContext)
{
try
{
return authContext.AcquireTokenAsync(apiResourceId, clientId, new UserPasswordCredential(user, pass)).Result;
}
catch (Exception)
{
return null;
}
}
This exception would occur when the application trying to find the signing key via the key identity in the token from Azure, however the key on Azure already rollover.
Please get a new access token in the windows service to fix this issue.

Azure WorkerRole: Certificate Key not valid for use in specified state

System.Security.Cryptography.CryptographicException: Key not valid for
use in specified state.
at
System.Security.Cryptography.CryptographicException.ThrowCryptographicException(Int32
hr) at System.Security.Cryptography.Utils._ExportKey(SafeKeyHandle
hKey, Int32 blobType, Object cspObject) at
System.Security.Cryptography.RSACryptoServiceProvider.ExportParameters(Boolean
includePrivateParameters) at
System.Security.Cryptography.RSA.ToXmlString(Boolean
includePrivateParameters)
Now, i belive this happens because that when Azure adds the Certificate to my WorkerRole deployment, it do not install the certificate with the option "Mark this Key as Exportable".
I need to add a certificate to my workerrole to beable to decypt a encryptet setting.
Anyone have any ideas about how i can make Azure Mark the certificates private key as exportable. or if it could be another issue.
Onstart:
try{
var conn = System.Text.UTF8Encoding.UTF8.GetString(Decrypt(Convert.FromBase64String(setting), true, cert));
}catch(Exception ex)
{
Trace.TraceError(ex.ToString());
}
Methods:
public static X509Certificate2 LoadCertificate(StoreName storeName,
StoreLocation storeLocation, string tprint)
{
X509Store store = new X509Store(storeName, storeLocation);
try
{
store.Open(OpenFlags.ReadOnly);
X509Certificate2Collection certificateCollection =
store.Certificates.Find(X509FindType.FindByThumbprint,
tprint, false);
if (certificateCollection.Count > 0)
{
// We ignore if there is more than one matching cert,
// we just return the first one.
return certificateCollection[0];
}
else
{
throw new ArgumentException("Certificate not found");
}
}
finally
{
store.Close();
}
}
public static byte[] Decrypt(byte[] encryptedData, bool fOAEP,
X509Certificate2 certificate)
{
if (encryptedData == null)
{
throw new ArgumentNullException("encryptedData");
}
if (certificate == null)
{
throw new ArgumentNullException("certificate");
}
using (RSACryptoServiceProvider provider = new RSACryptoServiceProvider())
{
// Note that we use the private key to decrypt
provider.FromXmlString(GetXmlKeyPair(certificate));
return provider.Decrypt(encryptedData, fOAEP);
}
}
public static string GetXmlKeyPair(X509Certificate2 certificate)
{
if (certificate == null)
{
throw new ArgumentNullException("certificate");
}
if (!certificate.HasPrivateKey)
{
throw new ArgumentException("certificate does not have a private key");
}
else
{
return certificate.PrivateKey.ToXmlString(true);
}
}
I found a solution.
the answer is given in another question of mine here: How can I get a certificate by name given in ServiceDefinition on Azure

Which thumb print should be used to list the hosted services using windows azure management api

I want to list all the hosted services using Azure service Management REST Api. And the msdn hlep explains a way to list hosted services. I have attached the example code given in msdn.
In the code they have used Version, Thumbprint and SubscriptionId.
In windows azure portal we can see a subcription has a subcription Id. And a certificate has a Thumbprint. There may be many hosted services in one subcription so many certificates as well. So what is the thumbprint the following code has mentioned..?
Should it checked with all the thumbprints of a subcription , to list all the hosted services in a subcription.
Why can't we get all the hosted services just using the subcriptionId (is it not secured?) Or is there a common certificate(so there is a thumbprint) for a subcription?
Please guide me,
Thanks.
namespace Microsoft.WindowsAzure.ServiceManagementRESTAPI.Samples
{
using System;
using System.Collections.Generic;
using System.Net;
using System.Security.Cryptography.X509Certificates;
using System.Xml;
using System.Xml.Linq;
public class Program
{
// Set these constants with your values to run the sample.
private const string Version = "2011-10-01";
private const string Thumbprint = "management-certificate-thumbprint";
private const string SubscriptionId = "subscription-id";
static void Main(string[] args)
{
try
{
// Obtain the certificate with the specified thumbprint
X509Certificate2 certificate = GetStoreCertificate(Thumbprint);
ListHostedServicesExample(SubscriptionId, certificate, Version);
}
catch (Exception ex)
{
Console.WriteLine("Exception caught in Main:");
Console.WriteLine(ex.Message);
}
Console.Write("Press any key to continue:");
Console.ReadKey();
}
public static void ListHostedServicesExample(
string subscriptionId,
X509Certificate2 certificate,
string version)
{
string uriFormat = "https://management.core.windows.net/{0}/" +
"services/hostedservices";
Uri uri = new Uri(String.Format(uriFormat, subscriptionId));
HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create(uri);
request.Method = "GET";
request.Headers.Add("x-ms-version", version);
request.ClientCertificates.Add(certificate);
request.ContentType = "application/xml";
XDocument responseBody = null;
HttpStatusCode statusCode;
HttpWebResponse response;
try
{
response = (HttpWebResponse)request.GetResponse();
}
catch (WebException ex)
{
// GetResponse throws a WebException for 400 and 500 status codes
response = (HttpWebResponse)ex.Response;
}
statusCode = response.StatusCode;
if (response.ContentLength > 0)
{
using (XmlReader reader = XmlReader.Create(response.GetResponseStream()))
{
responseBody = XDocument.Load(reader);
}
}
response.Close();
if (statusCode.Equals(HttpStatusCode.OK))
{
XNamespace wa = "http://schemas.microsoft.com/windowsazure";
XElement hostedServices = responseBody.Element(wa + "HostedServices");
Console.WriteLine(
"Hosted Services for Subscription ID {0}:{1}{2}",
subscriptionId,
Environment.NewLine,
hostedServices.ToString(SaveOptions.OmitDuplicateNamespaces));
}
else
{
Console.WriteLine("Call to List Hosted Services returned an error:");
Console.WriteLine("Status Code: {0} ({1}):{2}{3}",
(int)statusCode, statusCode, Environment.NewLine,
responseBody.ToString(SaveOptions.OmitDuplicateNamespaces));
}
return;
}
/// <summary>
/// Gets the certificate matching the thumbprint from the local store.
/// Throws an ArgumentException if a matching certificate is not found.
/// </summary>
/// <param name="thumbprint">The thumbprint of the certificate to find.</param>
/// <returns>The certificate with the specified thumbprint.</returns>
private static X509Certificate2 GetStoreCertificate(string thumbprint)
{
List<StoreLocation> locations = new List<StoreLocation>
{
StoreLocation.CurrentUser,
StoreLocation.LocalMachine
};
foreach (var location in locations)
{
X509Store store = new X509Store("My", location);
try
{
store.Open(OpenFlags.ReadOnly | OpenFlags.OpenExistingOnly);
X509Certificate2Collection certificates = store.Certificates.Find(
X509FindType.FindByThumbprint, thumbprint, false);
if (certificates.Count == 1)
{
return certificates[0];
}
}
finally
{
store.Close();
}
}
throw new ArgumentException(string.Format(
"A Certificate with Thumbprint '{0}' could not be located.",
thumbprint));
}
}
}
The certificate that you would want to use is the "Management Certificate". Here's the process for doing that:
Create a self-signed certificate on your computer (pfx file format). You may find this link useful: http://consultingblogs.emc.com/gracemollison/archive/2010/02/19/creating-and-using-self-signed-certificates-for-use-with-azure-service-management-api.aspx
Install that certificate in your local certificate store (preferably CurrentUser\My).
Export that certificate from your local certificate store on your computer in .cer file format.
Upload this certificate under management certificates section in the portal. To do so, login into Windows Azure portal (https://manage.windowsazure.com) and then click on "SETTINGS" tab and then click on "UPLOAD" button to choose and upload this file.
A few things to keep in mind:
You can have as many as 10 management certificates per subscription.
If you want your colleagues to use the same certificate, please share the pfx file created in step 1 and have them install the certificate in the certificate store of their local computer. Please don't give them .cer file created in step 3 as it does not have certificate's private data.
Hope this helps.

Resources