Initial commit: BARON SSO 샘플 (WebView OIDC PKCE 인증 라이브러리 + 데모 앱)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-16 10:10:37 +09:00
commit 3de67f0052
25 changed files with 1171 additions and 0 deletions

View File

@@ -0,0 +1,9 @@
<Application x:Class="BaronSoftware.SSO.Sample.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:BaronSoftware.Auth.Sample"
StartupUri="MainWindow.xaml">
<Application.Resources>
</Application.Resources>
</Application>

View File

@@ -0,0 +1,14 @@
using System.Configuration;
using System.Data;
using System.Windows;
namespace BaronSoftware.Auth.Sample
{
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application
{
}
}

View File

@@ -0,0 +1,10 @@
using System.Windows;
[assembly: ThemeInfo(
ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
//(used if a resource is not found in the page,
// or application resource dictionaries)
ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
//(used if a resource is not found in the page,
// app, or any theme specific resource dictionaries)
)]

View File

@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UseWPF>true</UseWPF>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\BaronSoftware.SSO\BaronSoftware.SSO.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,38 @@
<Window x:Class="BaronSoftware.SSO.Sample.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:BaronSoftware.SSO.Sample"
mc:Ignorable="d"
Title="BARON SSO 웹뷰 인증 샘플" Height="560" Width="840">
<Grid Margin="16">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0"
Text="BARON SSO (Ory Hydra · OIDC Authorization Code + PKCE) 웹뷰 인증 데모"
FontSize="16" FontWeight="Bold" Margin="0,0,0,12"/>
<StackPanel Grid.Row="1" Orientation="Horizontal" Margin="0,0,0,12">
<Button x:Name="LoginButton" Content="웹뷰로 로그인" Width="140" Height="34"
Click="LoginButton_Click"/>
<Button x:Name="TokenLoginBuggon" Content="토큰 로그인" Width="150" Height="34"
Margin="8,0,0,0" Click="TokenLoginBuggon_Click"/>
<Button x:Name="LogoutButton" Content="로그아웃" Width="100" Height="34"
Margin="8,0,0,0" Click="LogoutButton_Click"/>
<Button x:Name="SettingsButton" Content="설정 변경" Width="90" Height="34"
Margin="8,0,0,0" Click="SettingsButton_Click"/>
</StackPanel>
<Border Grid.Row="2" BorderBrush="#DDDDDD" BorderThickness="1" CornerRadius="4">
<TextBox x:Name="OutputBox" IsReadOnly="True" BorderThickness="0" Padding="10"
FontFamily="Consolas" FontSize="13" TextWrapping="Wrap"
VerticalScrollBarVisibility="Auto"
Text="‘웹뷰로 로그인’ → 토큰 저장 → 다음 실행 시 ‘자동 로그인(갱신).&#x0a;&#x0a;[콘솔 사전 설정 2가지]&#x0a; 1) 리디렉션 URI 설정 → 인증 콜백 URL 에 추가:&#x0a; http://127.0.0.1:8421/baron-sample/auth/callback&#x0a; 2) 스코프 추가 → 'offline_access' (refresh_token 발급용, 자동 로그인 필수)"/>
</Border>
</Grid>
</Window>

View File

