Skip to content

Commit d206d78

Browse files
authored
Add HTTP Strict Transport Security (HSTS) model settings (#19333)
1 parent 70588a6 commit d206d78

10 files changed

Lines changed: 239 additions & 25 deletions

File tree

AGENTS.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,13 @@ The project uses:
243243
- `AnalysisLevel` set to `latest-Recommended`
244244
- Specific CA rules are suppressed (see `Directory.Build.props`)
245245

246+
### Documentation
247+
248+
- Update the canonical page under `src\docs` whenever a change affects user-facing behavior, configuration, setup, public APIs, stereotypes, or extension points.
249+
- Add or update XML `<summary>` documentation for new or modified public interfaces, domain models, enums, and other public members that are part of the change.
250+
- Document each enum member individually when its behavior is relevant to users or downstream developers.
251+
- When XML documentation already exists, revise the existing block in place and keep `<param>` tags accurate and in signature order.
252+
246253
## Database Patterns
247254

248255
### YesSql Usage

src/OrchardCore.Modules/OrchardCore.Https/Drivers/HttpsSettingsDisplayDriver.cs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,10 @@ public override async Task<IDisplayResult> EditAsync(ISite site, HttpsSettings s
5555

5656
if (!isHttpsRequest)
5757
{
58-
await _notifier.WarningAsync(H["For safety, Enabling require HTTPS over HTTP has been prevented."]);
58+
await _notifier.WarningAsync(H["For safety, changing HTTPS-only settings over HTTP has been prevented."]);
5959
}
6060

61-
model.EnableStrictTransportSecurity = settings.EnableStrictTransportSecurity;
61+
model.StrictTransportSecurityMode = settings.StrictTransportSecurityMode;
6262
model.IsHttpsRequest = isHttpsRequest;
6363
model.RequireHttps = settings.RequireHttps;
6464
model.RequireHttpsPermanent = settings.RequireHttpsPermanent;
@@ -82,7 +82,12 @@ public override async Task<IDisplayResult> UpdateAsync(ISite site, HttpsSettings
8282

8383
await context.Updater.TryUpdateModelAsync(model, Prefix);
8484

85-
settings.EnableStrictTransportSecurity = model.EnableStrictTransportSecurity;
85+
if (!_httpContextAccessor.HttpContext.Request.IsHttps)
86+
{
87+
return await EditAsync(site, settings, context);
88+
}
89+
90+
settings.StrictTransportSecurityMode = model.StrictTransportSecurityMode;
8691
settings.RequireHttps = model.RequireHttps;
8792
settings.RequireHttpsPermanent = model.RequireHttpsPermanent;
8893
settings.SslPort = model.SslPort;
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
using System.Text.Json.Nodes;
2+
using OrchardCore.Data.Migration;
3+
using OrchardCore.Https.Settings;
4+
using OrchardCore.Settings;
5+
6+
namespace OrchardCore.Https.Migrations;
7+
8+
internal sealed class HttpsSettingsMigrations : DataMigration
9+
{
10+
private readonly ISiteService _siteService;
11+
12+
public HttpsSettingsMigrations(ISiteService siteService)
13+
{
14+
_siteService = siteService;
15+
}
16+
17+
[Obsolete]
18+
public async Task<int> CreateAsync()
19+
{
20+
var site = await _siteService.LoadSiteSettingsAsync();
21+
22+
if (site.Properties[nameof(HttpsSettings)] is not JsonObject settingsObject)
23+
{
24+
return 1;
25+
}
26+
27+
var requiresUpdate = false;
28+
29+
if (!settingsObject.ContainsKey(nameof(HttpsSettings.StrictTransportSecurityMode)) &&
30+
settingsObject[nameof(HttpsSettings.EnableStrictTransportSecurity)] is JsonValue strictTransportSecurityValue &&
31+
strictTransportSecurityValue.TryGetValue<bool>(out var enableStrictTransportSecurity))
32+
{
33+
settingsObject[nameof(HttpsSettings.StrictTransportSecurityMode)] = enableStrictTransportSecurity
34+
? nameof(HttpStrictTransportSecurityMode.Enabled)
35+
: nameof(HttpStrictTransportSecurityMode.Disabled);
36+
37+
requiresUpdate = true;
38+
}
39+
40+
if (settingsObject.Remove(nameof(HttpsSettings.EnableStrictTransportSecurity)))
41+
{
42+
requiresUpdate = true;
43+
}
44+
45+
if (requiresUpdate)
46+
{
47+
await _siteService.UpdateSiteSettingsAsync(site);
48+
}
49+
50+
return 1;
51+
}
52+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
namespace OrchardCore.Https.Settings;
2+
3+
/// <summary>
4+
/// Determines how HTTP Strict Transport Security (HSTS) is applied for the tenant.
5+
/// </summary>
6+
public enum HttpStrictTransportSecurityMode
7+
{
8+
/// <summary>
9+
/// Always disables HSTS regardless of the current environment.
10+
/// </summary>
11+
Disabled,
12+
13+
/// <summary>
14+
/// Always enables HSTS regardless of the current environment.
15+
/// </summary>
16+
Enabled,
17+
18+
/// <summary>
19+
/// Enables HSTS in the Production environment and disables it in other environments.
20+
/// </summary>
21+
FromConfiguration,
22+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,36 @@
1+
using System.Text.Json.Serialization;
2+
13
namespace OrchardCore.Https.Settings;
24

5+
/// <summary>
6+
/// Stores the HTTPS site settings for a tenant.
7+
/// </summary>
38
public class HttpsSettings
49
{
10+
/// <summary>
11+
/// Gets or sets the legacy HSTS toggle retained only so stored settings can be migrated to <see cref="StrictTransportSecurityMode"/>.
12+
/// </summary>
13+
[JsonIgnore]
14+
[Obsolete("This property is obsolete and will be removed in future releases. Use StrictTransportSecurityMode instead.")]
515
public bool EnableStrictTransportSecurity { get; set; }
16+
17+
/// <summary>
18+
/// Gets or sets how HTTP Strict Transport Security (HSTS) is applied.
19+
/// </summary>
20+
public HttpStrictTransportSecurityMode StrictTransportSecurityMode { get; set; } = HttpStrictTransportSecurityMode.Disabled;
21+
22+
/// <summary>
23+
/// Gets or sets a value indicating whether all requests should be redirected to HTTPS.
24+
/// </summary>
625
public bool RequireHttps { get; set; }
26+
27+
/// <summary>
28+
/// Gets or sets a value indicating whether HTTPS redirects should use a permanent redirect status code.
29+
/// </summary>
730
public bool RequireHttpsPermanent { get; set; }
31+
32+
/// <summary>
33+
/// Gets or sets the HTTPS port to use for redirection when it cannot be inferred automatically.
34+
/// </summary>
835
public int? SslPort { get; set; }
936
}

src/OrchardCore.Modules/OrchardCore.Https/Startup.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@
33
using Microsoft.AspNetCore.HttpsPolicy;
44
using Microsoft.AspNetCore.Routing;
55
using Microsoft.Extensions.DependencyInjection;
6+
using Microsoft.Extensions.Hosting;
7+
using OrchardCore.Data.Migration;
68
using OrchardCore.DisplayManagement.Handlers;
79
using OrchardCore.Https.Drivers;
10+
using OrchardCore.Https.Migrations;
811
using OrchardCore.Https.Services;
912
using OrchardCore.Https.Settings;
1013
using OrchardCore.Modules;
@@ -25,7 +28,9 @@ public override async ValueTask ConfigureAsync(IApplicationBuilder app, IEndpoin
2528
app.UseHttpsRedirection();
2629
}
2730

28-
if (settings.EnableStrictTransportSecurity)
31+
if (settings.StrictTransportSecurityMode == HttpStrictTransportSecurityMode.Enabled ||
32+
(settings.StrictTransportSecurityMode == HttpStrictTransportSecurityMode.FromConfiguration &&
33+
serviceProvider.GetRequiredService<IHostEnvironment>().IsProduction()))
2934
{
3035
app.UseHsts();
3136
}
@@ -36,6 +41,7 @@ public override void ConfigureServices(IServiceCollection services)
3641
services.AddSiteDisplayDriver<HttpsSettingsDisplayDriver>();
3742
services.AddNavigationProvider<AdminMenu>();
3843
services.AddSingleton<IHttpsService, HttpsService>();
44+
services.AddDataMigration<HttpsSettingsMigrations>();
3945

4046
services.AddPermissionProvider<Permissions>();
4147

src/OrchardCore.Modules/OrchardCore.Https/ViewModels/HttpsSettingsViewModel.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1+
using OrchardCore.Https.Settings;
2+
13
namespace OrchardCore.Https.ViewModels;
24

35
public class HttpsSettingsViewModel
46
{
57
public bool IsHttpsRequest { get; set; }
6-
public bool EnableStrictTransportSecurity { get; set; }
8+
public HttpStrictTransportSecurityMode StrictTransportSecurityMode { get; set; }
79
public bool RequireHttps { get; set; }
810
public bool RequireHttpsPermanent { get; set; }
911
public int? SslPort { get; set; }

src/OrchardCore.Modules/OrchardCore.Https/Views/HttpsSettings.Edit.cshtml

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,17 @@
1010
</div>
1111
</div>
1212

13-
<div class="ocat-wrapper" asp-validation-class-for="EnableStrictTransportSecurity">
14-
<div class="ocat-end-offset">
15-
<div class="form-check">
16-
<input type="checkbox" class="form-check-input" asp-for="EnableStrictTransportSecurity" asp-is-disabled="@(!Model.IsHttpsRequest)" />
17-
<label class="form-check-label" asp-for="EnableStrictTransportSecurity">@T["Enable HSTS"]</label>
18-
<span class="hint dashed">@T["Indicates to browsers that connecting without transport security (e.g SSL or TLS) isn't allowed."]</span>
19-
</div>
20-
</div>
21-
</div>
13+
<div class="ocat-wrapper" asp-validation-class-for="StrictTransportSecurityMode">
14+
<label asp-for="StrictTransportSecurityMode" class="ocat-label">@T["HSTS mode"]</label>
15+
<div class="ocat-end">
16+
<select asp-for="StrictTransportSecurityMode" class="form-select" asp-is-disabled="@(!Model.IsHttpsRequest)">
17+
<option value="@nameof(OrchardCore.Https.Settings.HttpStrictTransportSecurityMode.Disabled)">@T["Disabled"]</option>
18+
<option value="@nameof(OrchardCore.Https.Settings.HttpStrictTransportSecurityMode.Enabled)">@T["Enabled"]</option>
19+
<option value="@nameof(OrchardCore.Https.Settings.HttpStrictTransportSecurityMode.FromConfiguration)">@T["From environment — enabled in Production, disabled otherwise"]</option>
20+
</select>
21+
<span class="hint">@T["Determines whether HTTP Strict Transport Security (HSTS) headers are disabled by default, enabled explicitly, or follow the current environment."]</span>
22+
<div class="alert alert-danger mt-3">@T["Use HSTS with caution, as browsers may stop connecting over HTTP after receiving the header if HTTPS later becomes unavailable."]</div>
2223

23-
<div class="ocat-wrapper">
24-
<div class="ocat-end-offset">
25-
<div class="alert alert-danger">@T["This option should be enabled with caution, as it may prevent users from connecting if HTTPS was later disabled or wasn't available."]</div>
2624
</div>
2725
</div>
2826

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# HTTPS (`OrchardCore.Https`)
22

3-
The module will ensure HTTPS is used when accessing the website. You can force HTTPS on all pages, enable HSTS, and configure the HTTPS port.
3+
The module will ensure HTTPS is used when accessing the website. You can force HTTPS on all pages, choose how HSTS is applied, and configure the HTTPS port.
44

55
## Recipe Configuration
66

@@ -12,7 +12,7 @@ HTTPS settings can be configured using the `Settings` recipe step:
1212
{
1313
"name": "settings",
1414
"HttpsSettings": {
15-
"EnableStrictTransportSecurity": true,
15+
"StrictTransportSecurityMode": "Disabled",
1616
"RequireHttps": true,
1717
"RequireHttpsPermanent": false,
1818
"SslPort": 443
@@ -22,9 +22,17 @@ HTTPS settings can be configured using the `Settings` recipe step:
2222
}
2323
```
2424

25-
| Property | Type | Description |
26-
|---------------------------------|---------|------------------------------------------------------------------|
27-
| `EnableStrictTransportSecurity` | Boolean | Whether to enable HTTP Strict Transport Security (HSTS) headers. |
28-
| `RequireHttps` | Boolean | Whether to require HTTPS for all requests. |
29-
| `RequireHttpsPermanent` | Boolean | Whether to use a permanent (301) redirect for HTTPS redirection. |
30-
| `SslPort` | Integer | The port number for SSL connections. |
25+
| Property | Type | Description |
26+
|---------------------------------|---------|----------------------------------------------------------------------------------------------|
27+
| `StrictTransportSecurityMode` | String | Whether HTTP Strict Transport Security (HSTS) is `Disabled` by default, `Enabled`, or `FromConfiguration` (enabled in Production, disabled otherwise). |
28+
| `RequireHttps` | Boolean | Whether to require HTTPS for all requests. |
29+
| `RequireHttpsPermanent` | Boolean | Whether to use a permanent (301) redirect for HTTPS redirection. |
30+
| `SslPort` | Integer | The port number for SSL connections. |
31+
32+
### `StrictTransportSecurityMode` values
33+
34+
| Value | Behavior |
35+
|-------|----------|
36+
| `Disabled` | Always disables HSTS headers, regardless of environment. |
37+
| `Enabled` | Always enables HSTS headers, regardless of environment. |
38+
| `FromConfiguration` | Enables HSTS automatically in the `Production` environment and disables it in other environments. |
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
using System.Text.Json.Nodes;
2+
using Moq;
3+
using OrchardCore.Https.Settings;
4+
using OrchardCore.Settings;
5+
6+
namespace OrchardCore.Tests.Modules.OrchardCore.Https;
7+
8+
public class HttpsSettingsMigrationsTests
9+
{
10+
private const string LegacyEnableStrictTransportSecurityKey = "EnableStrictTransportSecurity";
11+
12+
[Fact]
13+
public async Task CreateAsyncMigratesLegacyHstsSetting()
14+
{
15+
var site = new SiteSettings();
16+
site.Properties[nameof(HttpsSettings)] = new JsonObject
17+
{
18+
[LegacyEnableStrictTransportSecurityKey] = true,
19+
};
20+
21+
var siteService = new Mock<ISiteService>();
22+
siteService.Setup(service => service.LoadSiteSettingsAsync()).ReturnsAsync(site);
23+
siteService.Setup(service => service.UpdateSiteSettingsAsync(site)).Returns(Task.CompletedTask);
24+
25+
var version = await InvokeCreateAsync(siteService.Object);
26+
27+
Assert.Equal(1, version);
28+
Assert.Equal(
29+
nameof(HttpStrictTransportSecurityMode.Enabled),
30+
site.Properties[nameof(HttpsSettings)]?[nameof(HttpsSettings.StrictTransportSecurityMode)]?.GetValue<string>());
31+
Assert.Null(site.Properties[nameof(HttpsSettings)]?[LegacyEnableStrictTransportSecurityKey]);
32+
siteService.Verify(service => service.UpdateSiteSettingsAsync(site), Times.Once);
33+
}
34+
35+
[Fact]
36+
public async Task CreateAsyncRemovesLegacySettingWithoutOverwritingMigratedValue()
37+
{
38+
var site = new SiteSettings();
39+
site.Properties[nameof(HttpsSettings)] = new JsonObject
40+
{
41+
[nameof(HttpsSettings.StrictTransportSecurityMode)] = nameof(HttpStrictTransportSecurityMode.Disabled),
42+
[LegacyEnableStrictTransportSecurityKey] = true,
43+
};
44+
45+
var siteService = new Mock<ISiteService>();
46+
siteService.Setup(service => service.LoadSiteSettingsAsync()).ReturnsAsync(site);
47+
siteService.Setup(service => service.UpdateSiteSettingsAsync(site)).Returns(Task.CompletedTask);
48+
49+
var version = await InvokeCreateAsync(siteService.Object);
50+
51+
Assert.Equal(1, version);
52+
Assert.Equal(
53+
nameof(HttpStrictTransportSecurityMode.Disabled),
54+
site.Properties[nameof(HttpsSettings)]?[nameof(HttpsSettings.StrictTransportSecurityMode)]?.GetValue<string>());
55+
Assert.Null(site.Properties[nameof(HttpsSettings)]?[LegacyEnableStrictTransportSecurityKey]);
56+
siteService.Verify(service => service.UpdateSiteSettingsAsync(site), Times.Once);
57+
}
58+
59+
private static async Task<int> InvokeCreateAsync(ISiteService siteService)
60+
{
61+
var migrationType = typeof(HttpsSettings).Assembly.GetType("OrchardCore.Https.Migrations.HttpsSettingsMigrations");
62+
if (migrationType is null)
63+
{
64+
throw new InvalidOperationException("Unable to locate the HTTPS settings migration type.");
65+
}
66+
67+
var migration = Activator.CreateInstance(migrationType, siteService);
68+
if (migration is null)
69+
{
70+
throw new InvalidOperationException("Unable to create the HTTPS settings migration instance.");
71+
}
72+
73+
var method = migrationType.GetMethod("CreateAsync");
74+
if (method is null)
75+
{
76+
throw new InvalidOperationException("Unable to locate the HTTPS settings migration CreateAsync method.");
77+
}
78+
79+
var result = method.Invoke(migration, null) as Task<int>;
80+
if (result is null)
81+
{
82+
throw new InvalidOperationException("Unable to invoke the HTTPS settings migration CreateAsync method.");
83+
}
84+
85+
return await result;
86+
}
87+
}

0 commit comments

Comments
 (0)