Smartersoft.Identity.Client.Assertion 0.9.0

dotnet add package Smartersoft.Identity.Client.Assertion --version 0.9.0                
NuGet\Install-Package Smartersoft.Identity.Client.Assertion -Version 0.9.0                
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Smartersoft.Identity.Client.Assertion" Version="0.9.0" />                
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add Smartersoft.Identity.Client.Assertion --version 0.9.0                
#r "nuget: Smartersoft.Identity.Client.Assertion, 0.9.0"                
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
// Install Smartersoft.Identity.Client.Assertion as a Cake Addin
#addin nuget:?package=Smartersoft.Identity.Client.Assertion&version=0.9.0

// Install Smartersoft.Identity.Client.Assertion as a Cake Tool
#tool nuget:?package=Smartersoft.Identity.Client.Assertion&version=0.9.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 optionally Get 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.

  1. Go to the KeyVault in Azure Portal
  2. Click certificates
  3. Click Generate/Import
  4. Enter any name (needed to get the certificate info later on)
  5. Pick a subject, I always use CN={app-name}.{company}.internal
  6. Set a Validity period (12 months is the default, which is fine)
  7. Leave Content Type to PKCS #12
  8. 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.
  9. Configure Advanced Policy Configuration! Set X.509 Key Usage Flags to Digital Signature only and Exportable Private Key to No. Leave the rest to their default setting.
  10. Click Create
  11. 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

    private readonly IMemoryCache? _injectedCache;
    public async Task<string> GetToken (CancellationToken cancellationToken)
    {
        // Create a token credential that suits your needs, used to access the KeyVault
        // You should get this from dependency injection as a singleton, because it will cache the token internally.
        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, _injectedCache)
            .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
        // You should get this from dependency injection as a singleton, because it will cache the token internally.
        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 CertificateClient.DownloadCertificateAsync method to Get the certificate information and Download the private key. 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?

  1. Generate an unsigned client assertion (just some json, Base64Url encoded)
  2. Converts the unsigned client assertion to bytes
  3. Asks the KeyVault to Sign the data.
  4. Encodes the signature Base64Url
  5. 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.

Open-source

This package is open-source for a reason. It's developed by Stephan van Rooij, people make mistakes. Always check out what's doing and make sure it doesn't do anything strange with the tokens.

Product 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last updated
0.9.0 351 5/17/2024
0.8.0 578 3/3/2024
0.7.0 1,006 8/1/2023
0.6.0 866 12/12/2022
0.5.0 565 6/27/2022
0.4.1 459 4/26/2022
0.2.2 498 3/4/2022
0.2.0 495 1/27/2022
0.1.5 435 1/20/2022