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(