@@ -0,0 +1,137 @@
using BaronSoftware;
using BaronSoftware.Auth;
using BaronSoftware.Auth.Sample;
using System;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
namespace BaronSoftware.SSO.Sample
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
private BaronSSO _license;
private readonly SampleSettings _settings;
public MainWindow()
{
InitializeComponent();
_settings = SampleSettings.Load();
ApplySettings();
}
/// <summary>현재 설정으로 SSO 클라이언트를 (재)생성한다.</summary>
private void ApplySettings() => _license = new BaronSSO(_settings.ToOidcOptions());
private void SettingsButton_Click(object sender, RoutedEventArgs e)
{
var dlg = new SettingsWindow(_settings.Oidc.Authority, _settings.Oidc.ClientId, _settings.Oidc.RedirectUri, _settings.Oidc.LogoutUri)
{
Owner = this
};
if (dlg.ShowDialog() != true) return;
_settings.Oidc.Authority = dlg.Authority;
_settings.Oidc.ClientId = dlg.ClientId;
_settings.Oidc.RedirectUri = dlg.RedirectUri;
_settings.Oidc.LogoutUri = dlg.LogoutUri;
try
{
_settings.Save();
ApplySettings();
OutputBox.Text =
"설정 저장 완료 ✔ (appsettings.json)\n\n" +
$"Authority : {_settings.Oidc.Authority}\n" +
$"ClientId : {_settings.Oidc.ClientId}\n" +
$"RedirectUri : {_settings.Oidc.RedirectUri}\n" +
$"LogoutUri : {_settings.Oidc.LogoutUri}\n\n" +
"‘웹뷰로 로그인’으로 적용된 설정을 테스트하세요.";
}
catch (Exception ex)
{
OutputBox.Text = $"설정 저장 실패:\n{ex.Message}";
}
}
private async void LoginButton_Click(object sender, RoutedEventArgs e)
=> await RunAsync("웹뷰 로그인", () => _license.SignInAsync());
private async void LogoutButton_Click(object sender, RoutedEventArgs e)
{
SetBusy(true);
OutputBox.Text = "로그아웃 중... (토큰 + 세션 쿠키 삭제)";
try
{
await _license.SignOutAsync();
OutputBox.Text = "로그아웃 완료 ✔\n" +
"- refresh_token 삭제\n" +
"- WebView SSO 세션 쿠키 삭제\n" +
"→ 다음 ‘웹뷰로 로그인’ 시 로그인 창이 다시 표시됩니다.";
}
finally
{
SetBusy(false);
}
}
private async Task RunAsync(string action, Func<Task> work)
{
SetBusy(true);
OutputBox.Text = $"{action} 진행 중...";
try
{
await work();
OutputBox.Text = $"{action} 완료 ✔\n\n";
OutputBox.Text += Format(_license.CurrentUser);
OutputBox.CaretIndex = 0; // 맨 위(요약)부터 보이도록
}
catch (OperationCanceledException)
{
OutputBox.Text = $"{action} 취소됨 (로그인 창이 닫힘).";
}
catch (Exception ex)
{
OutputBox.Text = $"{action} 실패:\n{ex.GetType().Name}: {ex.Message}";
}
finally
{
SetBusy(false);
}
}
private void SetBusy(bool busy)
=> LoginButton.IsEnabled = LogoutButton.IsEnabled = !busy;
private string Format(UserInfo u)
{
var sb = new StringBuilder();
sb.AppendLine("로그인 성공 ✔");
sb.AppendLine($"UserId(sub) : {u.UUID}");
sb.AppendLine($"Name : {u.Name}");
sb.AppendLine($"Email : {u.Email}");
sb.AppendLine($"Last Auth Time : {u.LastAuthTime}");
sb.AppendLine($"Claims Start------------ \n ");
sb.AppendLine(string.Join("\n", u.Claims.Select(kv => $" {kv.Key}: {kv.Value}")));
sb.AppendLine($"\nClaims End------------ \n ");
sb.AppendLine(!string.IsNullOrWhiteSpace(u.RefreshToken)
? "자동 로그인 : 사용 가능 (refresh_token 저장됨)"
: "자동 로그인 : 불가 — 리프레시 토큰 없음");
sb.AppendLine();
sb.AppendLine("==== token 엔드포인트 응답 (원본) ====");
sb.AppendLine(u.RawTokenResponse);
sb.AppendLine();
sb.AppendLine("==== userinfo 응답 ====");
sb.AppendLine(u.Raw);
return sb.ToString();
}
private async void TokenLoginBuggon_Click(object sender, RoutedEventArgs e) => await RunAsync("토큰 로그인", () => _license.SignInAsync(_license.CurrentUser.RefreshToken));
}
}

