Files
qbit-printing/dev/native-auth/client/Program.cs
T
james.maesandCodex 1a0964ef23 chore: enforce owned source copyright notices (#25)
* chore(standards): preserve owned-source copyright notices

Add accurate repository copyright and license comments to 50 owned source/build/configuration files. Preserve existing holders, upstream wrappers, legal payloads and schema/data exemptions. Exact inverse-byte reconstruction and applicable parser/token/AST checks prove comment-only changes; existing relevant regressions pass.

Co-Authored-By: Codex (GPT-6) <noreply@openai.com>
Signed-off-by: James Maes <james@kof22.com>

* chore: enforce owned source copyright notices

Check real leading comments against explicit repository provenance and retain existing native style gates.

Co-Authored-By: Codex GPT-6 <noreply@openai.com>
Signed-off-by: James Maes <james@kof22.com>

---------

Signed-off-by: James Maes <james@kof22.com>
Co-authored-by: Codex (GPT-6) <noreply@openai.com>
2026-11-01 12:41:00 -06:00

242 lines
13 KiB
C#

// Copyright 2026 Kingsrook, LLC
// SPDX-License-Identifier: Apache-2.0
using System.Net;
using System.Security.Authentication;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.RegularExpressions;
using Qqq.PrintAgent.Core.Authentication;
namespace Printing.NativeAuth;
internal static class Program
{
private const string CoreSource = "ecf0f550a0357cec6387c2b54efca128a405a629";
private const string ClientSource = "346fc285198908c47a2ee9db7b9adb53d5e88c3c";
private const string InputPath = "/work/input.json";
private const string ResultPath = "/work/result.json";
private const string TlsMarker = "/tmp/printing-native-untrusted-tls-passed";
private static readonly List<string> Checks = [];
private static string _stage = "input";
private static bool? _authorizationRequestMatches;
public static async Task<int> Main(string[] arguments)
{
if(arguments is ["--check-discovery"])
{
await NativeAuthDiscoveryChecks.RunAsync();
return 0;
}
JsonObject? input = null;
try
{
if(arguments.Length != 1 || arguments[0] is not ("assert-untrusted" or "test"))
{ throw new InvalidOperationException("An explicit fixture action is required."); }
if(new FileInfo(InputPath).Length is <= 0 or > 65536)
{ throw new InvalidOperationException("Fixture input size is invalid."); }
input = JsonNode.Parse(await File.ReadAllTextAsync(InputPath))!.AsObject();
ValidateInput(input);
using var deadline = new CancellationTokenSource(TimeSpan.FromMinutes(5));
if(arguments[0] == "assert-untrusted")
{
_stage = "untrusted-tls";
await AssertUntrustedAsync(new Uri(ProviderSetup.Text(input, "providerOrigin")), deadline.Token);
await File.WriteAllTextAsync(TlsMarker, "passed", deadline.Token);
return 0;
}
if(!File.Exists(TlsMarker)) { throw new InvalidOperationException("The ordinary TLS negative control did not run."); }
Checks.Add("untrusted-tls-refused");
await QualifyAsync(input, deadline.Token);
await WriteResultAsync(input, "PASS", null, null);
Console.WriteLine("Experimental native Authentik qualification passed; development-only unpublished core.");
return 0;
}
catch(Exception exception)
{
// Exception messages, response bodies, callback URLs and credentials are deliberately withheld.
await WriteResultAsync(input, "FAILED", exception.GetType().Name,
exception is QqqSessionException sessionException ? sessionException.Failure.ToString() : null);
Console.Error.WriteLine("Experimental native Authentik qualification failed at " + _stage + " (" + exception.GetType().Name + ").");
return 1;
}
}
private static void ValidateInput(JsonObject input)
{
if(!Regex.IsMatch(ProviderSetup.Text(input, "project"), "^qbit-printing-dev-[a-f0-9]{32}$", RegexOptions.CultureInvariant)
|| ProviderSetup.Text(input, "coreSource") != CoreSource || ProviderSetup.Text(input, "clientSource") != ClientSource
|| ProviderSetup.Text(input, "providerOrigin") != "https://auth.printing.internal"
|| ProviderSetup.Text(input, "qqqOrigin") != "https://qqq.printing.internal"
|| ProviderSetup.Text(input, "callback") != "https://callback.printing.internal/oauth/callback"
|| ProviderSetup.Text(input, "issuer") != "https://auth.printing.internal/application/o/" + ProviderSetup.Text(input, "project") + "/"
|| ProviderSetup.Text(input, "clientId") != ProviderSetup.Text(input, "project"))
{ throw new InvalidOperationException("Fixture identity or destination differs from the pinned local contract."); }
}
private static QqqSessionClient Session(Uri origin) => new(origin,
new HttpClientHandler { AllowAutoRedirect = false, UseCookies = false, UseProxy = false });
private static async Task AssertUntrustedAsync(Uri origin, CancellationToken token)
{
using var transport = new FixtureHttp(origin);
try { await transport.SendAsync("GET", "/-/health/live/", null, token); }
catch(HttpRequestException exception)
{
for(Exception? cause = exception; cause is not null; cause = cause.InnerException)
{
if(cause is AuthenticationException) { return; }
}
throw new InvalidOperationException("The negative TLS control failed for a reason other than certificate authentication.");
}
throw new InvalidOperationException("The test unexpectedly trusted its private CA before scoped installation.");
}
private static async Task QualifyAsync(JsonObject input, CancellationToken token)
{
var origin = new Uri(ProviderSetup.Text(input, "qqqOrigin"));
var providerOrigin = new Uri(ProviderSetup.Text(input, "providerOrigin"));
var callback = new Uri(ProviderSetup.Text(input, "callback"));
_stage = "readiness";
await WaitForQqqAsync(origin, token);
using var provider = new ProviderSetup(input);
_stage = "provider-readiness";
await provider.WaitReadyAsync(token);
_stage = "provider-configuration";
await provider.ConfigureAsync(token);
using var receiver = new CallbackReceiver(callback);
using var first = Session(origin);
_stage = "typed-native-discovery";
await NativeAuthDiscovery.DiscoverAsync(first, token);
QqqAuthenticationMetadata metadata = await first.GetAuthenticationMetadataAsync(token);
if(metadata.Values?.ClientId != ProviderSetup.Text(input, "clientId"))
{ throw new InvalidOperationException("Native metadata did not retain the configured client identifier."); }
Checks.Add("typed-native-grant");
_stage = "real-provider-login";
await LoginAsync(first, receiver, input, providerOrigin, callback, authorized: true, token);
_stage = "authorized-native-identity";
QqqSessionBinding identity = await first.GetSessionIdentityAsync(token);
if(string.IsNullOrWhiteSpace(identity.PrincipalId) || identity.TenantId != "printing-native-dev" || identity.DeviceId is not null)
{ throw new InvalidOperationException("The native subject did not receive the intended server-derived identity."); }
Checks.Add("real-provider-login");
_stage = "wrong-origin-cookie";
await AssertWrongOriginAsync(origin, first.ExportCredential(), token);
await first.GetSessionIdentityAsync(token);
Checks.Add("wrong-origin-cookie-denied");
_stage = "permission-denied-user";
using(var denied = Session(origin))
{
await LoginAsync(denied, receiver, input, providerOrigin, callback, authorized: false, token);
await ExpectFailureAsync(() => denied.GetSessionIdentityAsync(token), QqqSessionException.FailureKind.PermissionDenied);
await denied.SignOutAsync(token);
}
Checks.Add("permission-denied-user");
_stage = "logout-replay";
QqqSessionCredential retired = first.ExportCredential();
await first.SignOutAsync(token);
using(var replay = Session(origin))
{ await ExpectFailureAsync(() => replay.RestoreSessionAsync(retired, token), QqqSessionException.FailureKind.AuthenticationRequired); }
Checks.Add("logout-replay-denied");
_stage = "fresh-login-for-renewal";
using var renewable = Session(origin);
await LoginAsync(renewable, receiver, input, providerOrigin, callback, authorized: true, token);
QqqSessionBinding before = await renewable.GetSessionIdentityAsync(token);
HashSet<string> previousGrants = await provider.AccessGrantIdsAsync(token);
_stage = "server-refresh-after-expiry";
await Task.Delay(TimeSpan.FromSeconds(22), token);
await renewable.ResumeSessionAsync(token);
QqqSessionBinding after = await renewable.GetSessionIdentityAsync(token);
HashSet<string> refreshedGrants = await provider.AccessGrantIdsAsync(token);
if(before != after || !refreshedGrants.Except(previousGrants, StringComparer.Ordinal).Any())
{ throw new InvalidOperationException("Renewal did not preserve the identity and obtain a new actual provider grant."); }
Checks.Add("server-refresh-after-expiry");
_stage = "provider-revocation-after-expiry";
await provider.RevokeRefreshGrantsAsync(token);
await Task.Delay(TimeSpan.FromSeconds(22), token);
await ExpectFailureAsync(() => renewable.ResumeSessionAsync(token), QqqSessionException.FailureKind.AuthenticationRequired);
Checks.Add("provider-revocation-denied");
_stage = "complete";
}
private static async Task LoginAsync(QqqSessionClient client, CallbackReceiver receiver, JsonObject input,
Uri providerOrigin, Uri callback, bool authorized, CancellationToken token)
{
string role = authorized ? "authorized" : "denied";
_stage = role + "-begin-pkce";
QqqBrowserLogin attempt = await client.BeginBrowserLoginAsync(callback, token);
_stage = role + "-generated-authorization-request";
Dictionary<string, string> parameters = attempt.AuthorizationUri.Query.TrimStart('?').Split('&')
.Select(entry => entry.Split('=', 2)).ToDictionary(pair => Uri.UnescapeDataString(pair[0]), pair => Uri.UnescapeDataString(pair[1]), StringComparer.Ordinal);
_authorizationRequestMatches = attempt.AuthorizationUri.AbsolutePath == "/application/o/authorize/"
&& parameters.GetValueOrDefault("client_id") == ProviderSetup.Text(input, "clientId")
&& parameters.GetValueOrDefault("redirect_uri") == callback.AbsoluteUri
&& parameters.GetValueOrDefault("response_type") == "code"
&& parameters.GetValueOrDefault("code_challenge_method") == "S256"
&& parameters.GetValueOrDefault("state")?.Length == 43;
if(_authorizationRequestMatches != true)
{ throw new InvalidOperationException("The generated authorization request differs from the exact provider registration."); }
_stage = role + "-provider-flow";
await ProviderFlow.AuthenticateAsync(attempt.AuthorizationUri, providerOrigin, callback,
ProviderSetup.Text(input, role + "Username"), ProviderSetup.Text(input, role + "Password"), token);
_stage = role + "-callback";
Uri actualCallback = await receiver.ReadAsync(token);
_stage = role + "-exchange";
await client.CompleteBrowserLoginAsync(attempt, actualCallback, token);
}
private static async Task ExpectFailureAsync(Func<Task> operation, QqqSessionException.FailureKind expected)
{
try { await operation(); }
catch(QqqSessionException exception) when(exception.Failure == expected) { return; }
throw new InvalidOperationException("The expected native authentication or permission denial did not occur.");
}
private static async Task AssertWrongOriginAsync(Uri origin, QqqSessionCredential credential, CancellationToken token)
{
using var http = new HttpClient(new HttpClientHandler { AllowAutoRedirect = false, UseCookies = false, UseProxy = false })
{ Timeout = TimeSpan.FromSeconds(15), MaxResponseContentBufferSize = 65536 };
using var request = new HttpRequestMessage(HttpMethod.Get, new Uri(origin, "/qqq/v1/qbit/printing/session"));
request.Headers.Add("X-QQQ-Client", QqqSessionClient.ClientChannel);
request.Headers.Add("Cookie", "sessionUUID=" + credential.SessionToken);
request.Headers.Add("Origin", "https://wrong-origin.invalid");
using HttpResponseMessage response = await http.SendAsync(request, token);
if(response.StatusCode != HttpStatusCode.Forbidden)
{ throw new InvalidOperationException("The fixture did not reject a wrong-Origin cookie request."); }
}
private static async Task WaitForQqqAsync(Uri origin, CancellationToken token)
{
using var http = new FixtureHttp(origin);
for(int attempt = 0; attempt < 45; attempt++)
{
try
{
FixtureHttp.Reply response = await http.SendAsync("GET", "/qqq/v1/version", null, token);
if(response.Status == HttpStatusCode.OK && response.Json is not null) { return; }
}
catch(HttpRequestException) { }
await Task.Delay(TimeSpan.FromSeconds(2), token);
}
throw new InvalidOperationException("The owned native host did not become ready.");
}
private static Task WriteResultAsync(JsonObject? input, string status, string? failureType, string? sessionFailure)
=> File.WriteAllTextAsync(ResultPath, JsonSerializer.Serialize(new
{
status, scope = "DEVELOPMENT_ONLY_UNRELEASED", coreSource = CoreSource, clientSource = ClientSource,
project = input?["project"]?.ToString(), observedUtc = DateTimeOffset.UtcNow, stage = _stage, failureType,
sessionFailure, lastRequestKind = FixtureHttp.LastRequestKind, lastHttpStatus = FixtureHttp.LastStatus,
lastProviderComponent = ProviderFlow.LastComponent,
authorizationRequestMatches = _authorizationRequestMatches,
requestClassifications = FixtureHttp.Trace,
providerLookupKind = ProviderSetup.LastLookupKind, providerLookupCount = ProviderSetup.LastLookupCount,
providerReadinessCounts = ProviderSetup.ReadinessCounts,
checks = Checks, installedUiAcceptance = false, providerRefreshTokensInClient = false,
}, new JsonSerializerOptions { WriteIndented = true }));
}