using System.IdentityModel.Tokens.Jwt; using FluentAssertions; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Tokens; using SatelliteProvider.Api.Authentication; using SatelliteProvider.Tests.TestUtilities; using AuthExtensions = SatelliteProvider.Api.Authentication.AuthenticationServiceCollectionExtensions; 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; public AuthenticationServiceCollectionExtensionsTests() { _originalEnv = Environment.GetEnvironmentVariable(AuthExtensions.JwtSecretEnvVar); Environment.SetEnvironmentVariable(AuthExtensions.JwtSecretEnvVar, null); } public void Dispose() { Environment.SetEnvironmentVariable(AuthExtensions.JwtSecretEnvVar, _originalEnv); GC.SuppressFinalize(this); } [Fact] public void AddSatelliteJwt_RegistersJwtBearerScheme() { // Arrange var services = new ServiceCollection(); services.AddLogging(); var configuration = BuildConfiguration(("Jwt:Secret", ValidSecret)); // Act services.AddSatelliteJwt(configuration); var provider = services.BuildServiceProvider(); var schemeProvider = provider.GetRequiredService(); var scheme = schemeProvider.GetSchemeAsync(JwtBearerDefaults.AuthenticationScheme).GetAwaiter().GetResult(); // Assert scheme.Should().NotBeNull("JwtBearer scheme should be registered"); scheme!.HandlerType.Should().Be(typeof(JwtBearerHandler)); } [Fact] public void AddSatelliteJwt_ConfiguresTokenValidationParameters_AsPerContract() { // Arrange var services = new ServiceCollection(); services.AddLogging(); var configuration = BuildConfiguration(("Jwt:Secret", ValidSecret)); // Act services.AddSatelliteJwt(configuration); var provider = services.BuildServiceProvider(); var options = provider.GetRequiredService>().Get(JwtBearerDefaults.AuthenticationScheme); // Assert var p = options.TokenValidationParameters; p.ValidateIssuerSigningKey.Should().BeTrue(); p.ValidateLifetime.Should().BeTrue(); p.ValidateIssuer.Should().BeFalse(); p.ValidateAudience.Should().BeFalse(); p.RequireSignedTokens.Should().BeTrue(); p.RequireExpirationTime.Should().BeTrue(); p.ClockSkew.Should().Be(TimeSpan.FromSeconds(30)); p.IssuerSigningKey.Should().BeOfType(); } [Fact] public void AddSatelliteJwt_ThrowsOnMissingSecret() { // Arrange var services = new ServiceCollection(); var configuration = BuildConfiguration(); // Act var act = () => services.AddSatelliteJwt(configuration); // Assert act.Should().Throw() .WithMessage("*JWT secret is not configured*"); } [Fact] public void AddSatelliteJwt_ThrowsOnEmptySecret() { // Arrange var services = new ServiceCollection(); var configuration = BuildConfiguration(("Jwt:Secret", "")); // Act var act = () => services.AddSatelliteJwt(configuration); // Assert act.Should().Throw() .WithMessage("*JWT secret is not configured*"); } [Fact] public void AddSatelliteJwt_ThrowsOnShortSecret() { // Arrange var services = new ServiceCollection(); var configuration = BuildConfiguration(("Jwt:Secret", "too-short-secret")); // Act var act = () => services.AddSatelliteJwt(configuration); // Assert act.Should().Throw() .WithMessage("*at least 32 bytes*"); } [Fact] public void AddSatelliteJwt_PrefersEnvironmentVariableOverConfiguration() { // Arrange const string envSecret = "env-secret-also-longer-than-thirty-two-bytes-for-hmac"; Environment.SetEnvironmentVariable(AuthExtensions.JwtSecretEnvVar, envSecret); var services = new ServiceCollection(); services.AddLogging(); var configuration = BuildConfiguration(("Jwt:Secret", "config-secret-also-32-bytes-long-aaaaaaaaaa")); // Act services.AddSatelliteJwt(configuration); var provider = services.BuildServiceProvider(); var options = provider.GetRequiredService>().Get(JwtBearerDefaults.AuthenticationScheme); var token = JwtTokenFactory.Create(envSecret); 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"); } private static IConfiguration BuildConfiguration(params (string Key, string Value)[] pairs) { var builder = new ConfigurationBuilder(); if (pairs.Length > 0) { builder.AddInMemoryCollection(pairs.Select(p => new KeyValuePair(p.Key, p.Value))); } return builder.Build(); } }