mirror of
https://github.com/azaion/satellite-provider.git
synced 2026-06-21 09:01:16 +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:
@@ -16,3 +16,17 @@ GOOGLE_MAPS_API_KEY=
|
||||
#
|
||||
# Test/CI runs may use a clearly tagged TEST-ONLY value (still >=32 bytes).
|
||||
JWT_SECRET=
|
||||
|
||||
# JWT issuer / audience claims (AZ-494). Both are REQUIRED — the API
|
||||
# fails fast at startup if either is unset or whitespace-only.
|
||||
#
|
||||
# Production values MUST be confirmed by the admin team before deploy
|
||||
# (the admin API stamps the `iss` claim; satellite-provider validates
|
||||
# the `aud` claim). For local dev these can use the DEV-ONLY values
|
||||
# baked into appsettings.Development.json — leave blank to fall back.
|
||||
#
|
||||
# Example values (NEVER use these in prod):
|
||||
# JWT_ISSUER=DEV-ONLY-iss-admin-azaion-local
|
||||
# JWT_AUDIENCE=DEV-ONLY-aud-satellite-provider
|
||||
JWT_ISSUER=
|
||||
JWT_AUDIENCE=
|
||||
|
||||
@@ -8,6 +8,10 @@ public static class AuthenticationServiceCollectionExtensions
|
||||
{
|
||||
public const string JwtSecretEnvVar = "JWT_SECRET";
|
||||
public const string JwtSecretConfigKey = "Jwt:Secret";
|
||||
public const string JwtIssuerEnvVar = "JWT_ISSUER";
|
||||
public const string JwtIssuerConfigKey = "Jwt:Issuer";
|
||||
public const string JwtAudienceEnvVar = "JWT_AUDIENCE";
|
||||
public const string JwtAudienceConfigKey = "Jwt:Audience";
|
||||
public const int MinSecretByteLength = 32;
|
||||
|
||||
public static IServiceCollection AddSatelliteJwt(this IServiceCollection services, IConfiguration configuration)
|
||||
@@ -16,6 +20,8 @@ public static class AuthenticationServiceCollectionExtensions
|
||||
ArgumentNullException.ThrowIfNull(configuration);
|
||||
|
||||
var secret = ResolveSecretOrThrow(configuration);
|
||||
var issuer = ResolveRequiredOrThrow(configuration, JwtIssuerEnvVar, JwtIssuerConfigKey, "JWT issuer");
|
||||
var audience = ResolveRequiredOrThrow(configuration, JwtAudienceEnvVar, JwtAudienceConfigKey, "JWT audience");
|
||||
var signingKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secret));
|
||||
|
||||
services
|
||||
@@ -28,8 +34,10 @@ public static class AuthenticationServiceCollectionExtensions
|
||||
IssuerSigningKey = signingKey,
|
||||
ValidateLifetime = true,
|
||||
ClockSkew = TimeSpan.FromSeconds(30),
|
||||
ValidateIssuer = false,
|
||||
ValidateAudience = false,
|
||||
ValidateIssuer = true,
|
||||
ValidIssuer = issuer,
|
||||
ValidateAudience = true,
|
||||
ValidAudience = audience,
|
||||
RequireSignedTokens = true,
|
||||
RequireExpirationTime = true
|
||||
};
|
||||
@@ -63,4 +71,26 @@ public static class AuthenticationServiceCollectionExtensions
|
||||
|
||||
return secret;
|
||||
}
|
||||
|
||||
// AZ-494: required non-secret config (iss / aud). Fail-fast contract mirrors
|
||||
// JWT_SECRET — missing or whitespace-only values throw at startup so a
|
||||
// production deploy without the operator-confirmed values cannot silently
|
||||
// accept tokens with arbitrary issuer/audience claims.
|
||||
internal static string ResolveRequiredOrThrow(IConfiguration configuration, string envVar, string configKey, string humanLabel)
|
||||
{
|
||||
var value = Environment.GetEnvironmentVariable(envVar);
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
value = configuration[configKey];
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"{humanLabel} is not configured. Set the {envVar} environment variable " +
|
||||
$"or the {configKey} configuration key. (See AZ-494 task spec.)");
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,9 @@
|
||||
"DefaultConnection": "Host=localhost;Port=5432;Database=satelliteprovider;Username=postgres;Password=postgres"
|
||||
},
|
||||
"Jwt": {
|
||||
"Secret": "DEV-ONLY-DO-NOT-USE-IN-PROD-replace-with-real-secret-via-JWT_SECRET-env-var"
|
||||
"Secret": "DEV-ONLY-DO-NOT-USE-IN-PROD-replace-with-real-secret-via-JWT_SECRET-env-var",
|
||||
"Issuer": "DEV-ONLY-iss-admin-azaion-local",
|
||||
"Audience": "DEV-ONLY-aud-satellite-provider"
|
||||
},
|
||||
"MapConfig": {
|
||||
"Service": "GoogleMaps",
|
||||
|
||||
@@ -24,7 +24,9 @@
|
||||
"DefaultConnection": "Host=localhost;Database=satelliteprovider;Username=postgres;Password=postgres"
|
||||
},
|
||||
"Jwt": {
|
||||
"Secret": ""
|
||||
"Secret": "",
|
||||
"Issuer": "",
|
||||
"Audience": ""
|
||||
},
|
||||
"UavQuality": {
|
||||
"MinBytes": 5120,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,9 +21,13 @@ internal static class PerfBootstrap
|
||||
public static int MintToken()
|
||||
{
|
||||
string secret;
|
||||
string issuer;
|
||||
string audience;
|
||||
try
|
||||
{
|
||||
secret = JwtTestHelpers.ResolveSecretOrThrow();
|
||||
issuer = JwtTestHelpers.ResolveIssuerOrThrow();
|
||||
audience = JwtTestHelpers.ResolveAudienceOrThrow();
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
@@ -35,7 +39,9 @@ internal static class PerfBootstrap
|
||||
secret,
|
||||
PerfSubject,
|
||||
PerfTokenLifetime,
|
||||
new[] { new Claim(PermissionsClaimType, GpsPermission) });
|
||||
new[] { new Claim(PermissionsClaimType, GpsPermission) },
|
||||
issuer: issuer,
|
||||
audience: audience);
|
||||
|
||||
Console.Out.Write(token);
|
||||
return 0;
|
||||
|
||||
@@ -44,13 +44,17 @@ class Program
|
||||
?? "Host=postgres;Port=5432;Database=satelliteprovider;Username=postgres;Password=postgres";
|
||||
|
||||
string jwtSecret;
|
||||
string jwtIssuer;
|
||||
string jwtAudience;
|
||||
try
|
||||
{
|
||||
jwtSecret = JwtTestHelpers.ResolveSecretOrThrow();
|
||||
jwtIssuer = JwtTestHelpers.ResolveIssuerOrThrow();
|
||||
jwtAudience = JwtTestHelpers.ResolveAudienceOrThrow();
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
Console.WriteLine("❌ Integration tests cannot start without a valid JWT secret.");
|
||||
Console.WriteLine("❌ Integration tests cannot start: JWT configuration is incomplete.");
|
||||
Console.WriteLine($" {ex.Message}");
|
||||
return 1;
|
||||
}
|
||||
@@ -60,7 +64,7 @@ class Program
|
||||
Console.WriteLine($"API URL : {apiUrl}");
|
||||
Console.WriteLine($"Mode : {(TestRunMode.Smoke ? "smoke (fast subset, tightened timeouts)" : "full")}");
|
||||
Console.WriteLine($"State : {(keepState ? "keep (DB reset skipped)" : "reset (clean DB at startup, AZ-493)")}");
|
||||
Console.WriteLine($"Auth : JWT_SECRET resolved ({System.Text.Encoding.UTF8.GetByteCount(jwtSecret)} bytes)");
|
||||
Console.WriteLine($"Auth : JWT_SECRET resolved ({System.Text.Encoding.UTF8.GetByteCount(jwtSecret)} bytes); iss={jwtIssuer}; aud={jwtAudience}");
|
||||
Console.WriteLine();
|
||||
|
||||
using var httpClient = new HttpClient
|
||||
@@ -69,7 +73,7 @@ class Program
|
||||
Timeout = TimeSpan.FromMinutes(15)
|
||||
};
|
||||
|
||||
var defaultToken = JwtTokenFactory.Create(jwtSecret, JwtTestHelpers.DefaultSubject);
|
||||
var defaultToken = JwtTestHelpers.MintAuthenticated(jwtSecret);
|
||||
JwtTestHelpers.AttachDefaultAuthorization(httpClient, defaultToken);
|
||||
|
||||
try
|
||||
|
||||
@@ -4,7 +4,6 @@ using System.Net.Http.Json;
|
||||
using System.Security.Claims;
|
||||
using System.Text.Json;
|
||||
using Npgsql;
|
||||
using SatelliteProvider.TestSupport;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.Formats.Jpeg;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
@@ -50,7 +49,7 @@ public static class UavUploadTests
|
||||
}
|
||||
};
|
||||
using var client = CreateClient(apiUrl);
|
||||
AttachToken(client, JwtTokenFactory.Create(secret, JwtTestHelpers.DefaultSubject, extraClaims: GpsClaim()));
|
||||
AttachToken(client, JwtTestHelpers.MintAuthenticated(secret, extraClaims: GpsClaim()));
|
||||
|
||||
// Act
|
||||
var response = await PostBatch(client, metadata, new[] { CreateValidJpeg() });
|
||||
@@ -84,7 +83,7 @@ public static class UavUploadTests
|
||||
items = coords.Select(c => new { latitude = c.Latitude, longitude = c.Longitude, tileZoom = 18, tileSizeMeters = 200.0, capturedAt = DateTime.UtcNow.ToString("o") }).ToArray()
|
||||
};
|
||||
using var client = CreateClient(apiUrl);
|
||||
AttachToken(client, JwtTokenFactory.Create(secret, JwtTestHelpers.DefaultSubject, extraClaims: GpsClaim()));
|
||||
AttachToken(client, JwtTestHelpers.MintAuthenticated(secret, extraClaims: GpsClaim()));
|
||||
|
||||
var good = CreateValidJpeg();
|
||||
var wrongDimensions = CreateValidJpeg(width: 512, height: 512);
|
||||
@@ -150,7 +149,7 @@ public static class UavUploadTests
|
||||
}
|
||||
};
|
||||
using var client = CreateClient(apiUrl);
|
||||
AttachToken(client, JwtTokenFactory.Create(secret, JwtTestHelpers.DefaultSubject, extraClaims: GpsClaim()));
|
||||
AttachToken(client, JwtTestHelpers.MintAuthenticated(secret, extraClaims: GpsClaim()));
|
||||
|
||||
// Act
|
||||
var response = await PostBatch(client, metadata, new[] { CreateValidJpeg() });
|
||||
@@ -174,7 +173,7 @@ public static class UavUploadTests
|
||||
// Arrange
|
||||
var coord = NextTestCoordinate();
|
||||
using var client = CreateClient(apiUrl);
|
||||
AttachToken(client, JwtTokenFactory.Create(secret, JwtTestHelpers.DefaultSubject, extraClaims: GpsClaim()));
|
||||
AttachToken(client, JwtTestHelpers.MintAuthenticated(secret, extraClaims: GpsClaim()));
|
||||
|
||||
var firstMetadata = new
|
||||
{
|
||||
@@ -242,7 +241,7 @@ public static class UavUploadTests
|
||||
|
||||
// Arrange
|
||||
using var client = CreateClient(apiUrl);
|
||||
AttachToken(client, JwtTokenFactory.Create(secret, JwtTestHelpers.DefaultSubject, extraClaims: new[] { new Claim(PermissionsClaimType, "FL") }));
|
||||
AttachToken(client, JwtTestHelpers.MintAuthenticated(secret, extraClaims: new[] { new Claim(PermissionsClaimType, "FL") }));
|
||||
var coord = NextTestCoordinate();
|
||||
var metadata = new
|
||||
{
|
||||
@@ -284,7 +283,7 @@ public static class UavUploadTests
|
||||
var placeholder = new byte[] { 0xFF, 0xD8, 0xFF, 0xD9 };
|
||||
var files = Enumerable.Range(0, oversize).Select(_ => placeholder).ToArray();
|
||||
using var client = CreateClient(apiUrl);
|
||||
AttachToken(client, JwtTokenFactory.Create(secret, JwtTestHelpers.DefaultSubject, extraClaims: GpsClaim()));
|
||||
AttachToken(client, JwtTestHelpers.MintAuthenticated(secret, extraClaims: GpsClaim()));
|
||||
|
||||
// Act
|
||||
var response = await PostBatch(client, metadata, files);
|
||||
|
||||
@@ -14,7 +14,9 @@ public static class JwtTokenFactory
|
||||
string subject = DefaultSubject,
|
||||
TimeSpan? lifetime = null,
|
||||
IEnumerable<Claim>? extraClaims = null,
|
||||
string algorithm = SecurityAlgorithms.HmacSha256)
|
||||
string algorithm = SecurityAlgorithms.HmacSha256,
|
||||
string? issuer = null,
|
||||
string? audience = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(secret);
|
||||
|
||||
@@ -43,8 +45,8 @@ public static class JwtTokenFactory
|
||||
}
|
||||
|
||||
var token = new JwtSecurityToken(
|
||||
issuer: null,
|
||||
audience: null,
|
||||
issuer: issuer,
|
||||
audience: audience,
|
||||
claims: claims,
|
||||
notBefore: notBefore,
|
||||
expires: expires,
|
||||
@@ -53,9 +55,9 @@ public static class JwtTokenFactory
|
||||
return new JwtSecurityTokenHandler().WriteToken(token);
|
||||
}
|
||||
|
||||
public static string CreateExpired(string secret, string subject = DefaultSubject)
|
||||
public static string CreateExpired(string secret, string subject = DefaultSubject, string? issuer = null, string? audience = null)
|
||||
{
|
||||
return Create(secret, subject, lifetime: TimeSpan.FromMinutes(-5));
|
||||
return Create(secret, subject, lifetime: TimeSpan.FromMinutes(-5), issuer: issuer, audience: audience);
|
||||
}
|
||||
|
||||
public static string TamperSignature(string token)
|
||||
|
||||
+119
-13
@@ -15,17 +15,27 @@ namespace SatelliteProvider.Tests.Authentication;
|
||||
public class AuthenticationServiceCollectionExtensionsTests : IDisposable
|
||||
{
|
||||
private const string ValidSecret = "test-secret-that-is-definitely-longer-than-32-bytes";
|
||||
private readonly string? _originalEnv;
|
||||
private const string ValidIssuer = "https://test-issuer.example/";
|
||||
private const string ValidAudience = "satellite-provider-tests";
|
||||
private readonly string? _originalSecret;
|
||||
private readonly string? _originalIssuer;
|
||||
private readonly string? _originalAudience;
|
||||
|
||||
public AuthenticationServiceCollectionExtensionsTests()
|
||||
{
|
||||
_originalEnv = Environment.GetEnvironmentVariable(AuthExtensions.JwtSecretEnvVar);
|
||||
_originalSecret = Environment.GetEnvironmentVariable(AuthExtensions.JwtSecretEnvVar);
|
||||
_originalIssuer = Environment.GetEnvironmentVariable(AuthExtensions.JwtIssuerEnvVar);
|
||||
_originalAudience = Environment.GetEnvironmentVariable(AuthExtensions.JwtAudienceEnvVar);
|
||||
Environment.SetEnvironmentVariable(AuthExtensions.JwtSecretEnvVar, null);
|
||||
Environment.SetEnvironmentVariable(AuthExtensions.JwtIssuerEnvVar, null);
|
||||
Environment.SetEnvironmentVariable(AuthExtensions.JwtAudienceEnvVar, null);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Environment.SetEnvironmentVariable(AuthExtensions.JwtSecretEnvVar, _originalEnv);
|
||||
Environment.SetEnvironmentVariable(AuthExtensions.JwtSecretEnvVar, _originalSecret);
|
||||
Environment.SetEnvironmentVariable(AuthExtensions.JwtIssuerEnvVar, _originalIssuer);
|
||||
Environment.SetEnvironmentVariable(AuthExtensions.JwtAudienceEnvVar, _originalAudience);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
@@ -35,7 +45,7 @@ public class AuthenticationServiceCollectionExtensionsTests : IDisposable
|
||||
// Arrange
|
||||
var services = new ServiceCollection();
|
||||
services.AddLogging();
|
||||
var configuration = BuildConfiguration(("Jwt:Secret", ValidSecret));
|
||||
var configuration = BuildValidConfiguration();
|
||||
|
||||
// Act
|
||||
services.AddSatelliteJwt(configuration);
|
||||
@@ -54,7 +64,7 @@ public class AuthenticationServiceCollectionExtensionsTests : IDisposable
|
||||
// Arrange
|
||||
var services = new ServiceCollection();
|
||||
services.AddLogging();
|
||||
var configuration = BuildConfiguration(("Jwt:Secret", ValidSecret));
|
||||
var configuration = BuildValidConfiguration();
|
||||
|
||||
// Act
|
||||
services.AddSatelliteJwt(configuration);
|
||||
@@ -65,8 +75,10 @@ public class AuthenticationServiceCollectionExtensionsTests : IDisposable
|
||||
var p = options.TokenValidationParameters;
|
||||
p.ValidateIssuerSigningKey.Should().BeTrue();
|
||||
p.ValidateLifetime.Should().BeTrue();
|
||||
p.ValidateIssuer.Should().BeFalse();
|
||||
p.ValidateAudience.Should().BeFalse();
|
||||
p.ValidateIssuer.Should().BeTrue("AZ-494 flipped issuer validation on");
|
||||
p.ValidIssuer.Should().Be(ValidIssuer);
|
||||
p.ValidateAudience.Should().BeTrue("AZ-494 flipped audience validation on");
|
||||
p.ValidAudience.Should().Be(ValidAudience);
|
||||
p.RequireSignedTokens.Should().BeTrue();
|
||||
p.RequireExpirationTime.Should().BeTrue();
|
||||
p.ClockSkew.Should().Be(TimeSpan.FromSeconds(30));
|
||||
@@ -78,7 +90,9 @@ public class AuthenticationServiceCollectionExtensionsTests : IDisposable
|
||||
{
|
||||
// Arrange
|
||||
var services = new ServiceCollection();
|
||||
var configuration = BuildConfiguration();
|
||||
var configuration = BuildConfiguration(
|
||||
("Jwt:Issuer", ValidIssuer),
|
||||
("Jwt:Audience", ValidAudience));
|
||||
|
||||
// Act
|
||||
var act = () => services.AddSatelliteJwt(configuration);
|
||||
@@ -93,7 +107,10 @@ public class AuthenticationServiceCollectionExtensionsTests : IDisposable
|
||||
{
|
||||
// Arrange
|
||||
var services = new ServiceCollection();
|
||||
var configuration = BuildConfiguration(("Jwt:Secret", ""));
|
||||
var configuration = BuildConfiguration(
|
||||
("Jwt:Secret", ""),
|
||||
("Jwt:Issuer", ValidIssuer),
|
||||
("Jwt:Audience", ValidAudience));
|
||||
|
||||
// Act
|
||||
var act = () => services.AddSatelliteJwt(configuration);
|
||||
@@ -108,7 +125,10 @@ public class AuthenticationServiceCollectionExtensionsTests : IDisposable
|
||||
{
|
||||
// Arrange
|
||||
var services = new ServiceCollection();
|
||||
var configuration = BuildConfiguration(("Jwt:Secret", "too-short-secret"));
|
||||
var configuration = BuildConfiguration(
|
||||
("Jwt:Secret", "too-short-secret"),
|
||||
("Jwt:Issuer", ValidIssuer),
|
||||
("Jwt:Audience", ValidAudience));
|
||||
|
||||
// Act
|
||||
var act = () => services.AddSatelliteJwt(configuration);
|
||||
@@ -118,28 +138,114 @@ public class AuthenticationServiceCollectionExtensionsTests : IDisposable
|
||||
.WithMessage("*at least 32 bytes*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddSatelliteJwt_ThrowsOnMissingIssuer()
|
||||
{
|
||||
// Arrange — AZ-494 AC-4: fail-fast on missing issuer.
|
||||
var services = new ServiceCollection();
|
||||
var configuration = BuildConfiguration(
|
||||
("Jwt:Secret", ValidSecret),
|
||||
("Jwt:Audience", ValidAudience));
|
||||
|
||||
// Act
|
||||
var act = () => services.AddSatelliteJwt(configuration);
|
||||
|
||||
// Assert
|
||||
act.Should().Throw<InvalidOperationException>()
|
||||
.WithMessage("*JWT issuer is not configured*")
|
||||
.Where(ex => ex.Message.Contains(AuthExtensions.JwtIssuerEnvVar));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddSatelliteJwt_ThrowsOnEmptyIssuer()
|
||||
{
|
||||
// Arrange
|
||||
var services = new ServiceCollection();
|
||||
var configuration = BuildConfiguration(
|
||||
("Jwt:Secret", ValidSecret),
|
||||
("Jwt:Issuer", " "),
|
||||
("Jwt:Audience", ValidAudience));
|
||||
|
||||
// Act
|
||||
var act = () => services.AddSatelliteJwt(configuration);
|
||||
|
||||
// Assert
|
||||
act.Should().Throw<InvalidOperationException>()
|
||||
.WithMessage("*JWT issuer is not configured*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddSatelliteJwt_ThrowsOnMissingAudience()
|
||||
{
|
||||
// Arrange — AZ-494 AC-4: fail-fast on missing audience.
|
||||
var services = new ServiceCollection();
|
||||
var configuration = BuildConfiguration(
|
||||
("Jwt:Secret", ValidSecret),
|
||||
("Jwt:Issuer", ValidIssuer));
|
||||
|
||||
// Act
|
||||
var act = () => services.AddSatelliteJwt(configuration);
|
||||
|
||||
// Assert
|
||||
act.Should().Throw<InvalidOperationException>()
|
||||
.WithMessage("*JWT audience is not configured*")
|
||||
.Where(ex => ex.Message.Contains(AuthExtensions.JwtAudienceEnvVar));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddSatelliteJwt_ThrowsOnEmptyAudience()
|
||||
{
|
||||
// Arrange
|
||||
var services = new ServiceCollection();
|
||||
var configuration = BuildConfiguration(
|
||||
("Jwt:Secret", ValidSecret),
|
||||
("Jwt:Issuer", ValidIssuer),
|
||||
("Jwt:Audience", ""));
|
||||
|
||||
// Act
|
||||
var act = () => services.AddSatelliteJwt(configuration);
|
||||
|
||||
// Assert
|
||||
act.Should().Throw<InvalidOperationException>()
|
||||
.WithMessage("*JWT audience is not configured*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddSatelliteJwt_PrefersEnvironmentVariableOverConfiguration()
|
||||
{
|
||||
// Arrange
|
||||
const string envSecret = "env-secret-also-longer-than-thirty-two-bytes-for-hmac";
|
||||
const string envIssuer = "https://env-issuer.example/";
|
||||
const string envAudience = "env-audience";
|
||||
Environment.SetEnvironmentVariable(AuthExtensions.JwtSecretEnvVar, envSecret);
|
||||
Environment.SetEnvironmentVariable(AuthExtensions.JwtIssuerEnvVar, envIssuer);
|
||||
Environment.SetEnvironmentVariable(AuthExtensions.JwtAudienceEnvVar, envAudience);
|
||||
var services = new ServiceCollection();
|
||||
services.AddLogging();
|
||||
var configuration = BuildConfiguration(("Jwt:Secret", "config-secret-also-32-bytes-long-aaaaaaaaaa"));
|
||||
var configuration = BuildConfiguration(
|
||||
("Jwt:Secret", "config-secret-also-32-bytes-long-aaaaaaaaaa"),
|
||||
("Jwt:Issuer", "https://config-issuer.example/"),
|
||||
("Jwt:Audience", "config-audience"));
|
||||
|
||||
// Act
|
||||
services.AddSatelliteJwt(configuration);
|
||||
var provider = services.BuildServiceProvider();
|
||||
var options = provider.GetRequiredService<IOptionsMonitor<JwtBearerOptions>>().Get(JwtBearerDefaults.AuthenticationScheme);
|
||||
var token = JwtTokenFactory.Create(envSecret);
|
||||
var token = JwtTokenFactory.Create(envSecret, issuer: envIssuer, audience: envAudience);
|
||||
var handler = new JwtSecurityTokenHandler();
|
||||
var act = () => handler.ValidateToken(token, options.TokenValidationParameters, out _);
|
||||
|
||||
// Assert
|
||||
act.Should().NotThrow("token signed with env secret must validate when env secret takes precedence");
|
||||
act.Should().NotThrow("token signed with env secret + minted with env iss/aud must validate when env precedence applies");
|
||||
options.TokenValidationParameters.ValidIssuer.Should().Be(envIssuer);
|
||||
options.TokenValidationParameters.ValidAudience.Should().Be(envAudience);
|
||||
}
|
||||
|
||||
private static IConfiguration BuildValidConfiguration() => BuildConfiguration(
|
||||
("Jwt:Secret", ValidSecret),
|
||||
("Jwt:Issuer", ValidIssuer),
|
||||
("Jwt:Audience", ValidAudience));
|
||||
|
||||
private static IConfiguration BuildConfiguration(params (string Key, string Value)[] pairs)
|
||||
{
|
||||
var builder = new ConfigurationBuilder();
|
||||
|
||||
@@ -27,7 +27,7 @@ The three Layer-3 service components are compile-time siblings: each only refere
|
||||
**Authentication & Authorization** (AZ-487):
|
||||
- Validation library: `Microsoft.AspNetCore.Authentication.JwtBearer` 8.0.25 (matches `Microsoft.AspNetCore.OpenApi` 8.0.25; AZ-496 bumped both packages from 8.0.21 to close the cycle-1 D1 + cycle-2 D3 supply-chain findings).
|
||||
- Signing key: read from the `JWT_SECRET` environment variable (preferred) or the `Jwt:Secret` configuration key. Startup fails fast if the resolved secret is unset, empty, or shorter than 32 bytes (HMAC-SHA256 minimum per RFC 2104 §3).
|
||||
- Token contract: `ValidateIssuerSigningKey = true`, `ValidateLifetime = true`, `RequireSignedTokens = true`, `RequireExpirationTime = true`, `ValidateIssuer/Audience = false`, `ClockSkew = 30s`. The 5-minute JwtBearer default is intentionally tightened.
|
||||
- Token contract: `ValidateIssuerSigningKey = true`, `ValidateLifetime = true`, `RequireSignedTokens = true`, `RequireExpirationTime = true`, `ValidateIssuer = true` + `ValidIssuer = $JWT_ISSUER`, `ValidateAudience = true` + `ValidAudience = $JWT_AUDIENCE` (AZ-494), `ClockSkew = 30s`. The 5-minute JwtBearer default is intentionally tightened.
|
||||
- Authorization model: every endpoint registered in `Program.cs` is decorated with `.RequireAuthorization()`. AZ-488 adds `permissions`-claim policies on top of this baseline (UAV upload requires `GPS`).
|
||||
- Test infrastructure: `JwtTokenFactory` (unit tests) and `JwtTestHelpers` (integration tests) mint deterministic tokens against the same `JWT_SECRET`; the integration test runner attaches a default Bearer token to its shared `HttpClient` so legacy non-auth tests continue to exercise the protected endpoints unchanged.
|
||||
|
||||
@@ -142,7 +142,7 @@ The N-source storage contract is authoritative in `_docs/02_document/contracts/d
|
||||
|
||||
## 7. Security Architecture
|
||||
|
||||
**Authentication**: HS256 JWT Bearer tokens (AZ-487). Signing key from `JWT_SECRET` env var (≥ 32 bytes, validated at startup). `Microsoft.AspNetCore.Authentication.JwtBearer` validates signature, lifetime, and signing key; issuer and audience are intentionally not validated (suite contract does not specify expected values). ClockSkew tightened from JwtBearer default (5 min) to 30 s. Tokens are minted by the centralized Admin API per `suite/_docs/10_auth.md`.
|
||||
**Authentication**: HS256 JWT Bearer tokens (AZ-487 + AZ-494). Signing key from `JWT_SECRET` env var (≥ 32 bytes, validated at startup). Issuer and audience claims are validated against `JWT_ISSUER` / `JWT_AUDIENCE` env vars (AZ-494) — both required, fail-fast at startup if unset. `Microsoft.AspNetCore.Authentication.JwtBearer` validates signature, lifetime, signing key, issuer, and audience. ClockSkew tightened from JwtBearer default (5 min) to 30 s. Tokens are minted by the centralized Admin API per `suite/_docs/10_auth.md`; their `iss` and `aud` claims MUST match the satellite-provider configured values or validation rejects with 401.
|
||||
|
||||
**Authorization**: Every endpoint requires authentication via `.RequireAuthorization()`. Permission-claim enforcement is layered on top through the `PermissionsRequirement` authorization handler, which reads the `permissions` claim (accepting either repeated string claims OR a single JSON-array string). AZ-488 wires the `RequiresGpsPermission` policy on `POST /api/satellite/upload` — callers without `GPS` receive HTTP 403; other endpoints accept any authenticated principal.
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ Application entry point. Configures DI container, sets up middleware, defines mi
|
||||
7. Hosted services: `RegionProcessingService`, `RouteProcessingService`
|
||||
8. CORS policy: `TilesCors` — configured origins from `CorsConfig:AllowedOrigins`, falls back to allow-any
|
||||
9. JSON options: camelCase, case-insensitive
|
||||
10. **JWT authentication (AZ-487)**: `AddSatelliteJwt(builder.Configuration)` (extension in `SatelliteProvider.Api.Authentication`) registers `JwtBearer` with `TokenValidationParameters` set per the suite auth contract (signature + lifetime, no issuer/audience validation, 30 s clock skew, ≥ 32-byte HMAC key). Followed by `AddAuthorization` with the `RequiresGpsPermission` policy (AZ-488). The `PermissionsAuthorizationHandler` singleton supports both repeated-string and JSON-array shapes for the `permissions` claim.
|
||||
10. **JWT authentication (AZ-487 + AZ-494)**: `AddSatelliteJwt(builder.Configuration)` (extension in `SatelliteProvider.Api.Authentication`) registers `JwtBearer` with `TokenValidationParameters` set per the suite auth contract: signature + lifetime + issuer + audience validation, 30 s clock skew, ≥ 32-byte HMAC key. The `iss` value comes from `JWT_ISSUER` env (fallback `Jwt:Issuer` config); the `aud` value comes from `JWT_AUDIENCE` env (fallback `Jwt:Audience` config). All three values (secret, iss, aud) are fail-fast — the API throws `InvalidOperationException` at startup if any is unset or whitespace-only. Production deploys MUST set the env vars with admin-team-confirmed values; `appsettings.json` ships empty so the fail-fast triggers. `appsettings.Development.json` ships clearly-tagged DEV-ONLY values (`DEV-ONLY-iss-admin-azaion-local` / `DEV-ONLY-aud-satellite-provider`) so local dev works out-of-the-box. Followed by `AddAuthorization` with the `RequiresGpsPermission` policy (AZ-488).
|
||||
|
||||
### Startup
|
||||
1. Database migration via `DatabaseMigrator.RunMigrations()` — throws on failure
|
||||
@@ -85,6 +85,8 @@ All configuration sections are consumed here:
|
||||
- `UavQuality` (AZ-488) — `MinBytes`, `MaxBytes`, `MaxAgeDays`, `CapturedAtFutureSkewSeconds`, `MinLuminanceVariance`, `MaxBatchSize`, `LuminanceSampleSize`. Drives the 5-rule quality gate AND the per-request body-size limits.
|
||||
- `CorsConfig:AllowedOrigins`
|
||||
- `Jwt:Secret` — HMAC-SHA256 signing key for JWT validation (AZ-487). Resolution: `JWT_SECRET` env var (preferred, opaque production secret) → `Jwt:Secret` configuration key (`appsettings.Development.json` placeholder only). Startup fails fast if the resolved value is unset, empty, or shorter than 32 bytes.
|
||||
- `Jwt:Issuer` — Expected `iss` claim value (AZ-494). Resolution: `JWT_ISSUER` env → `Jwt:Issuer` config. Startup fails fast if unset/empty.
|
||||
- `Jwt:Audience` — Expected `aud` claim value (AZ-494). Resolution: `JWT_AUDIENCE` env → `Jwt:Audience` config. Startup fails fast if unset/empty.
|
||||
- `Serilog` section
|
||||
|
||||
## External Integrations
|
||||
|
||||
@@ -12,7 +12,7 @@ Console application that runs end-to-end integration tests against a live API in
|
||||
- `ComplexRouteTests` — routes with geofencing
|
||||
- `ExtendedRouteTests` — routes with `requestMaps: true` and tile ZIP creation
|
||||
- `MigrationTests` — direct PostgreSQL schema/index validation (no HTTP). AZ-484 cycle added: `NewUniqueConstraintIncludesSourceColumn_AZ484_AC1`, `BackfillUpdateAssignsGoogleMapsAndCapturedAt_AZ484_AC4`, `MultiSourceInsertCoexistsUnderNewIndex_AZ484_AC1`, `MostRecentAcrossSourcesSelection_AZ484_AC2`, `SameSourceUpsertReplacesPreviousRow_AZ484_AC3` (latter four use temp tables to keep production data untouched).
|
||||
- `JwtIntegrationTests` (added by AZ-487, cycle 2; helpers consolidated by AZ-491 cycle 3) — `AnonymousRequest_To_AnyEndpoint_Returns401`, `ExpiredToken_Returns401`, `InvalidSignature_Returns401`, `ValidToken_Returns200_OnHealthyEndpoint`, `SwaggerDocument_AdvertisesBearerSecurityScheme`. HS256 token minting lives in the shared `SatelliteProvider.TestSupport.JwtTokenFactory` (consumed via `ProjectReference`); runner-specific concerns (`JwtTestHelpers.ResolveSecretOrThrow`, `AttachDefaultAuthorization`, `DefaultSubject = "integration-tests"`) remain in this project. The test runner sets `JWT_SECRET` on the API container and attaches a Bearer token to every existing test's HTTP requests so the pre-cycle-2 suite continues to pass.
|
||||
- `JwtIntegrationTests` (added by AZ-487 cycle 2; helpers consolidated by AZ-491 cycle 3; iss/aud scenarios added by AZ-494 cycle 3) — `AnonymousRequest_To_AnyEndpoint_Returns401`, `ExpiredToken_Returns401`, `InvalidSignature_Returns401`, `ValidToken_Returns200_OnHealthyEndpoint`, `WrongIssuer_Returns401` (AZ-494 AC-1), `WrongAudience_Returns401` (AZ-494 AC-2), `SwaggerDocument_AdvertisesBearerSecurityScheme`. HS256 token minting lives in the shared `SatelliteProvider.TestSupport.JwtTokenFactory` (consumed via `ProjectReference`); runner-specific concerns (`JwtTestHelpers.ResolveSecretOrThrow` / `ResolveIssuerOrThrow` / `ResolveAudienceOrThrow`, `MintAuthenticated` / `MintExpired` convenience wrappers that auto-fill iss+aud from env, `AttachDefaultAuthorization`, `DefaultSubject = "integration-tests"`) remain in this project. The test runner sets `JWT_SECRET` + `JWT_ISSUER` + `JWT_AUDIENCE` on the API container and attaches a Bearer token (with matching iss/aud) to every existing test's HTTP requests so the pre-cycle-2 suite continues to pass.
|
||||
- `UavUploadTests` (added by AZ-488, cycle 2; coordinate-counter promoted to defense-in-depth by AZ-493 cycle 3) — `HappyPathSingleItem_PersistsRow`, `MixedBatch_ReturnsPerItemResults`, `MultiSourceCoexistence_AZ484_Cycle2`, `SameSourceUpsert_AZ484_Cycle2`, `NoToken_Returns401`, `ValidTokenWithoutGpsPermission_Returns403`, `OversizedBatch_Returns400`. The wall-clock-seeded `_coordinateCounter` is retained as a belt-and-suspenders safeguard alongside the AZ-493 startup DB-reset (below) — if a developer runs with `--keep-state`, or the DB-reset path is skipped for any reason, the wall-clock seed still spreads coordinates across runs so the per-source unique index does not collide.
|
||||
- `StubAndErrorContractTests` (existing) — updated in cycle 2 to drop the legacy `StubUpload_Returns501` expectation since AZ-488 implemented the endpoint.
|
||||
|
||||
@@ -20,7 +20,14 @@ Console application that runs end-to-end integration tests against a live API in
|
||||
- `Models.cs` — HTTP response DTOs for deserialization
|
||||
- `RouteTestHelpers.cs` — shared utilities (wait-for-completion polling, geofence polygon builders, test data)
|
||||
- `Program.cs` — test runner entry point (handles `--smoke` / `--full` mode selection, `--keep-state` opt-out flag, default-token issuance via `JwtTokenFactory`, the AZ-493 DB-reset hook, and the AZ-492 `--mint-only` / `--gen-uav-fixture` perf-bootstrap subcommands that short-circuit before any HTTP / DB setup)
|
||||
- `JwtTestHelpers.cs` — runner-side JWT concerns (`ResolveSecretOrThrow` reads the `JWT_SECRET` env var with size validation; `AttachDefaultAuthorization` puts a Bearer token on the shared `HttpClient`; `DefaultSubject = "integration-tests"` is the canonical runner subject value). Token *minting* lives in the shared `SatelliteProvider.TestSupport.JwtTokenFactory` (AZ-491) — runner-side concerns deliberately stay here.
|
||||
- `JwtTestHelpers.cs` — runner-side JWT concerns:
|
||||
- `ResolveSecretOrThrow` reads `JWT_SECRET` env var with size validation
|
||||
- `ResolveIssuerOrThrow` / `ResolveAudienceOrThrow` (AZ-494) read `JWT_ISSUER` / `JWT_AUDIENCE` with fail-fast contract
|
||||
- `MintAuthenticated(secret, …)` (AZ-494) auto-fills iss/aud from env and delegates to `JwtTokenFactory.Create`; accepts `overrideIssuer` / `overrideAudience` for negative-AC scenarios (`WrongIssuer_Returns401` / `WrongAudience_Returns401`)
|
||||
- `MintExpired(secret, …)` (AZ-494) mirrors `MintAuthenticated` for the expired-token fixture
|
||||
- `AttachDefaultAuthorization` puts a Bearer token on the shared `HttpClient`
|
||||
- `DefaultSubject = "integration-tests"` is the canonical runner subject value
|
||||
- Token *minting* lives in the shared `SatelliteProvider.TestSupport.JwtTokenFactory` (AZ-491) — runner-side concerns (env reads, HttpClient mutation, the iss/aud-aware mint wrapper) deliberately stay here.
|
||||
- `IntegrationTestDatabaseReset.cs` (AZ-493) — instance class with a single `EnsureCleanStateAsync()` method that truncates the integration-test target tables in FK-safe order. Guarded via `SatelliteProvider.TestSupport.IntegrationTestResetGuard` (env + Host allowlist) so it cannot run against a non-test database.
|
||||
- `PerfBootstrap.cs` (AZ-492) — static helpers for the perf harness bootstrap subcommands. `MintToken()` mints a 4-hour HS256 token with subject `perf-tests` and a `permissions: GPS` claim via the canonical `SatelliteProvider.TestSupport.JwtTokenFactory.Create`; `GenerateUavFixture(args)` writes a 256×256 random-noise JPEG via `SixLabors.ImageSharp` to the path passed on the CLI. Invoked from `scripts/run-performance-tests.sh` via `dotnet <SatelliteProvider.IntegrationTests.dll> --mint-only` and `--gen-uav-fixture <path>`.
|
||||
|
||||
@@ -44,6 +51,8 @@ Console application that runs end-to-end integration tests against a live API in
|
||||
- `INTEGRATION_KEEP_STATE` — set to `1` or `true` (or pass `--keep-state` to `Program.cs` / `scripts/run-tests.sh`) to skip the AZ-493 DB-reset hook. Useful for debugging a failed run.
|
||||
- `ASPNETCORE_ENVIRONMENT=Testing` — guard for the DB-reset hook. The reset refuses to run unless this is set (see Reliability § Test isolation below).
|
||||
- `JWT_SECRET` — shared HMAC secret with the API container; must be ≥ 32 bytes (UTF-8).
|
||||
- `JWT_ISSUER` — expected `iss` claim, must match the API container (AZ-494). Fail-fast at startup if unset.
|
||||
- `JWT_AUDIENCE` — expected `aud` claim, must match the API container (AZ-494). Fail-fast at startup if unset.
|
||||
- `DB_CONNECTION_STRING` — Npgsql connection string; the reset hook additionally requires the Host to be in the allowed-host list (`postgres`, `localhost`, `127.0.0.1`).
|
||||
|
||||
## Reliability
|
||||
|
||||
@@ -9,7 +9,8 @@ Existing baseline (pre-cycle-2) test classes cover `TileService`, `RegionService
|
||||
|
||||
### AZ-487 — JWT validation baseline
|
||||
- `Authentication/AuthenticationServiceCollectionExtensionsTests` — `AddSatelliteJwt_RegistersJwtBearerScheme`, `AddSatelliteJwt_ThrowsOnMissingSecret`, `AddSatelliteJwt_ThrowsOnShortSecret`.
|
||||
- `Authentication/JwtTokenFactoryTests` — `Create_ProducesTokenValidatedByMatchingParameters`, `CreateExpired_TokenFailsValidationWithLifetimeException`, `Create_WithExtraClaims_PropagatesClaimsThroughValidation`.
|
||||
- `Authentication/JwtTokenFactoryTests` — `Create_ProducesTokenValidatedByMatchingParameters`, `CreateExpired_TokenFailsValidationWithLifetimeException`, `Create_WithExtraClaims_PropagatesClaimsThroughValidation`, `TamperSignature_TokenFailsValidationWithSignatureException`.
|
||||
- `Authentication/AuthenticationServiceCollectionExtensionsTests` — covers `AddSatelliteJwt` registration + `TokenValidationParameters` shape (AZ-487, AZ-494). Includes AZ-494's fail-fast assertions: `AddSatelliteJwt_ThrowsOnMissingIssuer`, `AddSatelliteJwt_ThrowsOnEmptyIssuer`, `AddSatelliteJwt_ThrowsOnMissingAudience`, `AddSatelliteJwt_ThrowsOnEmptyAudience`, plus the updated `_ConfiguresTokenValidationParameters_AsPerContract` and `_PrefersEnvironmentVariableOverConfiguration` cases that assert `ValidateIssuer = true` + `ValidIssuer` + `ValidateAudience = true` + `ValidAudience`.
|
||||
- `TestUtilities/JwtTokenFactory` — helper that mints HS256 tokens with the same `TokenValidationParameters` used in production. Adjusts `notBefore` for negative-lifetime requests so `JwtSecurityToken` accepts the value and downstream lifetime validation can fire (`IDX12401` workaround documented inline).
|
||||
|
||||
### AZ-488 — UAV tile upload
|
||||
|
||||
@@ -56,6 +56,13 @@
|
||||
| AZ-488 AC-8 | Oversized batch (> `MaxBatchSize`) returns 400 envelope error | RL-05 (resource-limit); `UavUploadTests.OversizedBatch_Returns400` (integration) | ✓ |
|
||||
| AZ-488 AC-9 | Contract `uav-tile-upload.md` v1.0.0 frozen and matches implementation | doc-state AC; verified by Step 13 (Update Docs) review | ✓ |
|
||||
| AZ-488 AC-10 | All existing tests + new AZ-487/AZ-488 tests pass; no AZ-484 regression | Full `scripts/run-tests.sh --full` run (cycle 2 Step 11 — passed) | ✓ |
|
||||
| AZ-494 AC-1 | Wrong `iss` token returns 401 | `JwtIntegrationTests.WrongIssuer_Returns401` (integration) | ✓ |
|
||||
| AZ-494 AC-2 | Wrong `aud` token returns 401 | `JwtIntegrationTests.WrongAudience_Returns401` (integration) | ✓ |
|
||||
| AZ-494 AC-3 | Matching iss + aud accepted | `JwtIntegrationTests.ValidToken_Returns200_OnHealthyEndpoint` (integration; updated to mint via env iss/aud) | ✓ |
|
||||
| AZ-494 AC-4 | Missing config fails fast | `AuthenticationServiceCollectionExtensionsTests.AddSatelliteJwt_ThrowsOnMissingIssuer` + `_ThrowsOnEmptyIssuer` + `_ThrowsOnMissingAudience` + `_ThrowsOnEmptyAudience` (unit) | ✓ |
|
||||
| AZ-494 AC-5 | Existing tests pass with matched fixtures | Full integration suite reruns at Step 16 with `JwtTestHelpers.MintAuthenticated` (auto-fills iss/aud from env) | ✓ (gate verified at Step 16) |
|
||||
| AZ-494 AC-6 | Security artifacts updated (F-AUTH-2 → Resolved) | `_docs/05_security/security_report.md` + `owasp_review.md` updated this batch | ✓ |
|
||||
| AZ-494 AC-7 | Suite contract reflects reality | `suite/_docs/10_auth.md` lives outside this workspace; this cycle's deploy report documents that satellite-provider validates iss/aud locally and the prod values are admin-team-confirmed at deploy time | ◐ deferred (cross-repo write) |
|
||||
|
||||
## Restrictions → Test Mapping
|
||||
|
||||
@@ -77,8 +84,8 @@
|
||||
| AZ-484 Perf — `GetTilesByRegionAsync` p95 ≤ 1.10 × pre-AZ-484 baseline | AZ-484 task spec § Non-Functional Requirements | PT-07 (Implemented in AZ-492 — cold + warm distribution, p50/p95 reported; cross-commit baseline comparison remains operator-driven at Step 15) | ✓ |
|
||||
| AZ-484 Compatibility — no public HTTP response field added/removed; vestigial `maps_version`/`version` columns preserved (nullable) | AZ-484 task spec § Non-Functional Requirements | Existing integration suite (no API contract change observable); BT-01 / region status responses verify response shape | ✓ |
|
||||
| AZ-487 Performance — JWT validation < 1 ms overhead per request | AZ-487 task spec § Non-Functional Requirements | Not separately measured (HMAC-SHA256 + claims parse is sub-millisecond on any modern x86; no caching needed). Re-measure if PT-07 / PT-08 (AZ-492 harness) shows aggregate regression. | ◐ recorded |
|
||||
| AZ-487 Security — `RequireSignedTokens`, `RequireExpirationTime`, `ClockSkew = 30 s`, secret ≥ 32 bytes | AZ-487 task spec § Non-Functional Requirements + Constraints | `AuthenticationServiceCollectionExtensionsTests.AddSatelliteJwt_ThrowsOnShortSecret` (unit) + SEC-06/SEC-07 (blackbox) | ✓ |
|
||||
| AZ-487 Reliability — Fail-fast on missing / short `JWT_SECRET` at startup | AZ-487 task spec § Non-Functional Requirements | SEC-08 (behavioral) + unit `AddSatelliteJwt_ThrowsOnMissingSecret` | ✓ |
|
||||
| AZ-487 Security — `RequireSignedTokens`, `RequireExpirationTime`, `ClockSkew = 30 s`, secret ≥ 32 bytes, `iss` + `aud` validated (extended by AZ-494) | AZ-487 + AZ-494 task specs § Non-Functional Requirements + Constraints | `AuthenticationServiceCollectionExtensionsTests` (unit) + SEC-05..SEC-09 + AZ-494 AC-1/AC-2 wrong-iss/aud (integration) | ✓ |
|
||||
| AZ-487 Reliability — Fail-fast on missing / short `JWT_SECRET` at startup (extended by AZ-494 to iss + aud) | AZ-487 + AZ-494 task specs § Non-Functional Requirements | SEC-08 (behavioral) + unit `AddSatelliteJwt_ThrowsOnMissingSecret` + `_ThrowsOnMissingIssuer` + `_ThrowsOnMissingAudience` | ✓ |
|
||||
| AZ-488 Performance — Per-item gate cost < 50 ms; p95 batch-of-10 < 2 s | AZ-488 task spec § Non-Functional Requirements | PT-08 (Implemented in AZ-492 — 20-batch distribution, batch p95 gated at 2000 ms; per-item gate cost reported as derived proxy `batch_p95 / batch_size`. True per-call `UavTileQualityGate.Validate` timing requires server-side instrumentation — follow-up). | ✓ (batch p95) / ◐ (per-item proxy only) |
|
||||
| AZ-488 Reliability — File-first then DB row; per-item failures never fail the batch envelope (except 400/401/403) | AZ-488 task spec § Non-Functional Requirements | BT-14 (mixed-batch shows per-item isolation); `UavTileUploadHandlerTests.*PersistAsync*` (unit); reject reason `STORAGE_FAILURE` defined in contract for the orphan-row recovery path | ✓ |
|
||||
| AZ-488 Compatibility — Replaces 501 stub; coexists with AZ-484 `tile-storage` v1.0.0 contract on the write side | AZ-488 task spec § Non-Functional Requirements + Contract | `StubAndErrorContractTests` updated to drop the stub-501 expectation; BT-15 + BT-16 validate the AZ-484 invariants under live UAV writes | ✓ |
|
||||
|
||||
@@ -80,7 +80,7 @@ Source: cycle-2 retrospective top-3 improvement actions + carried-forward securi
|
||||
| AZ-491 | Consolidate JWT test-mint helpers | — (logically follows AZ-487 which introduced both copies) | 3 | To Do |
|
||||
| AZ-492 | Perf harness: PT-07 + PT-08 + JWT-attach in run-performance-tests.sh | AZ-487 (hard — Bearer token); AZ-491 (soft — token-mint reuse) | 3 | In Testing |
|
||||
| AZ-493 | Integration test DB-reset hook | — | 2 | To Do |
|
||||
| AZ-494 | JWT iss/aud validation (enable + configure) | AZ-487 (extends `AddSatelliteJwt`); external: admin team confirms iss/aud values | 2 | To Do (blocked on cross-team input) |
|
||||
| AZ-494 | JWT iss/aud validation (enable + configure) | AZ-487 (extends `AddSatelliteJwt`); external: admin team confirms iss/aud values | 2 | In Testing (Option B: plumbing implemented; prod iss/aud values gated by fail-fast startup) |
|
||||
| AZ-495 | Resolve doc-folder convention for WebApi component | — | 1 | To Do |
|
||||
| AZ-496 | Bump Microsoft.AspNetCore.OpenApi + JwtBearer to 8.0.25 | — | 2 | To Do |
|
||||
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
# Batch Report — Batch 05 cycle 3
|
||||
|
||||
**Batch**: 05 (cycle 3)
|
||||
**Tasks**: AZ-494 (JWT iss/aud validation — enable + configure)
|
||||
**Date**: 2026-05-12
|
||||
|
||||
## Task Results
|
||||
|
||||
| Task | Status | Files Modified | Tests | AC Coverage | Issues |
|
||||
|------|--------|---------------|-------|-------------|--------|
|
||||
| AZ-494_jwt_iss_aud_validation | Done (Option B) | 1 prod source + 1 TestSupport + 4 IntegrationTests + 1 unit test + 4 config + 4 scripts/compose + 6 docs | 4 new unit tests + 2 new integration scenarios; existing 13 unit + 8 integration cases re-tagged to mint via env iss/aud | 6/7 ACs addressed; AC-7 deferred (cross-repo `suite/_docs/10_auth.md` write) | 0 blockers; 1 Low (cross-repo doc), 1 acknowledged operational gate (admin team must supply real prod iss/aud — fail-fast at deploy enforces this). |
|
||||
|
||||
## AC Test Coverage: 6 of 7 addressed (AC-7 deferred — cross-repo)
|
||||
## Code Review Verdict: pending (this batch report precedes per-batch review)
|
||||
## Auto-Fix Attempts: 0
|
||||
## Stuck Agents: None
|
||||
|
||||
## What was implemented
|
||||
|
||||
The task spec offered three options for handling the blocker (admin team has not yet confirmed production `iss` / `aud` values). The user selected **Option B**: implement the full validation plumbing now with **clearly-tagged DEV-only values** in `appsettings.Development.json` so local tests work, but leave `appsettings.json` empty so production deploys without explicit `JWT_ISSUER` / `JWT_AUDIENCE` environment variables fail at startup, not at runtime.
|
||||
|
||||
The implementation therefore:
|
||||
|
||||
1. Enables `ValidateIssuer = true` and `ValidateAudience = true` in the production token-validation pipeline.
|
||||
2. Sources both values from `JWT_ISSUER` / `JWT_AUDIENCE` env vars with `Jwt:Issuer` / `Jwt:Audience` config keys as fallback (same resolution pattern as `JWT_SECRET` from AZ-487).
|
||||
3. Throws `InvalidOperationException` at startup if either value is missing or whitespace — the message names the env var, the config key, and the AZ-494 task spec.
|
||||
4. Threads `iss` / `aud` through the canonical `SatelliteProvider.TestSupport.JwtTokenFactory.Create` surface (the post-AZ-491 single source of truth) so every existing test path continues to mint matching tokens.
|
||||
5. Adds a thin convenience layer in `SatelliteProvider.IntegrationTests/JwtTestHelpers` (`MintAuthenticated`, `MintExpired`, `ResolveIssuerOrThrow`, `ResolveAudienceOrThrow`) so integration test call sites stay terse and centrally fail-fast on missing env vars.
|
||||
6. Adds two new negative integration tests (`WrongIssuer_Returns401`, `WrongAudience_Returns401`) and four new unit fail-fast tests (`AddSatelliteJwt_ThrowsOnMissingIssuer` / `_ThrowsOnEmptyIssuer` / `_ThrowsOnMissingAudience` / `_ThrowsOnEmptyAudience`).
|
||||
7. Updates security artefacts (`security_report.md` flips F-AUTH-2 to **RESOLVED**, `owasp_review.md` A07 reflects same), the architecture + module docs (`architecture.md`, `modules/api_program.md`, `modules/tests_integration.md`, `modules/tests_unit.md`), the cycle-2 deploy report (R3 follow-up note), and the traceability matrix (5 new rows for AZ-494 AC-1..AC-7).
|
||||
|
||||
### Added
|
||||
|
||||
- `SatelliteProvider.IntegrationTests/JwtTestHelpers.cs` — three new public helpers:
|
||||
- `ResolveIssuerOrThrow()` / `ResolveAudienceOrThrow()` — mirror the existing `ResolveSecretOrThrow` pattern (read env, throw `InvalidOperationException` with a humanised message if missing).
|
||||
- `MintAuthenticated(...)` — convenience wrapper: defaults issuer + audience to the env-resolved values, accepts explicit overrides for negative test cases.
|
||||
- `MintExpired(...)` — convenience wrapper for the existing `JwtTokenFactory.CreateExpired` overload, same env-resolution behaviour.
|
||||
- 4 unit tests in `SatelliteProvider.Tests/Authentication/AuthenticationServiceCollectionExtensionsTests.cs`: `AddSatelliteJwt_ThrowsOnMissingIssuer`, `AddSatelliteJwt_ThrowsOnEmptyIssuer`, `AddSatelliteJwt_ThrowsOnMissingAudience`, `AddSatelliteJwt_ThrowsOnEmptyAudience`.
|
||||
- 2 integration tests in `SatelliteProvider.IntegrationTests/JwtIntegrationTests.cs`: `WrongIssuer_Returns401`, `WrongAudience_Returns401`.
|
||||
|
||||
### Modified
|
||||
|
||||
- `SatelliteProvider.Api/Authentication/AuthenticationServiceCollectionExtensions.cs` — added `JwtIssuerEnvVar` / `JwtIssuerConfigKey` / `JwtAudienceEnvVar` / `JwtAudienceConfigKey` constants; flipped `ValidateIssuer` / `ValidateAudience` to `true` and wired `ValidIssuer` / `ValidAudience`; extracted a single `ResolveRequiredOrThrow` helper that all three required values now flow through.
|
||||
- `SatelliteProvider.TestSupport/JwtTokenFactory.cs` — `Create(...)` and `CreateExpired(...)` gained optional `issuer` / `audience` parameters defaulted to `null` (old call sites still produce identical tokens; new call sites pass real values).
|
||||
- `SatelliteProvider.IntegrationTests/PerfBootstrap.cs` — `MintToken()` now also resolves iss + aud and passes them through to `JwtTokenFactory.Create`. Without this the perf harness's bearer token would fail validation against the AZ-494-hardened API.
|
||||
- `SatelliteProvider.IntegrationTests/Program.cs` — JWT bootstrap now resolves all three required values (secret + iss + aud) inside a single `try/catch` and prints them at startup. The `MintAuthenticated` helper replaces the inline `JwtTokenFactory.Create` call that used to live in `Main`.
|
||||
- `SatelliteProvider.IntegrationTests/JwtIntegrationTests.cs` — `AnonymousRequest_*`, `ExpiredToken_Returns401`, `InvalidSignature_Returns401`, `ValidToken_Returns200_OnHealthyEndpoint` all migrated to `JwtTestHelpers.MintAuthenticated` / `JwtTestHelpers.MintExpired`. Adds the two new scenarios to `RunAll`.
|
||||
- `SatelliteProvider.IntegrationTests/UavUploadTests.cs` — every `JwtTokenFactory.Create(...)` call replaced with `JwtTestHelpers.MintAuthenticated(...)`. Direct `using SatelliteProvider.TestSupport;` dropped (no longer needed at this seam).
|
||||
- `SatelliteProvider.Tests/Authentication/AuthenticationServiceCollectionExtensionsTests.cs` — existing `_ConfiguresTokenValidationParameters_AsPerContract` and `_PrefersEnvironmentVariableOverConfiguration` cases updated to assert the AZ-494 contract; `BuildValidConfiguration()` now seeds iss + aud; static `[Fact]` setup/teardown saves and restores both env vars.
|
||||
- `SatelliteProvider.Api/appsettings.json` — added empty `Jwt.Issuer` and `Jwt.Audience` keys (the fail-fast contract requires env-var or non-empty config; empty here forces ops to supply env vars in prod).
|
||||
- `SatelliteProvider.Api/appsettings.Development.json` — placeholder dev values prefixed `DEV-ONLY-` so a grep for that prefix surfaces every "remember to replace" site.
|
||||
- `.env.example` — documents `JWT_ISSUER` and `JWT_AUDIENCE` with the fail-fast contract and a one-line example value pair (same `DEV-ONLY-` prefix).
|
||||
- `docker-compose.yml` / `docker-compose.tests.yml` — `JWT_ISSUER` and `JWT_AUDIENCE` now passed through to both the `api` and `integration-tests` services.
|
||||
- `scripts/run-tests.sh` / `scripts/run-performance-tests.sh` — `.env` load + fail-fast checks for both new vars mirror the existing `JWT_SECRET` flow; both are exported so Docker Compose and the perf bootstrap see them.
|
||||
|
||||
### Documentation
|
||||
|
||||
- `_docs/02_document/architecture.md` — token contract bullet + Security Architecture authentication paragraph updated.
|
||||
- `_docs/02_document/modules/api_program.md` — JWT authentication section + Configuration section.
|
||||
- `_docs/02_document/modules/tests_integration.md` — env-var prerequisites updated; `JwtIntegrationTests` and `JwtTestHelpers` entries describe the new AZ-494 surface.
|
||||
- `_docs/02_document/modules/tests_unit.md` — `AuthenticationServiceCollectionExtensionsTests` entry now lists the four AZ-494 fail-fast cases plus the updated config-precedence and contract assertions.
|
||||
- `_docs/02_document/tests/traceability-matrix.md` — 5 new rows for AC-1..AC-7 (AC-7 marked deferred); the AZ-487 NFR rows updated to acknowledge the AZ-494 extension.
|
||||
- `_docs/03_implementation/deploy_cycle2.md` — R3 follow-up note marked **RESOLVED in cycle 3 (AZ-494)** with the residual operational gate spelled out.
|
||||
- `_docs/05_security/security_report.md` — F-AUTH-2 flipped to **RESOLVED cycle 3 (AZ-494)**; verdict reconciliation + recommendations updated.
|
||||
- `_docs/05_security/owasp_review.md` — A07 row updated; new Low finding for residual "no token revocation list" gap noted as a separate follow-up.
|
||||
|
||||
## AC verification
|
||||
|
||||
| AC | Description | Verification |
|
||||
|---|---|---|
|
||||
| AC-1 | Wrong `iss` token returns 401 | `JwtIntegrationTests.WrongIssuer_Returns401` (integration; runtime gate at Step 16) |
|
||||
| AC-2 | Wrong `aud` token returns 401 | `JwtIntegrationTests.WrongAudience_Returns401` (integration; runtime gate at Step 16) |
|
||||
| AC-3 | Matching iss + aud accepted | `JwtIntegrationTests.ValidToken_Returns200_OnHealthyEndpoint` retains its assertion; tokens now minted via env-resolved iss/aud through `MintAuthenticated` |
|
||||
| AC-4 | Missing config fails fast | 4 new unit tests in `AuthenticationServiceCollectionExtensionsTests`; manual `docker compose up` without env vars throws `InvalidOperationException` per the contract |
|
||||
| AC-5 | Existing tests pass with matched fixtures | All `JwtTokenFactory.Create` direct call sites in the integration project removed in favour of `MintAuthenticated` (verified via `Grep`); unit suite still mints via the factory with explicit iss/aud |
|
||||
| AC-6 | Security artefacts updated | `security_report.md` + `owasp_review.md` updated this batch |
|
||||
| AC-7 | Suite-level contract reflects validation | **Deferred** — `suite/_docs/10_auth.md` lives in the parent monorepo, outside this workspace. Cross-repo write is out of scope for satellite-provider's autodev. `deploy_cycle2.md` notes the cross-repo obligation. |
|
||||
|
||||
## Static / process checks
|
||||
|
||||
- `dotnet format whitespace --verify-no-changes` will run as part of `scripts/run-tests.sh` at Step 16.
|
||||
- `ReadLints` on every modified C# file returned 0 warnings.
|
||||
- Repo-wide grep for `JwtTokenFactory.Create` confirms only `SatelliteProvider.Tests` (unit, which intentionally exercises the factory directly with explicit iss/aud) + `PerfBootstrap.MintToken` + `JwtTestHelpers.MintAuthenticated` / `MintExpired` call it now — the integration suite never bypasses the env-resolution wrapper.
|
||||
- `.env.example` keeps the `DEV-ONLY-` prefix grep-friendly so a future ops review can surface every placeholder site at once.
|
||||
|
||||
## Risks & follow-ups
|
||||
|
||||
- **Operational gate** (intentional, by Option B) — production deploy WITHOUT `JWT_ISSUER` + `JWT_AUDIENCE` env vars will fail at process start with the `InvalidOperationException` message documented above. This is the controlled deploy-time forcing function for admin-team confirmation.
|
||||
- **Cross-repo doc** (AC-7) — `suite/_docs/10_auth.md` write deferred. Will surface as a `_docs/_process_leftovers/` entry if the suite repo still needs the update after this autodev finishes.
|
||||
@@ -133,7 +133,7 @@ Promote to `stage` / `main` only after the consumer-coordination items in R1 + R
|
||||
|
||||
The cycle-2 audit (Step 14) flagged 2 new Medium findings — both bounded by mitigations and tracked as follow-ups, NOT blockers:
|
||||
|
||||
- **F-AUTH-2** — `iss`/`aud` not validated. Coordinate with admin team to define the values; flip `ValidateIssuer`/`ValidateAudience` to `true` in a small follow-up PBI when ready.
|
||||
- **F-AUTH-2** — `iss`/`aud` not validated. **RESOLVED in cycle 3 (AZ-494)** — code changes landed; `ValidateIssuer`/`ValidateAudience` now `true` against env-sourced `JWT_ISSUER` / `JWT_AUDIENCE`. The remaining operational item is admin-team confirmation of the production iss/aud values, which is gated by the fail-fast contract (production deploy without those values fails at startup, not at runtime).
|
||||
- **F-UAV-1 / F-DEPS-UAV** — ImageSharp 3.1.11 now decodes attacker-controlled JPEGs. Today's mitigations (magic-byte gate, size cap, scoped `try/catch`) are sufficient against current advisories. Subscribe to GHSA for `SixLabors.ImageSharp`; patch within 7 days of any new CVE.
|
||||
|
||||
Cycle-1 carry-overs (S1, S2, S4, D1, I3, I5) are unchanged — still flagged in `_docs/05_security/security_report.md` as the pre-public-network hardening backlog.
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
# Code Review — Batch 05 cycle 3 (AZ-494)
|
||||
|
||||
**Reviewer**: autodev in-band
|
||||
**Scope**: AZ-494 (JWT iss/aud validation) — implementation, tests, config, docs
|
||||
**Verdict**: **PASS_WITH_WARNINGS** — 0 Critical, 0 High, 0 Medium, 1 Low (cross-repo doc), 1 acknowledged operational gate by user decision.
|
||||
|
||||
## 1. Spec compliance
|
||||
|
||||
All ACs except AC-7 are addressed. AC-7 (suite-level contract) requires writing to a file in the parent monorepo (`suite/_docs/10_auth.md`), which sits outside this workspace and outside autodev's blast radius. The cycle-2 deploy report has been updated to document the cross-repo obligation so it doesn't get lost.
|
||||
|
||||
The user's Option B selection is honoured exactly:
|
||||
|
||||
- `appsettings.json` ships with empty `Jwt.Issuer` / `Jwt.Audience` → production deploy without env vars fails at startup.
|
||||
- `appsettings.Development.json` ships with `DEV-ONLY-`-prefixed placeholders → local + Docker-compose flows work without touching `.env`.
|
||||
- The env-var precedence + fail-fast pattern is identical to the existing `JWT_SECRET` flow from AZ-487, so there's no second mental model for operators to learn.
|
||||
|
||||
## 2. Code quality
|
||||
|
||||
| Area | Observation | Verdict |
|
||||
|---|---|---|
|
||||
| Single source of truth | `JwtTokenFactory.Create` remains the only `new JwtSecurityToken(...)` call site in the source tree (verified by grep). The new iss/aud params slot in alongside the existing `secret` / `subject` / `lifetime` / `extraClaims` / `algorithm` parameters — no parallel code path was introduced. | ✓ |
|
||||
| Helper layering | `JwtTestHelpers.MintAuthenticated` / `MintExpired` are thin wrappers (≤ 12 lines each) that resolve iss/aud from env and delegate to the factory. They live in `SatelliteProvider.IntegrationTests` because they read env vars — exactly the runner-side concern documented in `module-layout.md`. | ✓ |
|
||||
| Fail-fast extraction | The third copy of the "resolve required value, throw with a humanised message" pattern was extracted into a single `ResolveRequiredOrThrow` helper. `ResolveSecretOrThrow` remains separate because it has the additional ≥ 32-byte size check from AZ-487. This is acceptable: two distinct invariants. | ✓ |
|
||||
| Test mutual-isolation | Each new `[Fact]` saves and restores both `JWT_ISSUER` and `JWT_AUDIENCE` via `try/finally`, mirroring the existing pattern for `JWT_SECRET`. No parallel-test interference risk. | ✓ |
|
||||
| Surface area minimisation | `JwtTokenFactory.Create` gained two optional, nullable parameters at the END of the signature with defaults of `null`. Old callers behave identically. | ✓ |
|
||||
| Error message clarity | All three fail-fast messages name (a) the env var, (b) the config key, (c) the AZ-494 spec, and use the human label ("JWT issuer", "JWT audience"). | ✓ |
|
||||
| Logging | No new debug/trace logs added — only the existing startup-time `InvalidOperationException` path. This matches `coderule.mdc`. | ✓ |
|
||||
| Naming | `MintAuthenticated` / `MintExpired` / `ResolveIssuerOrThrow` / `ResolveAudienceOrThrow` all describe caller intent precisely. No vague "data"/"item"/"candidate" names. | ✓ |
|
||||
|
||||
## 3. Security
|
||||
|
||||
| Aspect | Status |
|
||||
|---|---|
|
||||
| F-AUTH-2 (iss/aud not validated) | **Resolved** — `ValidateIssuer = true` + `ValidateAudience = true`, both wired to real values |
|
||||
| Token revocation list | Still absent — listed as a new Low finding in `owasp_review.md` A07 |
|
||||
| Secret leakage | No iss/aud value is logged at WARN / ERROR / INFO. Only the startup banner (Console.Out) prints them — intentional, for diagnosis. | ✓ |
|
||||
| Production safety | `appsettings.json` empty → process won't start without explicit env vars. Cannot accidentally ship a hard-coded prod issuer. | ✓ |
|
||||
| Test/prod isolation | `DEV-ONLY-` prefix means a grep can surface placeholder values at any time. | ✓ |
|
||||
|
||||
## 4. Performance
|
||||
|
||||
No measurable impact. `TokenValidationParameters` now checks two additional string comparisons per request. Sub-microsecond per request — far below the 1 ms AZ-487 NFR budget. PT-07 / PT-08 from AZ-492 will pick up any aggregate regression at the next perf run.
|
||||
|
||||
## 5. Cross-task consistency
|
||||
|
||||
- AZ-487 (JWT base layer) — extended, not replaced. The `RequireSignedTokens` / `RequireExpirationTime` / `ClockSkew = 30 s` / secret ≥ 32 bytes invariants all remain.
|
||||
- AZ-491 (consolidate JWT test helpers) — the new `MintAuthenticated` / `MintExpired` wrappers strengthen the AZ-491 contract (delegate to the canonical factory; no parallel mint logic).
|
||||
- AZ-492 (perf harness) — `PerfBootstrap.MintToken` was updated in the same way as integration call sites. Without this update the perf harness would have started emitting 401s after AZ-494 landed.
|
||||
- AZ-493 (DB reset hook) — unaffected; runs against the same env.
|
||||
- AZ-495 (doc folder convention) — unaffected.
|
||||
- AZ-496 (bump aspnetcore 8.0.25) — directly relevant: AZ-494's `ValidateIssuer` / `ValidateAudience` semantics are unchanged across `Microsoft.AspNetCore.Authentication.JwtBearer` 8.0.21 → 8.0.25.
|
||||
|
||||
## 6. Findings
|
||||
|
||||
### Low
|
||||
|
||||
**L1 — Cross-repo doc deferred (AC-7)**
|
||||
- Where: `suite/_docs/10_auth.md` (outside this workspace).
|
||||
- Why noted: AC-7 explicitly calls for an update there; the autodev process treats this as out-of-scope per the workspace-boundary rule.
|
||||
- Disposition: documented in `deploy_cycle2.md`'s R3 follow-up section.
|
||||
|
||||
### Acknowledged operational gate (NOT a finding)
|
||||
|
||||
The Option B contract is **deliberately** that production deploys without real iss/aud values fail at startup. That's the user-selected forcing function for admin-team confirmation. Not a code defect.
|
||||
|
||||
## 7. AC-by-AC re-check
|
||||
|
||||
| AC | Verification | Verdict |
|
||||
|---|---|---|
|
||||
| AC-1 | `JwtIntegrationTests.WrongIssuer_Returns401` asserts 401 against `https://wrong-issuer.invalid/` | ✓ |
|
||||
| AC-2 | `JwtIntegrationTests.WrongAudience_Returns401` asserts 401 against `wrong-audience-not-satellite` | ✓ |
|
||||
| AC-3 | `JwtIntegrationTests.ValidToken_Returns200_OnHealthyEndpoint` mints through `MintAuthenticated` (env-sourced iss/aud) | ✓ |
|
||||
| AC-4 | 4 fail-fast unit tests + manual smoke ("docker compose up without env vars") path documented | ✓ |
|
||||
| AC-5 | All integration test call sites migrated to `MintAuthenticated`; verified via grep | ✓ |
|
||||
| AC-6 | `security_report.md` + `owasp_review.md` updated | ✓ |
|
||||
| AC-7 | Cross-repo write deferred; documented | ◐ deferred |
|
||||
|
||||
## 8. Verdict
|
||||
|
||||
**PASS_WITH_WARNINGS** — proceed to ship. The single Low finding is structurally outside this workspace.
|
||||
@@ -53,7 +53,7 @@ Cycle 1's A01 / A07 verdicts were `N/A (with caveat)` because the service shippe
|
||||
| A04 | Cryptographic Failures | N/A | **PASS** | HS256 token validation uses `Microsoft.IdentityModel`'s `SymmetricSecurityKey` with `RequireSignedTokens = true` and `RequireExpirationTime = true`. The `alg=none` bypass is blocked by `RequireSignedTokens`; algorithm-confusion is bounded because only one signing key is registered. Secret length ≥ 32 bytes enforced at startup. |
|
||||
| A05 | Injection | PASS | **PASS** | No new SQL / shell / template surfaces. The new JSON parse (`PermissionsAuthorizationHandler`) runs on signature-validated token bytes — see F-UAV-2 disposition. |
|
||||
| A06 | Insecure Design | FAIL (S3, S4, I3) | **FAIL** (+ F-AUTH-3, F-UAV-3) | Rate limiting still absent (now also a 401-flood vector). UAV reject reasons disclose gate structure — accepted UX trade-off, flagged for operator awareness. |
|
||||
| A07 | Identification & Authentication Failures | N/A (with caveat) | **PASS_WITH_WARNINGS** (+ F-AUTH-2) | HS256 with secret ≥ 32 bytes; lifetime + signature validation; ClockSkew = 30 s. **Warning**: `ValidateIssuer = false`, `ValidateAudience = false` per the suite contract — any service that holds `JWT_SECRET` can mint tokens accepted here. Track until admin team defines `iss`/`aud`. No token revocation list — leaked tokens stay valid until `exp`. |
|
||||
| A07 | Identification & Authentication Failures | N/A (with caveat) | **PASS_WITH_WARNINGS** → **PASS_WITH_WARNINGS** (cycle 3: F-AUTH-2 resolved by AZ-494) | HS256 with secret ≥ 32 bytes; lifetime + signature validation; ClockSkew = 30 s. **Cycle 3 (AZ-494)**: `ValidateIssuer` / `ValidateAudience` now `true`; values sourced from `JWT_ISSUER` / `JWT_AUDIENCE` env vars with fail-fast contract. Production iss/aud values are admin-team-confirmed at deploy time. Remaining warning: no token revocation list — leaked tokens stay valid until `exp`. |
|
||||
| A08 | Software or Data Integrity Failures | PASS | **PASS** | AZ-488 file-first-then-row write order documented; same migration / CI discipline as cycle 1. |
|
||||
| A09 | Security Logging Failures | PASS_WITH_WARNINGS (I4) | **PASS_WITH_WARNINGS** (unchanged) | No new logging changes; 401 responses are not currently aggregated for alerting (out of scope for internal service). |
|
||||
| A10 | Mishandling of Exceptional Conditions | PASS | **PASS** | UAV decode failures wrapped in scoped `try/catch` for `UnknownImageFormatException` / `InvalidImageContentException` — produce structured `INVALID_FORMAT` rejects, no stack-trace leak. SEC-11 test verifies reject details have no path / exception-type leakage.
|
||||
@@ -68,5 +68,6 @@ Cycle 1's A01 / A07 verdicts were `N/A (with caveat)` because the service shippe
|
||||
| A03 | F-DEPS-UAV — ImageSharp decode exposure widened | Medium | Phase 1 |
|
||||
| A06 | F-AUTH-3 — rate-limit gap now also covers 401 floods | Low (recurrence of I3) | Phase 2 |
|
||||
| A06 | F-UAV-3 — reject reasons disclose gate structure | Informational (accepted) | Phase 2 |
|
||||
| A07 | F-AUTH-2 — `iss`/`aud` not validated; no revocation list | Medium | Phase 2 |
|
||||
| A07 | F-AUTH-2 — `iss`/`aud` not validated | Medium → **Resolved cycle 3 (AZ-494)** | n/a |
|
||||
| A07 | No token revocation list (residual after AZ-494) | Low | Phase 2 — out of scope until requirement emerges |
|
||||
| (claim handler) | F-UAV-2 — `JsonDocument.Parse` on token claim values | Low | Phase 2 |
|
||||
|
||||
@@ -133,7 +133,7 @@ AZ-487 introduced a JWT validation baseline (HS256, `JWT_SECRET` env var, `.Requ
|
||||
| # | Severity | Category | Location | Title |
|
||||
|------------|---------------|------------------------------------------|-------------------------------------------------------------------------|-----------------------------------------------------------------------------|
|
||||
| F-AUTH-1 | Low (accepted)| A02 — Misconfiguration | `SatelliteProvider.Api/appsettings.Development.json:14` | DEV-ONLY JWT secret committed; env-var overrides; operator must verify in prod |
|
||||
| F-AUTH-2 | Medium | A07 — AuthN / Identification | `Authentication/AuthenticationServiceCollectionExtensions.cs:31-32` | `iss`/`aud` not validated (intentional — suite contract has not defined values) |
|
||||
| F-AUTH-2 | Medium | A07 — AuthN / Identification | `Authentication/AuthenticationServiceCollectionExtensions.cs:31-32` | `iss`/`aud` not validated — **RESOLVED cycle 3 (AZ-494): `ValidateIssuer` + `ValidateAudience` flipped to `true`; values sourced from `JWT_ISSUER` / `JWT_AUDIENCE` env vars with fail-fast contract; production admin-team values still to be confirmed before deploy** |
|
||||
| F-AUTH-3 | Low (rec. I3) | A06 — Insecure Design | every `/api/satellite/*` endpoint | No rate limiting on 401-producing paths (extends cycle-1 I3) |
|
||||
| F-UAV-1 | Medium | A03 — Supply Chain (exposure) | `Services.TileDownloader/UavTileQualityGate.cs:60-95` | ImageSharp decode now runs on attacker-controlled JPEGs (mitigations OK) |
|
||||
| F-UAV-2 | Low | A07 — AuthN claim parsing | `Authentication/PermissionsRequirement.cs:84-111` | `JsonDocument.Parse` on signature-validated claim values (bounded by header cap) |
|
||||
@@ -145,7 +145,7 @@ AZ-487 introduced a JWT validation baseline (HS256, `JWT_SECRET` env var, `.Requ
|
||||
|
||||
- No new Critical or High findings → cycle 2 does NOT escalate the verdict.
|
||||
- Two new Medium findings — both are *follow-ups under existing remediations*, not blockers:
|
||||
- F-AUTH-2 waits on the admin team defining `iss`/`aud` (already flagged in AZ-487 § Constraints).
|
||||
- F-AUTH-2 — **RESOLVED cycle 3 (AZ-494)**: validation flipped on, config plumbed through env + appsettings; production iss/aud values gated behind admin-team confirmation at deploy time.
|
||||
- F-UAV-1 + F-DEPS-UAV jointly say "subscribe to ImageSharp GHSA and bump aggressively" — no immediate change needed.
|
||||
- F-AUTH-1 and F-UAV-3 are explicitly accepted.
|
||||
- F-AUTH-3 + D3 fold into existing cycle-1 remediations (I3 rate limiting, D1 8.0.x patch bump).
|
||||
@@ -155,7 +155,7 @@ AZ-487 introduced a JWT validation baseline (HS256, `JWT_SECRET` env var, `.Requ
|
||||
### New / refreshed cycle-2 recommendations
|
||||
|
||||
- **Pre-deploy gate (operational, NOT code)**: `deploy/SKILL.md` must verify `JWT_SECRET` is set to a ≥ 32-byte value distinct from the DEV-ONLY placeholder. Cycle-2 deploys without this verification step are gated.
|
||||
- **Coordinate with admin team**: confirm expected `iss`/`aud` values; flip `ValidateIssuer` / `ValidateAudience` to `true` as soon as those values land. Track under AZ-487 § Constraints follow-up.
|
||||
- **Coordinate with admin team**: confirm expected `iss`/`aud` values for the prod `JWT_ISSUER` / `JWT_AUDIENCE` env vars. **Code change DONE cycle 3 (AZ-494)** — `ValidateIssuer` / `ValidateAudience` are now `true`; `appsettings.Development.json` ships clearly-tagged DEV-ONLY values so local dev works out-of-the-box; production `appsettings.json` ships empty values so the app fails fast at startup until the operator supplies the real values via env. The remaining work is purely operational.
|
||||
- **Bump 8.0.x ASP.NET Core packages together** — **DONE cycle 3 (AZ-496)**: both `Microsoft.AspNetCore.OpenApi` and `Microsoft.AspNetCore.Authentication.JwtBearer` bumped to `8.0.25` in `SatelliteProvider.Api.csproj`. Runtime base image uses floating `mcr.microsoft.com/dotnet/aspnet:8.0` so the deployed runtime auto-picks up the matching patch on next build.
|
||||
- **ImageSharp subscribe-and-bump policy**: add to the runbook — patch within 7 days of any `SixLabors.ImageSharp` GHSA. Reconsider sandboxing if the upload endpoint is exposed beyond the trust boundary documented in architecture.md § 7.
|
||||
- **Cycle-2 hardening backlog (Low priority)**:
|
||||
|
||||
@@ -21,6 +21,8 @@ services:
|
||||
- ASPNETCORE_ENVIRONMENT=Testing
|
||||
- DB_CONNECTION_STRING=Host=postgres;Port=5432;Database=satelliteprovider;Username=postgres;Password=postgres
|
||||
- JWT_SECRET=${JWT_SECRET}
|
||||
- JWT_ISSUER=${JWT_ISSUER}
|
||||
- JWT_AUDIENCE=${JWT_AUDIENCE}
|
||||
volumes:
|
||||
- ./ready:/app/ready
|
||||
- ./tiles:/app/tiles
|
||||
|
||||
@@ -30,6 +30,8 @@ services:
|
||||
- ConnectionStrings__DefaultConnection=Host=postgres;Port=5432;Database=satelliteprovider;Username=postgres;Password=postgres
|
||||
- MapConfig__ApiKey=${GOOGLE_MAPS_API_KEY}
|
||||
- JWT_SECRET=${JWT_SECRET}
|
||||
- JWT_ISSUER=${JWT_ISSUER}
|
||||
- JWT_AUDIENCE=${JWT_AUDIENCE}
|
||||
volumes:
|
||||
- ./tiles:/app/tiles
|
||||
- ./ready:/app/ready
|
||||
|
||||
@@ -36,8 +36,9 @@ echo ""
|
||||
PASS=0
|
||||
FAIL=0
|
||||
|
||||
# Load JWT_SECRET from .env if not already exported (mirrors run-tests.sh).
|
||||
if [[ -z "${JWT_SECRET:-}" ]] && [[ -f "$PROJECT_ROOT/.env" ]]; then
|
||||
# Load JWT_SECRET / JWT_ISSUER / JWT_AUDIENCE from .env if not already exported
|
||||
# (mirrors run-tests.sh). AZ-494: iss + aud are now required.
|
||||
if { [[ -z "${JWT_SECRET:-}" ]] || [[ -z "${JWT_ISSUER:-}" ]] || [[ -z "${JWT_AUDIENCE:-}" ]]; } && [[ -f "$PROJECT_ROOT/.env" ]]; then
|
||||
set -o allexport
|
||||
# shellcheck disable=SC1091
|
||||
source "$PROJECT_ROOT/.env"
|
||||
@@ -70,7 +71,16 @@ if [[ -z "${PERF_JWT_TOKEN:-}" ]]; then
|
||||
exit 3
|
||||
fi
|
||||
|
||||
export JWT_SECRET
|
||||
if [[ -z "${JWT_ISSUER:-}" ]]; then
|
||||
echo "ERROR: JWT_ISSUER is not set (AZ-494). Export it or add to .env so the minted token's iss matches the API."
|
||||
exit 3
|
||||
fi
|
||||
if [[ -z "${JWT_AUDIENCE:-}" ]]; then
|
||||
echo "ERROR: JWT_AUDIENCE is not set (AZ-494). Export it or add to .env so the minted token's aud matches the API."
|
||||
exit 3
|
||||
fi
|
||||
|
||||
export JWT_SECRET JWT_ISSUER JWT_AUDIENCE
|
||||
echo "Minting perf JWT via SatelliteProvider.IntegrationTests --mint-only..."
|
||||
if ! PERF_JWT_TOKEN=$(dotnet "$PERF_DLL" --mint-only); then
|
||||
echo "ERROR: --mint-only invocation failed (see stderr above)"
|
||||
|
||||
+16
-1
@@ -28,6 +28,10 @@ Environment:
|
||||
JWT_SECRET Required for any integration test mode. Shared HMAC secret used by the
|
||||
API and the integration test runner; must be at least 32 bytes (UTF-8).
|
||||
Loaded from .env or shell env.
|
||||
JWT_ISSUER Required for any integration test mode (AZ-494). Must match the value
|
||||
the API container validates. May be a DEV-ONLY value for local runs.
|
||||
JWT_AUDIENCE Required for any integration test mode (AZ-494). Same contract as
|
||||
JWT_ISSUER — must match what the API container validates.
|
||||
EOF
|
||||
}
|
||||
|
||||
@@ -73,7 +77,7 @@ if [[ "$mode" == "unit" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if { [[ -z "${GOOGLE_MAPS_API_KEY:-}" ]] || [[ -z "${JWT_SECRET:-}" ]]; } && [[ -f "$PROJECT_ROOT/.env" ]]; then
|
||||
if { [[ -z "${GOOGLE_MAPS_API_KEY:-}" ]] || [[ -z "${JWT_SECRET:-}" ]] || [[ -z "${JWT_ISSUER:-}" ]] || [[ -z "${JWT_AUDIENCE:-}" ]]; } && [[ -f "$PROJECT_ROOT/.env" ]]; then
|
||||
set -o allexport
|
||||
# shellcheck disable=SC1091
|
||||
source "$PROJECT_ROOT/.env"
|
||||
@@ -97,6 +101,17 @@ if (( jwt_secret_bytes < 32 )); then
|
||||
fi
|
||||
export JWT_SECRET
|
||||
|
||||
if [[ -z "${JWT_ISSUER:-}" ]]; then
|
||||
echo "ERROR: JWT_ISSUER is not set (export it or add to .env). API + integration tests require it (AZ-494)."
|
||||
exit 3
|
||||
fi
|
||||
if [[ -z "${JWT_AUDIENCE:-}" ]]; then
|
||||
echo "ERROR: JWT_AUDIENCE is not set (export it or add to .env). API + integration tests require it (AZ-494)."
|
||||
exit 3
|
||||
fi
|
||||
export JWT_ISSUER
|
||||
export JWT_AUDIENCE
|
||||
|
||||
echo "Step 1: Unit tests"
|
||||
docker run --rm -v "$PROJECT_ROOT:/src" -w /src mcr.microsoft.com/dotnet/sdk:8.0 \
|
||||
sh -c "dotnet restore SatelliteProvider.sln && dotnet test SatelliteProvider.Tests/SatelliteProvider.Tests.csproj --no-restore --configuration Release --collect:'XPlat Code Coverage' --results-directory /src/TestResults --logger 'console;verbosity=normal'"
|
||||
|
||||
Reference in New Issue
Block a user