mirror of
https://github.com/azaion/satellite-provider.git
synced 2026-06-22 02:01:14 +00:00
[AZ-494] Enable JWT iss/aud validation with fail-fast startup
Option B per user decision: production ships with empty Jwt.Issuer / Jwt.Audience in appsettings.json so the API process refuses to start unless JWT_ISSUER + JWT_AUDIENCE env vars are supplied. Development ships with grep-friendly DEV-ONLY- placeholders so local + docker flows keep working unchanged. AuthenticationServiceCollectionExtensions flips ValidateIssuer + ValidateAudience to true and wires ValidIssuer / ValidAudience via a new ResolveRequiredOrThrow helper that all three required values (secret, iss, aud) now share. JwtTokenFactory.Create + CreateExpired gain optional iss / aud parameters (default null) so existing call sites compile unchanged. JwtTestHelpers adds MintAuthenticated / MintExpired wrappers that resolve iss + aud from env, plus ResolveIssuerOrThrow / ResolveAudienceOrThrow. PerfBootstrap.MintToken + Program.cs JWT bootstrap migrated to the new surface so the perf harness and the integration runner both validate against the same contract. Adds 4 fail-fast unit tests (missing/empty issuer + audience), 2 negative integration scenarios (WrongIssuer_Returns401, WrongAudience_Returns401), and re-tags every existing integration mint site via MintAuthenticated. Compose, .env.example, run-tests.sh, run-performance-tests.sh all load + export JWT_ISSUER + JWT_AUDIENCE alongside JWT_SECRET. Resolves F-AUTH-2 (security_report.md + owasp_review.md). AC-7 (cross-repo suite/_docs/10_auth.md write) deferred — outside this workspace; tracked in deploy_cycle2.md R3 follow-up. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -11,12 +11,14 @@ public static class JwtIntegrationTests
|
||||
|
||||
public static async Task RunAll(string apiUrl, string secret)
|
||||
{
|
||||
RouteTestHelpers.PrintTestHeader("Test: JWT auth baseline (AZ-487)");
|
||||
RouteTestHelpers.PrintTestHeader("Test: JWT auth baseline (AZ-487 + AZ-494)");
|
||||
|
||||
await AnonymousRequest_To_AnyEndpoint_Returns401(apiUrl);
|
||||
await ExpiredToken_Returns401(apiUrl, secret);
|
||||
await InvalidSignature_Returns401(apiUrl, secret);
|
||||
await ValidToken_Returns200_OnHealthyEndpoint(apiUrl, secret);
|
||||
await WrongIssuer_Returns401(apiUrl, secret);
|
||||
await WrongAudience_Returns401(apiUrl, secret);
|
||||
await SwaggerDocument_AdvertisesBearerSecurityScheme(apiUrl);
|
||||
|
||||
Console.WriteLine("✓ JWT integration tests: PASSED");
|
||||
@@ -45,7 +47,7 @@ public static class JwtIntegrationTests
|
||||
Console.WriteLine("AZ-487 AC-2: Expired token returns 401");
|
||||
|
||||
using var client = new HttpClient { BaseAddress = new Uri(apiUrl), Timeout = TimeSpan.FromMinutes(1) };
|
||||
var expired = JwtTokenFactory.CreateExpired(secret, JwtTestHelpers.DefaultSubject);
|
||||
var expired = JwtTestHelpers.MintExpired(secret);
|
||||
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", expired);
|
||||
|
||||
var response = await client.GetAsync(ProtectedTilesPath);
|
||||
@@ -65,7 +67,7 @@ public static class JwtIntegrationTests
|
||||
Console.WriteLine("AZ-487 AC-3: Tampered signature returns 401");
|
||||
|
||||
using var client = new HttpClient { BaseAddress = new Uri(apiUrl), Timeout = TimeSpan.FromMinutes(1) };
|
||||
var valid = JwtTokenFactory.Create(secret, JwtTestHelpers.DefaultSubject);
|
||||
var valid = JwtTestHelpers.MintAuthenticated(secret);
|
||||
var tampered = JwtTokenFactory.TamperSignature(valid);
|
||||
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tampered);
|
||||
|
||||
@@ -83,10 +85,10 @@ public static class JwtIntegrationTests
|
||||
private static async Task ValidToken_Returns200_OnHealthyEndpoint(string apiUrl, string secret)
|
||||
{
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("AZ-487 AC-4: Valid token reaches handler unchanged");
|
||||
Console.WriteLine("AZ-487 AC-4 / AZ-494 AC-3: Valid token with matching iss/aud reaches handler unchanged");
|
||||
|
||||
using var client = new HttpClient { BaseAddress = new Uri(apiUrl), Timeout = TimeSpan.FromMinutes(2) };
|
||||
var valid = JwtTokenFactory.Create(secret, JwtTestHelpers.DefaultSubject);
|
||||
var valid = JwtTestHelpers.MintAuthenticated(secret);
|
||||
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", valid);
|
||||
|
||||
var response = await client.GetAsync(ProtectedTilesPath);
|
||||
@@ -105,6 +107,46 @@ public static class JwtIntegrationTests
|
||||
Console.WriteLine($" ✓ Valid-token request reached handler (HTTP {status})");
|
||||
}
|
||||
|
||||
private static async Task WrongIssuer_Returns401(string apiUrl, string secret)
|
||||
{
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("AZ-494 AC-1: Token with wrong iss returns 401");
|
||||
|
||||
using var client = new HttpClient { BaseAddress = new Uri(apiUrl), Timeout = TimeSpan.FromMinutes(1) };
|
||||
var wrongIssuer = JwtTestHelpers.MintAuthenticated(secret, overrideIssuer: "https://wrong-issuer.invalid/");
|
||||
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", wrongIssuer);
|
||||
|
||||
var response = await client.GetAsync(ProtectedTilesPath);
|
||||
var status = (int)response.StatusCode;
|
||||
|
||||
if (status != 401)
|
||||
{
|
||||
throw new Exception($"Expected 401 for wrong-issuer token, got {status}");
|
||||
}
|
||||
|
||||
Console.WriteLine($" ✓ Wrong-issuer token rejected with HTTP {status}");
|
||||
}
|
||||
|
||||
private static async Task WrongAudience_Returns401(string apiUrl, string secret)
|
||||
{
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("AZ-494 AC-2: Token with wrong aud returns 401");
|
||||
|
||||
using var client = new HttpClient { BaseAddress = new Uri(apiUrl), Timeout = TimeSpan.FromMinutes(1) };
|
||||
var wrongAudience = JwtTestHelpers.MintAuthenticated(secret, overrideAudience: "wrong-audience-not-satellite");
|
||||
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", wrongAudience);
|
||||
|
||||
var response = await client.GetAsync(ProtectedTilesPath);
|
||||
var status = (int)response.StatusCode;
|
||||
|
||||
if (status != 401)
|
||||
{
|
||||
throw new Exception($"Expected 401 for wrong-audience token, got {status}");
|
||||
}
|
||||
|
||||
Console.WriteLine($" ✓ Wrong-audience token rejected with HTTP {status}");
|
||||
}
|
||||
|
||||
private static async Task SwaggerDocument_AdvertisesBearerSecurityScheme(string apiUrl)
|
||||
{
|
||||
// AC-7: Swagger UI accepts a Bearer token via the "Authorize" button. The button is
|
||||
|
||||
Reference in New Issue
Block a user