This is part 4 of a series where I have been looking at moving to a SOLID approach of implementing configuration binding in ASP.Net Core using a bridging class to remove the need for consumers of the configuration object to use IOptions
In this post I move onto looking at injecting some functionality into the bridge class to decrypt settings and validate the settings read. Lastly I show registering the bridge class via multiple fine grained interfaces.
To follow along with this post, I suggest you download the full solution source code from the Github repo at https://github.com/configureappio/ConfiguarationBridgeCrypto as there is far too much code to display in this post.
I will start with the changes to the Startup.cs ConfigureServices method which gives a structure to the changes we will be making.
The main highlights are:
- A factory class is registered via its interface to decrypt values read from the settings
- A class via its interface is registered to validate the settings
- The bridge class is registered via an aggregate interface
- A resolution lambda is registered for each of the component interfaces that make up the aggregate interface.
The classes and interfaces are now looked at in more detail below.
For the purpose of this demonstration, I am assuming that for one reason or another, the standard secure configuration providers such as Azure Key Vault cannot be used for one reason or another, so we are having to deal with encrypting the settings ourselves.
Heath Warning !!!
In this demo, the encrypted settings are in the main appsettings.json. DO NOT DO THIS IN THE REAL WORLD! Stick to the mantra that you should not put any secrets in your code source control. Always think, "Would I be OK with the source code repo going open source?"
In the source code, I have included code to read from an external file outside of the web code location so that the secrets are maintained outside of source code, but the settings could come from environmental variables or the command line. If copying the source code that accompanies this post, I suggest copying the appsettings.json to the location shown and removing outside of source code. It is up to you where to store it.
To keep things clean, all encrypted values are held in a dictionary within the AppSettings class in a property called Secrets. It will be the responsibility of the bridge class to decrypt the secrets and inject them into the properties exposed via the interfaces (more on this later).
The appsettings.json and matching MyAppSettings class will therefore look like this:
At this point, the DI container has been configured to return IOptionsSnapshot
The secrets dictionary key/value pairs have been encrypted using a hash for the key and AESManaged for the value. Both have then been Base64 encoded so that they can be cleanly represented in the JSON.
In order for the bridge class to decrypt the dictionary, we will need a class that will get injected into the bridge class via an ICryptoAlgorithm interface. To keep things flexible, we will use a factory pattern to create the decryptor instance.
In the example code, I register the results of calling the factory as a singleton that. However, you may want to register the factory and inject that into classes if you want to use multiple cryptographic algorithms or salt/password combinations.
Once we have the decryptor registered, we need to apply it to the settings. For this, we will have a SettingsDecryptor class that implements an ISettingsDecrypt interface.
This is registered with the DI container services ready to be used by the bridge class. The Decrypt method in the example takes a plain text version of the dictionary key then
- hashes it with the method exposed by the injected decryptor,
- looks up the hashed key (that is in the appsettings.json)
- then decrypts the value for that key.
So with these pieces in place, we have the components for decrypting the settings in the bridge class.
Before looking at the bridge, we will look at injecting functionality to validate the settings.
By injecting a settings validator into the bridge class, we have the ability to catch any problems before the rest of our code tries to use the values (encrypted or not).
The class implements an interface that validates the settings and returns a boolean to indicate success or failure. If validation has failed, an AggregateException instance is available that holds one or more validation exceptions.
There are more elegant ways in which this can be approached, but I used this approach for simplicity to illustrate the principle of injecting a validator into the bridge.
Once the validator is registered as a DI service, we are now ready to register the bridge class that takes both the decryptor and validator.
The example above is fairly self-explanatory at a high level. The constructor takes the IOptionsShapshot
We then have a decryptor which is stored as an instance field for use by the property getters to decrypt values read from the settings object.
We then have a validator instance which we call immediately to validate the settings and throw an exception if there is a problem.
The three properties exposed are proxies to the underlying settings class, with decryption taking place where necessary.
The bridge class implements the IAppSettingsResolved interface which is an aggregate of three other interfaces.
This has been done to illustrate that the settings can be registered as multiple interfaces to allow for interface segregation as part of the SOLID approach. E.g. if a controller is only interested in the SQL Server connection string, it can just ask for that rather than the full IAppSettingsResolved. This makes it easier to implement just the required functionality in any mocks when unit testing or any other implementation you may want to register.
The registration above uses the service for IAppSettingsResolved to resolve the three other interfaces.
Having described the working parts above, we can come back to the ConfigureSevices method to tie it together.
Here we have done the following
- Registered an IOptionsSnapshot
using the Configure method to bind the "MyAppSettings" configuration section to an object instance
- Registered a decryption algorithm
- Registered a class instance to decrypt the key/value pairs in the Secrets dictionary using the algorithm
- Registered a class instance to validate the settingsop
- Registered a class instance to act as the bridge/proxy to the IOptionsSnapshot
and decrypt the values
- Registered the resolved bridge class using its multiple exposed interfaces for finer grained use
With the last of these in place, our controllers and any other dependant classes can choose whether to get the settings as a whole via IAppSettingsResolved or one of the finer grained interfaces
- IAppSettings - the non-encrypted values
- ISqlConnectionString - the decrypted SQL connection string
- IOracleConnectionString - the decrypted Oracle connection string
That wraps up this series of posts for now, but you may want to take things further now that the settings class can have functionality injected into it. Possibilities include
- Using connection string builders to create connection strings using multiple properties (some encrypted, some not) from the bound object
- Using the ICryptoFactory instead of ICryptoAlgorithm to use multiple algorithms for different properties
Don't forget, the full source code including a WPF app to encrypt the settings dictionary can the downloaded from the Github repo at https://github.com/configureappio/ConfiguarationBridgeCrypto
Thanks for reading.