From 82a856b61ed3bc620ec7f17eba1fa3439c0b1d5b Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Thu, 22 Oct 2026 06:47:36 -0400 Subject: [PATCH] fix(metrics-auth): DisableLogin no longer opens /metrics; log correlation pair; host-level tests - The metrics scheme skips its dashboard-cookie fallback while Dashboard:DisableLogin is on, so the dev auto-login handler cannot satisfy RequireAuthentication=true. - Invoke logs client_correlation_id <-> worker envelope id once at Debug. - MetricsEndpointAuthenticationTests drives the real pipeline: anonymous loopback 401 (no /login redirect), Viewer cookie 200, admin key 200, non-admin key 403, DisableLogin 401, /health/active 200. - Docs: DisableLogin behaviour; the /metrics limiter partition is per IP vs gRPC per connection, so only the per-key aggregate is shared. --- docs/DesignDecisions.md | 11 +- docs/GatewayConfiguration.md | 16 +- docs/Grpc.md | 2 +- .../Grpc/MxAccessGatewayService.cs | 13 ++ .../Authorization/MetricsAuthorization.cs | 14 +- .../Gateway/GatewayApplicationTests.cs | 64 ------ .../Grpc/MxAccessGatewayServiceTests.cs | 44 +++++ .../MetricsEndpointAuthenticationTests.cs | 182 ++++++++++++++++++ .../MetricsAuthorizationTests.cs | 4 +- 9 files changed, 276 insertions(+), 74 deletions(-) create mode 100644 src/ZB.MOM.WW.MxGateway.Tests/Gateway/MetricsEndpointAuthenticationTests.cs diff --git a/docs/DesignDecisions.md b/docs/DesignDecisions.md index 88d555e..52e70df 100644 --- a/docs/DesignDecisions.md +++ b/docs/DesignDecisions.md @@ -456,10 +456,13 @@ stays anonymous unconditionally. values. - **No environment bypass.** The requirement reads no options, so anonymous localhost and `Authentication:Mode=Disabled` — both of which satisfy the - dashboard Viewer policy — do not satisfy it. Only dev-only - `Dashboard:DisableLogin` (which authenticates every request as an - Administrator, and is rejected in Production) lets an unauthenticated scrape - through. + dashboard Viewer policy — do not satisfy it. Nor does dev-only + `Dashboard:DisableLogin`: its auto-login handler sits under the cookie scheme + name, so the metrics scheme skips the cookie fallback while it is on rather than + letting a dev flag silently open an endpoint the operator asked to protect. +- **Throttle sharing is partial.** The `/metrics` per-peer limiter partition is + the remote IP, while the gRPC one is the gRPC peer string (per connection), so + only the per-key-id aggregate is shared between the two paths. - **One scheme, plain 401.** Listing the cookie scheme on the policy directly would challenge an anonymous scrape with a 302 redirect to `/login`; a single scheme that falls back to the cookie internally keeps the challenge a `401` diff --git a/docs/GatewayConfiguration.md b/docs/GatewayConfiguration.md index 8323351..06cdd47 100644 --- a/docs/GatewayConfiguration.md +++ b/docs/GatewayConfiguration.md @@ -429,7 +429,7 @@ another repo without credentials, so gating it would break that consumer. | Option | Default | Description | |--------|---------|-------------| -| `MxGateway:Metrics:RequireAuthentication` | `false` | When `true`, `/metrics` requires an authenticated caller (SEC-14). A scrape is accepted when it presents an API key carrying the `admin` scope as `Authorization: Bearer mxgw__`, or — with no authorization header — a dashboard login cookie holding a real dashboard role (`Administrator` or `Viewer`). Anonymous localhost access and `Authentication:Mode=Disabled` do **not** satisfy it. An anonymous scrape gets `401` with `WWW-Authenticate: Bearer`; an authenticated caller without `admin` scope or a dashboard role gets `403`; a peer throttled by the API-key failure limiter (`MxGateway:Security:ApiKeyFailure*`, shared with the gRPC path) gets `429`. The default keeps the endpoint anonymous so existing scrapers keep working. | +| `MxGateway:Metrics:RequireAuthentication` | `false` | When `true`, `/metrics` requires an authenticated caller (SEC-14). A scrape is accepted when it presents an API key carrying the `admin` scope as `Authorization: Bearer mxgw__`, or — with no authorization header — a dashboard login cookie holding a real dashboard role (`Administrator` or `Viewer`). Anonymous localhost access, `Authentication:Mode=Disabled`, and `Dashboard:DisableLogin` do **not** satisfy it. An anonymous scrape gets `401` with `WWW-Authenticate: Bearer`; an authenticated caller without `admin` scope or a dashboard role gets `403`; a peer throttled by the API-key failure limiter (`MxGateway:Security:ApiKeyFailure*`, shared with the gRPC path) gets `429`. The default keeps the endpoint anonymous so existing scrapers keep working. | Configure a Prometheus scraper against an authenticated endpoint with a dedicated admin-scoped key, for example: @@ -445,8 +445,18 @@ scrape_configs: ``` The setting is read from configuration when endpoints are mapped, so changing it -requires a restart. With dev-only `Dashboard:DisableLogin=true` every request is -auto-authenticated as an Administrator, which also satisfies `/metrics`. +requires a restart. Dev-only `Dashboard:DisableLogin=true` registers an +always-succeeding auto-login handler under the dashboard cookie scheme; the +metrics scheme deliberately skips its cookie fallback while that flag is on, so +`DisableLogin` never silently opens a `/metrics` endpoint that +`RequireAuthentication` protects — only an admin-scoped API key is accepted then. + +The `/metrics` path reuses the gRPC API-key failure limiter +(`MxGateway:Security:ApiKeyFailure*`), but its per-peer partition is keyed on the +remote **IP address**, whereas the gRPC interceptor's is the gRPC peer string +(address and port — effectively per connection). The two paths therefore share +only the per-key-id aggregate layer (`ApiKeyFailureAggregateLimit`); per-peer +failure counts on one path do not throttle the other. ## Galaxy Options diff --git a/docs/Grpc.md b/docs/Grpc.md index dd1c7d9..c43508e 100644 --- a/docs/Grpc.md +++ b/docs/Grpc.md @@ -82,7 +82,7 @@ return mapper.MapCommandReply(workerReply); Carrying the enqueue timestamp into the worker layer is what lets queue-wait time be measured separately from worker-side execution time when troubleshooting timeouts. -Every `MxCommandReply` echoes the request's `client_correlation_id` in `correlation_id` — success, MXAccess failure, and constraint-denied replies alike, and an empty string when the caller sent none. The handler stamps it after mapping the worker reply, overwriting the internal gateway↔worker envelope id the worker reply carries: that id is a pipe-routing detail with no meaning to a client, and clients that sent no correlation id previously received it while clients that sent one received it only on denied replies. +Every `MxCommandReply` echoes the request's `client_correlation_id` in `correlation_id` — success, MXAccess failure, and constraint-denied replies alike, and an empty string when the caller sent none. The handler stamps it after mapping the worker reply, overwriting the internal gateway↔worker envelope id the worker reply carries: that id is a pipe-routing detail with no meaning to a client, and clients that sent no correlation id previously received it while clients that sent one received it only on denied replies. To keep the two ids joinable, `Invoke` logs the pair (command kind, session id, client correlation id, worker envelope correlation id — no values) once per call at `Debug`, so a client error report quoting its correlation id can be matched to gateway and worker log lines. An accepted gRPC command payload can still be too large for the worker pipe: the envelope built around it must fit `MxGateway:Worker:MaxMessageBytes`, which is validated at startup to sit at least a fixed envelope-overhead reserve above `MaxGrpcMessageBytes` (see the headroom rule in [Gateway Configuration](./GatewayConfiguration.md)) so this should not occur for a conformant payload, but if it does, `WorkerClient` raises `WorkerClientException(CommandTooLarge)` and `Invoke` reports `ResourceExhausted` for that command — the session is not faulted, so a client can retry with a smaller payload without reopening the session. diff --git a/src/ZB.MOM.WW.MxGateway.Server/Grpc/MxAccessGatewayService.cs b/src/ZB.MOM.WW.MxGateway.Server/Grpc/MxAccessGatewayService.cs index 201d3e0..826014c 100644 --- a/src/ZB.MOM.WW.MxGateway.Server/Grpc/MxAccessGatewayService.cs +++ b/src/ZB.MOM.WW.MxGateway.Server/Grpc/MxAccessGatewayService.cs @@ -148,6 +148,19 @@ public sealed class MxAccessGatewayService( // the same value the constraint-denied replies carry — so a client can match every // reply to its request. The worker reply carries the gateway's internal envelope id, // which is a gateway↔worker routing detail and is overwritten here, empty included. + // Record the pairing once, before the envelope id is overwritten, so a client error + // report quoting its correlation id can be matched to gateway and worker log lines + // (which carry the envelope id). Ids and the command kind only — never values. + if (logger.IsEnabled(LogLevel.Debug)) + { + logger.LogDebug( + "Invoke {CommandKind} for session {SessionId}: client correlation id {ClientCorrelationId} maps to worker envelope correlation id {WorkerCorrelationId}.", + command.Kind, + request.SessionId, + request.ClientCorrelationId, + publicReply.CorrelationId); + } + publicReply.CorrelationId = request.ClientCorrelationId; session.TrackCommandReply(commandToInvoke, publicReply); diff --git a/src/ZB.MOM.WW.MxGateway.Server/Security/Authorization/MetricsAuthorization.cs b/src/ZB.MOM.WW.MxGateway.Server/Security/Authorization/MetricsAuthorization.cs index 4779f16..d9ed334 100644 --- a/src/ZB.MOM.WW.MxGateway.Server/Security/Authorization/MetricsAuthorization.cs +++ b/src/ZB.MOM.WW.MxGateway.Server/Security/Authorization/MetricsAuthorization.cs @@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization; using Microsoft.Extensions.Options; using ZB.MOM.WW.Auth.Abstractions.ApiKeys; +using ZB.MOM.WW.MxGateway.Server.Configuration; using ZB.MOM.WW.MxGateway.Server.Dashboard; using ZB.MOM.WW.MxGateway.Server.Metrics; @@ -34,7 +35,9 @@ public static class MetricsAuthorizationDefaults /// cookie scheme on the policy directly would turn an anonymous scrape into a 302 redirect to the /// login page. A throttled key partition answers 429, mirroring the gRPC /// ResourceExhausted. Failure reasons are never echoed to the caller and the presented -/// secret is never logged. +/// secret is never logged. The limiter's per-peer partition here is the remote IP, whereas the gRPC +/// interceptor's is the gRPC peer string (address and port, i.e. per connection), so the two paths +/// share only the per-key-id aggregate layer. /// public sealed class MetricsAuthenticationHandler( IOptionsMonitor options, @@ -43,6 +46,7 @@ public sealed class MetricsAuthenticationHandler( IApiKeyVerifier apiKeyVerifier, ApiKeyFailureLimiter failureLimiter, IAuthenticationSchemeProvider schemeProvider, + IOptions gatewayOptions, GatewayMetrics metrics) : AuthenticationHandler(options, logger, encoder) { @@ -111,6 +115,14 @@ public sealed class MetricsAuthenticationHandler( // signed in to the dashboard can open /metrics in the same browser. private async Task AuthenticateDashboardCookieAsync() { + // Dashboard:DisableLogin registers an always-succeeding auto-login handler under the cookie + // scheme name. Falling back to it would let every anonymous request satisfy /metrics, so + // with DisableLogin on, RequireAuthentication=true accepts API keys only. + if (gatewayOptions.Value.Dashboard.DisableLogin) + { + return AuthenticateResult.NoResult(); + } + if (await schemeProvider.GetSchemeAsync(DashboardAuthenticationDefaults.AuthenticationScheme).ConfigureAwait(false) is null) { diff --git a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/GatewayApplicationTests.cs b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/GatewayApplicationTests.cs index a7676dc..1ebfbad 100644 --- a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/GatewayApplicationTests.cs +++ b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/GatewayApplicationTests.cs @@ -98,70 +98,6 @@ public sealed class GatewayApplicationTests } } - /// - /// With MxGateway:Metrics:RequireAuthentication=true an anonymous scrape is refused with a - /// plain 401 Bearer challenge (not a login redirect), while /health/active — polled by the - /// off-repo Overview dashboard without credentials — stays anonymous. - /// - /// A task that represents the asynchronous operation. - [Fact] - public async Task Build_WhenMetricsRequireAuthentication_RefusesAnonymousScrapeButNotHealth() - { - await using WebApplication app = GatewayApplication.Build( - ["--urls=http://127.0.0.1:0", "--MxGateway:Metrics:RequireAuthentication=true"]); - await app.StartAsync(); - try - { - using HttpClient client = new() { BaseAddress = new Uri(app.Urls.First()) }; - - using HttpResponseMessage metrics = await client.GetAsync("/metrics"); - using HttpRequestMessage badKeyRequest = new(HttpMethod.Get, "/metrics"); - badKeyRequest.Headers.TryAddWithoutValidation("Authorization", "Bearer mxgw_nobody_not-a-real-secret"); - using HttpResponseMessage badKey = await client.SendAsync(badKeyRequest); - using HttpResponseMessage health = await client.GetAsync("/health/active"); - - Assert.Equal(HttpStatusCode.Unauthorized, metrics.StatusCode); - Assert.Contains(metrics.Headers.WwwAuthenticate, value => value.Scheme == "Bearer"); - Assert.Equal(HttpStatusCode.Unauthorized, badKey.StatusCode); - Assert.NotEqual(HttpStatusCode.Unauthorized, health.StatusCode); - Assert.NotEqual(HttpStatusCode.Forbidden, health.StatusCode); - } - finally - { - await app.StopAsync(); - } - } - - /// - /// The dashboard-login path: with dev-only Dashboard:DisableLogin every request carries - /// the auto-login Administrator principal under the dashboard cookie scheme, which the metrics - /// scheme accepts when no authorization header is sent. - /// - /// A task that represents the asynchronous operation. - [Fact] - public async Task Build_WhenMetricsRequireAuthentication_AcceptsDashboardLogin() - { - await using WebApplication app = GatewayApplication.Build( - [ - "--urls=http://127.0.0.1:0", - "--MxGateway:Metrics:RequireAuthentication=true", - "--MxGateway:Dashboard:DisableLogin=true", - ]); - await app.StartAsync(); - try - { - using HttpClient client = new() { BaseAddress = new Uri(app.Urls.First()) }; - - using HttpResponseMessage response = await client.GetAsync("/metrics"); - - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - } - finally - { - await app.StopAsync(); - } - } - /// Verifies that Build maps dashboard and authentication endpoints when the dashboard is enabled. /// A task that represents the asynchronous operation. [Fact] diff --git a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Grpc/MxAccessGatewayServiceTests.cs b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Grpc/MxAccessGatewayServiceTests.cs index ce4cb9d..8ee15aa 100644 --- a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Grpc/MxAccessGatewayServiceTests.cs +++ b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Grpc/MxAccessGatewayServiceTests.cs @@ -505,6 +505,50 @@ public sealed class MxAccessGatewayServiceTests Assert.Equal(string.Empty, reply.CorrelationId); } + /// + /// Invoke logs the client correlation id alongside the worker envelope id once, at Debug, so + /// a client error report can be matched to gateway and worker logs. + /// + /// A task that represents the asynchronous operation. + [Fact] + public async Task Invoke_LogsClientToWorkerCorrelationPairAtDebug() + { + FakeSessionManager sessionManager = new() + { + InvokeReply = new WorkerCommandReply + { + Reply = new MxCommandReply + { + SessionId = "session-1", + CorrelationId = "envelope-7f3a", + Kind = MxCommandKind.AddItem, + ProtocolStatus = MxAccessGrpcMapper.Ok(), + AddItem = new AddItemReply { ItemHandle = 42 }, + }, + }, + }; + RecordingLogger logger = new(); + MxAccessGatewayService service = CreateService(sessionManager, logger: logger); + MxCommandRequest request = new() + { + SessionId = "session-1", + ClientCorrelationId = "client-trace-9", + Command = new MxCommand + { + Kind = MxCommandKind.AddItem, + AddItem = new AddItemCommand { ServerHandle = 12, ItemDefinition = "Galaxy.Tag.Value" }, + }, + }; + + await service.Invoke(request, new TestServerCallContext()); + + (LogLevel level, string message, _) = Assert.Single( + logger.Entries, + entry => entry.Message.Contains("envelope-7f3a", StringComparison.Ordinal)); + Assert.Equal(LogLevel.Debug, level); + Assert.Contains("client-trace-9", message, StringComparison.Ordinal); + } + /// /// StreamEvents hands the call's cancellation token to every response write, so a write /// blocked on a slow or vanished client is released when the call is cancelled. diff --git a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/MetricsEndpointAuthenticationTests.cs b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/MetricsEndpointAuthenticationTests.cs new file mode 100644 index 0000000..662c576 --- /dev/null +++ b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/MetricsEndpointAuthenticationTests.cs @@ -0,0 +1,182 @@ +using System.Net; +using System.Net.Http; +using System.Security.Claims; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using ZB.MOM.WW.Auth.ApiKeys.Admin; +using ZB.MOM.WW.MxGateway.Server; +using ZB.MOM.WW.MxGateway.Server.Dashboard; +using ZB.MOM.WW.MxGateway.Server.Security.Authorization; + +namespace ZB.MOM.WW.MxGateway.Tests.Gateway; + +/// +/// Host-level coverage of MxGateway:Metrics:RequireAuthentication=true (SEC-14): each case +/// builds the real gateway with and drives the full +/// middleware pipeline (authentication, authorization, the dashboard cookie scheme, the API-key +/// store) over HTTP on an ephemeral loopback port. Requests therefore arrive from loopback, which +/// is exactly the anonymous-localhost case the dashboard Viewer policy would admit and /metrics +/// must not. +/// +public sealed class MetricsEndpointAuthenticationTests +{ + // A fixed test pepper: the configuration-backed pepper provider reads MxGateway:ApiKeyPepper, + // and the auth store itself is isolated per test process by TestHostEnvironmentInitializer. + private const string TestPepper = "metrics-endpoint-tests-pepper-0123456789abcdef"; + + /// + /// An anonymous loopback scrape gets a plain 401 Bearer challenge — not a 302 to /login, and not + /// the anonymous-localhost admission the dashboard Viewer policy grants. + /// + /// A task that represents the asynchronous operation. + [Fact] + public async Task AnonymousLoopbackScrape_Returns401BearerNotLoginRedirect() + { + await using WebApplication app = await StartAsync(); + using HttpClient client = CreateClient(app); + + using HttpResponseMessage response = await client.GetAsync("/metrics"); + + Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); + Assert.Null(response.Headers.Location); + Assert.Contains(response.Headers.WwwAuthenticate, value => value.Scheme == "Bearer"); + } + + /// /health/active stays anonymous: the off-repo Overview dashboard polls it without credentials. + /// A task that represents the asynchronous operation. + [Fact] + public async Task HealthActive_StaysAnonymous() + { + await using WebApplication app = await StartAsync(); + using HttpClient client = CreateClient(app); + + using HttpResponseMessage response = await client.GetAsync("/health/active"); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + + /// A dashboard login cookie holding the Viewer role is accepted. + /// A task that represents the asynchronous operation. + [Fact] + public async Task DashboardViewerCookie_Returns200() + { + await using WebApplication app = await StartAsync(); + using HttpClient client = CreateClient(app); + using HttpRequestMessage request = new(HttpMethod.Get, "/metrics"); + request.Headers.Add("Cookie", MintDashboardCookie(app, DashboardRoles.Viewer)); + + using HttpResponseMessage response = await client.SendAsync(request); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + + /// A real API key with the admin scope is accepted. + /// A task that represents the asynchronous operation. + [Fact] + public async Task AdminScopedApiKey_Returns200() + { + await using WebApplication app = await StartAsync(); + string token = await CreateKeyAsync(app, [GatewayScopes.Admin]); + using HttpClient client = CreateClient(app); + + using HttpResponseMessage response = await SendWithBearerAsync(client, token); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + + /// A real API key without the admin scope authenticates but is forbidden. + /// A task that represents the asynchronous operation. + [Fact] + public async Task NonAdminApiKey_Returns403() + { + await using WebApplication app = await StartAsync(); + string token = await CreateKeyAsync(app, [GatewayScopes.EventsRead, GatewayScopes.InvokeRead]); + using HttpClient client = CreateClient(app); + + using HttpResponseMessage response = await SendWithBearerAsync(client, token); + + Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode); + } + + /// + /// Dev-only Dashboard:DisableLogin registers an always-succeeding auto-login handler under + /// the cookie scheme name; /metrics must not fall back to it, or the flag would silently open + /// an endpoint the operator explicitly asked to protect. + /// + /// A task that represents the asynchronous operation. + [Fact] + public async Task DisableLogin_DoesNotOpenMetrics() + { + await using WebApplication app = await StartAsync("--MxGateway:Dashboard:DisableLogin=true"); + using HttpClient client = CreateClient(app); + + using HttpResponseMessage response = await client.GetAsync("/metrics"); + + Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); + } + + private static async Task StartAsync(params string[] extraArgs) + { + // Ephemeral port (:0): xUnit runs collections in parallel, so a fixed port would collide. + WebApplication app = GatewayApplication.Build( + [ + "--urls=http://127.0.0.1:0", + "--MxGateway:Metrics:RequireAuthentication=true", + $"--MxGateway:ApiKeyPepper={TestPepper}", + .. extraArgs, + ]); + await app.StartAsync(); + return app; + } + + private static HttpClient CreateClient(WebApplication app) => + new(new HttpClientHandler { AllowAutoRedirect = false, UseCookies = false }) + { + BaseAddress = new Uri(app.Urls.First()), + }; + + private static async Task SendWithBearerAsync(HttpClient client, string token) + { + using HttpRequestMessage request = new(HttpMethod.Get, "/metrics"); + request.Headers.TryAddWithoutValidation("Authorization", $"Bearer {token}"); + return await client.SendAsync(request); + } + + // Creates a key through the same admin command set the CLI and dashboard use. Key ids are unique + // per call because the isolated auth store is shared by every test in the process. + private static async Task CreateKeyAsync(WebApplication app, string[] scopes) + { + ApiKeyAdminCommands commands = app.Services.GetRequiredService(); + await commands.InitDbAsync(remoteAddress: null, CancellationToken.None); + CreateKeyResult created = await commands.CreateKeyAsync( + $"metrics{Guid.NewGuid():N}", + "Metrics test key", + new HashSet(scopes, StringComparer.Ordinal), + null, + null, + remoteAddress: null, + CancellationToken.None); + return created.Token ?? throw new InvalidOperationException("Key creation returned no token."); + } + + // Protects a ticket with the running host's own cookie options (data-protection keys, ticket + // format, cookie name) — the cookie a real dashboard login would issue, minus the LDAP bind. + private static string MintDashboardCookie(WebApplication app, string role) + { + CookieAuthenticationOptions cookieOptions = app.Services + .GetRequiredService>() + .Get(DashboardAuthenticationDefaults.AuthenticationScheme); + ClaimsIdentity identity = new( + [new Claim(ClaimTypes.Name, "viewer-user"), new Claim(ClaimTypes.Role, role)], + DashboardAuthenticationDefaults.AuthenticationScheme, + ClaimTypes.Name, + ClaimTypes.Role); + AuthenticationTicket ticket = new( + new ClaimsPrincipal(identity), + DashboardAuthenticationDefaults.AuthenticationScheme); + return $"{cookieOptions.Cookie.Name}={cookieOptions.TicketDataFormat.Protect(ticket)}"; + } +} diff --git a/src/ZB.MOM.WW.MxGateway.Tests/Security/Authorization/MetricsAuthorizationTests.cs b/src/ZB.MOM.WW.MxGateway.Tests/Security/Authorization/MetricsAuthorizationTests.cs index 8d83767..6694744 100644 --- a/src/ZB.MOM.WW.MxGateway.Tests/Security/Authorization/MetricsAuthorizationTests.cs +++ b/src/ZB.MOM.WW.MxGateway.Tests/Security/Authorization/MetricsAuthorizationTests.cs @@ -6,6 +6,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using ZB.MOM.WW.Auth.Abstractions.ApiKeys; +using ZB.MOM.WW.MxGateway.Server.Configuration; using ZB.MOM.WW.MxGateway.Server.Dashboard; using ZB.MOM.WW.MxGateway.Server.Metrics; using ZB.MOM.WW.MxGateway.Server.Security.Authorization; @@ -190,7 +191,8 @@ public sealed class MetricsAuthorizationTests UrlEncoder.Default, verifier, limiter ?? new ApiKeyFailureLimiter(1000, TimeSpan.FromMinutes(1), 1024, 0, TimeSpan.Zero, TimeProvider.System), - new AuthenticationSchemeProvider(Options.Create(new AuthenticationOptions())), + new AuthenticationSchemeProvider(Options.Create(new Microsoft.AspNetCore.Authentication.AuthenticationOptions())), + Options.Create(new GatewayOptions()), new GatewayMetrics()); await handler.InitializeAsync( new AuthenticationScheme(