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; }
}
MemberValue
PrincipalThe 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.
IsAuthenticatedPrincipal is not null
IsTrustedTrue 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