TraxCaller
NO WARRANTY. Trax auth is plumbing, not a security product. You are solely responsible for securing systems that use it. See API Security.
The current caller, safe to inject anywhere. Where an injected TraxPrincipal
throws TraxPrincipalNotAvailableException for an anonymous caller, TraxCaller reports what it
finds and never throws, so it fits code that runs for anonymous and authenticated callers alike:
row-level query filters, subscription resolvers, junctions shared between gated and ungated trains.
Signature
namespace Trax.Api.Auth;
public sealed class TraxCaller
{
public TraxCaller(IHttpContextAccessor httpContextAccessor, ITrustedExecutionScope trustedScope);
public bool IsAuthenticated { get; } // Principal is not null
public bool IsTrusted { get; } // inside ITrustedExecutionScope.BeginTrusted(...)
public TraxPrincipal? Principal { get; }
}| Member | Value |
|---|---|
Principal | The request's TraxPrincipal, read from HttpContext.User, or null when there is no HttpContext, no authenticated user, or a user whose claims carry no Trax principal id. Its Id is qualified by the scheme, TraxApiKey:alice. |
IsAuthenticated | Principal is not null |
IsTrusted | True inside a trusted execution scope: the scheduler running queued work, a remote runner's run endpoint. Never set by anything on the API's own HTTP surface. |
Principal is read on every access, not when TraxCaller is built. A principal that authenticates
later in the request (Trax's query-model interceptor authenticates multi-scheme hosts just before
a GraphQL request executes) is visible to code that reads it afterwards, which is what lets an EF
query filter read it when the query runs.
On a subscription, the socket's principal is set on the connection's HttpContext.User at
connection_init, so TraxCaller injected into a subscribe resolver sees the subscriber.
Registration
TraxCaller is a scoped service registered by AddTraxPrincipalAccessor(), which every Trax auth
scheme calls (AddTraxApiKeyAuth, AddTraxJwtAuth, AddTraxOidcAuth). A host that registers its
scheme conditionally, such as demo keys only in Development, has no scheme in other environments,
and anything injecting TraxCaller then fails to resolve. Register the accessor directly; it is
idempotent:
using Trax.Api.Auth;
if (builder.Environment.IsDevelopment())
builder.Services.AddTraxApiKeyAuth(keys => keys.Add("member-key-do-not-use-in-production", id: "member", "Member"));
// TraxCaller resolves in every environment, and reports an anonymous caller where no scheme exists.
builder.Services.AddTraxPrincipalAccessor();Usage
A row-level filter's caller, bound over TraxCaller so the data layer does not depend on Trax auth:
public sealed class TraxLendingCaller(TraxCaller caller) : ILendingCaller
{
public string? PrincipalId => caller.Principal?.Id;
public bool IsLibrarian => caller.Principal?.Roles.Contains("Librarian") == true;
}A subscription resolver that admits only a room's participants:
public async ValueTask<ISourceStream<ChatEvent>> SubscribeToChatEventAsync(
Guid chatRoomId, TraxCaller caller, ChatDbContext db,
ITopicEventReceiver receiver, CancellationToken ct)
{
var userId = caller.Principal?.Id;
if (userId is null || !await db.Participants.AnyAsync(p => p.RoomId == chatRoomId && p.UserId == userId, ct))
throw new GraphQLException(ErrorBuilder.New().SetMessage("Not authorized.").SetCode("TRAX_AUTHORIZATION").Build());
return await receiver.SubscribeAsync<ChatEvent>($"ChatRoom:{chatRoomId}", ct);
}Inject TraxPrincipal instead when the caller must be authenticated and a missing one is a
configuration mistake you want to fail loudly, such as a junction of a [TraxAuthorize] train.
IsTrusted is true for whatever a scheduler runner's posture admits, so a filter that lets trusted
execution see every row treats that caller as the scheduler. Give a runner a posture that admits only
the scheduler; see The Scheduler and Remote Workers Are Trusted.
Package
dotnet add package Trax.Api.Auth