From 138fb281b55fd78001562feded11139fca4959ac Mon Sep 17 00:00:00 2001
From: "Christopher D. Cavell" <28095137+cdcavell@users.noreply.github.com>
Date: Sun, 19 Jul 2026 16:00:01 -0500
Subject: [PATCH 1/8] Add development-only insecure cookie override option
---
.../Options/ApplicationCookieAuthenticationOptions.cs | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/src/ProjectTemplate.Web/Authentication/Options/ApplicationCookieAuthenticationOptions.cs b/src/ProjectTemplate.Web/Authentication/Options/ApplicationCookieAuthenticationOptions.cs
index d527ba5..696e43c 100644
--- a/src/ProjectTemplate.Web/Authentication/Options/ApplicationCookieAuthenticationOptions.cs
+++ b/src/ProjectTemplate.Web/Authentication/Options/ApplicationCookieAuthenticationOptions.cs
@@ -39,4 +39,13 @@ public sealed class ApplicationCookieAuthenticationOptions
/// Gets or sets a value indicating whether the cookie expiration should be renewed during active use.
///
public bool SlidingExpiration { get; set; } = true;
+
+ ///
+ /// Gets or sets a value indicating whether local Development environments may issue the authentication cookie for
+ /// plain HTTP requests.
+ ///
+ ///
+ /// The default is . This override is rejected outside the Development environment.
+ ///
+ public bool AllowInsecureHttp { get; set; }
}
From 395b636fbddf5997901d67a972ac1e83547f3e64 Mon Sep 17 00:00:00 2001
From: "Christopher D. Cavell" <28095137+cdcavell@users.noreply.github.com>
Date: Sun, 19 Jul 2026 16:00:21 -0500
Subject: [PATCH 2/8] Require secure authentication cookies by default
---
.../AuthenticationServiceExtensions.cs | 26 ++++++++++++++++---
1 file changed, 23 insertions(+), 3 deletions(-)
diff --git a/src/ProjectTemplate.Web/Authentication/Extensions/AuthenticationServiceExtensions.cs b/src/ProjectTemplate.Web/Authentication/Extensions/AuthenticationServiceExtensions.cs
index e0ffb77..69a2631 100644
--- a/src/ProjectTemplate.Web/Authentication/Extensions/AuthenticationServiceExtensions.cs
+++ b/src/ProjectTemplate.Web/Authentication/Extensions/AuthenticationServiceExtensions.cs
@@ -17,7 +17,8 @@ namespace ProjectTemplate.Web.Authentication.Extensions;
public static class AuthenticationServiceExtensions
{
///
- /// Adds application authentication services based on configuration.
+ /// Adds application authentication services based on configuration using the secure cookie policy without an
+ /// environment-specific plain HTTP override.
///
/// The service collection to add authentication services to.
/// The application configuration source.
@@ -25,12 +26,28 @@ public static class AuthenticationServiceExtensions
public static IServiceCollection AddApplicationAuthentication(
this IServiceCollection services,
IConfiguration configuration)
+ {
+ return AddApplicationAuthentication(services, configuration, environment: null);
+ }
+
+ ///
+ /// Adds application authentication services based on configuration and the current hosting environment.
+ ///
+ /// The service collection to add authentication services to.
+ /// The application configuration source.
+ /// The current hosting environment.
+ /// The same instance for chaining.
+ public static IServiceCollection AddApplicationAuthentication(
+ this IServiceCollection services,
+ IConfiguration configuration,
+ IHostEnvironment? environment)
{
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(configuration);
services.AddTransient();
- services.AddSingleton, ApplicationAuthenticationOptionsValidator>();
+ services.AddSingleton>(
+ new ApplicationAuthenticationOptionsValidator(environment));
services
.AddOptions()
@@ -80,7 +97,10 @@ public static IServiceCollection AddApplicationAuthentication(
options.Cookie.Name = ".ProjectTemplate.Web.Authentication";
options.Cookie.HttpOnly = true;
options.Cookie.SameSite = SameSiteMode.Lax;
- options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
+ options.Cookie.SecurePolicy = applicationAuthenticationOptions.Cookie.AllowInsecureHttp &&
+ environment?.IsDevelopment() == true
+ ? CookieSecurePolicy.SameAsRequest
+ : CookieSecurePolicy.Always;
});
ApplicationAuthenticationOptions applicationAuthenticationOptions = configuration
From 61d6757c7232ce7d55732759481b90f4840054b6 Mon Sep 17 00:00:00 2001
From: "Christopher D. Cavell" <28095137+cdcavell@users.noreply.github.com>
Date: Sun, 19 Jul 2026 16:00:45 -0500
Subject: [PATCH 3/8] Reject insecure cookie override outside Development
---
.../ApplicationAuthenticationOptionsValidator.cs | 12 ++++++++++--
1 file changed, 10 insertions(+), 2 deletions(-)
diff --git a/src/ProjectTemplate.Web/Authentication/Options/ApplicationAuthenticationOptionsValidator.cs b/src/ProjectTemplate.Web/Authentication/Options/ApplicationAuthenticationOptionsValidator.cs
index 69a2bd2..c45e58f 100644
--- a/src/ProjectTemplate.Web/Authentication/Options/ApplicationAuthenticationOptionsValidator.cs
+++ b/src/ProjectTemplate.Web/Authentication/Options/ApplicationAuthenticationOptionsValidator.cs
@@ -7,8 +7,11 @@ namespace ProjectTemplate.Web.Authentication.Options;
///
/// Validates application authentication configuration during application startup.
///
-public sealed class ApplicationAuthenticationOptionsValidator : IValidateOptions
+public sealed class ApplicationAuthenticationOptionsValidator(IHostEnvironment? environment)
+ : IValidateOptions
{
+ private readonly IHostEnvironment? _environment = environment;
+
///
public ValidateOptionsResult Validate(string? name, ApplicationAuthenticationOptions options)
{
@@ -28,7 +31,7 @@ public ValidateOptionsResult Validate(string? name, ApplicationAuthenticationOpt
: ValidateOptionsResult.Fail(failures);
}
- private static void ValidateApplicationAuthentication(
+ private void ValidateApplicationAuthentication(
ApplicationAuthenticationOptions options,
ICollection failures)
{
@@ -66,6 +69,11 @@ private static void ValidateApplicationAuthentication(
options.Cookie.ExpireMinutes > 0,
"ProjectTemplate:Authentication:Cookie:ExpireMinutes must be greater than zero when application authentication is enabled.",
failures);
+
+ Require(
+ !options.Cookie.AllowInsecureHttp || _environment?.IsDevelopment() == true,
+ "ProjectTemplate:Authentication:Cookie:AllowInsecureHttp may only be enabled in the Development environment.",
+ failures);
}
private static void ValidateOpenIdConnectProvider(
From cc29257e3feb2b554776fdf04a3d98824cfc86c6 Mon Sep 17 00:00:00 2001
From: "Christopher D. Cavell" <28095137+cdcavell@users.noreply.github.com>
Date: Sun, 19 Jul 2026 16:01:02 -0500
Subject: [PATCH 4/8] Pass host environment to authentication registration
---
src/ProjectTemplate.Web/Program.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/ProjectTemplate.Web/Program.cs b/src/ProjectTemplate.Web/Program.cs
index 334360e..a2ee823 100644
--- a/src/ProjectTemplate.Web/Program.cs
+++ b/src/ProjectTemplate.Web/Program.cs
@@ -34,7 +34,7 @@
builder.Services.AddApplicationRequestLogging(builder.Configuration);
builder.Services.AddApplicationOpenTelemetry(builder.Configuration, builder.Environment);
builder.Services.AddApplicationProblemDetails(builder.Environment);
- builder.Services.AddApplicationAuthentication(builder.Configuration);
+ builder.Services.AddApplicationAuthentication(builder.Configuration, builder.Environment);
builder.Services.AddApplicationAuthorization(builder.Configuration);
builder.Services.AddApplicationDataAccess(builder.Configuration);
From be11364514581948635fba699116c3218dfa10e0 Mon Sep 17 00:00:00 2001
From: "Christopher D. Cavell" <28095137+cdcavell@users.noreply.github.com>
Date: Sun, 19 Jul 2026 16:01:20 -0500
Subject: [PATCH 5/8] Add authentication cookie Secure policy tests
---
.../AuthenticationCookieSecurePolicyTests.cs | 91 +++++++++++++++++++
1 file changed, 91 insertions(+)
create mode 100644 tests/ProjectTemplate.Web.Tests/AuthenticationCookieSecurePolicyTests.cs
diff --git a/tests/ProjectTemplate.Web.Tests/AuthenticationCookieSecurePolicyTests.cs b/tests/ProjectTemplate.Web.Tests/AuthenticationCookieSecurePolicyTests.cs
new file mode 100644
index 0000000..e30f184
--- /dev/null
+++ b/tests/ProjectTemplate.Web.Tests/AuthenticationCookieSecurePolicyTests.cs
@@ -0,0 +1,91 @@
+using Microsoft.AspNetCore.Authentication.Cookies;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.FileProviders;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Options;
+using ProjectTemplate.Web.Authentication.Extensions;
+using ProjectTemplate.Web.Authentication.Options;
+
+namespace ProjectTemplate.Web.Tests;
+
+public sealed class AuthenticationCookieSecurePolicyTests
+{
+ [Fact]
+ public void CookieSecurePolicy_DefaultsToAlways()
+ {
+ using ServiceProvider serviceProvider = CreateServiceProvider(
+ Environments.Production,
+ new Dictionary());
+
+ CookieAuthenticationOptions options = serviceProvider
+ .GetRequiredService>()
+ .Get(CookieAuthenticationDefaults.AuthenticationScheme);
+
+ Assert.Equal(CookieSecurePolicy.Always, options.Cookie.SecurePolicy);
+ }
+
+ [Fact]
+ public void CookieSecurePolicy_DevelopmentOverride_UsesSameAsRequest()
+ {
+ using ServiceProvider serviceProvider = CreateServiceProvider(
+ Environments.Development,
+ new Dictionary
+ {
+ ["ProjectTemplate:Authentication:Cookie:AllowInsecureHttp"] = "true"
+ });
+
+ CookieAuthenticationOptions options = serviceProvider
+ .GetRequiredService>()
+ .Get(CookieAuthenticationDefaults.AuthenticationScheme);
+
+ Assert.Equal(CookieSecurePolicy.SameAsRequest, options.Cookie.SecurePolicy);
+ }
+
+ [Fact]
+ public void CookieSecurePolicy_InsecureOverrideOutsideDevelopment_FailsValidation()
+ {
+ using ServiceProvider serviceProvider = CreateServiceProvider(
+ Environments.Production,
+ new Dictionary
+ {
+ ["ProjectTemplate:Authentication:Cookie:AllowInsecureHttp"] = "true"
+ });
+
+ OptionsValidationException exception = Assert.Throws(() =>
+ serviceProvider
+ .GetRequiredService>()
+ .Value);
+
+ Assert.Contains(
+ "ProjectTemplate:Authentication:Cookie:AllowInsecureHttp may only be enabled in the Development environment",
+ exception.Message,
+ StringComparison.Ordinal);
+ }
+
+ private static ServiceProvider CreateServiceProvider(
+ string environmentName,
+ IReadOnlyDictionary configurationValues)
+ {
+ IConfiguration configuration = new ConfigurationBuilder()
+ .AddInMemoryCollection(configurationValues)
+ .Build();
+ TestHostEnvironment environment = new(environmentName);
+ ServiceCollection services = new();
+ services.AddLogging();
+ services.AddApplicationAuthentication(configuration, environment);
+
+ return services.BuildServiceProvider(validateScopes: true);
+ }
+
+ private sealed class TestHostEnvironment(string environmentName) : IHostEnvironment
+ {
+ public string EnvironmentName { get; set; } = environmentName;
+
+ public string ApplicationName { get; set; } = "ProjectTemplate.Web.Tests";
+
+ public string ContentRootPath { get; set; } = AppContext.BaseDirectory;
+
+ public IFileProvider ContentRootFileProvider { get; set; } = new NullFileProvider();
+ }
+}
From 740cc7749cf2aa1a8acc37ed3ced99fd82a2dee1 Mon Sep 17 00:00:00 2001
From: "Christopher D. Cavell" <28095137+cdcavell@users.noreply.github.com>
Date: Sun, 19 Jul 2026 16:01:45 -0500
Subject: [PATCH 6/8] Document secure authentication cookie policy
---
docs/articles/authentication-hardening.md | 22 ++++++++++++++++++++++
1 file changed, 22 insertions(+)
diff --git a/docs/articles/authentication-hardening.md b/docs/articles/authentication-hardening.md
index d17d8ee..fb9ca64 100644
--- a/docs/articles/authentication-hardening.md
+++ b/docs/articles/authentication-hardening.md
@@ -27,6 +27,28 @@ Before deployment, confirm:
- Confirm login, callback, logout, and access-denied flows use the public origin.
- Test security headers, HTTPS redirection, and callbacks behind the deployed proxy.
+## Authentication Cookie Secure Policy
+
+The local authentication cookie uses `CookieSecurePolicy.Always` by default. The cookie therefore receives the `Secure` attribute independently of the request scheme perceived by the application. Production deployments must serve authentication flows over HTTPS; a reverse-proxy or forwarded-header misconfiguration does not downgrade this cookie to a non-secure cookie.
+
+Local plain-HTTP authentication is available only through an explicit Development-only override:
+
+```json
+{
+ "ProjectTemplate": {
+ "Authentication": {
+ "Cookie": {
+ "AllowInsecureHttp": true
+ }
+ }
+ }
+}
+```
+
+Place this override only in local `appsettings.Development.json`, user secrets, or another Development-only configuration source. When enabled in Development, the cookie uses `CookieSecurePolicy.SameAsRequest`, so an HTTP request can receive the cookie without the `Secure` attribute.
+
+Startup validation rejects `ProjectTemplate:Authentication:Cookie:AllowInsecureHttp=true` in Testing, Staging, Production, or any environment other than Development. Do not use the override to compensate for TLS, proxy, forwarded-header, certificate, or callback configuration problems.
+
## Redirect, Callback, ACS, and Logout URLs
- Register OIDC and OAuth redirect URIs exactly.
From cb297ef8c6395df5d2c205236b1e7fe3d29b74f6 Mon Sep 17 00:00:00 2001
From: "Christopher D. Cavell" <28095137+cdcavell@users.noreply.github.com>
Date: Sun, 19 Jul 2026 16:03:59 -0500
Subject: [PATCH 7/8] Import cookie Secure policy type in tests
---
.../AuthenticationCookieSecurePolicyTests.cs | 1 +
1 file changed, 1 insertion(+)
diff --git a/tests/ProjectTemplate.Web.Tests/AuthenticationCookieSecurePolicyTests.cs b/tests/ProjectTemplate.Web.Tests/AuthenticationCookieSecurePolicyTests.cs
index e30f184..26de2bc 100644
--- a/tests/ProjectTemplate.Web.Tests/AuthenticationCookieSecurePolicyTests.cs
+++ b/tests/ProjectTemplate.Web.Tests/AuthenticationCookieSecurePolicyTests.cs
@@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Authentication.Cookies;
+using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders;
From 176080f9755c9ad7e01de2e2889debd38a0bb380 Mon Sep 17 00:00:00 2001
From: "Christopher D. Cavell" <28095137+cdcavell@users.noreply.github.com>
Date: Sun, 19 Jul 2026 16:06:20 -0500
Subject: [PATCH 8/8] Validate insecure cookie override independently
---
.../ApplicationAuthenticationOptionsValidator.cs | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/src/ProjectTemplate.Web/Authentication/Options/ApplicationAuthenticationOptionsValidator.cs b/src/ProjectTemplate.Web/Authentication/Options/ApplicationAuthenticationOptionsValidator.cs
index c45e58f..2ef8fe0 100644
--- a/src/ProjectTemplate.Web/Authentication/Options/ApplicationAuthenticationOptionsValidator.cs
+++ b/src/ProjectTemplate.Web/Authentication/Options/ApplicationAuthenticationOptionsValidator.cs
@@ -50,6 +50,11 @@ private void ValidateApplicationAuthentication(
"ProjectTemplate:Authentication:DefaultSignInScheme is required.",
failures);
+ Require(
+ !options.Cookie.AllowInsecureHttp || _environment?.IsDevelopment() == true,
+ "ProjectTemplate:Authentication:Cookie:AllowInsecureHttp may only be enabled in the Development environment.",
+ failures);
+
if (!options.Enabled)
{
return;
@@ -69,11 +74,6 @@ private void ValidateApplicationAuthentication(
options.Cookie.ExpireMinutes > 0,
"ProjectTemplate:Authentication:Cookie:ExpireMinutes must be greater than zero when application authentication is enabled.",
failures);
-
- Require(
- !options.Cookie.AllowInsecureHttp || _environment?.IsDevelopment() == true,
- "ProjectTemplate:Authentication:Cookie:AllowInsecureHttp may only be enabled in the Development environment.",
- failures);
}
private static void ValidateOpenIdConnectProvider(