Loading a X509 certificate from Azure KeyVault into a .NET Core application
In a context where we are now using APIs a lot more than we used to, it becomes important to secure them. One way we can secure them is using the OAUTH/OpenId protocol, which relies on Json Web Tokens (JWTs). A JWT needs to be generated and digitally signed by the authority (what we call a Security Token Service (STS)) your APIs trust. They require signed JWTs to prevent attackers from altering or counterfeiting such tokens in an attempt to gain unauthorized access to the resources secured by the APIs.
A good open source implementation of such authority is IdentityServer4 which also gives you a lot more features than just being a STS. In development mode, IdentityServer4 provides you with a self-signed token certificate, which is great to get you started very easily.
Once in production, you will want to secure the certificate you use to sign your tokens in a secure place. In Azure, we are fortunate to have Azure KeyVault. In this article, I will show you how you can use Azure KeyVault to retrieve your certificate for token signing so you can use it with IdentityServer4.
As a side note, you can use this technique in another other .NET Core application you wish (and not only ASP.NET Core). Be reminded that managed identity works only when your application is deployed in your Azure cloud.
UPDATE 2021-11-08:
This article uses the old SDKs for Azure. Refer to my post here for the new SDK.
Setup
First, you need to add the following packages to your application
If you are using Managed Identities (see my post about that here!), add the following package as well
Microsoft.Azure.Services.AppAuthentication
This library makes it easy to fetch access tokens for Service-to-Azure-Service authentication
Uploading your certificate to KeyVault
For this technique to work, you need to upload your certificate. In this example, I will upload a PKCS #12 (PFX) certificate.
Using the Portal
In your Azure KeyVault resource, under the Certificates blade, click the Generate/Import button. Under Method of Certificate Creation, select import. Select your certificate, give it a name, enter the certificate password and it will be uploaded.
Note: Since we will be using in the SDK the GetSecretAsync method instead of GetCertificateAsync, remember to give your service principal the Get access policy for secrets. If you want to know why we are using GetSecretAsync instead of GetCertificateAsync, in a nutshell, it is because GetCertificateAsync doesn’t return the private key that we need to sign. Refer to the excellent blog post of AzIdentity for a detailed explanation.
Using PowerShell
To upload a certificate in the Certificate portion of the KeyVault, you can use the following PowerShell cmdlet.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
function Add-CertificateToKeyVaultCertificateIfNotExists { [CmdletBinding()] Param( [Parameter(Mandatory)] [string] $CertificatePath, [Parameter(Mandatory)] [SecureString] $CertificatePassword, [Parameter(Mandatory)] [string] $VaultName, [Parameter(Mandatory)] [string] $Name, [switch] $Replace ) Begin { $ErrorActionPreference = 'stop' } Process { $certificate = Get-AzKeyVaultCertificate -VaultName $VaultName -Name $Name if ($certificate -and $Replace -eq $FALSE) { Write-Verbose -Message ('Skipped replacing certificate ''{0}'' in vault ''{1}''.' -f $VaultName, $Name) } else { if ($Replace -eq $TRUE) { $vault = Get-AzKeyVault -VaultName $VaultName if ($vault.EnableSoftDelete) { Remove-AzKeyVaultCertificate -VaultName $VaultName -Name $Name -InRemovedState -Force } else { Remove-AzKeyVaultCertificate -VaultName $VaultName -Name $Name -Force } } Import-AzKeyVaultCertificate -VaultName $VaultName -Name $Name -Password $CertificatePassword -FilePath $CertificatePath } } } |
Giving your principal the required access policies
As I mentioned above, you need to give your service principal the Get permission for the Secrets.
Wiring it all up
Connecting to your KeyVault
If you are using Managed Identities, you can use the following code to go and fetch your certificate from your KeyVault
1 2 3 4 |
var azureServiceTokenProvider = new AzureServiceTokenProvider(); // using managed identities var kv = new KeyVaultClient(new KeyVaultClient.AuthenticationCallback(azureServiceTokenProvider.KeyVaultTokenCallback)); |
This will create a new KeyVault client that will go fetch the access token from your service principal (a.k.a your application) automatically and wire it up all up in the KeyVault client.
If you are using an application (creating an application is out of this scope, but it can be found in the Azure Active Directory resource, under App registrations), you can use the following code to authenticate.
1 2 3 4 5 6 7 8 9 10 11 |
var kv = new KeyVaultClient(async (authority, resource, scope) => { var authContext = new AuthenticationContext(authority); var clientCred = new ClientCredential("YOUR_APP_ID", "YOUR_APP_SECRET"); var result = await authContext.AcquireTokenAsync(resource, clientCred); if (result == null) throw new InvalidOperationException("Failed to obtain the JWT token"); return result.AccessToken; }); |
Accessing the certificate
1 2 |
var certificateSecret = await kv.GetSecretAsync($"https://{"YOUR_VAULT_NAME"}.vault.azure.net/", "YOUR_VAULT_CERTIFICATE_NAME"); var privateKeyBytes = Convert.FromBase64String(certificateSecret.Value); |
Creating the certificate
1 |
var certificate = new X509Certificate2(privateKeyBytes, (string)null); |
If you are using IdentityServer4 you can then add this certificate to the IdentityServerBuilder as such
1 |
builder.AddSigningCredential(certificate); |
Things to keep in mind
If you are deploying an ASP.NET Core application to be used with IIS, you will need to make sure to load the user profile otherwise you will get a cryptography exception. If you are using Azure WebApps, you can add the app setting WEBSITE_LOAD_USER_PROFILE with value 1 to load the user profile. A certificate that has a private key requires user profile and, by default, an Azure WebApp doesn’t create the user profile. This is only supported for Standard App Service plans and above.