Smartersoft.Identity.Client.Assertion
0.5.0
See the version list below for details.
dotnet add package Smartersoft.Identity.Client.Assertion --version 0.5.0
NuGet\Install-Package Smartersoft.Identity.Client.Assertion -Version 0.5.0
<PackageReference Include="Smartersoft.Identity.Client.Assertion" Version="0.5.0" />
paket add Smartersoft.Identity.Client.Assertion --version 0.5.0
#r "nuget: Smartersoft.Identity.Client.Assertion, 0.5.0"
// Install Smartersoft.Identity.Client.Assertion as a Cake Addin #addin nuget:?package=Smartersoft.Identity.Client.Assertion&version=0.5.0 // Install Smartersoft.Identity.Client.Assertion as a Cake Tool #tool nuget:?package=Smartersoft.Identity.Client.Assertion&version=0.5.0
Smartersoft.Identity.Client.Assertion
This package allows you to use Managed Identities with a multi tenant application. Your certificates used for getting access tokens with the Client Credential flow will be completely protected and can NEVER be extracted, not even by yourself.
Managed Identities are great but they don't support multi-tenant use cases, until now.
This library is created by Smartersoft B.V. and licensed as GPL-3.0-only.
More details on this library in this post
Prerequisites
- Azure resource with support for managed identities (Azure Functions, App Service, ...)
- KeyVault
Key Sign
(and optionallyGet Certificate
) permission on the KeyVault with the managed identity- Multi-tenant app registration
- Self-signed certificate in KeyVault, see below
Creating a certificate in KeyVault
When using a certificate for client assertions a self-signed certificate will suffice. It will only be used for digital signatures, so it doesn't matter if it's not from some known CA.
- Go to the KeyVault in Azure Portal
- Click certificates
- Click Generate/Import
- Enter any name (needed to get the certificate info later on)
- Pick a subject, I always use
CN={app-name}.{company}.internal
- Set a Validity period (
12 months
is the default, which is fine) - Leave Content Type to
PKCS #12
- Set Lifetime action Type to
E-mail all contacts...
instead of auto-renew. You'll need to know when you'll have to take action. - Configure Advanced Policy Configuration! Set X.509 Key Usage Flags to
Digital Signature
only and Exportable Private Key toNo
. Leave the rest to their default setting. - Click Create
- Click the new certificate, click the version, download in CER format (needed in app registration).
When creating a certificate in the KeyVault, it's IMPORTANT to configure the Advanced Policy Configuration. This allows you to mark the private key as NOT EXPORTABLE, which means that private key will NEVER leave that KeyVault.
Required usings
using Azure.Identity;
using Microsoft.Identity.Client;
using System;
using System.Threading;
using System.Threading.Tasks;
using Smartersoft.Identity.Client.Assertion;
Get access token using certificate in KeyVault
public async Task<string> GetToken (CancellationToken cancellationToken)
{
// Create a token credential that suits your needs, used to access the KeyVault
var tokenCredential = new DefaultAzureCredential();
const string clientId = "d294e746-425b-44fa-896c-dacf2c7938b8";
const string tenantId = "42a26c5d-b8ed-4f1b-8760-655f98154373";
const string KeyVaultUri = "https://{kv-domain}.vault.azure.net/";
const string certificateName = "some-certificate";
// Use the ConfidentialClientApplicationBuilder as usual
// but call `.WithKeyVaultCertificate(...)` instead of `.WithCertificate(...)`
var app = ConfidentialClientApplicationBuilder
.Create(clientId)
.WithAuthority(AzureCloudInstance.AzurePublic, tenantId)
.WithKeyVaultCertificate(new Uri(KeyVaultUri), certificateName, tokenCredential)
.Build();
// Use the app, just like before
var tokenResult = await app.AcquireTokenForClient(new[] { "https://graph.microsoft.com/.default" })
.ExecuteAsync(cancellationToken);
return tokenResult.AccessToken;
}
Get access token using certificate in KeyVault, more efficiently
To use the Client Assertion you'll need the Base64Url encoded certificate hash. This information about the certificate will almost never change, only after certificate renewal.
It can be loaded only once and saved in a config file to reduce the calls to the KeyVault, the code above calls the KeyVault twice for each call to get a client assertion.
public async Task<string> GetTokenEfficiently(CancellationToken cancellationToken)
{
// Create a token credential that suits your needs, used to access the KeyVault
var tokenCredential = new DefaultAzureCredential();
const string KeyVaultUri = "https://{kv-domain}.vault.azure.net/";
const string certificateName = "some-certificate";
Uri? keyId = null;
string? kid = null;
// Load once and save in Cache/Config/...
var certificateInfo = await ClientAssertionGenerator.GetCertificateInfoFromKeyVault(new Uri(KeyVaultUri), certificateName, tokenCredential, cancellationToken);
if (certificateInfo.Kid == null || certificateInfo.KeyId == null)
{
throw new Exception();
}
keyId = certificateInfo.KeyId;
kid = certificateInfo.Kid;
const string clientId = "d294e746-425b-44fa-896c-dacf2c7938b8";
const string tenantId = "42a26c5d-b8ed-4f1b-8760-655f98154373";
// Use the ConfidentialClientApplicationBuilder as usual
// but call `.WithKeyVaultKey(...)` instead of `.WithCertificate(...)`
var app = ConfidentialClientApplicationBuilder
.Create(clientId)
.WithAuthority(AzureCloudInstance.AzurePublic, tenantId)
.WithKeyVaultKey(keyId, kid, tokenCredential)
.Build();
// Use the app, just like before
var tokenResult = await app.AcquireTokenForClient(new[] { "https://graph.microsoft.com/.default" })
.ExecuteAsync(cancellationToken);
return tokenResult.AccessToken;
}
Security
Why is this solution more secure that others? This solution will prevent attackers getting persistent access in case of a breach.
All other samples I've seen use the SecretClient.GetSecretAsync method to Get the secret. It then converts it to a X509Certificate2 that is then used for signing. If the app can Get the secret, an attacker can do the same.
This way the seemingly secure certificate can be extracted by some malicious actor, and if the breach goes undetected they now have a certificate that can possibly access data in several tenants. Without getting noticed.
This solution does the signing in the KeyVault instead of on the client. The application doesn't need the private key. It just needs the Sign permission.
Off course this solution still needs a secure way to access the Key Vault, like a managed identity. But if you need to implement KeyVault access without managed identities, the attacker can only sign token requests during the breach. This way you'll always have a log file of the sign-in attempts, in your Azure AD. If they would succeed in extracting the certificate, the only logs would be in the client Azure AD.
How does this work?
- Generate an unsigned client assertion (just some json, Base64Url encoded)
- Converts the unsigned client assertion to bytes
- Asks the KeyVault to Sign the data.
- Encodes the signature Base64Url
- Appends the signature to the token
License
These packages are licensed under GPL-3.0
, if you wish to use this software under a different license. Or you feel that this really helped in your commercial application and wish to support us? You can get in touch and we can talk terms. We are available as consultants.
Product | Versions Compatible and additional computed target framework versions. |
---|---|
.NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. |
.NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
.NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
.NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
MonoAndroid | monoandroid was computed. |
MonoMac | monomac was computed. |
MonoTouch | monotouch was computed. |
Tizen | tizen40 was computed. tizen60 was computed. |
Xamarin.iOS | xamarinios was computed. |
Xamarin.Mac | xamarinmac was computed. |
Xamarin.TVOS | xamarintvos was computed. |
Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
- Azure.Identity (>= 1.6.0)
- Azure.Security.KeyVault.Certificates (>= 4.3.0)
- Azure.Security.KeyVault.Keys (>= 4.3.0)
- Microsoft.Identity.Client (>= 4.45.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.