Files
satellite-provider/SatelliteProvider.IntegrationTests/JwtTestHelpers.cs
T
Oleksandr Bezdieniezhnykh f979e18811 [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>
2026-05-12 02:28:48 +03:00

97 lines
3.6 KiB
C#

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()
{
var secret = Environment.GetEnvironmentVariable(JwtSecretEnvVar);
if (string.IsNullOrWhiteSpace(secret))
{
throw new InvalidOperationException(
$"{JwtSecretEnvVar} is not set in the integration test environment. " +
"It must match the JWT_SECRET configured for the API container.");
}
var byteLength = Encoding.UTF8.GetByteCount(secret);
if (byteLength < 32)
{
throw new InvalidOperationException(
$"{JwtSecretEnvVar} is {byteLength} bytes; the test runner requires at least 32 bytes to match API validation.");
}
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());
}
}