Skip to content

Authentication

Before a command can check a role, something must establish who sent the request. In the lightweight Cratis.Arc.Core host, IAuthenticationHandler implementations produce a ClaimsPrincipal. ASP.NET Core applications instead configure their host’s authentication; see Microsoft Identity integration.

Arc discovers IAuthenticationHandler implementations through IInstancesOf<IAuthenticationHandler> and tries them sequentially:

  1. AuthenticationResult.Anonymous means this handler does not apply; try the next one.
  2. AuthenticationResult.Succeeded(principal) stops the sequence and supplies the request principal.
  3. AuthenticationResult.Failed(reason) stops the sequence without authenticating.

Do not depend on a particular discovery order or assume individual DI registrations define that order. If mechanisms overlap, make their applicability unambiguous. An earlier success means later handlers cannot veto it.

Request

Applicable authentication handler

Validated principal

Arc authorization and validation

Command or query logic

This illustrative integration fragment defines the Arc adapter and an application-owned validator contract, not a runnable JWT implementation. Register a real ITokenValidator implementation before using it. The validator must verify signature, trusted issuer, audience, lifetime, and applicable revocation requirements using your identity library. Decoding a token is not validation.

using System.Security.Claims;
using Cratis.Arc.Authentication;
using Cratis.Arc.Http;
public interface ITokenValidator
{
Task<ClaimsPrincipal?> Validate(string token);
}
public class BearerTokenAuthenticationHandler(ITokenValidator validator) : IAuthenticationHandler
{
public async Task<AuthenticationResult> HandleAuthentication(IHttpRequestContext context)
{
if (!context.Headers.TryGetValue("Authorization", out var header) ||
!header.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
{
return AuthenticationResult.Anonymous;
}
var token = header["Bearer ".Length..].Trim();
if (token.Length == 0)
{
return AuthenticationResult.Failed("Invalid credentials");
}
try
{
var principal = await validator.Validate(token);
return principal?.Identity?.IsAuthenticated == true
? AuthenticationResult.Succeeded(principal)
: AuthenticationResult.Failed("Invalid credentials");
}
catch (Exception)
{
return AuthenticationResult.Failed("Authentication unavailable");
}
}
}

Missing bearer credentials let another mechanism try. Empty, invalid, or unverifiable bearer credentials never become a successful principal. Log operational failures server-side without recording tokens or disclosing validation internals to clients. API keys and passwords likewise need a real credential store and validator; never compare against sample hardcoded secrets or put credentials into claims.

Core already supplies Cratis.Arc.Identity.MicrosoftIdentityPlatformAuthenticationHandler, discovered with the other authentication handlers. Do not implement a second EasyAuth parser. It reads these forwarded headers:

HeaderPurpose
x-ms-client-principal-idUser ID
x-ms-client-principal-nameDisplay name
x-ms-client-principalBase64 JSON principal containing roles and claims

The handler requires all three headers, rejects an invalid principal representation, replaces forwarded subject/identifier claims, and takes the reserved MicrosoftIdentityPlatformClaims.IdentityProvider claim from the payload’s identityProvider field. This establishes single provenance within the payload, not authenticity of the sender.

In the ASP.NET Core package, the corresponding registration is builder.Services.AddMicrosoftIdentityPlatformIdentityAuthentication(). It is not the Core registration API. See Microsoft Identity Platform for that host’s setup and local-development principals.

The lightweight authentication middleware installs the successful principal on IHttpRequestContext.User. With handlers present, an endpoint not explicitly allowing anonymous access returns HTTP 401 if authentication does not succeed. An endpoint with AllowAnonymous = true proceeds even when credentials fail. If no handlers are available, the middleware currently proceeds without authenticating; metadata alone is not a fail-closed protection in that configuration.

Every handler still runs on an AllowAnonymous endpoint, even when it ultimately lets the request through, because establishing context.User there is often still wanted - Arc’s own identity, introspection, and observable query demultiplexer endpoints are all anonymous yet still depend on the principal being set for a signed-in caller. A handler that only ever rejects - never establishes an identity worth keeping - should not do that rejection work, including any logging, for a request the endpoint never required a credential for.

The middleware sets the matched endpoint’s metadata on IHttpRequestContext before it calls any handler, so a handler can check it directly:

public Task<AuthenticationResult> HandleAuthentication(IHttpRequestContext context)
{
if (!context.Headers.TryGetValue("X-API-Key", out var apiKey) || apiKey != expectedKey)
{
if (context.AllowsAnonymous())
{
// No credential was required here - stay quiet and let another mechanism (or the
// endpoint's own AllowAnonymous) decide. Do not log this as a rejection.
return Task.FromResult(AuthenticationResult.Anonymous);
}
logger.LogWarning("Rejected a request to {Path} without a valid API key", context.Path);
return Task.FromResult(AuthenticationResult.Failed("Missing or invalid API key"));
}
return Task.FromResult(AuthenticationResult.Succeeded(principal));
}

context.AllowsAnonymous() (from HttpRequestContextEndpointExtensions) is true only when the matched endpoint declared AllowAnonymous = true; a handler that never checks it keeps behaving exactly as before.

Arc command/query authorization is a separate pipeline check. Read the current principal through ICurrentPrincipalAccessor from Cratis.Arc.Authorization, not the client-readable identity cookie. See Authorization for roles, result status, and direct-call boundaries.

Before exposing the service, exercise missing credentials, malformed input, arbitrary tokens, expired/wrong-issuer/wrong-audience tokens, a genuinely valid token, forged forwarded headers, and backend access bypassing ingress. Also test endpoints with and without anonymous metadata and every accepted authentication mechanism. A valid-token-only test cannot establish a fail-closed boundary.

  • Authorization — apply authentication and role requirements.
  • Identity — supply frontend identity details without treating cookies as credentials.
  • Endpoint mapping — understand manual endpoint responsibilities.