Skip to content

Commit 95b813c

Browse files
authored
Merge pull request #67 from davidortinau/feature/41-security-headers
feat: add HTTPS enforcement and security headers (#41)
2 parents 29f0b83 + 1550397 commit 95b813c

File tree

17 files changed

+351
-50
lines changed

17 files changed

+351
-50
lines changed

.squad/agents/kaylee/history.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,21 @@
3939

4040
**Critical Path:** CoreSync SQLite→PostgreSQL migration (#55, XL).
4141

42+
### 2026-03-14 — Phase 2 (Secrets & Security) Completion
43+
44+
**Status:** COMPLETED
45+
**Issues:** #39 (user-secrets setup), #41 (security headers)
46+
47+
**Kaylee Completed #41 — Security Headers & HTTPS:**
48+
- Added shared `SecurityHeadersExtensions` in `src/Shared/SecurityHeadersExtensions.cs` (linked to web projects via `<Compile Include>`)
49+
- Security headers: X-Content-Type-Options: nosniff, X-Frame-Options: DENY, Referrer-Policy, Permissions-Policy (camera/mic/geo)
50+
- HTTPS redirect environment-aware (skipped in dev, Aspire proxy terminates TLS)
51+
- API explicit HSTS: 365-day max-age, includeSubDomains, preload
52+
- CORS: AllowWebApp policy (config-driven) + AllowDevClients (dev-only localhost)
53+
- AllowedHosts restrictions in Production appsettings
54+
55+
**Key Decision:** Linked source file instead of WebServiceDefaults to avoid ambiguous call errors with MAUI defaults.
56+
57+
**Wash Completed #39 (user-secrets setup):**
58+
- Kaylee coordination: CORS setup confirmed not required for MAUI clients (use service discovery)
59+
- Phase 2 ready for Phase 1 (Entra ID auth) — Captain has provisioned 3 app registrations

.squad/agents/wash/history.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,20 @@
3737

3838
**Key Dependencies:** Zoe coordinates Phase 1-3 decisions; Kaylee implements CI/deploy automation; Captain provides Azure portal access.
3939

40+
### 2026-03-14 — Phase 2 (Secrets) Completion
41+
42+
**Status:** COMPLETED
43+
**Issues:** #39 (user-secrets setup), #41 (security headers)
44+
45+
**Wash Completed #39:**
46+
- Initialized user-secrets for Api, WebApp
47+
- Created secrets.template.json with full inventory
48+
- Updated README with three secrets management paths
49+
- Documented AppHost → service flow via Aspire Parameters and env var normalization
50+
51+
**Kaylee Completed #41:**
52+
- Added SecurityHeadersExtensions to shared lib (linked to all web projects)
53+
- Implemented HSTS, CORS, AllowedHosts across API/WebApp/Marketing
54+
- Environment-aware HTTPS redirect
55+
56+
**Phase 2 Closed:** Ready to begin Phase 1 (Entra ID) now that Captain has provisioned app registrations.

.squad/decisions.md

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,72 @@ Comprehensive architecture plan for transitioning SentenceStudio from local-dev-
6868

6969
**Estimated Monthly Cost (Production):** ~$107-252
7070

71+
### 3. User-Secrets Workflow for Local Development (2026-03-14)
72+
73+
**Status:** IMPLEMENTED
74+
**Date:** 2026-03-14
75+
**Author:** Wash (Backend Dev)
76+
**Issue:** #39
77+
78+
Established .NET user-secrets pattern for secure local development across all server-side projects.
79+
80+
**Key Decisions:**
81+
- AppHost uses Aspire Parameters (`builder.AddParameter("openaikey", secret: true)`) resolving from AppHost user-secrets under `Parameters:openaikey`
82+
- Parameters passed to child projects via `.WithEnvironment("AI__OpenAI__ApiKey", openaikey)`
83+
- Aspire normalizes `__` to `:` in configuration across services
84+
- Three paths documented in README:
85+
- **Option A:** Aspire (recommended) — set secrets in AppHost, flow to all services
86+
- **Option B:** Standalone projects — per-project `dotnet user-secrets`
87+
- **Option C:** MAUI mobile/desktop — gitignored `appsettings.json` in AppLib
88+
89+
**Projects with UserSecretsId:**
90+
| Project | UserSecretsId |
91+
|---------|---------------|
92+
| AppHost | d8521a4e-969b-4696-9990-45dea324bda8 |
93+
| Api | 9ae3953f-a490-41b3-a2b8-a8e2555b4615 |
94+
| WebApp | 33f95f89-d495-4311-b6cb-53a47b5c34e6 |
95+
| Workers | dotnet-SentenceStudio.Workers-8ded0183-d135-40b2-b2d4-b49b096922b8 |
96+
97+
**Secrets Inventory:**
98+
| Secret | AppHost Parameter | Api Key | WebApp Key |
99+
|--------|-------------------|---------|------------|
100+
| OpenAI | Parameters:openaikey | AI:OpenAI:ApiKey | Settings:OpenAIKey |
101+
| ElevenLabs | Parameters:elevenlabskey | ElevenLabsKey | Settings:ElevenLabsKey |
102+
| Syncfusion | Parameters:syncfusionkey | N/A | N/A |
103+
104+
**No Data Impact:** No database changes, no secret migrations, AppHost user-secrets remain intact.
105+
106+
---
107+
108+
### 4. Security Headers and HTTPS Enforcement (2026-03-14)
109+
110+
**Status:** IMPLEMENTED
111+
**Date:** 2026-03-14
112+
**Author:** Kaylee (Full-stack Dev)
113+
**Issue:** #41
114+
115+
Added security hardening across API, WebApp, and Marketing services.
116+
117+
**Security Headers (all services):**
118+
- Shared extension `UseSecurityHeaders()` in `src/Shared/SecurityHeadersExtensions.cs`
119+
- Linked via `<Compile Include>` to prevent ambiguous call errors with MAUI defaults
120+
- Headers: X-Content-Type-Options: nosniff, X-Frame-Options: DENY, Referrer-Policy: strict-origin-when-cross-origin, Permissions-Policy: camera=(), microphone=(), geolocation=()
121+
122+
**HTTPS and HSTS:**
123+
- HTTPS redirect environment-aware: skipped in Development (Aspire terminates TLS at proxy)
124+
- API explicit HSTS: 365-day max-age, includeSubDomains, preload
125+
- WebApp and Marketing HSTS unchanged (already configured in non-dev block)
126+
127+
**CORS (API only):**
128+
- `AllowWebApp` policy: restricts to `Cors:AllowedOrigins` config
129+
- `AllowDevClients` policy: dev-only, localhost with credentials
130+
- Production origins in `appsettings.Production.json`
131+
- MAUI clients unaffected (service discovery, not browser CORS)
132+
133+
**AllowedHosts:** Production `appsettings.json` files restrict to specific domains, not wildcard.
134+
135+
**Deferred:** Production CORS fine-tuning (#62), CSP header (Blazor inline scripts), production auth (still DevAuthHandler).
136+
71137
## Governance
72138

73139
- All meaningful changes require team consensus

.squad/decisions/inbox/wash-user-secrets.md

Lines changed: 0 additions & 46 deletions
This file was deleted.
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# Orchestration Log: Zoe GitHub Issues Spawn
2+
3+
**Date:** 2026-03-13T2352
4+
**Agent:** Zoe (Lead)
5+
**Task:** Create 27 GitHub issues from Azure deployment plan
6+
**Status:** SUCCESS
7+
8+
## Outcome Summary
9+
10+
- **Issues Created:** 27 (#39#65)
11+
- **Repository:** davidortinau/SentenceStudio
12+
- **Labels Applied:** phase tags, size tags, squad tags, individual agent assignments
13+
- **Dependencies:** All 27 issues cross-referenced with dependency links
14+
15+
## Issue Mapping
16+
17+
**Phase 1 (Auth):** #42, #43, #44, #45, #46, #47
18+
**Phase 2 (Secrets):** #39, #40, #41, #54
19+
**Phase 3 (Infrastructure):** #48, #49, #50, #51, #52, #53, #55
20+
**Phase 4 (Pipeline):** #56, #57, #58, #59
21+
**Phase 5 (Hardening):** #60, #61, #62, #63, #64, #65
22+
23+
## Team Assignments
24+
25+
- **Zoe (Lead):** 14 issues — foundational auth, infrastructure, hardening decisions
26+
- **Kaylee (Full-stack):** 8 issues — WebApp OIDC, MAUI MSAL, CI/deploy, monitoring
27+
- **Captain (David):** 1 issue — Entra ID app registrations (Azure portal)
28+
29+
## Key Decisions Documented
30+
31+
1. **Reframed Issue #39:** User-secrets as team best practice (not security emergency)
32+
2. **Phase Execution Order:** Phase 2 → 1 → 3 → 4 → 5 (security-first approach)
33+
3. **Localhost-Testable:** Phase 1 auth entirely testable without Azure deployment
34+
4. **Critical Path:** CoreSync SQLite→PostgreSQL migration (Phase 3.7, XL)
35+
36+
## Files Affected
37+
38+
- `.squad/agents/zoe/history.md` — Updated with work session
39+
- `.squad/decisions/inbox/zoe-github-issues-created.md` — Decision record created
40+
- `.squad/decisions/inbox/zoe-azure-auth-plan.md` — Existing plan reference
41+
42+
## Next Actions
43+
44+
Scribe to merge inbox decisions into `decisions.md`, propagate cross-agent updates, and commit.
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# Orchestration Log: Kaylee (agent-6)
2+
3+
**Date:** 2026-03-14T01:00:00Z
4+
**Agent:** Kaylee (Full-stack Dev)
5+
**Mode:** Background, Standard
6+
**Issue:** #41
7+
8+
## Outcome
9+
10+
DONE
11+
12+
## Work Summary
13+
14+
- Implemented security hardening across API, WebApp, and Marketing services
15+
- Created shared extension `UseSecurityHeaders()` in `src/Shared/SecurityHeadersExtensions.cs` with security headers:
16+
- X-Content-Type-Options: nosniff (prevent MIME-sniffing)
17+
- X-Frame-Options: DENY (prevent clickjacking)
18+
- Referrer-Policy: strict-origin-when-cross-origin
19+
- Permissions-Policy: camera=(), microphone=(), geolocation=()
20+
- Implemented environment-aware HTTPS redirect: skipped in Development (Aspire terminates TLS at proxy)
21+
- Added HSTS to API: 365-day max-age, includeSubDomains, preload
22+
- Implemented CORS policies on API:
23+
- `AllowWebApp` policy: restricts to configured origins
24+
- `AllowDevClients` policy: dev-only, allows localhost with credentials
25+
- Created `appsettings.Production.json` for all three services with AllowedHosts restrictions
26+
27+
## Branch
28+
29+
`feature/41-security-headers`
30+
31+
## Key Decisions
32+
33+
- Linked source file for `SecurityHeadersExtensions.cs` instead of WebServiceDefaults (avoids ambiguous call errors with MAUI defaults)
34+
- CORS not applied to MAUI clients (use service discovery, not browser CORS)
35+
- Production CORS fine-tuning deferred to #62
36+
- Content-Security-Policy header deferred (complex for Blazor inline scripts)
37+
38+
## Blockers
39+
40+
None
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Orchestration Log: Wash (agent-5)
2+
3+
**Date:** 2026-03-14T01:00:00Z
4+
**Agent:** Wash (Backend Dev)
5+
**Mode:** Background, Standard
6+
**Issue:** #39
7+
8+
## Outcome
9+
10+
DONE
11+
12+
## Work Summary
13+
14+
- Initialized `.NET user-secrets` for `SentenceStudio.Api` and `SentenceStudio.WebApp`
15+
- Created `secrets.template.json` at repo root documenting all secret keys organized by project context
16+
- Updated `README.md` (Section 3) with three paths for secrets management:
17+
- Option A: Aspire (recommended) — set secrets in AppHost, flow to all services
18+
- Option B: Standalone projects — per-project `dotnet user-secrets`
19+
- Option C: MAUI mobile/desktop — gitignored `appsettings.json`
20+
- Documented secret flow: AppHost uses Aspire Parameters → `.WithEnvironment()` → services via env var normalization (`__``:`)
21+
- Captured all UserSecretsId GUIDs and secrets inventory (OpenAI, ElevenLabs, Syncfusion)
22+
23+
## Branch
24+
25+
`feature/39-user-secrets`
26+
27+
## Key Decisions
28+
29+
- AppHost's existing user-secrets remain intact — no migration needed
30+
- Parameter flow leverages Aspire's `__` to `:` normalization for clean cross-platform config
31+
- No database changes; no secrets moved or deleted
32+
33+
## Blockers
34+
35+
None

src/SentenceStudio.Api/Program.cs

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
using SentenceStudio.Contracts.Vocabulary;
1818
using SentenceStudio.Data;
1919
using SentenceStudio.Domain.Abstractions;
20+
using SentenceStudio.Infrastructure;
2021
using SentenceStudio.Services;
2122
using SentenceStudio.Shared.Models;
2223

@@ -31,6 +32,42 @@
3132
builder.Services.AddAuthorization();
3233
builder.Services.AddScoped<ITenantContext, TenantContext>();
3334

35+
// CORS — basic policies for known callers.
36+
// Production fine-tuning is tracked in issue #62.
37+
var allowedOrigins = builder.Configuration.GetSection("Cors:AllowedOrigins").Get<string[]>();
38+
builder.Services.AddCors(options =>
39+
{
40+
if (allowedOrigins?.Length > 0)
41+
{
42+
options.AddPolicy("AllowWebApp", policy =>
43+
{
44+
policy.WithOrigins(allowedOrigins)
45+
.AllowAnyHeader()
46+
.AllowAnyMethod();
47+
});
48+
}
49+
50+
if (builder.Environment.IsDevelopment())
51+
{
52+
options.AddPolicy("AllowDevClients", policy =>
53+
{
54+
policy.SetIsOriginAllowed(origin =>
55+
Uri.TryCreate(origin, UriKind.Absolute, out var uri)
56+
&& uri.Host is "localhost" or "127.0.0.1")
57+
.AllowAnyHeader()
58+
.AllowAnyMethod()
59+
.AllowCredentials();
60+
});
61+
}
62+
});
63+
64+
builder.Services.AddHsts(options =>
65+
{
66+
options.MaxAge = TimeSpan.FromDays(365);
67+
options.IncludeSubDomains = true;
68+
options.Preload = true;
69+
});
70+
3471
// Database - shared with WebApp server instance
3572
var serverDbFolder = Path.Combine(
3673
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
@@ -68,7 +105,19 @@
68105
app.MapOpenApi();
69106
}
70107

71-
app.UseHttpsRedirection();
108+
if (!app.Environment.IsDevelopment())
109+
{
110+
app.UseHsts();
111+
}
112+
113+
// Skip HTTPS redirect in development — Aspire may terminate TLS at the proxy.
114+
if (!app.Environment.IsDevelopment())
115+
{
116+
app.UseHttpsRedirection();
117+
}
118+
119+
app.UseSecurityHeaders();
120+
app.UseCors(app.Environment.IsDevelopment() ? "AllowDevClients" : "AllowWebApp");
72121
app.UseAuthentication();
73122
app.UseAuthorization();
74123
app.UseMiddleware<TenantContextMiddleware>();

src/SentenceStudio.Api/SentenceStudio.Api.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,8 @@
2020
<ProjectReference Include="..\SentenceStudio.ServiceDefaults\SentenceStudio.ServiceDefaults.csproj" />
2121
</ItemGroup>
2222

23+
<ItemGroup>
24+
<Compile Include="..\Shared\SecurityHeadersExtensions.cs" Link="Infrastructure\SecurityHeadersExtensions.cs" />
25+
</ItemGroup>
26+
2327
</Project>
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"AllowedHosts": "api.sentencestudio.com;sentencestudio.com",
3+
"Cors": {
4+
"AllowedOrigins": [
5+
"https://sentencestudio.com",
6+
"https://www.sentencestudio.com"
7+
]
8+
}
9+
}

0 commit comments

Comments
 (0)