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()
{
// UI 스레드에서 호출돼도 데드락이 없도록 Task.Run(스레드풀)에서 실행한다.
// (실제 WebView 창은 SsoClient가 전용 STA 스레드에서 띄우므로 STA/MTA 무관하게 동작)
// .GetAwaiter().GetResult()로 원본 예외를 그대로 전파한다(AggregateException 래핑 방지).
Task.Run(() => SignInAsync()).GetAwaiter().GetResult();
}
public void SignOut()
{
Task.Run(() => SignOutAsync()).GetAwaiter().GetResult();
}
///
/// 웹뷰 로그인 창을 띄워 인증합니다. (비동기)
///
///
/// ⚠️ 절대 UI 스레드에서 .Wait() / .Result 로 블로킹하지 마세요. 데드락이 발생합니다.
///
/// [데드락이 나는 이유 — sync-over-async]
/// 1) WPF UI 스레드에는 DispatcherSynchronizationContext 가 설치돼 있습니다.
/// 2) 이 메서드를 UI 스레드에서 호출하면, 내부의 첫 await
/// (CheckConnection → GetDiscoveryAsync → httpclient.GetStringAsync 등)에서
/// ConfigureAwait(false) 를 쓰지 않으므로 "연속(continuation)을 UI 스레드로 되돌려" 실행하도록
/// 컨텍스트를 캡처합니다.
/// 3) 그런데 호출 측이 .Wait() 로 UI 스레드를 막으면 메시지펌프가 멈춥니다.
/// 4) await 대상(HTTP 등)이 스레드풀에서 끝나고 연속을 UI 디스패처 큐에 post 하지만,
/// UI 스레드가 막혀 있어 그 큐를 영원히 처리하지 못합니다 → Task 미완료 → .Wait() 무한 대기 → 데드락.
/// (로그인 창에 도달하기 전, 첫 HTTP await 에서 이미 멈춥니다.)
///
/// [올바른 사용법]
/// • UI 스레드라면 블로킹하지 말고 await 하세요: await auth.SignInAsync();
/// • 동기적으로 써야 하면 동기 진입점 을 쓰세요.
/// SignIn() 은 Task.Run(() => SignInAsync()).GetAwaiter().GetResult() 로,
/// SignInAsync 를 SynchronizationContext 가 없는 스레드풀 스레드에서 실행하므로
/// 연속이 UI 스레드로 되돌아올 필요가 없어 데드락이 발생하지 않습니다.
/// (실제 WebView 로그인 창은 SsoClient 가 전용 STA 스레드에서 띄우므로 STA/MTA 무관하게 동작합니다.)
///
public async Task SignInAsync()
{
UserInfo? user = null;
if (option.EnableAutoLogin)
user = UserInfo.FromSsoFile();
await SignInAsync(user?.RefreshToken);
}
///
/// refresh_token 으로 무창 인증을 시도하고, 불가하면 웹뷰 로그인으로 진행합니다. (비동기)
///
///
/// UI 스레드에서 .Wait()/.Result 로 블로킹하면 데드락입니다. 자세한 이유는
/// 의 설명을 참고하세요. (UI 스레드면 await, 동기 호출은 )
///
public async Task SignInAsync(string refreshToken)
{
UserInfo user = null;
var isConnected = await CheckConnection();
if (isConnected)
{
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);
if (user == null)
throw new InvalidUserException($"Broken user token. Token {token.ToString()}, UserJson {userJson}");
}
else
{
if (!option.EnableOffline)
throw new InvalidOperationException("Network isn't available.");
user = UserInfo.FromSsoFile();
if (user == null)
throw new InvalidUserException("Not found sso data for offline.");
}
// 가족사가 아니고, 인증도 실패 시
if (user.IsFamily())
option?.FamilyValidator?.Validate(user);
else
option?.ExtraUserValidator?.Validate(user);
CurrentUser = user;
CurrentUser.Save();
}
///
/// 로그아웃: 현재 사용자 정보 + 저장된 refresh_token + WebView SSO 세션 쿠키를 모두 삭제합니다.
/// 세션 쿠키까지 지우므로 다음 로그인 시 로그인 창이 다시 표시됩니다.
///
public async Task SignOutAsync()
{
try
{
//SSO 서버 세션 종료(RP-Initiated Logout) + 로컬 WebView 세션 쿠키 정리
UserInfo.Clear();
await client.LogoutAsync(CurrentUser?.IdToken);
}
catch
{
// 로그아웃 실패해도 로컬 사용자 정보는 이미 삭제했으므로 로컬 로그아웃으로 간주.
}
}
private async Task CheckConnection()
{
try
{
await client.GetDiscoveryAsync();
return true;
}
catch
{
return false;
}
}
}
}