View File

@@ -0,0 +1,77 @@
using BaronSoftware;
using BaronSoftware.Auth;
using BaronSoftware.SSO;
using System;
using System.IO;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace BaronSoftware.Auth.Sample
{
/// <summary>
/// 실행 파일과 같은 폴더의 appsettings.json에서 접속 정보를 읽는다.
/// 파일이 없으면 내장 기본값을 사용하고, JSON 형식 오류는 예외로 알린다.
/// </summary>
public sealed class SampleSettings
{
public const string FileName = "appsettings.json";
public OidcSection Oidc { get; set; } = new();
public sealed class OidcSection
{
public string Authority { get; set; }
public string ClientId { get; set; }
public string RedirectUri { get; set; }
public string LogoutUri { get; set; }
public string Scope { get; set; }
}
private static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNameCaseInsensitive = true,
ReadCommentHandling = JsonCommentHandling.Skip,
AllowTrailingCommas = true,
};
/// <summary>실행 파일 옆 appsettings.json을 로드. 파일이 없으면 기본값.</summary>
public static SampleSettings Load()
{
var path = Path.Combine(AppContext.BaseDirectory, FileName);
if (!File.Exists(path))
return new SampleSettings();
try
{
var json = File.ReadAllText(path);
return JsonSerializer.Deserialize<SampleSettings>(json, JsonOptions) ?? new SampleSettings();
}
catch (JsonException ex)
{
throw new InvalidOperationException(
$"설정 파일({FileName}) 형식 오류: {ex.Message}", ex);
}
}
private static readonly JsonSerializerOptions SaveOptions = new()
{
WriteIndented = true,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
};
/// <summary>현재 설정을 실행 파일 옆 appsettings.json에 저장한다.</summary>
public void Save()
{
var path = Path.Combine(AppContext.BaseDirectory, FileName);
File.WriteAllText(path, JsonSerializer.Serialize(this, SaveOptions));
}
public BaronSSOOption ToOidcOptions() => new()
{
Authority = Oidc.Authority,
ClientId = Oidc.ClientId,
RedirectUri = Oidc.RedirectUri,
};
}
}

View File

@@ -0,0 +1,50 @@
<Window x:Class="BaronSoftware.SSO.Sample.SettingsWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="설정 변경 (appsettings.json)" Height="380" Width="620"
WindowStartupLocation="CenterOwner" ResizeMode="NoResize" ShowInTaskbar="False">
<Grid Margin="16">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="110"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Grid.ColumnSpan="2" Margin="0,0,0,12" Foreground="#555"
Text="값을 수정하고 ‘저장’하면 appsettings.json에 기록되고 즉시 적용됩니다."/>
<TextBlock Grid.Row="1" Grid.Column="0" Text="Authority" VerticalAlignment="Center" Margin="0,4"/>
<TextBox Grid.Row="1" Grid.Column="1" x:Name="AuthorityBox" Margin="0,4" Padding="4"
VerticalContentAlignment="Center"/>
<TextBlock Grid.Row="2" Grid.Column="0" Text="ClientId" VerticalAlignment="Center" Margin="0,4"/>
<TextBox Grid.Row="2" Grid.Column="1" x:Name="ClientIdBox" Margin="0,4" Padding="4"
VerticalContentAlignment="Center"/>
<TextBlock Grid.Row="3" Grid.Column="0" Text="RedirectUri" VerticalAlignment="Center" Margin="0,4"/>
<TextBox Grid.Row="3" Grid.Column="1" x:Name="RedirectUriBox" Margin="0,4" Padding="4"
VerticalContentAlignment="Center"/>
<TextBlock Grid.Row="4" Grid.Column="0" Text="LogoutUri" VerticalAlignment="Center" Margin="0,4"/>
<TextBox Grid.Row="4" Grid.Column="1" x:Name="LogoutUriBox" Margin="0,4" Padding="4"
VerticalContentAlignment="Center"/>
<TextBlock Grid.Row="5" Grid.ColumnSpan="2" Margin="0,10,0,0" TextWrapping="Wrap"
Foreground="#777" FontSize="12"
Text="• Authority : OIDC 서버. 예) https://sso.hmac.kr/oidc 또는 http://localhost:5000/oidc&#x0a;• ClientId : 콘솔 '앱 자격 증명'의 퍼블릭 클라이언트 ID&#x0a;• RedirectUri : 콘솔에 등록된 로그인 콜백과 문자 그대로 일치&#x0a;• LogoutUri : 콘솔 'post_logout_redirect_uri'에 등록된 로그아웃 콜백 (서버 세션 종료용)"/>
<StackPanel Grid.Row="6" Grid.ColumnSpan="2" Orientation="Horizontal"
HorizontalAlignment="Right" Margin="0,12,0,0">
<Button Content="저장" Width="90" Height="32" Click="Save_Click" IsDefault="True"/>
<Button Content="취소" Width="90" Height="32" Margin="8,0,0,0" Click="Cancel_Click" IsCancel="True"/>
</StackPanel>
</Grid>
</Window>

