로그아웃 시 WebView 창/에러 페이지 노출 방지 (오프스크린 end_session)
- LoginWindow.EndSessionAsync 추가: 1×1 화면 밖 WebView2로 end_session을 수행해 로그아웃 시 창이나 네비게이션 에러 페이지(ERR_ABORTED 등)가 보이지 않게 함. post_logout_redirect_uri 복귀를 NavigationStarting에서 가로채 종료하고, 복귀를 못 가로채도 timeout 후 조용히 반환. - SsoClient.LogoutAsync: 보이는 AuthenticateAsync 대신 EndSessionAsync 사용. - BaronSSO.SignOutAsync: CurrentUser가 null이면 파일에서 로드 후 로그아웃. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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<bool> CheckConnection()
|
||||
{
|
||||
try
|
||||
|
||||
@@ -109,6 +109,63 @@ namespace BaronSoftware.SSO
|
||||
}
|
||||
});
|
||||
|
||||
/// <summary>
|
||||
/// SSO 로그아웃(end_session)을 화면 밖(오프스크린) WebView2에서 수행합니다.
|
||||
/// 로그인 창과 달리 1×1 화면 밖 창을 쓰므로, 사용자에게 WebView 창이나
|
||||
/// 네비게이션 에러 페이지(ERR_ABORTED 등)가 일절 보이지 않습니다.
|
||||
/// end_session_endpoint를 로드하고 post_logout_redirect_uri로의 복귀를 가로채 종료하며,
|
||||
/// 복귀를 끝내 가로채지 못해도(서버/네트워크 문제) timeoutMs 후 조용히 반환합니다.
|
||||
/// </summary>
|
||||
internal static Task EndSessionAsync(
|
||||
string endSessionUrl, string postLogoutRedirectUri, int timeoutMs = 10000)
|
||||
=> RunOnDedicatedUiThreadAsync(async () =>
|
||||
{
|
||||
var tcs = new TaskCompletionSource<bool>(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();
|
||||
}
|
||||
});
|
||||
|
||||
/// <summary>
|
||||
/// 호출 스레드(MTA/STA)와 무관하게, 전용 STA 스레드에서 자체 Dispatcher 메시지펌프를 돌려
|
||||
/// WPF/WebView2 UI 작업을 실행하고 그 결과를 Task로 반환한다.
|
||||
|
||||
@@ -72,12 +72,15 @@ namespace BaronSoftware.SSO
|
||||
|
||||
/// <summary>
|
||||
/// refreshToken 기반 인증: 저장된 refresh_token으로 창 없이(無窓) 토큰을 재발급한다.
|
||||
/// refresh_token이 만료/무효 등으로 실패하면 웹뷰 대화형 로그인(LoginWindow)으로 폴백한다.
|
||||
/// </summary>
|
||||
public async Task<TokenResponse> LoginAsync(string refreshToken)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(refreshToken))
|
||||
return await LoginAsync();
|
||||
|
||||
try
|
||||
{
|
||||
var disc = await GetDiscoveryAsync();
|
||||
return await ExchangeAsync(disc, new Dictionary<string, string>
|
||||
{
|
||||
@@ -86,6 +89,13 @@ namespace BaronSoftware.SSO
|
||||
["client_id"] = options.ClientId,
|
||||
});
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 저장된 refresh_token이 만료/폐기/무효 → 자동 로그인 실패.
|
||||
// 사용자가 다시 로그인할 수 있도록 웹뷰 로그인 창을 띄운다.
|
||||
return await LoginAsync();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>.well-known/openid-configuration 문서를 1회 로드 후 캐시.</summary>
|
||||
public async Task<JsonNode> GetDiscoveryAsync()
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user