diff --git a/src/ZB.MOM.WW.ScadaBridge.InboundAPI/EndpointExtensions.cs b/src/ZB.MOM.WW.ScadaBridge.InboundAPI/EndpointExtensions.cs index d1049cb0..5c70ed53 100644 --- a/src/ZB.MOM.WW.ScadaBridge.InboundAPI/EndpointExtensions.cs +++ b/src/ZB.MOM.WW.ScadaBridge.InboundAPI/EndpointExtensions.cs @@ -82,7 +82,7 @@ public static class EndpointExtensions // Auth re-arch (A+B) + X-API-Key restore: the inbound credential is accepted // from EITHER the Authorization header ("Bearer sbk__") OR the - // legacy "X-API-Key: sbk__" header (raw token). Both are passed + // alternate "X-API-Key: sbk__" header (raw token). Both are passed // to the SAME shared ZB.MOM.WW.Auth.ApiKeys verifier — the parser strips an // optional "Bearer " prefix and otherwise accepts a bare token, so the // peppered-HMAC constant-time secret compare is identical for both transports. diff --git a/tests/ZB.MOM.WW.ScadaBridge.InboundAPI.Tests/EndpointExtensionsTests.cs b/tests/ZB.MOM.WW.ScadaBridge.InboundAPI.Tests/EndpointExtensionsTests.cs index 5fd18d9a..4a9f792c 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.InboundAPI.Tests/EndpointExtensionsTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.InboundAPI.Tests/EndpointExtensionsTests.cs @@ -116,7 +116,7 @@ public sealed class EndpointExtensionsTests : IDisposable [Fact] public async Task HappyPath_ValidXApiKeyInScope_Returns200WithScriptResultJson() { - // X-API-Key restore: the SAME seeded key, presented in the legacy + // X-API-Key restore: the SAME seeded key, presented in the alternate // "X-API-Key: sbk__" header (raw token, no "Bearer " prefix), // must authenticate through the SAME shared verifier and reach the script — // proving the broadened credential extraction feeds X-API-Key to VerifyAsync. @@ -213,22 +213,28 @@ public sealed class EndpointExtensionsTests : IDisposable } [Fact] - public async Task MissingBearer_Returns401() + public async Task WhitespaceAuthorization_ValidXApiKey_Returns200() { - var method = SeedMethod(1, "noKey", "return 1;"); + // IsNullOrWhiteSpace fall-through: a PRESENT but whitespace-only Authorization + // header is treated as absent, so a valid X-API-Key in the same request must + // still authenticate (200). This pins the IsNullOrWhiteSpace check: without + // it, a non-null-but-blank Authorization value would be passed straight to + // the verifier (which would fail), and the X-API-Key would never be consulted. + var method = SeedMethod(1, "echo", "return Parameters[\"value\"];", + """[{"name":"value","type":"Integer","required":true}]"""); using var host = await BuildHostAsync(method); - await SeedKeyAsync(host, "key1", "noKey-caller", new[] { "noKey" }); + var token = await SeedKeyAsync(host, keyId: "key1", displayName: "echo-caller", + scopes: new[] { "echo" }); var client = host.GetTestClient(); - // No Authorization header — auth should reject with 401. - var request = new HttpRequestMessage(HttpMethod.Post, "/api/noKey") - { - Content = new StringContent("{}", Encoding.UTF8, "application/json"), - }; + var request = BuildPostWithApiKeyHeader("echo", """{"value":7}""", token); + request.Headers.TryAddWithoutValidation("Authorization", " "); var response = await client.SendAsync(request); + var body = await response.Content.ReadAsStringAsync(); - Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Contains("7", body); } [Fact] @@ -553,7 +559,7 @@ public sealed class EndpointExtensionsTests : IDisposable } /// - /// X-API-Key restore: builds a POST carrying the credential in the legacy + /// X-API-Key restore: builds a POST carrying the credential in the alternate /// X-API-Key: sbk_<keyId>_<secret> header (the RAW token, no /// "Bearer " prefix) instead of Authorization. The endpoint must extract this /// header value and feed it to the SAME shared verifier.