Initial commit: BARON SSO 샘플 (WebView OIDC PKCE 인증 라이브러리 + 데모 앱)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-16 10:10:37 +09:00
commit 3de67f0052
25 changed files with 1171 additions and 0 deletions

View File

@@ -0,0 +1,103 @@
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<string, object> 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<string>() ?? "";
result.Name = userjson["name"]?.GetValue<string>() ?? "";
result.Email = userjson["email"]?.GetValue<string>() ?? "";
//result.SubEmails = userjson["sub_emails"]?.AsArray().Select(x => x.GetValue<string>()).ToArray() ?? Array.Empty<string>();
result.TenantId = userjson["tenant_id"]?.GetValue<string>() ?? "";
result.JoinedTenantIds = userjson["joined_tenants"]?.AsArray().Select(x => x.GetValue<string>()).ToArray() ?? Array.Empty<string>();
result.Claims = userjson["rp_claims"]?.AsObject().ToDictionary(x => x.Key, x => (object)x.Value["value"]) ?? new Dictionary<string, object>();
result.LastAuthTime = DateTimeOffset.FromUnixTimeSeconds(userjson["auth_time"]?.GetValue<long>() ?? 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<UserInfo>(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);
}
}
}