AddTraxApiKeyAuth

> NO WARRANTY. Trax auth is plumbing, not a security product. You are solely responsible for securing systems that use it. See API Security.

Registers the Trax API-key authentication scheme, its authorization policy (ApiKeyDefaults.PolicyName), the combined TraxAuthPolicy, IHttpContextAccessor, and a one-shot startup disclaimer log.

Signatures

public static AuthenticationBuilder AddTraxApiKeyAuth(
    this IServiceCollection services,
    Action<ApiKeyBuilder> configure,
    Action<ApiKeyAuthenticationOptions>? configureOptions = null);
 
public static AuthenticationBuilder AddTraxApiKeyAuth<TResolver>(
    this IServiceCollection services,
    Action<ApiKeyAuthenticationOptions>? configureOptions = null)
    where TResolver : class, ITraxPrincipalResolver<string>;

Two paths, picked by shape of the credential source:

OverloadUse whenResolver lifetime
AddTraxApiKeyAuth(Action<ApiKeyBuilder>)Keys are a static set known at startup (config, secret manager, constants).Singleton HashedApiKeyResolver built from the configured entries.
AddTraxApiKeyAuth<TResolver>()Keys come from a runtime source that needs scoped DI dependencies (DbContext, distributed cache, HTTP client).Scoped, resolved from DI per request.

Static key set (builder overload)

Keys registered through the builder are salted and SHA-256 hashed at startup and compared with CryptographicOperations.FixedTimeEquals on every request. Cleartext comparison is not reachable from consumer code.

services.AddTraxApiKeyAuth(keys => keys
    .Add("admin-key",  id: "admin",  "Admin", "Player")
    .Add("player-key", id: "player", "Player"));

When the principal needs a display name distinct from its id, or custom claims, use the factory overload of Add:

services.AddTraxApiKeyAuth(keys => keys
    .Add("alice-key", () => new TraxPrincipal("alice", "Alice Liddell", ["User"])));

For production hosts that load salt and hash bytes from a secret manager (cleartext never enters the process), use AddHashed:

var admin = builder.Configuration.GetSection("ApiKeys:Admin");
 
services.AddTraxApiKeyAuth(keys => keys.AddHashed(
    salt:   Convert.FromBase64String(admin["Salt"]!),
    sha256: Convert.FromBase64String(admin["Hash"]!),
    id:     "admin",
    "Admin"));

DI-scoped resolver (generic overload)

For keys backed by a database, cache, or HTTP service, implement ITraxPrincipalResolver<string>:

public sealed class MyApiKeyResolver(ApplicationDbContext db) : ITraxPrincipalResolver<string>
{
    public async ValueTask<TraxPrincipal?> ResolveAsync(string apiKey, CancellationToken ct)
    {
        var user = await db.ApiKeys.FirstOrDefaultAsync(k => k.Hash == Hash(apiKey), ct);
        return user is null
            ? null
            : new TraxPrincipal(user.Id, user.DisplayName, user.Roles);
    }
}
 
services.AddTraxApiKeyAuth<MyApiKeyResolver>();

The resolver is resolved per request, so scoped dependencies work as expected. Hash comparisons are the consumer's responsibility on this path; use CryptographicOperations.FixedTimeEquals against a stored hash, never a cleartext ==.

ApiKeyBuilder methods

MethodPurpose
Add(string key, string id, params string[] roles)Cleartext key; principal id doubles as display name. Covers the common case.
Add(string key, Func<TraxPrincipal> principalFactory)Cleartext key with full principal control (distinct display name, custom claims).
AddHashed(byte[] salt, byte[] sha256, string id, params string[] roles)Pre-hashed key; cleartext never enters the process.
AddHashed(byte[] salt, byte[] sha256, Func<TraxPrincipal> principalFactory)Pre-hashed key with full principal control.

Build() is internal. The extension method calls it and throws InvalidOperationException if no keys were registered.

Return Semantics

Header stateResult
AbsentAuthenticateResult.NoResult() (permits [AllowAnonymous])
Present more than onceAuthenticateResult.Fail("Multiple API keys presented."). The resolver is not invoked.
Present, resolver returns nullAuthenticateResult.Fail(...)
Present, resolver throwsAuthenticateResult.Fail(...) (logged at Warning, not Error)
Present, resolver returns TraxPrincipalAuthenticateResult.Success(ticket)

Protecting Endpoints

AddTraxApiKeyAuth does not set a default authentication scheme. ASP.NET Core will not invoke the API-key handler for a plain [Authorize] attribute or .RequireAuthorization() with no argument. Name the scheme or policy explicitly:

// Single-scheme case: use the policy registered by AddTraxApiKeyAuth
app.UseTraxGraphQL(configure: endpoint => endpoint
    .RequireAuthorization(ApiKeyDefaults.PolicyName));
 
// Mixed-scheme case (API key OR JWT OR...): use the combined Trax policy
app.UseTraxGraphQL(configure: endpoint => endpoint
    .RequireAuthorization(TraxAuthClaimTypes.TraxAuthPolicy));

TraxAuthClaimTypes.TraxAuthPolicy is the name every Trax auth package registers into. Each AddTrax*Auth call adds its scheme to that policy's allowed-schemes list, so a route protected by TraxAuthPolicy accepts credentials from any configured Trax scheme.

If you want API-key auth to act as the default for the whole app, pass the scheme name to AddAuthentication directly:

services.AddAuthentication(ApiKeyDefaults.SchemeName);
services.AddTraxApiKeyAuth(keys => keys.Add("...", id: "..."));

That is an application-level choice, not one Trax makes for you.