SSO 로그아웃(RP-Initiated end_session) 구현 및 세션 쿠키 정리 보강

- BaronSSO.SignOutAsync: id_token_hint 기반 end_session 로그아웃 + 로컬 세션 정리
- SsoClient.LogoutAsync: end_session_endpoint 이동 후 post_logout 복귀를 WebView에서 가로채기
- BaronSSOOption.PostLogoutRedirectUri 추가
- LoginWindow: 쿠키 삭제를 ClearBrowsingDataAsync(완료 대기)로 변경해 재로그인 자동 SSO 통과 방지
- UserInfo: IsFamily/IsCenter 대소문자 무시 비교를 StringComparer로 수정(빌드 오류 해소)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-16 12:25:41 +09:00
parent 3de67f0052
commit 1c41230021
8 changed files with 101 additions and 57 deletions

View File

@@ -1,9 +1,4 @@
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
{
@@ -65,7 +60,14 @@ namespace BaronSoftware.SSO
throw new InvalidOperationException($"Failed to get userinfo : {token.ToString()}");
user = UserInfo.FromJson(userJson, token);
option?.Validator?.Validate(user);
if (user == null || option?.Validator == null)
throw new NullReferenceException("user or validator option is null");
// 가족사가 아니고, 인증도 실패 시
if(!user.IsFamily()&& !option.Validator.Validate(user))
throw new InvalidUserException("Failed to authorize user");
CurrentUser = user;
CurrentUser.Save();
}
@@ -76,17 +78,11 @@ namespace BaronSoftware.SSO
/// </summary>
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);
//SSO 서버 세션 종료(RP-Initiated Logout) + 로컬 WebView 세션 쿠키 정리
UserInfo.Clear();
await client.LogoutAsync(CurrentUser?.IdToken);
}
catch
{
@@ -94,21 +90,6 @@ namespace BaronSoftware.SSO
}
}
/// <summary>UserInfo의 원본 토큰 응답에서 id_token을 추출한다(로그아웃 id_token_hint용).</summary>
private static string ExtractIdToken(UserInfo user)
{
if (string.IsNullOrEmpty(user?.RawTokenResponse))
return null;
try
{
return JsonNode.Parse(user.RawTokenResponse)?["id_token"]?.GetValue<string>();
}
catch
{
return null;
}
}
private async Task<bool> CheckConnection()
{
try