Files
DodoSSH/src/DodoSSH.Client.Auth/OidcClientOptions.cs
T
jaap-jan 7a3a521c59
ci / build and test (push) Failing after 2s
ci / android head (push) Failing after 1s
Give the phone the rest of its screens, and a way in
All seven screens of the design, plus the two it does not draw because it starts at an
enrolled phone: naming a server, and choosing a passphrase.

The five states docs/android-port.md worried about losing at 360dp are all here and none
of them softened. The changed-key refusal is a full-screen panel rather than a bottom
sheet, because a sheet is swipe-to-dismiss by convention and that screen must have no way
forward. The recovery code raises FLAG_SECURE for its own state and lowers it afterwards,
so the sentence about screenshots is true rather than decorative. The delete
confirmations keep their counts and replace the row in place.

Signing in works, and the seam it needed is worth more than the implementation:
IAuthorizationCallback now sits between OidcClient and the loopback listener, so the two
heads differ in where the response arrives and in nothing else. PKCE, the state check,
discovery, the token exchange and the key binding stay one implementation — a second OIDC
client would be a second place for a security bug to live. The phone registers a
private-use scheme with the system rather than binding a loopback port, which on a shared
device any other app can do first.

The accessory key row needed TerminalWorkspace.SendInputAsync: ordinary typing goes from
the renderer straight down the socket, and there was no way in for the keys a software
keyboard does not have. Ctrl latches, because one thumb cannot chord, and the latch is
drawn — a modifier that is on and does not look on is how somebody sends ^L to a database
prompt believing they typed an l.

597 client tests green, including two new ones for the input path and one for the
terminal surface command. Nothing has run on a device.
2026-07-31 21:43:11 +02:00

67 lines
2.9 KiB
C#

namespace DodoSSH.Client.Auth;
/// <summary>
/// Everything needed to talk to one identity provider.
/// </summary>
/// <remarks>
/// Populated from the DodoSSH server's <c>/.well-known/dodossh-configuration</c>, which is the whole
/// onboarding story: the user types one server URL and the client discovers the authority, the client
/// id and the scopes from it.
/// </remarks>
public sealed record OidcClientOptions
{
/// <summary>The provider's issuer URL.</summary>
public required Uri Authority { get; init; }
/// <summary>The public client identifier registered for the desktop app.</summary>
public required string ClientId { get; init; }
/// <summary>
/// Scopes requested at sign-in.
/// </summary>
/// <remarks>
/// <c>offline_access</c> is what yields a refresh token, and without one the user re-authenticates
/// through the browser every time the access token expires.
/// </remarks>
public IReadOnlyList<string> Scopes { get; init; } = ["openid", "profile", "email", "offline_access"];
/// <summary>Path the loopback listener answers the redirect on.</summary>
public string RedirectPath { get; init; } = "/callback";
/// <summary>
/// How the authorization response is received. Loopback unless a head substitutes one.
/// </summary>
/// <remarks>
/// <para>
/// A factory rather than an instance, because a callback owns a socket or a system registration and
/// must not outlive one sign-in. It takes the redirect path so the default keeps behaving exactly as
/// it did when it was constructed inline.
/// </para>
/// <para>
/// The Android head replaces this: a loopback redirect on a shared device is the attack RFC 8252 §8.3
/// names, since any other application can bind the port. See <see cref="IAuthorizationCallback"/>.
/// </para>
/// </remarks>
public Func<string, IAuthorizationCallback> CallbackFactory { get; init; } =
path => new LoopbackCallbackListener(path);
/// <summary>Whether provider metadata must be served over HTTPS. Only false for local development.</summary>
public bool RequireHttpsMetadata { get; init; } = true;
/// <summary>How long to wait for the user to finish in the browser.</summary>
/// <remarks>
/// Generous, because the user may have to find a password manager, complete a second factor, or
/// approve a push notification on another device.
/// </remarks>
public TimeSpan BrowserTimeout { get; init; } = TimeSpan.FromMinutes(5);
/// <summary>Page shown in the browser once the callback is captured.</summary>
public string CompletionHtml { get; init; } =
"""
<!doctype html><html><head><meta charset="utf-8"><title>DodoSSH</title></head>
<body style="font-family:system-ui;margin:4rem;text-align:center">
<h1>Signed in</h1><p>You can close this tab and return to DodoSSH.</p>
</body></html>
""";
}