mirror of
https://github.com/azaion/satellite-provider.git
synced 2026-06-22 03:21:15 +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:
@@ -1,11 +1,15 @@
|
||||
using System.Net.Http.Headers;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using SatelliteProvider.TestSupport;
|
||||
|
||||
namespace SatelliteProvider.IntegrationTests;
|
||||
|
||||
public static class JwtTestHelpers
|
||||
{
|
||||
public const string JwtSecretEnvVar = "JWT_SECRET";
|
||||
public const string JwtIssuerEnvVar = "JWT_ISSUER";
|
||||
public const string JwtAudienceEnvVar = "JWT_AUDIENCE";
|
||||
public const string DefaultSubject = "integration-tests";
|
||||
|
||||
public static string ResolveSecretOrThrow()
|
||||
@@ -28,8 +32,65 @@ public static class JwtTestHelpers
|
||||
return secret;
|
||||
}
|
||||
|
||||
// AZ-494: runner-side resolvers for the iss / aud values the API enforces.
|
||||
// Both MUST be present and match the API container's values, or the API
|
||||
// will reject every minted token at validation time.
|
||||
public static string ResolveIssuerOrThrow()
|
||||
{
|
||||
var issuer = Environment.GetEnvironmentVariable(JwtIssuerEnvVar);
|
||||
if (string.IsNullOrWhiteSpace(issuer))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"{JwtIssuerEnvVar} is not set in the integration test environment. " +
|
||||
"It must match the JWT_ISSUER configured for the API container (AZ-494).");
|
||||
}
|
||||
return issuer;
|
||||
}
|
||||
|
||||
public static string ResolveAudienceOrThrow()
|
||||
{
|
||||
var audience = Environment.GetEnvironmentVariable(JwtAudienceEnvVar);
|
||||
if (string.IsNullOrWhiteSpace(audience))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"{JwtAudienceEnvVar} is not set in the integration test environment. " +
|
||||
"It must match the JWT_AUDIENCE configured for the API container (AZ-494).");
|
||||
}
|
||||
return audience;
|
||||
}
|
||||
|
||||
public static void AttachDefaultAuthorization(HttpClient httpClient, string token)
|
||||
{
|
||||
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
|
||||
}
|
||||
|
||||
// AZ-494: convenience wrapper. Reads JWT_ISSUER / JWT_AUDIENCE from env
|
||||
// (cached for the runner's lifetime via resolve calls) and delegates to
|
||||
// the canonical JwtTokenFactory.Create. Test scenarios that need WRONG
|
||||
// iss/aud (e.g. negative AC tests) pass overrides explicitly.
|
||||
public static string MintAuthenticated(
|
||||
string secret,
|
||||
string? subject = null,
|
||||
IEnumerable<Claim>? extraClaims = null,
|
||||
TimeSpan? lifetime = null,
|
||||
string? overrideIssuer = null,
|
||||
string? overrideAudience = null)
|
||||
{
|
||||
return JwtTokenFactory.Create(
|
||||
secret,
|
||||
subject ?? DefaultSubject,
|
||||
lifetime,
|
||||
extraClaims,
|
||||
issuer: overrideIssuer ?? ResolveIssuerOrThrow(),
|
||||
audience: overrideAudience ?? ResolveAudienceOrThrow());
|
||||
}
|
||||
|
||||
public static string MintExpired(string secret, string? subject = null)
|
||||
{
|
||||
return JwtTokenFactory.CreateExpired(
|
||||
secret,
|
||||
subject ?? DefaultSubject,
|
||||
issuer: ResolveIssuerOrThrow(),
|
||||
audience: ResolveAudienceOrThrow());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user