Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v7

- name: Setup .NET
uses: actions/setup-dotnet@v4
uses: actions/setup-dotnet@v6
with:
dotnet-version: |
10.0.x
Expand All @@ -40,11 +40,13 @@ jobs:
mkdir -p artifacts/nupkg
dotnet pack src/SafeWebCore/SafeWebCore.csproj -c ${{ env.Configuration }} --no-build -o artifacts/nupkg
dotnet pack src/SafeWebCore.FraudDetection/SafeWebCore.FraudDetection.csproj -c ${{ env.Configuration }} --no-build -o artifacts/nupkg
dotnet pack src/SafeWebCore.JwtBearer/SafeWebCore.JwtBearer.csproj -c ${{ env.Configuration }} --no-build -o artifacts/nupkg

dotnet pack src/SafeWebCore.Analyzers/SafeWebCore.Analyzers.csproj -c ${{ env.Configuration }} --no-build -o artifacts/nupkg
dotnet pack src/SafeWebCore.Testing/SafeWebCore.Testing.csproj -c ${{ env.Configuration }} --no-build -o artifacts/nupkg

- name: Upload packages
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: nupkgs
path: artifacts/nupkg/*
Expand Down
8 changes: 5 additions & 3 deletions .github/workflows/nuget-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v7
with:
fetch-depth: 0

- name: Setup .NET
uses: actions/setup-dotnet@v4
uses: actions/setup-dotnet@v6
with:
dotnet-version: '10.0.x'

Expand All @@ -40,9 +40,11 @@ jobs:
dotnet pack src/SafeWebCore.Analyzers/SafeWebCore.Analyzers.csproj --configuration Release --no-build --output artifacts/nuget || echo "Analyzers pack skipped"
dotnet pack src/SafeWebCore.Testing/SafeWebCore.Testing.csproj --configuration Release --no-build --output artifacts/nuget || echo "Testing pack skipped"
dotnet pack src/SafeWebCore.FraudDetection/SafeWebCore.FraudDetection.csproj --configuration Release --no-build --output artifacts/nuget || echo "FraudDetection pack skipped"
dotnet pack src/SafeWebCore.JwtBearer/SafeWebCore.JwtBearer.csproj --configuration Release --no-build --output artifacts/nuget || echo "JwtBearer pack skipped"


- name: Upload NuGet artifacts
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: nuget-packages
path: artifacts/nuget/*.nupkg
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added
- `NetSecureHeadersOptions.PathPolicy(pathPrefix, customize)` extension method — creates a path policy that **inherits** the global configuration and only overrides explicitly set values. This prevents accidental security header downgrades (for example a weaker HSTS value on `/api`).
- **New companion module: `SafeWebCore.JwtBearer`** — makes a misconfigured JWT authority fail loud (or fail fast) at startup instead of silently returning `401` for everything. Solves dotnet/aspnetcore#67991 (reported by Stephan van Rooij) today, while the .NET team schedules the fix for .NET 12 Planning. Implements `AddJwtBearerAuthorityValidation`, `AddJwtBearerHardening`, and the one-liner `AddSafeWebCoreJwtBearer`.
- `SafeWebCore.JwtBearer` token hardening — optional: require signed tokens / reject `alg: none`, algorithm allow-list, `typ` header checks (`JWT`/`at+jwt`), audience/issuer enforcement, maximum clock skew and token lifetime, `jti`/`nbf`/`iat` requirements.
- `SafeWebCore.JwtBearer` runtime metadata logging — wraps the OpenID Connect configuration manager so metadata retrieval failures are logged at Error (4xx) / Warning level during runtime, not only at startup.
- `examples/JwtBearerDemo` — runnable reproduction of issue #67991 (broken vs. fixed behavior) and `StephanReproIntegrationTests` proving both sides.

- `NetSecureHeadersOptions.ApplyPreset(...)` is now **public** — the official inheritance mechanism to copy all values from another options instance (for example the global options) before applying overrides.
- `NetSecureHeadersOptions.Clone()` — creates an independent copy of an options instance for safe mutation.

Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@
- 🔌 **Extensible** — add custom `IHeaderPolicy` implementations for any header
- 📊 **CSP violation reporting** — built-in middleware for `/csp-report` endpoint using Reporting API v1
- 🔍 **Diagnostics preview** — `MapSafeWebCoreDiagnostics(...)` for an opt-in JSON preview of effective headers, path-policy resolution, and CSP mode
- 🔐 **JWT authority validation** — opt-in `SafeWebCore.JwtBearer` companion module turns a broken JWT authority into a fail-fast startup error or a loud Error log (solves [dotnet/aspnetcore#67991](https://github.com/dotnet/aspnetcore/issues/67991), reported by Stephan van Rooij), plus optional token hardening

- 📈 **Opt-in metrics** — `System.Diagnostics.Metrics` counters for core middleware and fraud detection
- 🚨 **Fraud action pipeline** — `IFraudEventSink` / `FraudEvent` for reacting to fraud analysis results (logging, webhooks, custom actions)
- 📦 **Companion packages** — `SafeWebCore.FraudDetection`, `SafeWebCore.Analyzers` (preview), and `SafeWebCore.Testing` (preview)
Expand Down Expand Up @@ -375,6 +377,8 @@ Three complete, runnable ASP.NET Core applications demonstrating different integ
| [**MinimalApi**](examples/MinimalApi/) | Minimal API | One-line A+ setup, inline nonce, CSP reporting, health probes |
| [**MvcApp**](examples/MvcApp/) | MVC + Razor Views | Typed policy builders, path policies, nonce TagHelpers, controller attributes |
| [**ApiService**](examples/ApiService/) | Web API Controllers | Custom CSP report sink, endpoint overrides, API preset |
| [**JwtBearerDemo**](examples/JwtBearerDemo/) | Minimal API + JWT | Fixed the dotnet/aspnetcore#67991 silent-401 bug: broken vs. fail-fast authority startup |


Each example is fully functional out of the box — just `dotnet run` from the example directory.

Expand Down
4 changes: 3 additions & 1 deletion SafeWebCore.slnx
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
<Solution>
<Solution>
<Folder Name="/src/">
<Project Path="src/SafeWebCore/SafeWebCore.csproj" />
<Project Path="src/SafeWebCore.FraudDetection/SafeWebCore.FraudDetection.csproj" />
<Project Path="src/SafeWebCore.Analyzers/SafeWebCore.Analyzers.csproj" />
<Project Path="src/SafeWebCore.Testing/SafeWebCore.Testing.csproj" />
<Project Path="src/SafeWebCore.JwtBearer/SafeWebCore.JwtBearer.csproj" />
</Folder>
<Folder Name="/tests/">
<Project Path="tests/SafeWebCore.Tests/SafeWebCore.Tests.csproj" />
<Project Path="tests/SafeWebCore.FraudDetection.Tests/SafeWebCore.FraudDetection.Tests.csproj" />
<Project Path="tests/SafeWebCore.JwtBearer.Tests/SafeWebCore.JwtBearer.Tests.csproj" />
</Folder>
<Folder Name="/examples/">
<Project Path="examples/MinimalApi/MinimalApi.csproj" />
Expand Down
26 changes: 26 additions & 0 deletions docs/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,32 @@ CSP violations are logged to `csp-violations.jsonl` next to the binary.

---

## 🔐 JWT Bearer Example (SafeWebCore.JwtBearer)

**Location:** `examples/JwtBearerDemo/`

### What it demonstrates

This example is the reproduction of **dotnet/aspnetcore#67991** (reported by Stephan van Rooij,
[@svrooij](https://github.com/svrooij)): a misspelled JWT authority
(`organisations` instead of `organizations`) makes the app start normally while every request
returns `401` — and the root cause is logged below the default `Microsoft.AspNetCore: Warning`
threshold. The .NET team scheduled the fix for **.NET 12 Planning** only.

With `SafeWebCore.JwtBearer` enabled, the same app **refuses to start** on a permanent (HTTP 4xx)
authority failure — or logs a loud `Error` when `FailFast = false`.

### Run

```bash
cd examples/JwtBearerDemo
dotnet run
```

Flip `const bool enableFix` in `Program.cs` to switch between the broken and the fixed behavior.
See `examples/JwtBearerDemo/README.md` for the full before/after walkthrough.


## 📊 Feature Matrix

| Feature | MinimalApi | MvcApp | ApiService |
Expand Down
57 changes: 57 additions & 0 deletions docs/nuget-packages.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ This document describes every packable project: identity, contents, publish stat
|-----------|----------------|-----------|------------|----------------|
| **SafeWebCore** | `1.3.5` | **Published** (`1.0.0`–`1.3.5`) | `SafeWebCore.1.3.5.nupkg` + `.snupkg` | Do **not** republish 1.3.5. Ship next as **1.4.0** (or later) after promoting Unreleased work |
| **SafeWebCore.FraudDetection** | `1.0.0` | **Not published** | `SafeWebCore.FraudDetection.1.0.0.nupkg` | **Primary new package candidate** for first public release |
| **SafeWebCore.JwtBearer** | `1.0.0` | **Not published** | `SafeWebCore.JwtBearer.1.0.0.nupkg` | **New package candidate** — JWT authority fail-fast + token hardening |

| **SafeWebCore.Analyzers** | `1.0.0-preview.1` | **Not published** | `SafeWebCore.Analyzers.1.0.0-preview.1.nupkg` | Publish as **preview** only |
| **SafeWebCore.Testing** | `1.0.0-preview.1` | **Not published** | `SafeWebCore.Testing.1.0.0-preview.1.nupkg` | Publish as **preview** only |

Expand All @@ -27,6 +29,8 @@ From repo root:
```bash
dotnet pack src/SafeWebCore/SafeWebCore.csproj -c Release -o artifacts/nupkg
dotnet pack src/SafeWebCore.FraudDetection/SafeWebCore.FraudDetection.csproj -c Release -o artifacts/nupkg
dotnet pack src/SafeWebCore.JwtBearer/SafeWebCore.JwtBearer.csproj -c Release -o artifacts/nupkg

dotnet pack src/SafeWebCore.Analyzers/SafeWebCore.Analyzers.csproj -c Release -o artifacts/nupkg
dotnet pack src/SafeWebCore.Testing/SafeWebCore.Testing.csproj -c Release -o artifacts/nupkg
```
Expand Down Expand Up @@ -196,6 +200,57 @@ Core is already at **1.3.5** while FraudDetection starts at **1.0.0**. That is n

---

# Package 2b — SafeWebCore.JwtBearer (new companion, unpublished)

## Identity

| Field | Value |
|-------|--------|
| PackageId | `SafeWebCore.JwtBearer` |
| Version | `1.0.0` (csproj) |
| Authors / Company | MPCoreDeveloper / Posseth Software |
| License | MIT |
| Project URL | https://github.com/MPCoreDeveloper/SafeWebCore |
| Readme in package | `src/SafeWebCore.JwtBearer/README.md` |
| Icon | `icon.png` |
| TFM | `net10.0` |

## Purpose

Solves [dotnet/aspnetcore#67991](https://github.com/dotnet/aspnetcore/issues/67991)
(reported by **Stephan van Rooij**): a misspelled JWT authority silently 401s every request
because the metadata failure is logged only at Information level, below the default log
threshold. The module adds a startup authority guard (fail fast or loud Error), optional token
hardening, and runtime metadata-failure logging — before the .NET team's planned fix in
**.NET 12 Planning**.

## What consumers get

```bash
dotnet add package SafeWebCore.JwtBearer
```

- `AddJwtBearerAuthorityValidation` — startup authority guard (Stephan's fix).
- `AddJwtBearerHardening` — optional token-validation hardening (algorithms, typ, audience/issuer, max lifetime, ...).
- `AddSafeWebCoreJwtBearer` — one-line registration (AddJwtBearer + hardening + guard).

## Readiness scorecard

| Check | Status |
|-------|--------|
| Builds in solution | ✅ |
| Unit + integration tests (xUnit v3) | ✅ (33, incl. Stephan's reproduction) |
| Public API baseline (`PublicAPI.*.txt`) | ✅ |
| README + icon packed | ✅ (verified) |
| `ci.yml` / `nuget-publish.yml` pack step | ✅ (added) |

### Publish command

```bash
dotnet pack src/SafeWebCore.JwtBearer/SafeWebCore.JwtBearer.csproj -c Release -o artifacts/nupkg
dotnet nuget push artifacts/nupkg/SafeWebCore.JwtBearer.1.0.0.nupkg --api-key %NUGET_API_KEY% --source https://api.nuget.org/v3/index.json
```

# Package 3 — SafeWebCore.Analyzers (preview candidate)

## Identity
Expand Down Expand Up @@ -359,6 +414,8 @@ Publish **FraudDetection 1.0.0** alone (no core bump). Valid because packages ar
# After version bumps, changelog, and tests
dotnet pack src/SafeWebCore/SafeWebCore.csproj -c Release -o artifacts/nupkg
dotnet pack src/SafeWebCore.FraudDetection/SafeWebCore.FraudDetection.csproj -c Release -o artifacts/nupkg
dotnet pack src/SafeWebCore.JwtBearer/SafeWebCore.JwtBearer.csproj -c Release -o artifacts/nupkg

dotnet pack src/SafeWebCore.Analyzers/SafeWebCore.Analyzers.csproj -c Release -o artifacts/nupkg
dotnet pack src/SafeWebCore.Testing/SafeWebCore.Testing.csproj -c Release -o artifacts/nupkg

Expand Down
10 changes: 10 additions & 0 deletions docs/projects.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ SafeWebCore/
├── src/ # Shipable libraries
│ ├── SafeWebCore/ # Core security-headers middleware (NuGet)
│ ├── SafeWebCore.FraudDetection/ # Optional fraud module (NuGet candidate)
│ ├── SafeWebCore.JwtBearer/ # Optional JWT authority validation + hardening module (NuGet)

│ ├── SafeWebCore.Analyzers/ # Roslyn analyzers (NuGet preview candidate)
│ └── SafeWebCore.Testing/ # Test helpers (NuGet preview candidate)
├── tests/ # Unit / integration tests
Expand All @@ -37,6 +39,8 @@ SafeWebCore/
|---------|------|
| SafeWebCore | `src/SafeWebCore/SafeWebCore.csproj` |
| SafeWebCore.FraudDetection | `src/SafeWebCore.FraudDetection/SafeWebCore.FraudDetection.csproj` |
| SafeWebCore.JwtBearer | `src/SafeWebCore.JwtBearer/SafeWebCore.JwtBearer.csproj` |

| SafeWebCore.Analyzers | `src/SafeWebCore.Analyzers/SafeWebCore.Analyzers.csproj` |
| SafeWebCore.Testing | `src/SafeWebCore.Testing/SafeWebCore.Testing.csproj` |
| SafeWebCore.Tests | `tests/SafeWebCore.Tests/SafeWebCore.Tests.csproj` |
Expand Down Expand Up @@ -475,17 +479,23 @@ SafeWebCore.Tests ──────────────► SafeWebCore
SafeWebCore.Testing ────────────► SafeWebCore
SafeWebCore.Benchmarks ─────────► SafeWebCore
examples/* ─────────────────────► SafeWebCore
SafeWebCore.JwtBearer.Tests ► SafeWebCore.JwtBearer


SafeWebCore.FraudDetection.Tests ► SafeWebCore.FraudDetection

SafeWebCore.Analyzers (standalone Roslyn package)
SafeWebCore.FraudDetection (standalone; no dependency on SafeWebCore)
SafeWebCore.JwtBearer (standalone; depends on Microsoft.AspNetCore.Authentication.JwtBearer)

```

---

# Project maturity matrix

| SafeWebCore.JwtBearer | Production-ready code, unpublished | **Yes** (first release candidate) | `1.0.0` after checklist (packaging/CI) |

| Project | Maturity | Ship as NuGet? | Recommended version policy |
|---------|----------|----------------|----------------------------|
| SafeWebCore | Production (published) | **Yes** (already) | SemVer stable; next feature drop → bump beyond 1.3.5 |
Expand Down
15 changes: 15 additions & 0 deletions examples/JwtBearerDemo/JwtBearerDemo.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<RootNamespace>SafeWebCore.Examples.JwtBearerDemo</RootNamespace>
<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="../../src/SafeWebCore.JwtBearer/SafeWebCore.JwtBearer.csproj" />
</ItemGroup>

</Project>
8 changes: 8 additions & 0 deletions examples/JwtBearerDemo/JwtBearerDemo.http
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
@JwtBearerDemo_HostAddress = http://localhost:5120

### Weather forecast (protected endpoint)
### In the BROKEN setup this returns 401 UNAUTHORIZED with error="invalid_token"
### and nothing useful in the logs. In the FIXED setup the app refuses to start.
GET {{JwtBearerDemo_HostAddress}}/weatherforecast
Authorization: Bearer eyJhbGciOiJub25lIn0.eyJzdWIiOiJkZW1vLXRva2VuIn0.sig
Accept: application/json
78 changes: 78 additions & 0 deletions examples/JwtBearerDemo/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
using Microsoft.AspNetCore.Authentication.JwtBearer;
using SafeWebCore.JwtBearer.Extensions;

var builder = WebApplication.CreateBuilder(args);

// ============================================================================
// SafeWebCore.JwtBearer demo - reproduction of dotnet/aspnetcore#67991
// reported by Stephan van Rooij (@svrooij)
// https://github.com/dotnet/aspnetcore/issues/67991
//
// FIXED (default): the SafeWebCore guard eagerly validates the authority metadata
// at startup. HTTP 4xx from the metadata endpoint (the permanent
// misconfiguration) stops the application with a clear error, or logs an Error
// if you prefer to keep the app running so all requests fail closed until the
// authority is fixed.
//
// BROKEN (dotnet run -- --broken): the guard is skipped. The misspelled authority
// lets the app start and run normally, every request returns 401 invalid_token,
// and nothing rises above the default Microsoft.AspNetCore: Warning log level.
// ============================================================================
var brokenMode = args.Contains("--broken");

builder.Services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
// The classic typo from issue #67991: "organisations" instead of "organizations".
// The discovery document for this url returns HTTP 400 (not 404), so the app
// starts fine but no signing keys are ever loaded.
options.Authority = "https://login.microsoftonline.com/organisations/v2.0";

// Block insecure HTTP metadata and enforce HTTPS for token validation.
options.RequireHttpsMetadata = true;

// Token validation settings taken verbatim from the issue reproduction.
options.TokenValidationParameters.ValidateAudience = true;
options.TokenValidationParameters.ValidAudience = "api://safe-web-core-demo";
options.TokenValidationParameters.ValidateIssuer = false; // allow tokens from multiple tenants
options.TokenValidationParameters.ValidateLifetime = true;
options.TokenValidationParameters.ValidateIssuerSigningKey = true;
options.TokenValidationParameters.ClockSkew = TimeSpan.FromSeconds(15);
options.TokenValidationParameters.RequireSignedTokens = true;
options.TokenValidationParameters.RequireExpirationTime = true;

options.Events = new JwtBearerEvents
{
OnAuthenticationFailed = context =>
{
// In the broken setup, set a breakpoint here to see:
// IDX10500: Signature validation failed. No security keys were provided.
return Task.CompletedTask;
},
OnChallenge = context => Task.CompletedTask,
};
});

if (!brokenMode)
{
// SafeWebCore.JwtBearer fix
// ------------------------
// Eagerly loads the OpenID Connect metadata at startup so a broken authority is
// never a silent availability incident. FailFast = true makes a permanent
// misconfiguration (HTTP 4xx) fail the startup, exactly as requested in #67991.
// Set FailFast = false to keep the app running with a loud Error log instead.
builder.Services.AddJwtBearerAuthorityValidation(options => options.FailFast = true);
}

builder.Services.AddAuthorization();

var app = builder.Build();

app.UseAuthentication();
app.UseAuthorization();

app.MapGet("/weatherforecast", () => Results.Ok(new { forecast = "Sunny" }))
.RequireAuthorization();

await app.RunAsync();
14 changes: 14 additions & 0 deletions examples/JwtBearerDemo/Properties/launchSettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "http://localhost:5120",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
Loading
Loading