That was the interesting part of this configuration update. The visible code change was tiny: the committed value for SSO:AzureAD_ClientSecret was removed from appsettings.json and appsettings.Production.json, leaving the key present but empty. The actual secret was then expected to come from PaaSPortal-managed application settings at deployment time.
On paper, that sounds almost too small to write about. In practice, it is exactly the kind of change that shows whether an ASP.NET Core application has been wired correctly for modern platform configuration.
When configuration is already flowing through the built-in ASP.NET Core pipeline, moving a secret from file storage to platform storage should not require rewriting the app. It should require trusting the configuration stack you already have.
In this case, the application was already configured to build configuration from JSON files and then layer environment variables on top:
public static IConfiguration Configuration { get; } =
new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", false, true)
.AddJsonFile(
$"appsettings.{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT")?.ToLower()}.json",
true,
true)
.AddEnvironmentVariables()
.Build();
That last line is the whole story. Once environment variables are part of the configuration chain, a platform like PaaSPortal can supply deployment-time values without forcing the rest of the application to care where they came from.
Keep the Key, Remove the Secret
The implementation detail I like most here is that the key itself stays in the settings structure, but the committed secret value does not.
That gives the team a few practical benefits at once:
- the shape of the configuration remains visible to developers
- deployment documentation stays aligned with the runtime key names
- sensitive values stop living in source-controlled JSON
- rotation becomes a platform operation instead of a repository change
This is a subtle but important distinction. Removing the entire key can make the system harder to reason about because developers lose the configuration contract. Leaving the key in place but blank makes the contract explicit while keeping ownership of the value in the deployment platform.
Why the App Still Starts Cleanly
The reason this change works cleanly is that the startup path already reads the Azure AD B2C settings through IConfiguration rather than through hard-coded file access.
var b2cClientSecret = FirstNonEmpty(
_configuration["SSO:AzureAD_ClientSecret"],
_configuration["SSO:AzureAD:ClientSecret"],
_configuration["UE:AzureAD:ClientSecret"]);
This is doing more than simple lookup. It gives the application a compatibility layer across multiple key conventions while still delegating the actual value source to the ASP.NET Core configuration system.
That means the app does not need to know whether the secret came from:
appsettings.jsonappsettings.Production.json- a PaaSPortal application setting exposed as an environment variable
- some future configuration provider added later
It only needs to know the key names it is willing to accept.
This Pattern Was Already Used Elsewhere
Another reason the change feels solid is that the codebase already has a configuration abstraction for runtime settings:
public class ConfigurationService : IConfigurationService
{
private readonly IConfiguration _configuration;
public ConfigurationService(IConfiguration configuration) => _configuration = configuration;
public T GetAppSetting<T>(string key, T defaultValue)
{
var value = _configuration[key];
...
}
public string GetConnectionString(string name)
{
var entry = _configuration[$"ConnectionStrings:{name}"];
return entry;
}
}
That service is registered centrally and reused across the application for external API settings, media integrations, scheduled jobs, and other runtime configuration. In other words, the codebase was already organized around configuration providers, not file parsing. That is exactly the foundation you want before moving sensitive settings into a platform portal.
Why PaaSPortal Is the Better Owner for This Value
Not every configuration value deserves the same treatment. A client ID is usually far less sensitive than a client secret. Redirect paths and policy names are application behavior. Secrets are operational credentials. Those should not have the same ownership model.
Moving the Azure AD client secret into PaaSPortal changes who is responsible for the value:
- the repository owns the configuration schema
- the application owns how the key is resolved
- the deployment platform owns the secret value itself
That split is healthier for real systems. It reduces accidental exposure, avoids unnecessary config churn in pull requests, and makes secret rotation possible without application edits.
The Technical Lesson Is About Precedence, Not Just Storage
What makes this change reliable is not just “we used PaaSPortal.” It is that the application already respected configuration precedence correctly.
Once environment variables are layered after JSON, the platform can override file-based defaults naturally. That is why a change like this can stay small. The platform does not need a special code path if the configuration pipeline already understands precedence.
This is one of the most useful design habits in ASP.NET Core applications:
- read settings through
IConfigurationor options binding - avoid direct file assumptions in business code
- leave room for multiple key shapes during migrations
- treat secrets as deployment-owned values, not repo-owned values
Why This Was More Than a Cleanup Commit
At first glance, the change looks like a secret-removal cleanup. It was that, but it was also a platform-alignment change. It moved the application closer to the way cloud deployments are supposed to work: code defines the configuration contract, and the platform provides the sensitive values at runtime.
That is why I like small diffs like this one. They often reveal whether the architecture beneath them is healthy. If deleting a secret from JSON breaks the app, the configuration model is too tightly coupled to files. If deleting it works because the platform already supplies the key, the application is using ASP.NET Core configuration the way it was meant to be used.
That is not just cleaner. It is safer, easier to operate, and much closer to what we want from production configuration management.