using System.Net.Mail; using System.Security.Cryptography; using System.Text.Json; using System.Text.RegularExpressions; namespace HrynCo.NotificationService.DAL.Abstract.PushChannels; // Parse only service-account fields; never honor caller-supplied OAuth endpoints. public sealed class FirebaseServiceAccountCredentials { public required string ProjectId { get; init; } public required string ClientEmail { get; init; } public required string PrivateKey { get; init; } public static bool IsValidProjectId(string projectId) => Regex.IsMatch(projectId, "^[a-z][a-z0-9-]{4,28}[a-z0-9]$", RegexOptions.CultureInvariant, TimeSpan.FromSeconds(1)); public static FirebaseServiceAccountCredentials Parse(string json, string projectId) { if (!IsValidProjectId(projectId) || json.Length > 32768) throw new FormatException("Invalid Firebase configuration."); using var document = JsonDocument.Parse(json); var root = document.RootElement; if (Read(root, "type") != "service_account" || Read(root, "project_id") != projectId) throw new FormatException("The service account must belong to the selected Firebase project."); var email = Read(root, "client_email"); if (!MailAddress.TryCreate(email, out var address) || address.Address != email) throw new FormatException("Invalid service account email."); var key = Read(root, "private_key"); using var rsa = RSA.Create(); rsa.ImportFromPem(key); // Exporting private parameters also rejects a public-key-only PEM. rsa.ExportParameters(true); return new FirebaseServiceAccountCredentials { ProjectId = projectId, ClientEmail = email, PrivateKey = key }; } private static string Read(JsonElement root, string name) { if (root.ValueKind != JsonValueKind.Object || !root.TryGetProperty(name, out var value) || value.ValueKind != JsonValueKind.String) throw new FormatException("Missing service account fields."); return value.GetString() ?? string.Empty; } }