Initial commit: BARON SSO 샘플 (WebView OIDC PKCE 인증 라이브러리 + 데모 앱)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
126
BaronSoftware.SSO/BaronSSO.cs
Normal file
126
BaronSoftware.SSO/BaronSSO.cs
Normal file
@@ -0,0 +1,126 @@
|
||||
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
|
||||
{
|
||||
/// <summary>
|
||||
/// baron-sso(Ory Hydra · OIDC PKCE)로 로그인하고 사용자 정보를 보관하는 바론의 인증모듈.
|
||||
/// </summary>
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>웹뷰 로그인 창을 띄워 인증합니다.</summary>
|
||||
public async Task SignInAsync() => await SignInAsync(null);
|
||||
|
||||
/// <summary>웹뷰 로그인 창을 띄워 인증합니다.</summary>
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 로그아웃: 현재 사용자 정보 + 저장된 refresh_token + WebView 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);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 로그아웃 실패해도 로컬 사용자 정보는 이미 삭제했으므로 로컬 로그아웃으로 간주.
|
||||
}
|
||||
}
|
||||
|
||||
/// <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
|
||||
{
|
||||
await client.GetDiscoveryAsync();
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user