diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Security/IInboundApiKeyAdmin.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Security/IInboundApiKeyAdmin.cs
index 64b26b1d..ac1db4c1 100644
--- a/src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Security/IInboundApiKeyAdmin.cs
+++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Security/IInboundApiKeyAdmin.cs
@@ -34,6 +34,13 @@ public sealed record InboundApiKeyCreated(string KeyId, string Token);
/// their method-scopes. The interface lives in Commons and is deliberately free of any
/// dependency on the underlying auth library, so consumers depend only on this contract.
///
+///
+/// Mutating operations (, ,
+/// , ) may throw on
+/// store-level or configuration failures (e.g. an unavailable pepper) rather than
+/// exclusively signalling failure via their bool return — callers must handle
+/// exceptions in addition to checking the return value.
+///
public interface IInboundApiKeyAdmin
{
/// Creates a new key scoped to and returns its
@@ -58,9 +65,11 @@ public interface IInboundApiKeyAdmin
Task DeleteAsync(string keyId, CancellationToken ct = default);
/// Returns the method-scope set for a key, or an empty list if not found.
+ /// Enumerates the full key list (O(n)); intended for admin-scale use, not hot paths.
Task> GetMethodsForKeyAsync(string keyId, CancellationToken ct = default);
/// Returns the identifiers of all keys whose scopes contain
/// .
+ /// Enumerates the full key list (O(n)); intended for admin-scale use, not hot paths.
Task> GetKeysForMethodAsync(string methodName, CancellationToken ct = default);
}
diff --git a/src/ZB.MOM.WW.ScadaBridge.Security/LibraryInboundApiKeyAdmin.cs b/src/ZB.MOM.WW.ScadaBridge.Security/LibraryInboundApiKeyAdmin.cs
index bfb02cb5..778ef99c 100644
--- a/src/ZB.MOM.WW.ScadaBridge.Security/LibraryInboundApiKeyAdmin.cs
+++ b/src/ZB.MOM.WW.ScadaBridge.Security/LibraryInboundApiKeyAdmin.cs
@@ -34,8 +34,11 @@ public sealed class LibraryInboundApiKeyAdmin : IInboundApiKeyAdmin
public async Task CreateAsync(
string name, IReadOnlyCollection methods, CancellationToken ct = default)
{
+ ArgumentException.ThrowIfNullOrWhiteSpace(name);
ArgumentNullException.ThrowIfNull(methods);
+ // "N" format = 32 hex chars, no hyphens/underscores — the library rejects underscores
+ // in keyId because they delimit the sbk__ token.
var keyId = Guid.NewGuid().ToString("N");
var result = await _admin.CreateKeyAsync(
keyId, name, methods.ToHashSet(StringComparer.Ordinal),
diff --git a/tests/ZB.MOM.WW.ScadaBridge.Security.Tests/LibraryInboundApiKeyAdminTests.cs b/tests/ZB.MOM.WW.ScadaBridge.Security.Tests/LibraryInboundApiKeyAdminTests.cs
index 31c8de26..2f277dba 100644
--- a/tests/ZB.MOM.WW.ScadaBridge.Security.Tests/LibraryInboundApiKeyAdminTests.cs
+++ b/tests/ZB.MOM.WW.ScadaBridge.Security.Tests/LibraryInboundApiKeyAdminTests.cs
@@ -54,6 +54,18 @@ public sealed class LibraryInboundApiKeyAdminTests : IAsyncLifetime
_sut = new LibraryInboundApiKeyAdmin(commands);
}
+ [Theory]
+ [InlineData(null)]
+ [InlineData("")]
+ [InlineData(" ")]
+ public async Task CreateAsync_NullOrWhitespaceName_Throws(string? name)
+ {
+ // ThrowIfNullOrWhiteSpace throws ArgumentNullException for null and ArgumentException
+ // for empty/whitespace; both are ArgumentException subtypes, so ThrowsAnyAsync covers all.
+ await Assert.ThrowsAnyAsync(
+ () => _sut.CreateAsync(name!, new[] { "MethodA" }, CancellationToken.None));
+ }
+
[Fact]
public async Task CreateAsync_ReturnsKeyIdAndToken_TokenStartsWith_sbk()
{
diff --git a/tests/ZB.MOM.WW.ScadaBridge.Security.Tests/SecurityTests.cs b/tests/ZB.MOM.WW.ScadaBridge.Security.Tests/SecurityTests.cs
index 3eabd496..2fe3dcb3 100644
--- a/tests/ZB.MOM.WW.ScadaBridge.Security.Tests/SecurityTests.cs
+++ b/tests/ZB.MOM.WW.ScadaBridge.Security.Tests/SecurityTests.cs
@@ -443,6 +443,9 @@ public class SecurityReviewRegressionTests
services.AddLogging();
services.AddDataProtection();
services.AddSecurity();
+ // Explicitly set RequireHttpsCookie=true so the test asserts SecurePolicy.Always
+ // without relying on the SecurityOptions default value.
+ services.Configure(o => o.RequireHttpsCookie = true);
using var provider = services.BuildServiceProvider();
var cookieOptions = provider