View File

@@ -0,0 +1,33 @@
using System.Windows;
namespace BaronSoftware.SSO.Sample
{
/// <summary>접속 설정(Authority/ClientId/RedirectUri/LogoutUri)을 편집하는 모달 다이얼로그.</summary>
public partial class SettingsWindow : Window
{
public string Authority { get; private set; } = "";
public string ClientId { get; private set; } = "";
public string RedirectUri { get; private set; } = "";
public string LogoutUri { get; private set; } = "";
public SettingsWindow(string authority, string clientId, string redirectUri, string logoutUri)
{
InitializeComponent();
AuthorityBox.Text = authority ?? "";
ClientIdBox.Text = clientId ?? "";
RedirectUriBox.Text = redirectUri ?? "";
LogoutUriBox.Text = logoutUri ?? "";
}
private void Save_Click(object sender, RoutedEventArgs e)
{
Authority = AuthorityBox.Text.Trim();
ClientId = ClientIdBox.Text.Trim();
RedirectUri = RedirectUriBox.Text.Trim();
LogoutUri = LogoutUriBox.Text.Trim();
DialogResult = true;
}
private void Cancel_Click(object sender, RoutedEventArgs e) => DialogResult = false;
}
}

View File

@@ -0,0 +1,21 @@
{
// ─────────────────────────────────────────────────────────────
// BARON SSO 샘플 접속 설정
// 값을 수정한 뒤 앱을 다시 실행하면 반영됩니다. (재컴파일 불필요)
// 이 파일은 실행 파일(.exe)과 같은 폴더에 있어야 합니다.
// ─────────────────────────────────────────────────────────────
"Oidc": {
// OIDC Issuer. Discovery 문서: {Authority}/.well-known/openid-configuration
"Authority": "https://sso.hmac.kr/oidc",
// 퍼블릭 클라이언트 ID (콘솔 '앱 자격 증명' 화면)
"ClientId": "4e4c88fc-2b0a-4b8b-b9b5-fb407cdbbfac",
// 콘솔 '리디렉션 URI 설정'에 등록된 값과 문자 그대로 일치해야 함
"RedirectUri": "http://127.0.0.1:8421/baron-sample/auth/callback",
// 로그아웃 후 돌아올 URL. 콘솔의 'post_logout_redirect_uri'에 등록되어 있어야 함
// (미지정 시 로그아웃은 쿠키 삭제 폴백으로 동작 — 서버 세션이 남을 수 있음)
"LogoutUri": "http://127.0.0.1:8421/baron-sample/logout/callback"
}
}