Compare commits

Author SHA1 Message Date
claude-botandClaude Fable 5.1 f1464cf005 fix(auth): trim configured and presented API keys
ci/woodpecker/push/test Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
The workstation key in OpenBao ends with a newline, so the 64-byte key a
client presents never matched the 65-byte configured value. Whitespace
around a key carries no entropy and is a store artefact, not identity.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-24 17:06:49 +02:00
2 changed files with 8 additions and 1 deletions
@@ -32,11 +32,13 @@ public sealed class ApiKeyAuthenticationHandler(
}
if (presented is null)
return Task.FromResult(AuthenticateResult.NoResult());
presented = presented.Trim();
var presentedBytes = Encoding.UTF8.GetBytes(presented);
foreach (var child in configuration.GetSection("Auth:ApiKeys").GetChildren())
{
var configured = child.Value;
// Secret stores hand keys over with a trailing newline more often than not.
var configured = child.Value?.Trim();
if (string.IsNullOrEmpty(configured)) continue;
if (CryptographicOperations.FixedTimeEquals(presentedBytes, Encoding.UTF8.GetBytes(configured)))
{
+5
View File
@@ -26,6 +26,7 @@ public sealed class WikiHostFactory : WebApplicationFactory<Program>
builder.UseEnvironment("Testing");
builder.UseSetting("Wiki:Webhook:Secret", WebhookSecret);
builder.UseSetting("Auth:ApiKeys:tester", ApiKey);
builder.UseSetting("Auth:ApiKeys:newline", "newline-key\n");
builder.UseSetting("Auth:ReverseProxy:TrustedProxies:0", "127.0.0.1");
builder.UseSetting("Wiki:Index:File:Directory", Path.Combine(Path.GetTempPath(), "wiki-host-tests", Guid.NewGuid().ToString("N")));
builder.ConfigureServices(services =>
@@ -207,6 +208,10 @@ public class EndpointTests(WikiHostFactory factory) : IClassFixture<WikiHostFact
var anonymous = await Client.PostAsJsonAsync("/api/v1/facts", new { title = "x", kind = "fact" });
Assert.Equal(HttpStatusCode.Unauthorized, anonymous.StatusCode);
var trimmed = new HttpRequestMessage(HttpMethod.Post, "/api/v1/facts") { Content = JsonContent.Create(new { title = "x", kind = "hypothesis" }) };
trimmed.Headers.Add("X-Api-Key", "newline-key");
Assert.Equal(HttpStatusCode.UnprocessableEntity, (await Client.SendAsync(trimmed)).StatusCode); // authenticated, then rejected on kind
var hypothesis = new HttpRequestMessage(HttpMethod.Post, "/api/v1/facts") { Content = JsonContent.Create(new { title = "t", source = "s", kind = "hypothesis", content = new string('x', 60) }) };
hypothesis.Headers.Add("X-Api-Key", WikiHostFactory.ApiKey);
Assert.Equal(HttpStatusCode.UnprocessableEntity, (await Client.SendAsync(hypothesis)).StatusCode);