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; 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 bool IsFamily { get; private set; } public string RefreshToken { get; private set; } public string TenantId { get; private set; } public string[] JoinedTenantIds { get; private set; } public DateTime LastAuthTime {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.RefreshToken = reposeToken.RefreshToken ?? ""; result.UUID = userjson["sub"]?.GetValue() ?? ""; result.Name = userjson["name"]?.GetValue() ?? ""; result.Email = userjson["email"]?.GetValue() ?? ""; //result.SubEmails = userjson["sub_emails"]?.AsArray().Select(x => x.GetValue()).ToArray() ?? Array.Empty(); result.TenantId = userjson["tenant_id"]?.GetValue() ?? ""; result.JoinedTenantIds = userjson["joined_tenants"]?.AsArray().Select(x => x.GetValue()).ToArray() ?? Array.Empty(); result.Claims = userjson["rp_claims"]?.AsObject().ToDictionary(x => x.Key, x => (object)x.Value["value"]) ?? new Dictionary(); result.LastAuthTime = DateTimeOffset.FromUnixTimeSeconds(userjson["auth_time"]?.GetValue() ?? 0).DateTime; return result; } //// 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); } } }