Duende (Identity Server), Certificates and Postman. What could go wrong? Signed JWT Requests the "easy" way. Part 03
Welcome to the last post of the series, and this will be involving a little bit of azure, some architecture design and of course C#.
We will be doing this post backwards, I will show you first how to set up Azure Key Vault, setup visual studio, use the library, show you the architecture and then the code explanation.
Azure Key Vault
We will be hosting our certificate in azure. First step is to create an azure key vault, don't worry, it costs money but not a lot, around 15 cents (USD) a month give or take to use this awesome service.
Once the key vault is created, go to the certificates section and create a self-signed certificate.
Or you can upload a self-signed certificate, the same one we have been using from the last posts.
https://darthseldon.net/certificate-reader/


Next, we will need to create an access policy or add the role of key vault administrator to your user.
The access policy will grant a user specific actions it can perform, but the key vault administrator role will be the same but in RBAC instead of specific type of policy.
Hopefully you are using the same certificate when you setup duende, if you did not, just update your base64 public cert in the configuration with the new cert.
Now we will set up visual studio
Visual Studio Setup
Under Tools->Options, search for the azure service authentication

Enter your azure credentials, this will create a token that can be used to access azure resources.
Now for the code
GitHub
You can obtain the source code for this post at...
Once you clone the project, you can run the only unit test that is there, obviously we will need to setup the appsettings file in order to test it.


You will notice 3 main sections
- InMemoryCacheSettings. Self-explanatory, but we will be caching the certificate in memory so we can avoid expensive round trips to the cloud.
- AzureKeyVaultSettings. Since we are hosting the cert in azure, this section has all the info to retrieve it; has a reference to InMemoryCacheSettings.
- IdpServerClientSettings. Our Duende (or any other IDP) information to obtain the token; has a reference to the AzureKeyVaultSettings.
Now wait a minute (yes, a Moana reference, when you have young kids, you will do the same) the settings are almost empty; we will be setting those settings in the secrets file, that way you can avoid checking in your credentials and important information (I am watching you Uber)
Right click on the unit tests project and select manage user secrets

You can copy and paste the following JSON
{
"AzureKeyVaultSettings:AzureKeyVaultUri": "https://yourkeyvault.vault.azure.net/",
"IdpServerClientSettings:AuthorizationServer": "https://yourduendeidp.madeup",
"IdpServerClientSettings:ClientId": "jwt.signed.requests",
"IdpServerClientSettings:Audience": "https://yourduendeidp.madeup",
"IdpServerClientSettings:KeyVaultCertificateName": "youruploadedcertname",
"IdpServerClientSettings:Scope": "api.readonly"
}
Basically, we are overriding these settings, each setting is a JSON path expression. You will need to place your own URLs, client id, scope, etc. to match your own IDP.
Now debug the unit test

If everything worked correctly, you could inspect the result and it will have something like.

Success!!! You can convert this library into a nuget package and publish it. You can see my other post on how to do that from GitHub. Also, I need to write another post on how to create the packages.
https://darthseldon.net/publishing-nuget-packages-with-github-actions/
Architecture
The class diagram starts with the IdentityServerClient, which is our facade to all the functionality.

