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