Files
qbit-printing/dev/native-auth/client/ProviderFlow.cs
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

96 lines
5.5 KiB
C#

// Copyright 2026 Kingsrook, LLC
// SPDX-License-Identifier: Apache-2.0
using System.Net;
using System.Text.Json.Nodes;
namespace Printing.NativeAuth;
/// <summary>Uses Authentik's real synthetic-user login stages; never fabricates a session or code.</summary>
internal static class ProviderFlow
{
internal static string? LastComponent { get; private set; }
internal static async Task AuthenticateAsync(Uri authorization, Uri origin, Uri callback, string username, string password, CancellationToken token)
{
using var browser = new FixtureHttp(origin, cookies: true);
using var callbackHttp = new FixtureHttp(new Uri(callback.GetLeftPart(UriPartial.Authority)));
Uri next = authorization;
for(int hop = 0; hop < 24; hop++)
{
if(next.GetLeftPart(UriPartial.Authority) == callback.GetLeftPart(UriPartial.Authority))
{
if(next.AbsolutePath != callback.AbsolutePath) { throw new InvalidOperationException("Unexpected callback path."); }
FixtureHttp.Reply result = await callbackHttp.SendAsync("GET", next, null, token);
if(result.Status != HttpStatusCode.OK) { throw new InvalidOperationException("The HTTPS callback receiver refused the provider redirect."); }
return;
}
if(next.GetLeftPart(UriPartial.Authority) != origin.GetLeftPart(UriPartial.Authority))
{ throw new InvalidOperationException("A provider redirect left the fixture."); }
FixtureHttp.Reply page = await browser.SendAsync("GET", next, null, token);
if(page.Status is HttpStatusCode.Redirect or HttpStatusCode.SeeOther or HttpStatusCode.TemporaryRedirect)
{
next = new Uri(next, page.Location ?? throw new InvalidOperationException("Missing provider redirect."));
continue;
}
if(page.Status != HttpStatusCode.OK || !next.AbsolutePath.StartsWith("/if/flow/", StringComparison.Ordinal))
{ throw new InvalidOperationException("Unexpected provider login navigation."); }
string slug = next.AbsolutePath["/if/flow/".Length..].Trim('/');
if(slug.Length == 0 || slug.Any(character => !char.IsAsciiLetterOrDigit(character) && character != '-'))
{ throw new InvalidOperationException("Unexpected provider flow identifier."); }
var executor = new Uri(origin, "/api/v3/flows/executor/" + slug + "/?query=" + Uri.EscapeDataString(next.Query.TrimStart('?')));
JsonObject challenge = await ChallengeAsync(browser, executor, await browser.SendAsync("GET", executor, null, token), token);
bool redirected = false;
for(int stage = 0; stage < 10; stage++)
{
string component = ProviderSetup.Text(challenge, "component");
LastComponent = component switch
{
"ak-stage-identification" => "identification",
"ak-stage-password" => "password",
"ak-stage-consent" => "consent",
"xak-flow-redirect" => "redirect",
"ak-stage-access-denied" => "access-denied",
"ak-stage-flow-error" => "flow-error",
_ => "unsupported",
};
if(component == "xak-flow-redirect")
{
next = new Uri(origin, ProviderSetup.Text(challenge, "to"));
redirected = true;
break;
}
JsonObject response = component switch
{
"ak-stage-identification" => new JsonObject { ["component"] = component, ["uid_field"] = username },
"ak-stage-password" => new JsonObject { ["component"] = component, ["password"] = password },
"ak-stage-consent" => new JsonObject { ["component"] = component, ["consent"] = true },
_ => throw new InvalidOperationException("The provider requested an unsupported test login stage."),
};
challenge = await ChallengeAsync(browser, executor, await browser.SendAsync("POST", executor, response, token), token);
if(challenge["response_errors"] is JsonObject errors && errors.Count != 0)
{ throw new InvalidOperationException("The real provider rejected the synthetic login response."); }
}
if(!redirected) { throw new InvalidOperationException("Provider stage bound exceeded."); }
}
throw new InvalidOperationException("Provider redirect bound exceeded.");
}
private static async Task<JsonObject> ChallengeAsync(FixtureHttp browser, Uri executor, FixtureHttp.Reply response, CancellationToken token)
{
// Authentik uses POST/redirect/GET when a real stage accepts its answer.
// Follow only that same HTTPS executor; other navigation uses typed flow redirects.
for(int redirect = 0; redirect < 8; redirect++)
{
if(response.Status == HttpStatusCode.OK) { return FixtureHttp.RequireJson(response, HttpStatusCode.OK); }
if(response.Status is not (HttpStatusCode.Redirect or HttpStatusCode.SeeOther) || response.Location is null)
{ throw new InvalidOperationException("Unexpected provider stage response."); }
var location = new Uri(executor, response.Location);
if(location.GetLeftPart(UriPartial.Authority) != executor.GetLeftPart(UriPartial.Authority)
|| location.AbsolutePath != executor.AbsolutePath)
{ throw new InvalidOperationException("A stage redirect left its exact HTTPS executor."); }
response = await browser.SendAsync("GET", location, null, token);
}
throw new InvalidOperationException("Provider stage redirect bound exceeded.");
}
}