We have 3 different Interfaces that we will be using, ISettings, ICache and IKeyVault.
ICache will be using it implement concrete classes for different types of cache, right now we will be only implementing InMemoryCache, but we will be adding Redis in the future.
IKeyVault follows the same rule, right now we will be using Azure Key Vault, but we can implement local certs, java web stores, AWS, etc.
ISettings will help us select a particular ICache, IKeyVault while injecting our code, you can see that on the unit test. But for now, know that we will be using mostly the Name property.
Each concrete implementation has its own settings section. Now this is important, as you might only use .net core, but what happens if you use this code in .net framework (yes it happens a lot). You can read your settings in multiple ways and fill the settings instance as you might see fit. in our case we are adding a json configuration file and extracting sections, but you can use the options pattern or create your own xml reader for .net framework.
Finally, we implement the interfaces and have helper classes. Simple.
Caching in memory
ICache interface.
/// <summary>
/// Interface Cache
/// </summary>
public interface ICache
{
#region Properties
/// <summary>
/// Gets the name.
/// </summary>
/// <value>
/// The name.
/// </value>
string Name { get; }
#endregion Properties
#region Methods
/// <summary>
/// Deletes the entry.
/// </summary>
/// <param name="identifier">Identifier.</param>
/// <returns>True or false if the entry was deleted</returns>
Task<bool> DeleteEntry(string identifier);
/// <summary>
/// Gets the value asynchronous.
/// </summary>
/// <param name="identifier">Identifier.</param>
/// <returns>Value</returns>
Task<string> GetValueAsync(string identifier);
/// <summary>
/// Gets the value asynchronous.
/// </summary>
/// <typeparam name="T">Generic</typeparam>
/// <param name="identifier">Identifier.</param>
/// <returns>Generic</returns>
Task<T> GetValueAsync<T>(string identifier);
/// <summary>
/// Sets the value asynchronous.
/// </summary>
/// <param name="identifier">Identifier.</param>
/// <param name="valueToStore">Value to store.</param>
/// <returns>True or false if the value was set</returns>
Task<bool> SetValueAsync(string identifier, string valueToStore);
/// <summary>
/// Sets the value asynchronous.
/// </summary>
/// <param name="identifier">Identifier.</param>
/// <param name="valueToStore">Value to store.</param>
/// <param name="expiration">Expiration.</param>
/// <returns>True or false if the value was set</returns>
Task<bool> SetValueAsync(string identifier, string valueToStore, TimeSpan expiration);
/// <summary>
/// Sets the value asynchronous.
/// </summary>
/// <typeparam name="T">Generic</typeparam>
/// <param name="identifier">Identifier.</param>
/// <param name="objectToSerialize">Object to serialize.</param>
/// <returns>True or false if the value was set</returns>
Task<bool> SetValueAsync<T>(string identifier, T objectToSerialize);
/// <summary>
/// Sets the value asynchronous.
/// </summary>
/// <typeparam name="T">Generic</typeparam>
/// <param name="identifier">Identifier.</param>
/// <param name="objectToSerialize">Object to serialize.</param>
/// <param name="expiration">Expiration.</param>
/// <returns>True or false if the value was set</returns>
Task<bool> SetValueAsync<T>(string identifier, T objectToSerialize, TimeSpan expiration);
/// <summary>
/// Does the identifier exists?
/// </summary>
/// <param name="identifier">Identifier.</param>
/// <returns>True or false if the identifier exists</returns>
Task<bool> DoesIdentifierExists(string identifier);
#endregion Methods
}
The interface is straight forward, just methods to add and set values.
Next, we will be adding our helper class for in memory handling. I will be using a generics and semaphores as we want the code to be thread safe.
/// <summary>
/// Memory Cache Helper
/// </summary>
/// <typeparam name="T">Generic.</typeparam>
public class Memory<T>
{
#region Members
/// <summary>
/// Memory Cache
/// </summary>
private readonly MemoryCache _memoryCache = new MemoryCache(new MemoryCacheOptions { ExpirationScanFrequency = TimeSpan.FromHours(1) });
/// <summary>
/// Locks
/// </summary>
private readonly ConcurrentDictionary<object, SemaphoreSlim> _locks = new ConcurrentDictionary<object, SemaphoreSlim>();
#endregion Members
#region Methods
/// <summary>
/// Gets or creates entry.
/// </summary>
/// <param name="key">Key.</param>
/// <param name="createItem">The create item delegate.</param>
/// <returns>Generic</returns>
public async Task<T> GetOrCreate(object key, Func<Task<T>> createItem)
{
if (!_memoryCache.TryGetValue(key, out T cacheEntry))// Look for cache key.
{
SemaphoreSlim mylock = _locks.GetOrAdd(key, k => new SemaphoreSlim(1, 1));
//notify all threads trying to access this value to await creation
await mylock.WaitAsync();
try
{
if (!_memoryCache.TryGetValue(key, out cacheEntry))
{
// Key not in cache, so get data, this is a delegate function
cacheEntry = await createItem();
_memoryCache.Set(key, cacheEntry);
}
}
finally
{
mylock.Release();
}
}
return cacheEntry;
}
/// <summary>
/// Gets or creates entry.
/// </summary>
/// <param name="key">Key.</param>
/// <param name="createItem">The create item delegate.</param>
/// <param name="timeSpan">Expiration time span.</param>
/// <returns>Generic</returns>
public async Task<T> GetOrCreate(object key, Func<Task<T>> createItem, TimeSpan timeSpan)
{
if (!_memoryCache.TryGetValue(key, out T cacheEntry))// Look for cache key.
{
SemaphoreSlim mylock = _locks.GetOrAdd(key, k => new SemaphoreSlim(1, 1));
//notify all threads trying to access this value to await creation
await mylock.WaitAsync();
try
{
if (!_memoryCache.TryGetValue(key, out cacheEntry))
{
// Key not in cache, so get data, this is a delegate function
cacheEntry = await createItem();
//obtain absolute expiration relative to now in time span format
_memoryCache.Set(key, cacheEntry, timeSpan);
}
}
finally
{
mylock.Release();
}
}
return cacheEntry;
}
/// <summary>
/// Deletes the entry.
/// </summary>
/// <param name="key">Key.</param>
/// <returns>True or false if the entry was deleted</returns>
public async Task<bool> DeleteEntry(object key)
{
SemaphoreSlim mylock = _locks.GetOrAdd(key, k => new SemaphoreSlim(1, 1));
//notify all threads trying to access this value to await creation
await mylock.WaitAsync();
try
{
_memoryCache.Remove(key);
}
finally
{
mylock.Release();
}
return true;
}
/// <summary>
/// Determines whether [is identifier exists].
/// </summary>
/// <param name="key">Key.</param>
/// <returns>
/// <c>true</c> if [is identifier exists] [the specified key]; otherwise, <c>false</c>.
/// </returns>
public async Task<bool> DoesIdentifierExists(object key)
{
try
{
var cached = _memoryCache.Get(key);
if (cached != null)
return true;
return false;
}
catch (Exception)
{
return false;
}
}
#endregion Methods
}
Next it will be the implementation of the interface for in memory and using the helper class.
/// <summary>
/// In Memory Cache
/// </summary>
/// <seealso cref="DarthSeldon.Services.Caching.Abstractions.ICache" />
public class InMemoryCache : ICache
{
#region Members
/// <summary>
/// In Memory Settings
/// </summary>
private readonly InMemoryCacheSettings _settings;
/// <summary>
/// Memory cache
/// </summary>
private static readonly Memory<string> _memoryCache = new Memory<string>();
/// <summary>
/// Cache name
/// </summary>
private string _cacheName;
#endregion Members
#region Properties
/// <summary>
/// Gets the name of the cache.
/// </summary>
/// <value>
/// The name of the cache.
/// </value>
public string Name { get => _cacheName; }
#endregion Properties
#region Constructor
/// <summary>
/// Initializes a new instance of the <see cref="InMemoryCache" /> class.
/// </summary>
/// <param name="settings">Settings.</param>
public InMemoryCache(ISettings settings)
{
_settings = settings as InMemoryCacheSettings;
_cacheName = _settings.Name;
}
#endregion Constructor
#region Methods
/// <summary>
/// Deletes the entry.
/// </summary>
/// <param name="identifier">Identifier.</param>
/// <returns>True or false if the entry was deleted</returns>
public async Task<bool> DeleteEntry(string identifier)
{
return await _memoryCache.DeleteEntry(identifier);
}
/// <summary>
/// Gets the value asynchronous.
/// </summary>
/// <param name="identifier">Identifier.</param>
/// <returns>Value</returns>
public async Task<string> GetValueAsync(string identifier)
{
return await _memoryCache.GetOrCreate(identifier, () =>
{
throw new ApplicationException($"No value found for key: {identifier}");
});
}
/// <summary>
/// Gets the value asynchronous.
/// </summary>
/// <typeparam name="T">Generic</typeparam>
/// <param name="identifier">Identifier.</param>
/// <returns>Generic</returns>
public async Task<T> GetValueAsync<T>(string identifier)
{
var cached = await _memoryCache.GetOrCreate(identifier, () =>
{
throw new ApplicationException($"No value found for key: {identifier}");
});
return JsonConvert.DeserializeObject<T>(cached);
}
/// <summary>
/// Sets the value asynchronous.
/// </summary>
/// <param name="identifier">Identifier.</param>
/// <param name="valueToStore">Value to store.</param>
/// <returns>True or false if the value was set</returns>
public async Task<bool> SetValueAsync(string identifier, string valueToStore)
{
if (_settings.IsUseExpiration)
_ = await _memoryCache.GetOrCreate(identifier, () =>
{
return Task.FromResult(valueToStore);
});
else
_ = await _memoryCache.GetOrCreate(identifier, () =>
{
return Task.FromResult(valueToStore);
}, timeSpan: TimeSpan.FromDays(_settings.DefaultExpirationInDays));
return true;
}
/// <summary>
/// Sets the value asynchronous.
/// </summary>
/// <param name="identifier">Identifier.</param>
/// <param name="valueToStore">Value to store.</param>
/// <param name="expiration">Expiration.</param>
/// <returns>True or false if the value was set</returns>
public async Task<bool> SetValueAsync(string identifier, string valueToStore, TimeSpan expiration)
{
_ = await _memoryCache.GetOrCreate(identifier, () =>
{
return Task.FromResult(valueToStore);
}, expiration);
return true;
}
/// <summary>
/// Sets the value asynchronous.
/// </summary>
/// <typeparam name="T">Generic</typeparam>
/// <param name="identifier">Identifier.</param>
/// <param name="objectToSerialize">Object to serialize.</param>
/// <returns>True or false if the value was set</returns>
public async Task<bool> SetValueAsync<T>(string identifier, T objectToSerialize)
{
var serialized = JsonConvert.SerializeObject(objectToSerialize);
if (_settings.IsUseExpiration)
_ = await SetValueAsync(identifier, serialized);
else
_ = await SetValueAsync(identifier, serialized, TimeSpan.FromDays(_settings.DefaultExpirationInDays));
return true;
}
/// <summary>
/// Sets the value asynchronous.
/// </summary>
/// <typeparam name="T">Generic</typeparam>
/// <param name="identifier">Identifier.</param>
/// <param name="objectToSerialize">Object to serialize.</param>
/// <param name="expiration">Expiration.</param>
/// <returns>True or false if the value was set</returns>
public async Task<bool> SetValueAsync<T>(string identifier, T objectToSerialize, TimeSpan expiration)
{
var serialized = JsonConvert.SerializeObject(objectToSerialize);
_ = await SetValueAsync(identifier, serialized, expiration);
return true;
}
/// <summary>
/// Determines whether [is identifier exists].
/// </summary>
/// <param name="identifier">Identifier.</param>
/// <returns>True or false if the identifier exists</returns>
public async Task<bool> DoesIdentifierExists(string identifier)
{
return await _memoryCache.DoesIdentifierExists(identifier);
}
#endregion Methods
}
And last for cache a settings class, we will be using this for placing the settings in a JSON file, database or whatever we can think of.
/// <summary>
/// Settings Interface
/// </summary>
public interface ISettings
{
#region Properties
/// <summary>
/// Gets or sets the name.
/// </summary>
/// <value>
/// Name.
/// </value>
string Name { get; set; }
/// <summary>
/// Gets or sets the type.
/// </summary>
/// <value>
/// Type.
/// </value>
string Type { get; set; }
#endregion Properties
}
/// <summary>
/// In Memory Settings
/// </summary>
/// <seealso cref="DarthSeldon.Settings.Abstractions.ISettings" />
public class InMemoryCacheSettings : ISettings
{
#region Properties
/// <summary>
/// Gets or sets the name.
/// </summary>
/// <value>
/// Name.
/// </value>
public string Name { get; set; }
/// <summary>
/// Gets or sets the type.
/// </summary>
/// <value>
/// Type.
/// </value>
public string Type { get; set; } = "MemoryCache";
/// <summary>
/// Gets or sets a value indicating whether this instance uses expiration of the cache.
/// </summary>
/// <value>
/// <c>true</c> if this instance is use expiration; otherwise, <c>false</c>.
/// </value>
public bool IsUseExpiration { get; set; }
/// <summary>
/// Gets or sets the default expiration in days. Default value 7.
/// </summary>
/// <value>
/// The default expiration in days.
/// </value>
public int DefaultExpirationInDays { get; set; } = 7;
#endregion Properties
}
You will notice the ISettings interface; I usually use this for dependency injection, serialization and deserialization. In another post I will show you what I mean.
Next key vault classes
Azure Key Vault C#
Again, we will try to abstract the key vault functionality with and IKeyVault interface.
/// <summary>
/// Interface Key Vault
/// </summary>
public interface IKeyVault
{
#region Properties
/// <summary>
/// Gets the name.
/// </summary>
/// <value>
/// The name.
/// </value>
string Name { get; }
#endregion Properties
#region Methods
/// <summary>
/// Gets the secret.
/// </summary>
/// <param name="name">The name.</param>
/// <returns>Secret</returns>
Task<string> GetSecret(string name);
/// <summary>
/// Sets the secret.
/// </summary>
/// <param name="key">The key.</param>
/// <param name="value">The value.</param>
/// <returns>Tur or false if the secret was set</returns>
Task<bool> SetSecret(string key, string value);
/// <summary>
/// Obtains the certificate.
/// </summary>
/// <param name="key">Key.</param>
/// <returns>Certificate</returns>
X509Certificate2 ObtainCertificate(string key);
/// <summary>
/// Obtains the certificate.
/// </summary>
/// <param name="key">Key.</param>
/// <returns>Certificate</returns>
Task<X509Certificate2> ObtainCertificateAsync(string key);
/// <summary>
/// Obtains the list of secrets.
/// </summary>
/// <returns>List of secrets</returns>
IReadOnlyCollection<string> ObtainListOfSecrets();
#endregion Methods
}
And the implementation of the interface with the Azure Implementation
/// <summary>
/// Azure Key Vault Helper
/// </summary>
/// <seealso cref="DarthSeldon.Services.KeyVault.Abstractions.IKeyVault" />
public class AzureKeyVault : IKeyVault
{
#region Members
/// <summary>
/// Settings
/// </summary>
private readonly AzureKeyVaultSettings _settings;
/// <summary>
/// Client secret credential
/// </summary>
private readonly ClientSecretCredential _clientSecretCredential;
/// <summary>
/// Secret Client
/// </summary>
private readonly SecretClient _client;
/// <summary>
/// Cache
/// </summary>
private readonly ICache _cache;
/// <summary>
/// Key vault name
/// </summary>
private readonly string _keyVaultName;
#endregion Members
#region Properties
/// <summary>
/// Gets the name.
/// </summary>
/// <value>
/// The name.
/// </value>
public string Name => _keyVaultName;
#endregion Properties
#region Constructors
/// <summary>
/// Initializes a new instance of the <see cref="AzureKeyVault" /> class.
/// </summary>
/// <param name="settings">Settings.</param>
/// <param name="iCaches">Caches.</param>
/// <exception cref="System.ArgumentException">The Azure Key Vault Settings are not initialized
/// or
/// The Azure Key Vault settings are not initialized correctly</exception>
/// <exception cref="ArgumentException">The Azure Key Vault Settings are not initialized
/// or
/// The Azure Key Vault settings are not initialized correctly</exception>
public AzureKeyVault(ISettings settings, IEnumerable<ICache> iCaches)
{
_settings = settings as AzureKeyVaultSettings;
if (_settings == null)
throw new ArgumentException("The Azure Key Vault Settings are not initialized");
if (string.IsNullOrEmpty(_settings.Name))
throw new ArgumentException("The Azure Key Vault settings are not initialized correctly, name is missing or is a duplicate from settings");
_keyVaultName = _settings.Name;
if (_settings.IsManagedServiceIdentity)
{
if (_settings.IsUseVisualStudioIdentity)
_client = new SecretClient(new Uri(_settings.AzureKeyVaultUri), new VisualStudioCredential());
else
_client = new SecretClient(new Uri(_settings.AzureKeyVaultUri), new DefaultAzureCredential());
}
else
{
if (string.IsNullOrEmpty(_settings.TenantId) || string.IsNullOrEmpty(_settings.ClientId) || string.IsNullOrEmpty(_settings.ClientSecret) || string.IsNullOrEmpty(_settings.AzureKeyVaultUri))
throw new ArgumentException("The Azure Key Vault settings are not initialized correctly");
_clientSecretCredential = new ClientSecretCredential(_settings.TenantId, _settings.ClientId, _settings.ClientSecret);
_client = new SecretClient(new Uri(_settings.AzureKeyVaultUri), _clientSecretCredential);
}
if (_settings.IsUseCache)
{
_cache = iCaches.FirstOrDefault(x => x.Name == _settings.CacheSettingsName);
if (_cache == null)
throw new ArgumentException($"The cache with name {_settings.CacheSettingsName} is not found");
}
}
#endregion Constructors
#region Methods
/// <summary>
/// Gets the secret.
/// </summary>
/// <param name="name">Name.</param>
/// <returns>Secret</returns>
/// <exception cref="System.ArgumentException">The secret key/name is required</exception>
/// <exception cref="System.ApplicationException">The cache is not initialized</exception>
public async Task<string> GetSecret(string name)
{
if (string.IsNullOrEmpty(name))
throw new ArgumentException("The secret key/name is required");
if (!_settings.IsUseCache)
{
var result = await _client.GetSecretAsync(name);
return result.Value.Value;
}
else
{
if (_cache != null)
{
try
{
return await _cache.GetValueAsync(name);
}
catch (Exception)
{
try
{
var result = await _client.GetSecretAsync(name);
await _cache.SetValueAsync(name, result.Value.Value);
return result.Value.Value;
}
catch (Exception ex)
{
throw new ApplicationException($"The cache is not initialized. {ex.Message}");
}
}
}
throw new ApplicationException("The cache is not initialized");
}
}
/// <summary>
/// Sets the secret.
/// </summary>
/// <param name="key">Key.</param>
/// <param name="value">Value.</param>
/// <returns>True or false if the secret was set</returns>
/// <exception cref="System.ArgumentException">The secret key/name is required
/// or
/// The secret value is required</exception>
/// <exception cref="System.ApplicationException">The cache is not initialized
/// or</exception>
/// <exception cref="ApplicationException"></exception>
public async Task<bool> SetSecret(string key, string value)
{
if (string.IsNullOrEmpty(key))
throw new ArgumentException("The secret key/name is required");
if (string.IsNullOrEmpty(value))
throw new ArgumentException("The secret value is required");
var keyVaultSecret = new KeyVaultSecret(key, value);
try
{
var result = await _client.SetSecretAsync(keyVaultSecret);
if (_settings.IsUseCache)
{
if (_cache == null)
throw new ApplicationException("The cache is not initialized");
return await _cache.SetValueAsync(key, value);
}
}
catch (Exception ex)
{
throw new ApplicationException(ex.Message);
}
return true;
}
/// <summary>
/// Obtains the certificate.
/// </summary>
/// <param name="key">Key.</param>
/// <returns>Certificate</returns>
/// <exception cref="System.ArgumentException">The certificate key/name is required</exception>
/// <exception cref="System.ApplicationException">The cache is not initialized
/// or</exception>
/// <exception cref="ApplicationException"></exception>
public X509Certificate2 ObtainCertificate(string key)
{
if (string.IsNullOrEmpty(key))
throw new ArgumentException("The certificate key/name is required");
try
{
if (!_settings.IsUseCache)
{
var secret = _client.GetSecret(key);
return new X509Certificate2(Convert.FromBase64String(secret.Value.Value), (string)null, X509KeyStorageFlags.Exportable);
}
else
{
if (_cache == null)
throw new ApplicationException("The cache is not initialized");
try
{
var cachedItem = _cache.GetValueAsync(key).Result;
return new X509Certificate2(Convert.FromBase64String(cachedItem), (string)null, X509KeyStorageFlags.Exportable);
}
catch (Exception)
{
try
{
var secret = _client.GetSecret(key);
_ = _cache.SetValueAsync(key, secret.Value.Value).Result;
return new X509Certificate2(Convert.FromBase64String(secret.Value.Value), (string)null, X509KeyStorageFlags.Exportable);
}
catch (Exception ex)
{
throw new ApplicationException(ex.Message);
}
}
}
}
catch (Exception ex)
{
throw new ApplicationException(ex.Message);
}
}
/// <summary>
/// Obtains the certificate asynchronous.
/// </summary>
/// <param name="key">Key.</param>
/// <returns>Certificate</returns>
/// <exception cref="System.ArgumentException">The certificate key/name is required</exception>
/// <exception cref="System.ApplicationException">The cache is not initialized
/// or</exception>
public async Task<X509Certificate2> ObtainCertificateAsync(string key)
{
if (string.IsNullOrEmpty(key))
throw new ArgumentException("The certificate key/name is required");
try
{
if (!_settings.IsUseCache)
{
var secret = await _client.GetSecretAsync(key);
return new X509Certificate2(Convert.FromBase64String(secret.Value.Value), (string)null, X509KeyStorageFlags.Exportable);
}
else
{
if (_cache == null)
throw new ApplicationException("The cache is not initialized");
try
{
var cachedItem = await _cache.GetValueAsync(key);
return new X509Certificate2(Convert.FromBase64String(cachedItem), (string)null, X509KeyStorageFlags.Exportable);
}
catch (Exception)
{
try
{
var secret = await _client.GetSecretAsync(key);
_ = await _cache.SetValueAsync(key, secret.Value.Value);
return new X509Certificate2(Convert.FromBase64String(secret.Value.Value), (string)null, X509KeyStorageFlags.Exportable);
}
catch (Exception ex)
{
throw new ApplicationException(ex.Message);
}
}
}
}
catch (Exception ex)
{
throw new ApplicationException(ex.Message);
}
}
/// <summary>
/// Deletes the cache entry.
/// </summary>
/// <param name="key">Key.</param>
/// <returns>True or false if the cache entry was deleted</returns>
/// <exception cref="System.ArgumentException">The key/name is required to delete a cached entry</exception>
/// <exception cref="System.ApplicationException">The cache is not initialized</exception>
/// <exception cref="ArgumentException">The key/name is required to delete a cached entry</exception>
public async Task<bool> DeleteCacheEntry(string key)
{
if (string.IsNullOrEmpty(key))
throw new ArgumentException("The key/name is required to delete a cached entry");
if (!_settings.IsUseCache)
{
return true;
}
if (_cache == null)
throw new ApplicationException("The cache is not initialized");
return await _cache.DeleteEntry(key);
}
/// <summary>
/// Obtains the list of secrets.
/// </summary>
/// <returns>List of secrets</returns>
public IReadOnlyCollection<string> ObtainListOfSecrets()
{
var secrets = new List<string>();
var allSecrets = _client.GetPropertiesOfSecrets();
foreach (var secretProperties in allSecrets)
secrets.Add(secretProperties.Name);
return secrets;
}
}
You will notice the section
if (_settings.IsManagedServiceIdentity)
{
if (_settings.IsUseVisualStudioIdentity)
_client = new SecretClient(new Uri(_settings.AzureKeyVaultUri), new VisualStudioCredential());
else
_client = new SecretClient(new Uri(_settings.AzureKeyVaultUri), new DefaultAzureCredential());
}
else
{
if (string.IsNullOrEmpty(_settings.TenantId) || string.IsNullOrEmpty(_settings.ClientId) || string.IsNullOrEmpty(_settings.ClientSecret) || string.IsNullOrEmpty(_settings.AzureKeyVaultUri))
throw new ArgumentException("The Azure Key Vault settings are not initialized correctly");
_clientSecretCredential = new ClientSecretCredential(_settings.TenantId, _settings.ClientId, _settings.ClientSecret);
_client = new SecretClient(new Uri(_settings.AzureKeyVaultUri), _clientSecretCredential);
}
Here we can set different authentication methods, we can use the visual studio credentials we set on our previous step or use the managed identity of azure (if deployed to azure) or even using good old client id and secret. However, we will focus ourselves with visual studio credentials.
Finally, the settings for the azure key vault.
/// <summary>
/// Azure Key Vault Settings
/// </summary>
/// <seealso cref="DarthSeldon.Settings.Abstractions.ISettings" />
public class AzureKeyVaultSettings : ISettings
{
#region Properties
/// <summary>
/// Gets or sets the name.
/// </summary>
/// <value>
/// Name.
/// </value>
public string Name { get; set; }
/// <summary>
/// Gets or sets the type.
/// </summary>
/// <value>
/// Type.
/// </value>
public string Type { get; set; } = "AzureKeyVaultSettings";
/// <summary>
/// Gets or sets the tenant identifier.
/// </summary>
/// <value>
/// The tenant identifier.
/// </value>
public string TenantId { get; set; }
/// <summary>
/// Gets or sets the client identifier.
/// </summary>
/// <value>
/// The client identifier.
/// </value>
public string ClientId { get; set; }
/// <summary>
/// Gets or sets the client secret.
/// </summary>
/// <value>
/// The client secret.
/// </value>
public string ClientSecret { get; set; }
/// <summary>
/// Gets or sets the azure key vault URI.
/// </summary>
/// <value>
/// The azure key vault URI.
/// </value>
public string AzureKeyVaultUri { get; set; }
/// <summary>
/// Gets or sets a value indicating whether this instance is use cache.
/// </summary>
/// <value>
/// <c>true</c> if this instance is use cache; otherwise, <c>false</c>.
/// </value>
public bool IsUseCache { get; set; } = false;
/// <summary>
/// Gets or sets the expiration in days. Default value 7.
/// </summary>
/// <value>
/// The expiration in days.
/// </value>
public int DefaultExpirationInDays { get; set; } = 7;
/// <summary>
/// Gets or sets the name of the cache settings.
/// </summary>
/// <value>
/// The name of the cache settings.
/// </value>
public string CacheSettingsName { get; set; }
/// <summary>
/// Gets or sets a value indicating whether this instance is managed service identity.
/// </summary>
/// <value>
/// <c>true</c> if this instance is managed service identity; otherwise, <c>false</c>.
/// </value>
public bool IsManagedServiceIdentity { get; set; } = false;
/// <summary>
/// Gets or sets a value indicating whether this instance is use visual studio identity.
/// </summary>
/// <value>
/// <c>true</c> if this instance is use visual studio identity; otherwise, <c>false</c>.
/// </value>
public bool IsUseVisualStudioIdentity { get; set; } = false;
#endregion Properties
}
Unit testing and dependency injection
/// <summary>
/// Initializes a new instance of the <see cref="UnitTestsIdentityServerClient"/> class.
/// </summary>
public UnitTestsIdentityServerClient()
{
var rootDirectory = new DirectoryInfo(Assembly.GetExecutingAssembly().Location)?.Parent?.Parent?.Parent?.Parent?.FullName;
if (rootDirectory == null)
throw new ApplicationException("No appsettings were found");
var fileName = "appsettings.json";
_configuration = new ConfigurationBuilder()
.SetBasePath(rootDirectory)
.AddJsonFile(fileName)
.AddUserSecrets<UnitTestsIdentityServerClient>()
.Build();
var services = new ServiceCollection();
services.AddSingleton<ICache, InMemoryCache>((c) =>
{
var settings = _configuration.GetSection("InMemoryCacheSettings").Get<InMemoryCacheSettings>();
return new InMemoryCache(settings);
});
services.AddScoped<IKeyVault, AzureKeyVault>((c) =>
{
var settings = _configuration.GetSection("AzureKeyVaultSettings").Get<AzureKeyVaultSettings>();
var caches = c.GetServices<ICache>();
return new AzureKeyVault(settings, caches);
});
services.AddScoped((c) =>
{
var settings = _configuration.GetSection("IdpServerClientSettings").Get<IdentityServerClientSettings>();
var caches = c.GetServices<ICache>();
var keyvaults = c.GetServices<IKeyVault>();
return new IdentityServerClient(settings, caches, keyvaults);
});
_serviceProvider = services.BuildServiceProvider();
}
In the constructor we will initialize a service collection and reading the configuration (yeah, yeah! the path to the root directory, it works right!)
Then we add the services to the collection and build the provider
/// <summary>
/// Tests the set secret.
/// </summary>
/// <exception cref="System.ArgumentNullException">Identity Server Client is not initialized</exception>
[Fact]
public async Task TestSetSecret()
{
var client = _serviceProvider.GetService<IdentityServerClient>();
if (client == null)
{
throw new ArgumentNullException("Identity Server Client is not initialized");
}
var result = await client.ObtainAuthorizationTokenSignedJWTAsync();
Assert.True(true);
}
And to test it, we get the service and call the method.
DI will take care of injecting the services. Note that the code uses Services instead of Service, this is if in the future we have more implementations of ICache and IKeyVault and we need to retrieve one in particular by name.
Happy Coding!!!