using BaronSoftware.SSO.Exceptions; using System.IO; using System.Net.Http; using System.Net.Sockets; using System.Text.Json.Nodes; using System.Threading.Tasks; namespace BaronSoftware.SSO { /// /// baron-sso(Ory Hydra · OIDC PKCE)로 로그인하고 사용자 정보를 보관하는 바론의 인증모듈. /// public class BaronSSO { private readonly SsoClient client; private readonly BaronSSOOption option; public UserInfo CurrentUser { get; private set; } public BaronSSO(BaronSSOOption option) { if(option == null) throw new ArgumentNullException(nameof(option)); if (!string.IsNullOrWhiteSpace(option.Authority)) GlobalConfigs.SsoUri = option.Authority; client = SsoClient.Create(option); this.option = option; } public void SignIn() { // STA Thread 이슈로 인해, Task.Run() 으로 감싸서 동기화 함수 구현. Task.Run(async () => await SignInAsync()).Wait(); } public void SignOut() { Task.Run(async () => await SignOutAsync()).Wait(); } /// 웹뷰 로그인 창을 띄워 인증합니다. public async Task SignInAsync() => await SignInAsync(null); /// 웹뷰 로그인 창을 띄워 인증합니다. public async Task SignInAsync(string refreshToken) { UserInfo user = null; if(option.EnableAutoLogin) user = UserInfo.FromSsoFile(); var isConnected = await CheckConnection(); if(!isConnected && option.EnableOffline) { if (user == null) throw new InvalidUserException("Not found authorized last user."); option?.Validator?.Validate(user); return; } var token = await client.LoginAsync(refreshToken); var userJson = await client.GetUserInfoAsync(token.AccessToken); if (string.IsNullOrEmpty(userJson)) throw new InvalidOperationException($"Failed to get userinfo : {token.ToString()}"); user = UserInfo.FromJson(userJson, token); option?.Validator?.Validate(user); CurrentUser = user; CurrentUser.Save(); } /// /// 로그아웃: 현재 사용자 정보 + 저장된 refresh_token + WebView SSO 세션 쿠키를 모두 삭제합니다. /// 세션 쿠키까지 지우므로 다음 로그인 시 로그인 창이 다시 표시됩니다. /// public async Task SignOutAsync() { // 1) 로그아웃 전에 id_token을 확보(서버 세션 종료용 id_token_hint). Clear 이전에 추출해야 한다. var idToken = ExtractIdToken(CurrentUser) ?? ExtractIdToken(UserInfo.FromSsoFile()); // 2) 로컬 사용자 정보 삭제 UserInfo.Clear(); CurrentUser = null; // 3) SSO 서버 세션 종료(RP-Initiated Logout) + 로컬 WebView 세션 쿠키 정리 try { await client.LogoutAsync(idToken); } catch { // 로그아웃 실패해도 로컬 사용자 정보는 이미 삭제했으므로 로컬 로그아웃으로 간주. } } /// UserInfo의 원본 토큰 응답에서 id_token을 추출한다(로그아웃 id_token_hint용). private static string ExtractIdToken(UserInfo user) { if (string.IsNullOrEmpty(user?.RawTokenResponse)) return null; try { return JsonNode.Parse(user.RawTokenResponse)?["id_token"]?.GetValue(); } catch { return null; } } private async Task CheckConnection() { try { await client.GetDiscoveryAsync(); return true; } catch { return false; } } } }