using System.ComponentModel.DataAnnotations; using System.Security.Cryptography; namespace Tiku.Application.Security; public sealed class JwtOptions { [Required] public string Issuer { get; set; } = "tiku-backend"; [Required] public string Audience { get; set; } = "tiku-api"; [Required] public string KeyId { get; set; } = "development-ephemeral"; public string PrivateKeyPem { get; set; } = string.Empty; public Dictionary PublicKeys { get; set; } = new(StringComparer.Ordinal); [Range(1, 1440)] public int AccessTokenMinutes { get; set; } = 15; [Range(1, 365)] public int RefreshTokenDays { get; set; } = 30; public static bool BeValid(JwtOptions options, bool isProduction) { if (string.IsNullOrWhiteSpace(options.Issuer) || string.IsNullOrWhiteSpace(options.Audience) || string.IsNullOrWhiteSpace(options.KeyId) || options.AccessTokenMinutes != 15 || (isProduction && string.Equals( options.KeyId, "development-ephemeral", StringComparison.Ordinal))) return false; if (string.IsNullOrWhiteSpace(options.PrivateKeyPem)) { if (isProduction) return false; } else if (!IsValidRsaPem(options.PrivateKeyPem, true)) { return false; } return options.PublicKeys.All(pair => !string.IsNullOrWhiteSpace(pair.Key) && !string.Equals(pair.Key, options.KeyId, StringComparison.Ordinal) && IsValidRsaPem(pair.Value, false)); } private static bool IsValidRsaPem(string pem, bool requirePrivateKey) { try { using var rsa = RSA.Create(); rsa.ImportFromPem(pem); if (rsa.KeySize < 2048) return false; if (requirePrivateKey) _ = rsa.ExportParameters(true); return true; } catch (Exception exception) when ( exception is ArgumentException or CryptographicException) { return false; } } }