Initial commit: BARON SSO 샘플 (WebView OIDC PKCE 인증 라이브러리 + 데모 앱)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
9
BaronSoftware.SSO.Sample/App.xaml
Normal file
9
BaronSoftware.SSO.Sample/App.xaml
Normal 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>
|
||||
14
BaronSoftware.SSO.Sample/App.xaml.cs
Normal file
14
BaronSoftware.SSO.Sample/App.xaml.cs
Normal 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
|
||||
{
|
||||
}
|
||||
|
||||
}
|
||||
10
BaronSoftware.SSO.Sample/AssemblyInfo.cs
Normal file
10
BaronSoftware.SSO.Sample/AssemblyInfo.cs
Normal 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)
|
||||
)]
|
||||
21
BaronSoftware.SSO.Sample/BaronSoftware.SSO.Sample.csproj
Normal file
21
BaronSoftware.SSO.Sample/BaronSoftware.SSO.Sample.csproj
Normal 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>
|
||||
38
BaronSoftware.SSO.Sample/MainWindow.xaml
Normal file
38
BaronSoftware.SSO.Sample/MainWindow.xaml
Normal 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="‘웹뷰로 로그인’ → 토큰 저장 → 다음 실행 시 ‘자동 로그인(갱신)’.

[콘솔 사전 설정 2가지]
 1) 리디렉션 URI 설정 → 인증 콜백 URL 에 추가:
 http://127.0.0.1:8421/baron-sample/auth/callback
 2) 스코프 추가 → 'offline_access' (refresh_token 발급용, 자동 로그인 필수)"/>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Window>
|
||||
137
BaronSoftware.SSO.Sample/MainWindow.xaml.cs
Normal file
137
BaronSoftware.SSO.Sample/MainWindow.xaml.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
77
BaronSoftware.SSO.Sample/SampleSettings.cs
Normal file
77
BaronSoftware.SSO.Sample/SampleSettings.cs
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
50
BaronSoftware.SSO.Sample/SettingsWindow.xaml
Normal file
50
BaronSoftware.SSO.Sample/SettingsWindow.xaml
Normal 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
• ClientId : 콘솔 '앱 자격 증명'의 퍼블릭 클라이언트 ID
• RedirectUri : 콘솔에 등록된 로그인 콜백과 문자 그대로 일치
• 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>
|
||||
33
BaronSoftware.SSO.Sample/SettingsWindow.xaml.cs
Normal file
33
BaronSoftware.SSO.Sample/SettingsWindow.xaml.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
21
BaronSoftware.SSO.Sample/appsettings.json
Normal file
21
BaronSoftware.SSO.Sample/appsettings.json
Normal 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"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user