diff --git a/BaronSoftware.SSO/BaronSSO.cs b/BaronSoftware.SSO/BaronSSO.cs index 9bbc12e..a88e0c3 100644 --- a/BaronSoftware.SSO/BaronSSO.cs +++ b/BaronSoftware.SSO/BaronSSO.cs @@ -119,6 +119,9 @@ namespace BaronSoftware.SSO { try { + if (CurrentUser == null) + CurrentUser = UserInfo.FromSsoFile(); + //SSO 서버 세션 종료(RP-Initiated Logout) + 로컬 WebView 세션 쿠키 정리 UserInfo.Clear(); await client.LogoutAsync(CurrentUser?.IdToken); @@ -129,6 +132,7 @@ namespace BaronSoftware.SSO } } + private async Task CheckConnection() { try diff --git a/BaronSoftware.SSO/Features/LoginWindow/LoginWindow.xaml.cs b/BaronSoftware.SSO/Features/LoginWindow/LoginWindow.xaml.cs index 3cb1806..a4667f4 100644 --- a/BaronSoftware.SSO/Features/LoginWindow/LoginWindow.xaml.cs +++ b/BaronSoftware.SSO/Features/LoginWindow/LoginWindow.xaml.cs @@ -109,6 +109,63 @@ namespace BaronSoftware.SSO } }); + /// + /// SSO 로그아웃(end_session)을 화면 밖(오프스크린) WebView2에서 수행합니다. + /// 로그인 창과 달리 1×1 화면 밖 창을 쓰므로, 사용자에게 WebView 창이나 + /// 네비게이션 에러 페이지(ERR_ABORTED 등)가 일절 보이지 않습니다. + /// end_session_endpoint를 로드하고 post_logout_redirect_uri로의 복귀를 가로채 종료하며, + /// 복귀를 끝내 가로채지 못해도(서버/네트워크 문제) timeoutMs 후 조용히 반환합니다. + /// + internal static Task EndSessionAsync( + string endSessionUrl, string postLogoutRedirectUri, int timeoutMs = 10000) + => RunOnDedicatedUiThreadAsync(async () => + { + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + var holder = new Window + { + Width = 1, + Height = 1, + Left = -32000, + Top = -32000, + WindowStyle = WindowStyle.None, + ShowInTaskbar = false, + ShowActivated = false, + Title = string.Empty, + }; + var web = new WebView2(); + holder.Content = web; + holder.Show(); // WebView2 초기화에 필요한 HWND 확보(화면 밖) + + void OnNavStarting(object? s, CoreWebView2NavigationStartingEventArgs e) + { + if (e.Uri.StartsWith(postLogoutRedirectUri, StringComparison.OrdinalIgnoreCase)) + { + e.Cancel = true; // 복귀 주소 실제 로딩 차단 + tcs.TrySetResult(true); + } + } + + try + { + await web.EnsureCoreWebView2Async(); + web.CoreWebView2.NavigationStarting += OnNavStarting; + web.CoreWebView2.Navigate(endSessionUrl); + + // 복귀를 가로채면 즉시 완료, 못 가로채도 timeout 후 조용히 종료한다. + await Task.WhenAny(tcs.Task, Task.Delay(timeoutMs)); + } + catch + { + // 오프스크린이라 어떤 네비게이션 에러도 사용자에게 노출되지 않는다. 조용히 무시. + } + finally + { + web.Dispose(); + holder.Close(); + } + }); + /// /// 호출 스레드(MTA/STA)와 무관하게, 전용 STA 스레드에서 자체 Dispatcher 메시지펌프를 돌려 /// WPF/WebView2 UI 작업을 실행하고 그 결과를 Task로 반환한다. diff --git a/BaronSoftware.SSO/OIDC/SsoClient.cs b/BaronSoftware.SSO/OIDC/SsoClient.cs index 22d699e..989893d 100644 --- a/BaronSoftware.SSO/OIDC/SsoClient.cs +++ b/BaronSoftware.SSO/OIDC/SsoClient.cs @@ -72,19 +72,29 @@ namespace BaronSoftware.SSO /// /// refreshToken 기반 인증: 저장된 refresh_token으로 창 없이(無窓) 토큰을 재발급한다. + /// refresh_token이 만료/무효 등으로 실패하면 웹뷰 대화형 로그인(LoginWindow)으로 폴백한다. /// public async Task LoginAsync(string refreshToken) { if (string.IsNullOrWhiteSpace(refreshToken)) return await LoginAsync(); - var disc = await GetDiscoveryAsync(); - return await ExchangeAsync(disc, new Dictionary + try { - ["grant_type"] = "refresh_token", - ["refresh_token"] = refreshToken, - ["client_id"] = options.ClientId, - }); + var disc = await GetDiscoveryAsync(); + return await ExchangeAsync(disc, new Dictionary + { + ["grant_type"] = "refresh_token", + ["refresh_token"] = refreshToken, + ["client_id"] = options.ClientId, + }); + } + catch + { + // 저장된 refresh_token이 만료/폐기/무효 → 자동 로그인 실패. + // 사용자가 다시 로그인할 수 있도록 웹뷰 로그인 창을 띄운다. + return await LoginAsync(); + } } /// .well-known/openid-configuration 문서를 1회 로드 후 캐시. @@ -121,15 +131,9 @@ namespace BaronSoftware.SSO $"&post_logout_redirect_uri={Uri.EscapeDataString(options.PostLogoutRedirectUri)}" + $"&state={PKCEUtil.RandomToken()}"; - try - { - // 전용 STA 스레드에서 로그아웃(end_session) 창을 띄운다. - await LoginWindow.AuthenticateAsync(url, options.PostLogoutRedirectUri); - } - catch (OperationCanceledException) - { - // 사용자가 로그아웃 창을 닫아도 로컬 정리는 계속 진행한다. - } + // 화면 밖(오프스크린) WebView2로 end_session을 수행해 + // 로그아웃 시 WebView 창/에러 페이지가 사용자에게 보이지 않게 한다. + await LoginWindow.EndSessionAsync(url, options.PostLogoutRedirectUri); } } catch