When every unauthorized request becomes the same dashboard line, diagnosis turns into guessing. ASP.NET Core 10 authentication metrics give me a better split: did the handler have nothing to authenticate, reject supplied credentials, or accept them? That distinction matters because a client deployment that drops credentials needs a different response from a surge of malformed or expired credentials.
ASP.NET Core 10 added built-in authentication and authorization instruments to System.Diagnostics.Metrics. I can collect them without rewriting each handler, and I can lock their behavior into an offline test before wiring up a production exporter.
Why one 401 hides two different problems
A protected endpoint normally challenges an unauthenticated caller. The final status is 401 whether the caller sent nothing or the handler rejected what it received.
The authentication duration histogram exposes the missing context through aspnetcore.authentication.result:
| Result | What the handler reported | A common interpretation |
|---|---|---|
none |
No authentication result | No applicable credentials were available |
failure |
Authentication failed | Supplied credentials were rejected or processing failed |
success |
A principal was created | Authentication completed successfully |
_OTHER |
Another framework result | Preserve it as an explicit catch-all |
none is a handler result, not a universal synonym for “missing Authorization header.” A policy scheme or custom handler can make a different choice. I verify the behavior of the schemes I actually deploy instead of building an alert from the label alone.
Likewise, success means the handler produced an authentication ticket. Authorization can still deny that principal, so it does not promise a 2xx response.
The separate aspnetcore.authentication.challenges counter answers another question: how often was a scheme challenged? Both a none result and a failure result can be followed by a challenge, so challenge count cannot replace the result split. A challenge is an authentication operation, not an HTTP-status counter; a cookie handler can redirect instead of returning 401.
ASP.NET Core 10 authentication metrics to collect
The ASP.NET Core 10 release notes list the new authentication and authorization coverage. The detailed built-in security metrics reference defines the instruments and attributes.
For this incident pattern, I start with the Microsoft.AspNetCore.Authentication meter:
-
aspnetcore.authentication.authenticate.durationis a histogram in seconds with conditional result and scheme attributes. -
aspnetcore.authentication.challengescounts challenges by scheme. -
aspnetcore.authentication.forbidscounts authenticated callers denied access. -
aspnetcore.authentication.sign_insandsign_outscover explicit sign-in and sign-out operations.
The framework may also attach error.type when authentication fails or an operation ends with an error. That is intentionally an exception type, not an exception message. I keep it that way: token text, user IDs, email addresses, and arbitrary error messages do not belong in metric attributes.
With OpenTelemetry, the key configuration is to include the meter in the metrics pipeline:
builder.Services.AddOpenTelemetry()
.WithMetrics(metrics =>
metrics.AddMeter("Microsoft.AspNetCore.Authentication"));
An exporter and backend are still required for production storage. The built-in instrumentation only creates measurements.
Lock the result split into a test
I prefer a small regression harness over discovering the metric shape after a dashboard ships. The complete sample uses TestServer, a fake authentication handler, and MeterListener; it opens no network socket and needs no identity provider.
It sends three requests:
await ExpectStatusAsync(client, "https://dev.to/private", null, HttpStatusCode.Unauthorized);
await ExpectStatusAsync(client, "https://dev.to/private", rejectedCredential, HttpStatusCode.Unauthorized);
await ExpectStatusAsync(client, "https://dev.to/private", acceptedCredential, HttpStatusCode.OK);
In this deliberately single-scheme app, the fake handler maps the absent header to none and the rejected value to failure. Authorization separately challenges both unauthenticated requests, so both return 401. The accepted credential produces success and returns 200. The verifier checks the exact result set, the fixed DemoToken scheme, two challenges, error.type=System.InvalidOperationException for the failed result, the documented attribute-name allowlist, and that neither credential nor the exception message appears in an attribute value.
I do not assert duration values. A histogram measurement depends on the machine and runtime conditions; the contract I care about here is the instrument name and bounded attributes. Microsoft’s metrics testing guidance follows the same principle of collecting measurements around an action and asserting the useful result.
Limits and alerting choices
These metrics are operational aggregates, not an authentication audit trail. They do not tell me which user failed, why a particular token was rejected, or whether an event was malicious. For security investigation I still need carefully controlled identity-provider events, logs, and traces.
I also avoid treating histogram samples as request counts. One HTTP request can involve more than one scheme, forwarding, or an explicit authentication call. I aggregate by the small configured scheme set and result, then correlate with HTTP traffic rather than assuming a one-to-one relationship.
A practical first dashboard separates these signals:
- rising
nonewith rising challenges can point to callers that stopped sending applicable credentials; - rising
failuremeans the handler rejected or failed to process credentials, so I inspect handler logs and provider health next; - rising forbids means authentication succeeded but authorization denied access.
That split is narrow, stable, and much safer than adding user-specific dimensions. Which distinction would shorten your next 401 investigation: none versus failure, scheme, or challenge versus forbid?
Cheers!