postit: trailing-slash regression + loopback browser cleanup; identityserver: BC cert loader + SecurityKey
PostIt
- LoginPage renders StatusMessage as a read-only TextBox so the
text is mouse-selectable and copyable (no copy button).
- LoginPageViewModel exposes ExternalUrl (Authentication.Authority
with trailing slash stripped) and DiscoveryUrl
(ExternalUrl + '/.well-known/openid-configuration'). LoginAsync
surfaces the discovery URL before the call and suffixes it onto
every error message, so reachability issues are diagnosable by
pasting the URL into a browser.
- LoopbackBrowser (used for the OIDC redirect listener on desktop)
now bounds the GetContextAsync wait at 5 minutes and calls both
Stop() and Close() in the finally, so the listener is always
released even if the user abandons the flow. Without this, the
next PostIt launch fails with 'Failed to listen on prefix
http://127.0.0.1:7890/ because it conflicts with an existing
registration on the machine.'
- Add LoginPageViewModelTests.LoginAsync_works_when_authority_has_trailing_slash
to lock in the discovery URL contract.
- Add LoopbackBrowserTests covering both timeout and happy-path
listener cleanup.
Yavsc.Org
- Drop the CustomEntries['jwks_uri'] override from commit 84160f07.
IdentityServer8 reserves that key and rejects the override with
'Discovery custom entry jwks_uri cannot be added, because it
already exists.' The default /.well-known/openid-configuration/jwks
endpoint is what DiscoveryKeyEndpoint actually serves.
- Replace X509Certificate2.CreateFromPemFile + the 3-arg
X509Certificate2(path, key, X509KeyStorageFlags) ctor with a
BouncyCastle-backed loader. The BCL path raised
InvalidOperationException during AddSigningCredential and aborted
the runtime with SIGABRT (code=6/ABRT, libcoreclr.so stack) on
the production EC Let's Encrypt cert. BouncyCastle 2.6.2
PemReader accepts PKCS#1 + PKCS#8 EC/RSA PEMs uniformly; RSA
path uses DotNetUtilities.ToRSA, EC path rebuilds ECDsa from
ECParameters with the curve dispatched by NIST order bit length
(256/384/521).
- Switch the signing credential handed to IdentityServer8 from
X509Certificate2 to a SigningCredentials built from a SecurityKey
(RsaSecurityKey / ECDsaSecurityKey). The cert loaded fine but
IdentityServer8's key material service reads cert.PrivateKey at
runtime — on Linux that handle is not retained across the
X509Certificate2 / runtime boundary, so CreateJwkDocumentAsync
raised NullReferenceException on the first GET /jwks. The
SecurityKey is a pure managed object whose Key is a live
AsymmetricAlgorithm, which survives every read IdentityServer
does.
- Add BouncyCastle.Cryptography 2.6.2 to src/Yavsc.Org/Yavsc.Org.csproj
and the matching PackageVersion in Directory.Packages props.
- Wrap the loader in a try/catch that prints the full managed
stack to stderr on failure, so future PEM-format issues surface
in journalctl instead of being hidden behind SIGABRT.
This commit is contained in:
parent
2263311e1b
commit
d62e59ba30
8 changed files with 380 additions and 32 deletions
|
|
@ -29,7 +29,16 @@ namespace PostIt.Services;
|
|||
{
|
||||
Process.Start(new ProcessStartInfo(options.StartUrl) { UseShellExecute = true });
|
||||
|
||||
var context = await listener.GetContextAsync().ConfigureAwait(false);
|
||||
// Bound the wait so the port is released even if the user
|
||||
// closes the browser without completing the flow. Without
|
||||
// this, a crashed/abandoned login keeps the HttpListener
|
||||
// bound and the next PostIt launch fails with
|
||||
// "Failed to listen on prefix … because it conflicts with
|
||||
// an existing registration".
|
||||
using var waitCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
waitCts.CancelAfter(TimeSpan.FromMinutes(5));
|
||||
|
||||
var context = await listener.GetContextAsync().WaitAsync(waitCts.Token).ConfigureAwait(false);
|
||||
var response = context.Response;
|
||||
var responseString = "<html><body>Authentication complete. You can close this window.</body></html>";
|
||||
var buffer = Encoding.UTF8.GetBytes(responseString);
|
||||
|
|
@ -44,13 +53,28 @@ namespace PostIt.Services;
|
|||
Response = raw
|
||||
};
|
||||
}
|
||||
catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
// Bound hit: user did not complete the flow within 5 minutes.
|
||||
return new BrowserResult
|
||||
{
|
||||
ResultType = BrowserResultType.Timeout,
|
||||
Error = "Timed out waiting for the browser to return the authorization code.",
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new BrowserResult { ResultType = BrowserResultType.UnknownError, Error = ex.Message };
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Stop() aborts GetContextAsync (releases the bound port);
|
||||
// Close() disposes the underlying socket. Both are idempotent
|
||||
// and safe to call after Stop() already succeeded, so calling
|
||||
// both covers cases where one path throws before the other
|
||||
// gets a chance (e.g. process-level socket cleanup on Linux).
|
||||
try { listener.Stop(); } catch { }
|
||||
try { listener.Close(); } catch { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,20 +59,11 @@ public partial class Settings : ObservableObject
|
|||
/// (no client secret). The browser implementation should be supplied
|
||||
/// per-platform by the caller.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <see cref="AuthenticationSettings.Authority"/> is normalised by
|
||||
/// trimming any trailing slash before being handed to <c>OidcClient</c>.
|
||||
/// <c>OidcClient</c> derives the discovery URL from
|
||||
/// <c>Authority + "/.well-known/openid-configuration"</c>; leaving a
|
||||
/// trailing slash in place would produce a double-slash URL that some
|
||||
/// servers reject with 404.
|
||||
/// </remarks>
|
||||
internal OidcClientOptions GetOidcClientOptions(IdentityModel.OidcClient.Browser.IBrowser? browser = null)
|
||||
{
|
||||
var authority = Authentication.Authority?.TrimEnd('/') ?? string.Empty;
|
||||
var options = new OidcClientOptions
|
||||
{
|
||||
Authority = authority,
|
||||
Authority = Authentication.Authority,
|
||||
ClientId = Authentication.ClientId,
|
||||
RedirectUri = RedirectUri,
|
||||
Scope = string.Join(' ', this.Scopes),
|
||||
|
|
|
|||
|
|
@ -94,6 +94,14 @@ public partial class LoginPageViewModel : ViewModelBase
|
|||
/// </summary>
|
||||
public Func<IBrowser?>? BrowserFactoryOverride { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional override used by tests. When set, this delegate replaces
|
||||
/// the call to <see cref="Settings.Load"/> at the start of
|
||||
/// <see cref="LoginAsync"/>, so tests can inject a Settings object
|
||||
/// without it being overwritten by the user/embedded default.
|
||||
/// </summary>
|
||||
public Func<Task>? SettingsLoadOverride { get; set; }
|
||||
|
||||
public LoginPageViewModel() : this(new Settings(), browserFactoryOverride: null)
|
||||
{
|
||||
// Load settings eagerly so RegisterUrl / ForgotPasswordUrl are
|
||||
|
|
@ -121,7 +129,22 @@ public partial class LoginPageViewModel : ViewModelBase
|
|||
try
|
||||
{
|
||||
this.IsBusy = true;
|
||||
Settings.Load().Wait();
|
||||
(SettingsLoadOverride ?? Settings.Load)().Wait();
|
||||
|
||||
// Guard: if the authority is empty (no user settings file and
|
||||
// the embedded default couldn't be loaded for any reason),
|
||||
// refuse to call OidcClient. IdentityModel would otherwise
|
||||
// build a bogus authorize URL like "http://127.0.0.1:1/"
|
||||
// from an empty Authority, which the browser then refuses to
|
||||
// open with a confusing "Cette adresse est interdite"
|
||||
// (or equivalent) message. Tell the operator exactly what
|
||||
// to fix instead.
|
||||
if (string.IsNullOrWhiteSpace(Settings.Authentication?.Authority))
|
||||
{
|
||||
this.IsBusy = false;
|
||||
StatusMessage = $"Configuration manquante — édite {SettingsFileHint()} et renseigne Authentication.Authority";
|
||||
return;
|
||||
}
|
||||
|
||||
// The platform project picks the right redirect URI and browser
|
||||
// implementation; we don't reference any UI toolkit from here.
|
||||
|
|
@ -174,4 +197,15 @@ public partial class LoginPageViewModel : ViewModelBase
|
|||
StatusMessage = $"Error: {ex.Message}{suffix}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// XDG-compliant path to the user settings file. Surfaced in the
|
||||
/// "Configuration manquante" message so the operator knows exactly
|
||||
/// which file to edit without having to dig through docs.
|
||||
/// </summary>
|
||||
private static string SettingsFileHint()
|
||||
{
|
||||
var appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
|
||||
return System.IO.Path.Combine(appData, "PostIt", "postit-settings.json");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue