Tharga.Team.Service
3.8.3
See the version list below for details.
dotnet add package Tharga.Team.Service --version 3.8.3
NuGet\Install-Package Tharga.Team.Service -Version 3.8.3
<PackageReference Include="Tharga.Team.Service" Version="3.8.3" />
<PackageVersion Include="Tharga.Team.Service" Version="3.8.3" />
<PackageReference Include="Tharga.Team.Service" />
paket add Tharga.Team.Service --version 3.8.3
#r "nuget: Tharga.Team.Service, 3.8.3"
#:package Tharga.Team.Service@3.8.3
#addin nuget:?package=Tharga.Team.Service&version=3.8.3
#tool nuget:?package=Tharga.Team.Service&version=3.8.3
Tharga Team Service
Server-side API-key authentication, authorization enforcement, controller registration, OpenAPI/Swagger setup, and audit logging for ASP.NET Core projects. Targets .NET 9.0 and .NET 10.0.
Features
- API key authentication - Reads the
X-API-KEYheader, validates against a store, and populatesTeamKey,AccessLevel, and scope claims. - Access level authorization -
AccessLevelProxy<T>enforces[RequireAccessLevel]on service methods viaDispatchProxy. - Scope authorization -
ScopeProxy<T>enforces[RequireScope]with audit logging. - Works in API and interactive Blazor - the proxies resolve the caller via
ITeamPrincipalAccessor(default:IHttpContextAccessor).AddThargaTeamBlazorswaps in a circuit-aware accessor (HttpContext when present, elseAuthenticationStateProvider), so one[RequireScope]/[RequireAccessLevel]enforces both surfaces. Register a customITeamPrincipalAccessorto plug in another principal source. - Controller + Swagger registration - Single-call setup for MVC controllers, OpenAPI document with API key security scheme, and Swagger UI.
- API key management - Default MongoDB-backed
ApiKeyAdministrationServicewith key hashing. Configurable viaApiKeyOptions— see API key options. - Audit logging -
CompositeAuditLoggerwithILoggerand MongoDB backends. ⚠️ Stores toILoggeronly by default — see Audit logging. - API-key lifecycle hook - Capture the private token on create/recycle (plus a delete signal) via
IApiKeyLifecycleHandler— see Capturing the private token. - Pluggable - Implement
IApiKeyAdministrationService(from Tharga.Team) to bring your own storage backend.
Quick start
using Tharga.Team;
using Tharga.Team.Service;
// Program.cs
builder.Services.AddThargaControllers();
builder.Services.AddAuthentication()
.AddThargaApiKeyAuthentication();
builder.Services.AddThargaApiKeys();
var app = builder.Build();
app.UseThargaControllers();
app.UseAuthentication();
app.UseAuthorization();
app.Run();
Reading the audit log over REST
AddThargaControllers registers one controller of its own — GET /api/audit — so audit data is reachable
from a script or an agent, not only from the Blazor view.
GET /api/audit?teamKey=ABC123&from=2026-01-01&take=100
X-API-KEY: <key>
Filters: teamKey, from, to, feature, action, success, skip, take (capped at 500).
Omitting teamKey reads across all teams and requires a system audit:read grant.
Authorization is the same AuditAccess.CanRead rule the Blazor AuditLogView uses, so the two surfaces
cannot drift. A team grant reaches only its own team; audit:read is registered at
AccessLevel.Administrator, so Viewer- and User-level callers are refused even for their own team.
Denials are 403 rather than 404, so they do not reveal whether a team exists.
Which credentials reach the API
ThargaControllerOptions.AuthenticationSchemes lists the schemes Tharga's controllers accept, defaulting
to the API-key scheme. Add your own to also admit a signed-in user:
using Microsoft.AspNetCore.Authentication.Cookies;
builder.Services.AddThargaControllers(o =>
o.AuthenticationSchemes.Add(CookieAuthenticationDefaults.AuthenticationScheme));
A policy naming no scheme falls back to the application's default scheme — OIDC in a Blazor host — so an unauthenticated API call gets a 302 to a login page rather than a 401, and an agent following it receives HTML with a 200. Naming schemes explicitly is what avoids that.
Customizing the OpenAPI document
AddThargaControllers owns the OpenAPI document (it registers the API-key security scheme on it). To add your own IOpenApiDocumentTransformer / IOpenApiOperationTransformer — for example, to filter the generated spec down to the operations the current caller is authorized for — use the ConfigureOpenApi hook instead of calling AddOpenApi("v1", …) yourself:
builder.Services.AddThargaControllers(o =>
o.ConfigureOpenApi(api => api.AddDocumentTransformer<ScopeFilteringDocumentTransformer>()));
The callback receives the same OpenApiOptions Tharga configures, so your transformers run against the document Tharga already manages. Multiple ConfigureOpenApi calls compose (each runs, in registration order). This avoids a second AddOpenApi("v1", …) registration — which would leave it ambiguous whether your document composes with or overrides Tharga's, and, in .NET 10, forces the OpenAPI XML-comment source generator to emit an interceptor into your project (requiring <InterceptorsNamespaces>$(InterceptorsNamespaces);Microsoft.AspNetCore.OpenApi.Generated</InterceptorsNamespaces> just to compile).
.NET 10+ only. On .NET 9 the document is built by Swashbuckle and this hook is not present; use Swashbuckle's
IDocumentFilter/IOperationFilterthere.
System API keys
For infrastructure-level credentials that aren't tied to a team (MCP gatekeepers, CI/CD callers, cross-team admin tooling), use system keys — API keys with no TeamKey.
Create and manage them via the <SystemApiKeyView /> component in Tharga.Team.Blazor (gated by the Developer role), or programmatically via IApiKeyAdministrationService.CreateSystemKeyAsync(name, scopes, expiryDate, createdBy).
System keys authenticate through the same X-API-KEY header. The principal they produce carries the IsSystemKey=true claim and the explicit scopes granted at creation time — no TeamKey claim.
Protect system-only endpoints with the system policy:
app.UseThargaMcp().RequireAuthorization(ApiKeyConstants.SystemPolicyName);
The two policies are mutually exclusive: ApiKeyPolicy rejects system keys, SystemApiKeyPolicy rejects team keys.
What a key can reach
The boundary between the two kinds of key is a security guarantee, not a convention, and it is worth knowing precisely before handing either one out:
| Team key | System key | |
|---|---|---|
| Claims issued | TeamKey + scopes as team grants |
IsSystemKey + scopes as system grants |
| Its own team | ✅ subject to access level | n/a — a system key has no team |
| Another team | ❌ always, even when naming that team explicitly | consent-dependent |
| System-wide | ❌ always | ✅ for the scopes granted at creation |
Two properties follow, and both are covered by tests:
- The team a key acts for comes from the key record, never the request. Knowing a team's key is not authority over it — naming another team is futile rather than merely refused.
- A team grant never satisfies a system check. The scope claim carries its provenance, so a team key
holding
audit:readcannot use it to read across teams. This is why the two claim types exist.
Access level still applies within the team. audit:read is registered at AccessLevel.Administrator, so a
Viewer-level key holding a team's credential is refused its own team's audit log — holding a team's key is
not the same as holding every grant inside it.
Team API keys
Protect endpoints with the built-in policy:
[Authorize(Policy = ApiKeyConstants.PolicyName)]
[ApiController]
[Route("api/[controller]")]
public class MyController : ControllerBase
{
[HttpGet]
public IActionResult Get()
{
var teamKey = User.FindFirst(TeamClaimTypes.TeamKey)?.Value;
return Ok(new { teamKey });
}
}
Enforce access levels on services:
public interface IMyService
{
[RequireAccessLevel(AccessLevel.Viewer)]
IAsyncEnumerable<Item> GetAsync();
[RequireAccessLevel(AccessLevel.User)]
Task<Item> AddAsync(string name);
}
// Program.cs
builder.Services.AddScopedWithAccessLevel<IMyService, MyService>();
API key options
API key behaviour is configured via ApiKeyOptions (passed to AddThargaApiKeyAuthentication, or o.ApiKey under AddThargaTeam):
| Option | Default | Purpose |
|---|---|---|
AdvancedMode |
false |
When false, keys are auto-created per team and only refresh/lock are exposed. When true, full CRUD (name, access level, roles, scope overrides, expiry). |
AutoKeyCount |
2 |
Number of keys auto-created per team in simple mode. |
AutoLockKeys |
false |
Lock keys immediately after creation so the raw value is shown only once. |
MaxExpiryDays |
365 |
Caps expiry for team and system keys. null = no cap. |
LastUsedThrottle |
1 min |
Minimum interval between LastUsedAt writes for a key (avoids a DB write per request). TimeSpan.Zero = stamp every request. |
MinKeyLength / MaxKeyLength |
24 / 32 |
Random alphanumeric length of the key secret (base62, ≈5.95 bits/char). The length is chosen at random in [Min, Max] per key. ~190-bit at the default 32; 43 ≈ 256-bit. Floor 24 (≈143-bit). |
Audit logging
AddThargaAuditLogging records mutations (team-service operations and API-key management) and authorization events via CompositeAuditLogger.
builder.Services.AddThargaAuditLogging(o =>
{
o.StorageMode = AuditStorageMode.MongoDB; // see gotcha below
o.RetentionDays = 90; // null (or <= 0) = keep forever
});
⚠️ Gotcha:
StorageModedefaults toLoggeronly, so the MongoDB-backedAuditLogViewstays empty until you setAuditStorageMode.MongoDB(orLogger | MongoDB).AuditStorageModeis a[Flags]enum.
| Option | Default | Notes |
|---|---|---|
StorageMode |
Logger |
[Flags]: Logger, MongoDB, or both. Set MongoDB to populate AuditLogView. |
CallerFilter / EventFilter |
Api\|Web / All |
[Flags] — which caller sources / event types to record. |
ExcludedActions / ExcludedEndpoints |
empty | Skip noisy actions (e.g. "read") or endpoints. |
RetentionDays |
90 |
int? → MongoDB TTL index (Timestamp_TTL). null or <= 0 = keep forever (no TTL index). Changing/removing the TTL on an existing collection may need a manual index drop. |
BatchSize / FlushIntervalSeconds |
100 / 5 |
Background MongoDB writer tuning. |
Auditing background work
Code with no HTTP request behind it — a hosted service, a message handler, a scheduled job — has no
principal to attribute. It used to be recorded as CallerType.User with a null identity, i.e. a row
claiming a person did it. It now records Unknown unless you declare an actor:
using var _ = auditContext.Push(new AuditActor("nightly-retention", CorrelationId: runId));
auditLogger.Log(auditEntryFactory.Create("retention", "sweep", teamKey: teamKey));
// CallerType.System, CallerSource.Background, that identity and correlation id
Build the entry with IAuditEntryFactory: IAuditLogger.Log takes a pre-built entry and does not
consult the ambient actor, so one you construct by hand will not carry it. Tharga.Team.Sample has a
working example in SampleBackgroundJob.
IAuditContextAccessor is registered by AddThargaAuditLogging() regardless of storage mode. The scope
is AsyncLocal, so it survives await and nested calls, and restores the outer actor on dispose. An
authenticated caller always wins over an ambient actor, so a scope left open on a pooled thread cannot
relabel a real user's action; an anonymous request does not win.
CallerFilterand background entries. A source that is neitherApinorWebis matched againstApi | Web, so background entries are recorded under the default filter — and, less obviously, under a filter narrowed to just one of them. There is noBackgroundflag to include or exclude them independently. If you need that distinction, say so and it can be added; the current behaviour errs toward recording.
Operation metadata
Audited operations record what changed on AuditEntry.Metadata — create captures the team name,
rename the old and new name, a role change the old and new access level, consent the old and new level and
roles, and so on (keys are defined on AuditMetadataKeys). Capturing a "before" value is best-effort and
never fails the operation. Metadata is shown as an expandable row in AuditLogView, in CSV export (a
JSON-encoded Metadata column), JSON export, and the Logger output.
Adding your own metadata
Register an IAuditEnricher to attach host-defined metadata to every entry the toolkit writes:
public sealed class RequestIdAuditEnricher(IHttpContextAccessor http) : IAuditEnricher
{
public void Enrich(AuditEntry entry, IDictionary<string, string> metadata)
{
if (http.HttpContext?.TraceIdentifier is { } id) metadata["request.id"] = id;
}
}
builder.Services.AddThargaAuditEnricher<RequestIdAuditEnricher>();
Enrichers run in registration order for every entry that passes the filters. The merge is add-only —
an enricher cannot overwrite a key the toolkit (or an earlier enricher) set — is resolved as a
singleton (read request state via IHttpContextAccessor), and one that throws is logged and skipped so
enrichment can never fail the audited operation.
See the implementation guide for the full reference.
Capturing the private token
The private token is shown once and never persisted, logged, or exposed over an API. To capture it (e.g. to re-deliver a minted key), register an IApiKeyLifecycleHandler — it receives the token on create and recycle/regenerate, plus a tokenless delete signal:
public class MyHandler(ISecretProtector protector, IMyStore store) : IApiKeyLifecycleHandler
{
public Task OnApiKeyLifecycleAsync(ApiKeyLifecycleContext ctx) => ctx.Reason switch
{
ApiKeyLifecycleReason.Deleted => store.RemoveAsync(ctx.ApiKeyId),
_ => store.SaveAsync(ctx.ApiKeyId, protector.Protect(ctx.PrivateToken), ctx.TeamKey, ctx.Tags),
};
}
builder.AddThargaTeam(o => o.AddApiKeyLifecycleHandler<MyHandler>());
A throwing handler propagates out of the originating operation (capture failures are not swallowed). You own whatever you capture — encrypt it at rest.
Dependencies
- Tharga.Team - Domain models, authorization primitives, and service abstractions.
- Tharga.MongoDB - MongoDB repository infrastructure.
- Tharga.Toolkit - Shared utilities including API key hashing.
- Swashbuckle.AspNetCore - Swagger UI generation.
Related packages
| Package | Description |
|---|---|
| Tharga.Team | Domain models and authorization primitives (plain .NET, WASM-safe) |
| Tharga.Team.Blazor | Team-specific Blazor UI components |
| Tharga.Blazor | Generic Blazor UI components |
| Tharga.Team.MongoDB | MongoDB persistence for teams and users |
Links
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Microsoft.AspNetCore.OpenApi (>= 10.0.10)
- Swashbuckle.AspNetCore (>= 10.2.3)
- Tharga.MongoDB (>= 2.14.2)
- Tharga.Team (>= 3.8.3)
NuGet packages (4)
Showing the top 4 NuGet packages that depend on Tharga.Team.Service:
| Package | Downloads |
|---|---|
|
Tharga.Team.Blazor
Team management Blazor components for multi-tenant applications. Works with both Blazor Server and WebAssembly. |
|
|
Tharga.Platform.Mcp
Platform bridge for Tharga.Mcp. Provides Platform-backed IMcpContext, scope enforcement, audit logging, and authentication for MCP tool and resource invocations. |
|
|
Tharga.Team.Mcp
Team bridge for Tharga.Mcp. Provides Team-backed IMcpContext, scope enforcement, audit logging, and authentication for MCP tool and resource invocations. |
|
|
Tharga.Team.Support
Support and notifications for Tharga Team — route audited events to Slack channels, with the message shaped per event. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 3.10.3 | 0 | 8/2/2026 |
| 3.10.2 | 25 | 8/2/2026 |
| 3.10.1 | 36 | 8/2/2026 |
| 3.10.0 | 45 | 8/1/2026 |
| 3.9.0 | 39 | 8/1/2026 |
| 3.8.3 | 49 | 8/1/2026 |
| 3.8.2 | 40 | 8/1/2026 |
| 3.8.1 | 50 | 7/31/2026 |
| 3.8.0 | 59 | 7/31/2026 |
| 3.7.0 | 118 | 7/30/2026 |
| 3.6.1 | 105 | 7/27/2026 |
| 3.6.0 | 111 | 7/27/2026 |
| 3.5.4 | 124 | 7/27/2026 |
| 3.5.3 | 120 | 7/27/2026 |
| 3.5.2 | 123 | 7/26/2026 |
| 3.5.1 | 134 | 7/24/2026 |
| 3.5.0 | 128 | 7/24/2026 |
| 3.2.1 | 158 | 7/21/2026 |
| 3.2.0 | 134 | 7/20/2026 |
| 3.1.7 | 144 | 7/7/2026 |