Build Charac server and client with OIDC, SSH sessions, and packages
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<RootNamespace>Charac.Client</RootNamespace>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
<Using Include="Microsoft.Extensions.Configuration" />
|
||||
<Compile Include="../../src/client/OidcLogin.cs" Link="OidcLogin.cs" />
|
||||
<Compile Include="../../src/server/Authentication/OidcSettings.cs" Link="OidcSettings.cs" />
|
||||
<Compile Include="../../src/server/Hosting/WorkspaceAccessConfiguration.cs" Link="WorkspaceAccessConfiguration.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="../../src/client/Assets/Fonts/*.woff2" LogicalName="Charac.Fonts.%(Filename)%(Extension)" />
|
||||
<None Include="../../src/client/Assets/Fonts/IBM-Plex-LICENSE.txt" Link="Assets/Fonts/IBM-Plex-LICENSE.txt" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,88 @@
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text.Json;
|
||||
using Charac.Server.Authentication;
|
||||
using Charac.Server.Hosting;
|
||||
|
||||
namespace Charac.Client;
|
||||
|
||||
internal static class Program
|
||||
{
|
||||
public static async Task<int> Main(string[] args)
|
||||
{
|
||||
if (args is ["--help"] or ["-h"])
|
||||
{
|
||||
PrintUsage();
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!TryReadConfigPath(args, out var configPath))
|
||||
{
|
||||
PrintUsage();
|
||||
return 2;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var configuration = WorkspaceAccessConfiguration.Load(configPath);
|
||||
var oidc = OidcSettings.FromConfiguration(configuration) ??
|
||||
throw new InvalidOperationException("Configure [oidc] issuer and client_id before running this demo.");
|
||||
var server = ReadServerUri(configuration["oidc:server_url"]);
|
||||
|
||||
using var http = new HttpClient { Timeout = TimeSpan.FromSeconds(30) };
|
||||
var accessToken = await OidcLogin.LoginAsync(http, oidc.Issuer, oidc.ClientId,
|
||||
oidc.DiscoveryUrl);
|
||||
Console.WriteLine("authentik/OIDC login succeeded. Access token remains in this process.");
|
||||
|
||||
if (server is not null)
|
||||
{
|
||||
using var request = new HttpRequestMessage(HttpMethod.Get, new Uri(server, "/identity"));
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
|
||||
using var response = await http.SendAsync(request);
|
||||
response.EnsureSuccessStatusCode();
|
||||
using var identity = await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync());
|
||||
Console.WriteLine($"Verified issuer: {identity.RootElement.GetProperty("issuer").GetString()}");
|
||||
Console.WriteLine($"Verified subject: {identity.RootElement.GetProperty("subject").GetString()}");
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
catch (Exception exception) when (exception is HttpRequestException or TaskCanceledException or
|
||||
OperationCanceledException or InvalidOperationException or JsonException or IOException or
|
||||
KeyNotFoundException or ArgumentException)
|
||||
{
|
||||
Console.Error.WriteLine($"OIDC demo failed: {exception.Message}");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryReadConfigPath(string[] args, out string? configPath)
|
||||
{
|
||||
configPath = null;
|
||||
if (args.Length == 0)
|
||||
return true;
|
||||
if (args is ["--config", var path] && !string.IsNullOrWhiteSpace(path) &&
|
||||
!path.StartsWith("--", StringComparison.Ordinal))
|
||||
{
|
||||
configPath = path;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static Uri? ReadServerUri(string? value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
return null;
|
||||
if (!Uri.TryCreate(value.Trim(), UriKind.Absolute, out var server) ||
|
||||
(server.Scheme != Uri.UriSchemeHttps && !(server.Scheme == Uri.UriSchemeHttp && server.IsLoopback)) ||
|
||||
server.UserInfo.Length != 0 || server.AbsolutePath != "/" ||
|
||||
server.Query.Length != 0 || server.Fragment.Length != 0)
|
||||
throw new InvalidOperationException("[oidc] server_url must be an HTTPS origin or a loopback HTTP origin.");
|
||||
return server;
|
||||
}
|
||||
|
||||
private static void PrintUsage()
|
||||
{
|
||||
Console.WriteLine("Usage: dotnet run --project demo/oidc-cli -- [--config <workspace-access.config path>]");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
# CLI 唤起 authentik:OIDC Playground
|
||||
|
||||
这个独立小程序演示现有 Client 的登录代码:读取 authentik 的 OIDC discovery,生成 PKCE 与 `state`,监听 `127.0.0.1` 临时端口,通过系统浏览器打开授权页面,接收 `/callback` 授权码,再向 token endpoint 交换 access token。它复用 `src/client/OidcLogin.cs`、Server 的 OIDC 设置校验及 INI 路径解析,不需要额外 NuGet 包,也不建立 SSH 会话。
|
||||
|
||||
先在 authentik 创建 OAuth2/OIDC Application 与 **public client**,允许 Authorization Code + PKCE,配置 `openid` scope、非对称签名密钥,以及允许的 loopback Redirect URI。动态端口可使用 Redirect URIs 正则 `^http://127\.0\.0\.1:[0-9]+/callback$`。不要把 Client Secret 放进 CLI。步骤参考 [authentik 官方文档](https://docs.goauthentik.io/add-secure-apps/providers/oauth2/)。
|
||||
|
||||
在仓库根目录的 `workspace-access.config` 中配置与 Server 相同的 `[oidc]` 字段:
|
||||
|
||||
```ini
|
||||
[oidc]
|
||||
issuer=https://auth.example.com/application/o/workspace-access/
|
||||
client_id=<authentik-public-client-id>
|
||||
; 只有 discovery 路径不能由 issuer 推导时才填写,例如 authentik 全局 issuer 模式
|
||||
; discovery_url=https://auth.example.com/application/o/workspace-access/.well-known/openid-configuration
|
||||
|
||||
; 可选:调用已启动的 Server /identity,让 Server 验证令牌并显示身份键
|
||||
server_url=http://127.0.0.1:5080
|
||||
```
|
||||
|
||||
在仓库根目录运行:
|
||||
|
||||
```powershell
|
||||
dotnet run --project demo/oidc-cli
|
||||
```
|
||||
|
||||
需要使用其他文件时,只传路径:`dotnet run --project demo/oidc-cli -- --config .\workspace-access.config`。未指定时,优先查找仓库根目录的 `workspace-access.config`,与 Server 相同。程序会尝试打开浏览器,并同时打印授权 URL。浏览器回到本机后会显示 RAC 的授权回调页;它只表示已收到授权码,CLI 仍需向 authentik 交换令牌。只有终端显示登录成功才算完成。令牌交换失败时会显示 HTTP 状态及 OAuth 错误代码,不输出 access token。配置 `[oidc] server_url` 且 Server 已启用 OIDC 和数据库时,demo 还会请求 `/identity`,显示用于映射的 `(issuer, subject)`;不配置该字段时只验证 authentik 令牌交换。
|
||||
|
||||
默认 discovery URL 为 `<issuer 去掉末尾斜杠>/.well-known/openid-configuration`。authentik 使用全局 issuer 模式,或其他 Provider 的 discovery 路径不同的时候,请在 `[oidc]` 设置 `discovery_url`。`issuer` 必须与 discovery 文档中的 `issuer` 完全一致。浏览器回调必须发生在运行 CLI 的同一台设备上;无本机浏览器的设备授权流程不在此演示范围。
|
||||
|
||||
Server 返回的 `(issuer, subject)` 是稳定的外部身份键,不是用户资料。Server 本地若需要易读名称,应称为 **user label**:它只用于管理员识别和显示,不能代替身份键,也不参与授权判断。用户的姓名、邮箱等资料由 authentik 或其他身份提供方管理;RAC 不应把自己的 PostgreSQL 身份映射表当作用户资料主库。
|
||||
|
||||
回调页的西文使用随程序集嵌入的 IBM Plex Mono Regular/Bold,中文使用系统字体回退。字体以 data URL 随 HTML 返回,无需外部字体请求;CSP 仅为字体允许 `data:` 来源。字体来自 [IBM Plex 官方仓库固定版本](https://github.com/IBM/plex/tree/763c36ef9117782905ae010056dfbe8fd2653a25/packages/plex-mono),按 [SIL Open Font License 1.1](../../src/client/Assets/Fonts/IBM-Plex-LICENSE.txt) 分发,许可证随构建与发布输出保留。
|
||||
|
||||
该流程现已接入正式 Client:使用 `login --server <origin>` 验证登录,或 `connect --server <origin> --target <id>` 登录并进入交互工作区。正式 Client 从 Server 取得 OIDC 配置;本 demo 仍直接读 INI,用于独立排查身份提供方。命令及验收边界见[正式 Client 流程](../../docs/client-server-transport.md)。
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"version": 2,
|
||||
"dependencies": {
|
||||
"net10.0": {}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user