using System.Text; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.IdentityModel.Tokens; namespace SatelliteProvider.Api.Authentication; 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) { ArgumentNullException.ThrowIfNull(services); 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 .AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuerSigningKey = true, IssuerSigningKey = signingKey, ValidateLifetime = true, ClockSkew = TimeSpan.FromSeconds(30), ValidateIssuer = true, ValidIssuer = issuer, ValidateAudience = true, ValidAudience = audience, RequireSignedTokens = true, RequireExpirationTime = true }; }); return services; } internal static string ResolveSecretOrThrow(IConfiguration configuration) { var secret = Environment.GetEnvironmentVariable(JwtSecretEnvVar); if (string.IsNullOrWhiteSpace(secret)) { secret = configuration[JwtSecretConfigKey]; } if (string.IsNullOrWhiteSpace(secret)) { throw new InvalidOperationException( $"JWT secret is not configured. Set the {JwtSecretEnvVar} environment variable " + $"or the {JwtSecretConfigKey} configuration key to a value of at least {MinSecretByteLength} bytes."); } var byteLength = Encoding.UTF8.GetByteCount(secret); if (byteLength < MinSecretByteLength) { throw new InvalidOperationException( $"JWT secret is too short ({byteLength} bytes). HMAC-SHA256 requires at least {MinSecretByteLength} bytes " + $"per RFC 2104 §3. Set {JwtSecretEnvVar} or {JwtSecretConfigKey} to a longer value."); } 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; } }