using Microsoft.Win32; using System; using System.IO; using System.Linq; using System.Printing; using System.Security.Cryptography; using System.Text; using System.Text.Json; using System.Text.Json.Nodes; using System.Text.RegularExpressions; namespace BaronSoftware.SSO { public class UserInfo { public string UUID { get; private set; } public string Name { get; private set; } public string Email { get; private set; } public string[] SubEmails { get; private set; } public string RefreshToken { get; private set; } public string TenantId { get; private set; } /// 'tenants' 클레임 안에 등장하는 모든 테넌트 id 목록(상위/조상 테넌트 포함, 중복 제거). public string[] AllTenantIds { get; private set; } public long LastAuthUnixTimeStamp { get; private set; } public DateTime LastAuthTime => DateTimeOffset.FromUnixTimeSeconds(LastAuthUnixTimeStamp).LocalDateTime; public string IdToken { get; private set; } public string Raw { get; private set; } public string RawTokenResponse { get; private set; } public Dictionary Claims { get; private set; } internal static UserInfo FromJson(string rawjson, TokenResponse reposeToken) { var userjson = JsonNode.Parse(rawjson); var result = new UserInfo(); result.Raw = rawjson; result.RawTokenResponse = reposeToken.RawJson; result.IdToken = reposeToken.IdToken; result.RefreshToken = reposeToken.RefreshToken ?? ""; result.UUID = userjson["sub"]?.GetValue() ?? ""; result.Name = userjson["name"]?.GetValue() ?? ""; result.Email = userjson["email"]?.GetValue() ?? ""; result.TenantId = userjson["tenant_id"]?.GetValue() ?? ""; result.AllTenantIds = ExtractTenantIds(userjson); result.Claims = userjson["rp_claims"]?.AsObject().ToDictionary(x => x.Key, x => (object)x.Value["value"]) ?? new Dictionary(); result.LastAuthUnixTimeStamp = userjson["auth_time"]?.GetValue() ?? 0; return result; } /// /// 'tenants' 객체 안에 등장하는 모든 테넌트 id를 정규식으로 수집한다(상위/조상 포함, 중복 제거). /// private static string[] ExtractTenantIds(JsonNode userjson) { // 'tenants' 하위의 모든 "id":"..." 값을 뽑는 정규식 (상위 테넌트 + ancestors 포함) Regex regex = new("\"id\"\\s*:\\s*\"([^\"]+)\"", RegexOptions.Compiled); var tenantsJson = userjson?["tenants"]?.ToJsonString(); if (string.IsNullOrEmpty(tenantsJson)) return Array.Empty(); var ids = new List(); var seen = new HashSet(); foreach (Match m in regex.Matches(tenantsJson)) { var id = m.Groups[1].Value; if (!string.IsNullOrEmpty(id) && seen.Add(id)) ids.Add(id); } return ids.ToArray(); } //// root = Ory Hydra id_token payload 또는 /userinfo 응답. //// 클레임은 (Descope의 customAttributes 하위가 아니라) 최상위에 평탄하게 들어오며, //// 상세 항목은 profile 스코프가 동의되었을 때만 포함됩니다. //// (baron-sso backend: buildOidcClaimsFromTraits 참고) internal void Save() { var json = JsonSerializer.Serialize(this); var bytes = Encoding.UTF8.GetBytes(json); var protectedBytes = ProtectedData.Protect(bytes, optionalEntropy: null, DataProtectionScope.CurrentUser); if (File.Exists(GlobalConfigs.SsoFilePath)) File.Delete(GlobalConfigs.SsoFilePath); if (!Directory.Exists(Path.GetDirectoryName(GlobalConfigs.SsoFilePath))) Directory.CreateDirectory(Path.GetDirectoryName(GlobalConfigs.SsoFilePath)); File.WriteAllBytes(GlobalConfigs.SsoFilePath, protectedBytes); } internal static UserInfo? FromSsoFile() { try { ValidateSsoFile(); var bytes = ProtectedData.Unprotect(File.ReadAllBytes(GlobalConfigs.SsoFilePath), optionalEntropy: null, DataProtectionScope.CurrentUser); var json = Encoding.UTF8.GetString(bytes); var user = JsonSerializer.Deserialize(json); return user; } catch { return null; // 손상/복호화 불가 시 무시 } } private static void ValidateSsoFile() { //SSO 인증 저장파일을 검증 if (!File.Exists(GlobalConfigs.SsoFilePath)) throw new FileNotFoundException("Not found sso file", GlobalConfigs.SsoFilePath); var file = new FileInfo(GlobalConfigs.SsoFilePath); if (DateTime.Now < file.LastWriteTime) { //파일의 최종 수정시간이 현재 시간보다 미래인 경우, 시스템 시계가 변경되었거나 파일이 조작되었을 가능성이 있음 throw new InvalidDataException("Invalid sso file: last write time is in the future"); } } public static void Clear() { if (File.Exists(GlobalConfigs.SsoFilePath)) File.Delete(GlobalConfigs.SsoFilePath); } public bool IsCenter() { return AllTenantIds.Contains(GlobalConfigs.CenterTanant_UUID, StringComparer.OrdinalIgnoreCase); } public bool IsFamily() { return AllTenantIds.Contains(GlobalConfigs.FamilyTanant_UUID, StringComparer.OrdinalIgnoreCase); } } }