Tharga Platform — Implementation Guide
Step-by-step instructions for adding Tharga Platform features to a Blazor application.
Recommended: Single-call setup
For most applications, use AddThargaPlatform to register everything in one call:
using Tharga.Team.Blazor.Framework;
builder.AddThargaPlatform(o =>
{
o.Blazor.Title = "My App";
o.Blazor.RegisterTeamService<MyTeamService, MyUserService>();
// Optional: scopes, roles, audit
o.ConfigureScopes = scopes => { /* ... */ };
o.ConfigureTenantRoles = roles => { /* ... */ };
o.Audit = new AuditOptions();
});
// MongoDB persistence (always separate — requires your entity types)
builder.Services.AddMongoDB(o => { /* connection config */ });
builder.Services.AddThargaTeamRepository(o =>
{
o.UseUserEntity<MyUserEntity>();
o.UseTeamEntity<MyTeamEntity, MyTeamMember>();
});
var app = builder.Build();
app.UseThargaPlatform();
This replaces Steps 1–8 below. Set sub-options to null to skip features you don't need (e.g. o.Controllers = null, o.ApiKey = null).
Advanced: Step-by-step setup
Use the individual Add* methods when you need partial or custom registration. Each step is a self-contained feature that builds on previous steps. Add only what you need.
Secrets: Several steps require sensitive configuration values (client IDs, connection strings, API keys). These should never be committed to source control. Use Manage User Secrets in Visual Studio (right-click the Server project > Manage User Secrets) or run
dotnet user-secrets initfollowed bydotnet user-secrets set "Section:Key" "value"from the Server project directory.
Dependency overview
Step 1: UI Foundation (Tharga.Blazor)
│
Step 2: Authentication (Tharga.Team.Blazor)
│
├── Step 3: API Controllers & Swagger (Tharga.Team.Service)
│
├── Step 4: Team Management (Tharga.Team.Blazor + Tharga.Team.MongoDB)
│ │
│ ├── Step 5: API Key Authentication (Tharga.Team.Service)
│ │
│ ├── Step 6: Scopes (Tharga.Team + Tharga.Team.Service)
│ │ │
│ │ └── Step 7: Tenant Roles (Tharga.Team)
│ │
│ └── Step 8: Audit Logging (Tharga.Team.Service)
Step 1: UI Foundation
Adds Radzen-based UI components: buttons, breadcrumbs, error boundary, loading indicators, and layout primitives.
Packages
dotnet add package Tharga.Blazor
Program.cs (Server)
using Tharga.Blazor.Framework;
builder.Services.AddRadzenComponents();
builder.Services.AddRadzenCookieThemeService(o =>
o.StorageKeyName = "ThemeStorageName");
builder.Services.AddThargaBlazor(o => o.Title = "My App");
AddThargaBlazor registers BreadCrumbService, BlazoredLocalStorage, and BlazorOptions. It also supports binding from appsettings.json:
{
"Tharga": {
"Blazor": {
"Title": "My App"
}
}
}
builder.Services.AddThargaBlazor(configuration: builder.Configuration);
Code configuration takes precedence over appsettings.json.
Program.cs (Client — if using WebAssembly)
builder.Services.AddRadzenComponents();
builder.Services.AddThargaBlazor();
_Imports.razor (both projects)
@using Radzen
@using Radzen.Blazor
@using Tharga.Blazor
@using Tharga.Blazor.Framework
@using Tharga.Blazor.Framework.Buttons
@using Tharga.Blazor.Features.BreadCrumbs
App.razor
Add to <head>:
<RadzenTheme Theme="material" />
Add to <body>:
<script src="@Assets["_content/Tharga.Blazor/tharga.blazor.js"]"></script>
<script src="_content/Radzen.Blazor/Radzen.Blazor.js"></script>
What becomes available
| Component | Description |
|---|---|
<ActionButton> |
Button with built-in busy state and error handling |
<CancelButton> |
Cancel button |
<CopyButton> |
Copy-to-clipboard button |
<StandardButton> |
General purpose button |
<BreadCrumbs> |
Breadcrumb navigation (registered by AddThargaBlazor) |
<Title> |
Page title (reads from BlazorOptions.Title) |
<CustomErrorBoundary> |
Error boundary with correlation ID |
<ExpandableCard> |
Collapsible card |
<Loading> |
Loading indicator — use instead of hardcoded "Loading..." text |
<DateTimeView> |
Formatted date/time display |
<TimeSpanView> |
Formatted time span display |
Layout
Replace the default Bootstrap layout with Radzen layout components: RadzenLayout, RadzenHeader, RadzenSidebar, RadzenBody, RadzenFooter, RadzenPanelMenu.
Verification
The app should render with Radzen styling. Buttons and layout components should work without errors.
Step 2: Authentication
Adds Azure AD (CIAM) authentication with Cookie + OIDC, login/logout endpoints, and auth UI components.
Requires: Step 1
Packages
dotnet add package Tharga.Team.Blazor
Microsoft.Identity.Webis included transitively — no need to add it separately.
Configuration
Add an AzureAd section to appsettings.json. Values are environment-specific:
{
"AzureAd": {
"Authority": "",
"ClientId": "",
"TenantId": "",
"CallbackPath": ""
}
}
- Authority — varies by identity provider (e.g. CIAM:
https://<tenant>.ciamlogin.com/<domain>, standard Entra ID:https://login.microsoftonline.com/<tenant-id>/v2.0) - ClientId — from the Azure app registration
- TenantId — from the Azure app registration
- CallbackPath — varies by setup (e.g.
/signin-oidc,/authentication/login-callback)
Secrets:
ClientIdandTenantIdmay be considered sensitive depending on your environment. Put them in Manage User Secrets if you prefer not to commit them.
Program.cs
using Tharga.Team.Blazor.Features.Authentication;
// Service registration
builder.AddThargaAuth();
// After builder.Build()
app.UseThargaAuth();
Options
builder.AddThargaAuth(o =>
{
o.LoginPath = "/sign-in"; // default: "/login"
o.LogoutPath = "/sign-out"; // default: "/logout"
o.ValidateConfiguration = false; // default: true — throws at startup if AzureAd section is missing
});
_Imports.razor
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.Authorization
@using Tharga.Team.Blazor.Features.Authentication
@using Tharga.Team.Blazor.Framework
Note:
Tharga.Team.Blazor.Frameworkprovides theRolesclass used inAuthorizeView(e.g.Roles.Developer,Roles.TeamMember).
What becomes available
| Component | Namespace | Description |
|---|---|---|
<LoginDisplay /> |
Tharga.Team.Blazor.Features.Authentication |
Profile menu with Gravatar when authenticated, login button when not. Navigates to /login, /logout, and profile/team pages. |
<UserProfileView /> |
Tharga.Team.Blazor.Features.User |
Displays user's Gravatar, profile info, and authentication claims in an expandable card. |
Usage
Add <LoginDisplay /> to NavMenu.razor header.
Create a profile page:
@page "/profile"
@using Tharga.Team.Blazor.Features.User
@attribute [Authorize]
<UserProfileView />
Version notes
UseThargaAuth()requires >= 2.0.1-pre.1 for correct async login behavior. Version 2.0.0 usedResults.Challenge(synchronous) which caused DNS errors with some Azure AD configurations.
Verification
The login button should appear. Clicking it redirects to Azure AD. After login, the profile menu shows with the user's Gravatar.
Step 3: API Controllers & Swagger
Adds MVC controller support with OpenAPI documentation and Swagger UI.
Requires: Step 2
Packages
dotnet add package Tharga.Team.Service
Program.cs
// Service registration
builder.Services.AddThargaControllers();
// After builder.Build()
app.UseThargaControllers();
Options
builder.Services.AddThargaControllers(o =>
{
o.SwaggerTitle = "My API v1"; // default: "API v1"
o.SwaggerRoutePrefix = "api-docs"; // default: "swagger"
});
What becomes available
- MVC controller routing
- OpenAPI endpoint with API key security scheme
- Swagger UI at
/<SwaggerRoutePrefix> - API key header convention (
X-API-KEY)
Usage
Create controllers as usual:
[ApiController]
[Route("api/[controller]")]
public class MyController : ControllerBase
{
[HttpGet]
public IActionResult Get() => Ok("Hello");
}
Verification
Navigate to /swagger — the Swagger UI should load with your controllers listed.
Step 4: Team Management
Adds multi-tenant team management with MongoDB persistence, team selection, member management, and claims augmentation.
Requires: Step 2, and a MongoDB database (via Tharga.MongoDB)
Packages
dotnet add package Tharga.Team.MongoDB
Tharga.Teamis included transitively viaTharga.Team.Blazor. You also needTharga.MongoDB.Blazorconfigured separately — see Tharga.MongoDB docs.
Configuration
Add a MongoDB connection string to appsettings.json:
{
"ConnectionStrings": {
"Default": ""
}
}
Secrets: The connection string contains credentials. Put it in Manage User Secrets.
Program.cs
// Service registration
builder.Services.AddThargaTeamBlazor(o =>
{
o.Title = "My App";
o.AutoCreateFirstTeam = true; // default: false — auto-creates a team for first-time users
o.ShowMemberRoles = false; // default: false — shows tenant role assignment in team UI
o.ShowScopeOverrides = false; // default: false — shows scope override controls in TeamComponent (team-member UI). For ApiKeyView, opt in via the [Parameter] ShowScopeOverrides on the component itself; the two flags are intentionally independent.
o.RegisterTeamService<MyTeamService, MyUserService>();
});
builder.Services.AddThargaTeamRepository(o =>
{
o.RegisterUserRepository<UserEntity>();
o.RegisterTeamRepository<TeamEntity, TeamMember>();
});
Custom collection names: If you need to change the MongoDB collection names (e.g. when sharing a database with a legacy app), set
TeamCollectionNameandUserCollectionName:builder.Services.AddThargaTeamRepository(o => { o.TeamCollectionName = "MyTeams"; // default: "Team" o.UserCollectionName = "MyUsers"; // default: "User" o.RegisterUserRepository<UserEntity>(); o.RegisterTeamRepository<TeamEntity, TeamMember>(); });
Note:
AddThargaTeamBlazor()internally callsAddThargaBlazor(), soBreadCrumbServiceandBlazoredLocalStorageare registered automatically.
Implementing the required types
You need to create entity and service types that extend the base classes:
Entities
public record UserEntity : EntityBase, IUser
{
public required string Key { get; init; }
public required string Identity { get; init; }
public required string EMail { get; init; }
public string? Name { get; init; } // populate from 'name' claim for display names
}
public record TeamEntity : TeamEntityBase<TeamMember>;
public record TeamMember : TeamMemberBase;
UserService
public class MyUserService : UserServiceRepositoryBase<UserEntity>
{
public MyUserService(AuthenticationStateProvider asp, IUserRepository<UserEntity> repo)
: base(asp, repo) { }
protected override Task<UserEntity> CreateUserEntityAsync(ClaimsPrincipal principal, string identity)
{
var email = principal.FindFirst(ClaimTypes.Email)?.Value
?? principal.FindFirst("preferred_username")?.Value
?? "unknown";
var name = principal.FindFirst("name")?.Value;
return Task.FromResult(new UserEntity
{
Key = Guid.NewGuid().ToString("N")[..10].ToUpperInvariant(),
Identity = identity,
EMail = email,
Name = name
});
}
}
Tip: Populate
IUser.Namefrom thenameclaim — it's used for default team names and member display names. If not set, the display name is derived from the email (e.g.john.doe@example.combecomesJohn Doe).
TeamService
public class MyTeamService : TeamServiceRepositoryBase<TeamEntity, TeamMember>
{
public MyTeamService(IUserService us, ITeamRepository<TeamEntity, TeamMember> repo, IMongoDbServiceFactory msf)
: base(us, repo, msf) { }
protected override Task<TeamEntity> CreateTeam(string teamKey, string name, IUser user, string displayName = null)
{
return Task.FromResult(new TeamEntity
{
Key = teamKey,
Name = name,
Members =
[
new TeamMember
{
Key = user.Key,
Name = displayName, // resolved from IUser.Name or email
AccessLevel = AccessLevel.Owner,
State = MembershipState.Member
}
]
});
}
protected override Task<TeamMember> CreateTeamMember(InviteUserModel model)
{
// Invitation and State are auto-generated by the base class if not set.
// You only need to set them here if you want custom behavior.
return Task.FromResult(new TeamMember
{
Key = null, // assigned when the user accepts the invite
Name = model.Name,
AccessLevel = model.AccessLevel
});
}
}
Auto-generated fields: When
CreateTeamMemberreturns a member withoutInvitation, the base class auto-generates it using the model's email, a new GUID invite key, and the current timestamp. Similarly,Statedefaults toMembershipState.Invitedif not set. You can still set these explicitly if you need custom behavior.
_Imports.razor
@using Tharga.Team.Blazor.Features.Team
What becomes available
| Component | Description |
|---|---|
<TeamSelector /> |
Dropdown to switch between teams |
<TeamComponent /> |
Full team management (create, rename, delete, members) |
<TeamInviteView /> |
Pending invitation view |
<UsersView /> |
Admin user list |
<ApiKeyView /> |
API key management (requires Step 5). Shows Created and Last used columns per key, and a Tags column (chips for keys in ChipTagKeys, plus an (i) tooltip of all tags). Opt-in [Parameter] flags: ShowAuditLogButton, ShowScopeOverrides (Scopes column + create-card multi-select + Edit-Scopes dialog per row), ChipTagKeys |
<AuditLogView /> |
Audit log viewer (requires Step 8) |
Roles.TeamMember |
Role claim added to authenticated team members |
Roles.Developer |
Role for developer-only UI sections |
The TeamClaimsAuthenticationStateProvider automatically augments the authentication state with team claims (TeamKey, AccessLevel, scopes) based on the selected team.
Note: Team management works without scopes or tenant roles. The
ShowMemberRolesandShowScopeOverridesoptions only take effect when the corresponding registries are registered (Step 6 and Step 7). Without them, the team UI shows access levels only — which is sufficient for many applications.
Claims Enrichment
Team, role, access level, and scope claims are automatically enriched on the ClaimsPrincipal when a team is selected. Platform provides two enrichment paths:
| Path | How it works | Hosting models |
|---|---|---|
| Server-side (default) | IClaimsTransformation reads the selected_team_id cookie during the HTTP pipeline |
Blazor Server, SSR, Hybrid |
| Client-side | AuthenticationStateProvider decorator reads from LocalStorage via JS interop |
Standalone WASM only |
The server-side path is always registered — no configuration needed. It adds:
team_id— selected team keyTeamKey— team key claimRole: TeamMember— membership roleRole: Team{AccessLevel}— access level role (e.g.TeamOwner,TeamAdministrator)AccessLevel— raw access level value- Scope claims — all effective scopes for the member's access level, roles, and overrides
SkipAuthStateDecoration (default: true)
This setting controls whether the client-side enrichment path is also registered:
true(default) — Only server-side enrichment. Works for Blazor Server, SSR, and Hybrid apps. No JS interop is used. This is the recommended setting for most applications.false— Additionally registers a client-sideAuthenticationStateProviderdecorator that enriches claims via LocalStorage/JS interop. Only needed for standalone Blazor WebAssembly apps that have no server-side HTTP pipeline.
Warning: Setting
SkipAuthStateDecoration = falseon a Server/SSR app will cause a blank page (silent deadlock from JS interop during prerendering).
Which setting do I need?
| App type | Setting |
|---|---|
| Blazor Server | true (default) — no config needed |
| Blazor Server with SSR | true (default) — no config needed |
| Blazor Hybrid (Server + WASM) | true (default) — server enriches claims for all render modes |
| Standalone Blazor WASM | false — needs client-side enrichment |
Custom Claims Enricher
If you need to inject custom claims (e.g. global roles from a database) before team member lookup and consent evaluation, implement ITeamClaimsEnricher and register it:
public class MyClaimsEnricher : ITeamClaimsEnricher
{
private readonly IMyUserDatabase _db;
public MyClaimsEnricher(IMyUserDatabase db) => _db = db;
public async Task EnrichAsync(ClaimsIdentity identity)
{
var roles = await _db.GetGlobalRolesAsync(identity.Name);
foreach (var role in roles)
{
if (!identity.HasClaim(c => c.Type == ClaimTypes.Role && c.Value == role))
identity.AddClaim(new Claim(ClaimTypes.Role, role));
}
}
}
Register via options:
builder.Services.AddThargaTeamBlazor(o =>
{
o.AddClaimsEnricher<MyClaimsEnricher>();
// ...
});
Or via AddThargaPlatform:
builder.AddThargaPlatform(o =>
{
o.Blazor.AddClaimsEnricher<MyClaimsEnricher>();
});
The enricher runs once per request inside TeamServerClaimsTransformation, before member lookup and consent evaluation. It supports full dependency injection (constructor injection). Duplicate claims are automatically prevented.
Use cases:
- Assign global roles (e.g.
Developer,SystemAdministrator) based on user identity - Add custom claims from external systems before team consent is evaluated
- Enrich the principal with application-specific metadata
Verification
After login, the team selector should appear. Creating a team should persist to MongoDB. Switching teams should update the claims.
Step 5: API Key Authentication
Adds API key authentication so external clients can call your API using X-API-KEY headers.
Requires: Step 3, Step 4
Program.cs
Extend the existing AddThargaTeamBlazor call to register the API key service, and add API key authentication:
builder.Services.AddThargaTeamBlazor(o =>
{
// ... existing team config ...
o.RegisterApiKeyAdministrationService<MyApiKeyService>();
});
builder.Services.AddThargaApiKeys();
// Chain onto the existing authentication registration:
builder.Services.AddAuthentication()
.AddThargaApiKeyAuthentication();
Options
.AddThargaApiKeyAuthentication(o =>
{
o.AdvancedMode = false; // default: false — simple mode auto-generates keys
o.AutoKeyCount = 2; // default: 2 — number of auto-generated keys in simple mode
o.AutoLockKeys = false; // default: false — auto-lock keys after creation
o.MaxExpiryDays = 365; // default: 365 — maximum key expiry in days (null = no cap)
o.LastUsedThrottle = TimeSpan.FromMinutes(1); // default: 1 min — min interval between "last used" timestamp writes per key (TimeSpan.Zero = stamp every request)
o.MinKeyLength = 32; // default: 32 — alphanumeric chars in the key secret; fixed length unless MaxKeyLength is set (floor 24 ≈143-bit; team + system keys)
o.MaxKeyLength = null; // default: null — when set, the length is random in [MinKeyLength, MaxKeyLength] per key instead of fixed
});
What becomes available
- API key authentication handler (validates
X-API-KEYheader) [Authorize(Policy = "ApiKeyPolicy")]attribute for controllers- API key management UI via
<ApiKeyView />(from Step 4) - Constants in
ApiKeyConstants.HeaderName,ApiKeyConstants.PolicyName
_Imports.razor (if referencing constants)
@using Tharga.Team.Service
System-set tags
API keys can carry system-set tags — a key-value list (IReadOnlyList<Tag>, record Tag(string Key, string Value)) set by backend code at creation. Tags are backend-only: there's a tags parameter on CreateKeyAsync, no mutation API, and no input in the ApiKeyView create card — so an operator can't add or re-point them from the UI.
await apiKeyManagementService.CreateKeyAsync(
teamKey, "Firewall opener", AccessLevel.Custom,
scopeOverrides: new[] { "firewall:open" },
tags: new[] { new Tag("Type", "firewall"), new Tag("firewall.groupId", "ABC123") });
- Surfaced as claims. Each tag becomes a
tag.{Key}claim on the authenticated principal (TeamClaimTypes.TagPrefix = "tag.") — no DB round-trip to read a key's binding. Because it's a list, a key may carry the same key twice (e.g.Type=firewall+Type=PIM), producing twotag.Typeclaims; read them withuser.FindAll("tag.Type"). - Displayed read-only.
ApiKeyViewshows all tags in an(i)tooltip; passChipTagKeysto render selected keys as chips (e.g.ChipTagKeys="@(new[] { "Type" })"). - Legacy data. Pre-tags keys stored an empty
Tagsdocument; reads tolerate this automatically (it deserializes as no tags). To purge the legacy field, callIApiKeyRepository.CleanLegacyTagsAsync()once (server-side, safe to repeat).
Lifecycle hook (capturing the private token)
The private API token is shown once at creation and is otherwise unrecoverable — it's never persisted, logged, or exposed programmatically. If a host needs to capture and re-deliver a key (e.g. minting a scoped key to hand out repeatedly), register an IApiKeyLifecycleHandler. It receives the token at the moment it exists — on create and recycle/regenerate — plus a tokenless delete signal so the host can purge its own copy.
public class MyApiKeyHandler(ISecretProtector protector, IMyKeyStore store) : IApiKeyLifecycleHandler
{
public async Task OnApiKeyLifecycleAsync(ApiKeyLifecycleContext ctx)
{
switch (ctx.Reason)
{
case ApiKeyLifecycleReason.Created:
case ApiKeyLifecycleReason.Recycled:
await store.SaveAsync(ctx.ApiKeyId, protector.Protect(ctx.PrivateToken), ctx.TeamKey, ctx.Tags);
break;
case ApiKeyLifecycleReason.Deleted:
await store.RemoveAsync(ctx.ApiKeyId);
break;
}
}
}
// after AddThargaPlatform / AddThargaApiKeys:
builder.Services.AddThargaApiKeyLifecycleHandler<MyApiKeyHandler>();
- What you get —
ApiKeyLifecycleContext:Reason,ApiKeyId(the stable public id),PrivateToken(non-null on Created/Recycled, null on Deleted),TeamKey(null for system keys),IsSystemKey,Name,Tags. Applies to both team and system keys. - Error policy — if the handler throws, the originating
CreateKey/RefreshKey/DeleteKeythrows too (capture failures are not swallowed). Note this does not roll back: a thrown create still leaves the key in storage and a thrown recycle has already rotated the secret — treat a failure as "operation failed" and reconcile (re-recycle, or delete the orphan). - Scope — fires only on explicit create/recycle/delete. Simple-mode auto-generated keys (created lazily by
GetKeysAsync) and lock/scope/role edits do not fire it. - Security — the token is handed only to in-process handlers you registered; it is still never persisted or logged by the platform. You own whatever you capture (encrypt it at rest).
- Multiple handlers can be registered; all are invoked.
Verification
Create an API key via the UI, then call your API with X-API-KEY: <key> header. The request should authenticate successfully.
Step 6: Scopes
Adds fine-grained permission scopes that control access to service methods. Scopes are resolved per team member based on their access level, tenant roles, and scope overrides.
Requires: Step 4
Program.cs
using Tharga.Team;
using Tharga.Team.Service;
// Define scopes with default minimum access levels
builder.Services.AddThargaScopes(scopes =>
{
scopes.Register("feature:read", AccessLevel.Viewer);
scopes.Register("feature:write", AccessLevel.User);
scopes.Register("feature:manage", AccessLevel.Administrator);
});
// Register services with scope enforcement
builder.Services.AddScopedWithScopes<IMyService, MyService>();
Service implementation
Decorate service methods with the required scope:
public class MyService : IMyService
{
[RequireScope("feature:read")]
public Task<Data> GetAsync() { ... }
[RequireScope("feature:write")]
public Task SaveAsync(Data data) { ... }
}
The ScopeProxy<T> automatically checks that the current user has the required scope before calling the method. If the scope is denied, an UnauthorizedAccessException is thrown.
How scopes are resolved
- Access level — Owner and Administrator get all scopes. User gets scopes at User or Viewer level. Viewer gets only Viewer-level scopes.
Customgets no base scopes (and is exempt from the Owner/Administrator "all scopes" rule). - Tenant roles — Additional scopes granted by assigned roles (see Step 7).
- Scope overrides — Per-member overrides set in the team management UI (when
ShowScopeOverrides = true).
AccessLevel.Custom— least-privilege keys/members. UseCustomwhen a principal should carry only its explicitly assigned roles and scope overrides, with nothing inherited from the access-level tier — e.g. a machine API key minted with a single scope. Its effective scopes are exactlyroles ∪ scopeOverrides. Set it explicitly: a key created without an access level still defaults to a non-Customlevel.Customis surfaced in theApiKeyViewcreate card; it is intentionally hidden from the team-member pickers until member scope/role editing lands (#76).
Built-in scopes
| Scope | Default level | Source |
|---|---|---|
team:read |
— | TeamScopes.Read |
team:manage |
— | TeamScopes.Manage |
member:invite |
— | TeamScopes.MemberInvite |
member:remove |
— | TeamScopes.MemberRemove |
member:role |
— | TeamScopes.MemberRole |
apikey:manage |
— | ApiKeyScopes.Manage |
Alternative: Access level enforcement
For simpler cases where scopes are overkill, use access level enforcement instead:
builder.Services.AddScopedWithAccessLevel<IMyService, MyService>();
[RequireAccessLevel(AccessLevel.Administrator)]
public Task DeleteAsync(string id) { ... }
A
Customprincipal is the lowest tier and fails every[RequireAccessLevel]gate (includingViewer). Authorize such principals with scope-based checks ([RequireScope]) rather than access-level enforcement.
Verification
Call a scope-protected method as a Viewer when it requires User level — it should be denied. Elevate the member's access level and retry — it should succeed.
Step 7: Tenant Roles
Adds named roles that bundle scopes together, making it easier to manage permissions for team members.
Requires: Step 6
Program.cs
builder.Services.AddThargaTenantRoles(roles =>
{
roles.Register("Editor", new[] { "feature:read", "feature:write" });
roles.Register("Auditor", new[] { "feature:read", "audit:read" });
});
Team UI
Role assignment is a component parameter, not a global option. Set ShowRoles="true" on <TeamComponent>
(and on <ApiKeyView> to assign roles to keys):
<TeamComponent TMember="MyMember" ShowRoles="true" ShowScopeOverrides="true" />
How it works
When a team member is assigned the "Editor" role, they automatically receive the feature:read and feature:write scopes in addition to their access-level scopes. Roles are combined — a member with both "Editor" and "Auditor" gets all scopes from both. Members/keys store the role names; the scopes are resolved live from the registry (change a role's scopes and it applies to all assignees).
Verification
Assign a role to a team member, then verify they can access methods protected by the role's scopes.
Step 7b: Managing roles & scopes (reference)
A principal's effective scopes are the union of four sources:
| Source | Applies to | Configured via |
|---|---|---|
| Access level → scopes | team members, team API keys | o.ConfigureScopes (scope's default min level); AccessLevel.Custom grants no base scopes |
| Tenant roles → scopes | team members, team API keys | o.ConfigureTenantRoles (role → scopes) |
| Scope overrides (explicit) | team members, team API keys | per-principal, edited in the UI |
| System scopes (global, flat) | system API keys, and users via role mapping | o.ConfigureSystemScopes; o.ConfigureSystemRoles (app role → system scopes) |
All four surface as Scope claims, so service methods gate uniformly with [RequireScope("…")] regardless of whether the caller is a team member, a team key, a system key, or a privileged user.
System scopes & privileged users
System scopes are global capabilities (no access-level hierarchy):
o.ConfigureSystemScopes = s =>
{
s.Register("system:teams:read", "Read any team's data (cross-tenant).");
s.Register("system:metrics:read", "Read infrastructure metrics.");
};
// Map app/global roles to system scopes so privileged USERS gain them (team-independent).
o.ConfigureSystemRoles = r =>
{
r.Map("Developer", "system:teams:read", "system:metrics:read", "apikey:manage", "audit:read");
};
- System API keys are minted with an explicit system-scope list (
SystemApiKeyViewpicker readsConfigureSystemScopes). - Users with a mapped app role (e.g.
Developer) receive the mapped scopes as claims viaTeamServerClaimsTransformation— even with no team selected. Mapapikey:manage/audit:readto a role to grant that role cross-team key/audit management. - Map external IdP role claims to internal role names with an
ITeamClaimsEnricher(runs first), e.g.Dev → Developer.
Consent (cross-team access)
A team can consent to grant a global role access to its data, at a chosen access level:
o.Blazor.Consent.Roles = ["Developer"]; // which roles a team may consent to
o.Blazor.Consent.ShowToggle = true; // show the consent picker in TeamComponent
o.Blazor.Consent.AccessLevel = AccessLevel.Viewer; // default level when the consent doesn't carry one
The team admin picks the access level when consenting (Viewer/User/Administrator); a consented user gains that team's scopes at that level. The granted level is team.ConsentAccessLevel ?? Consent.AccessLevel.
Component parameter reference
| Component | Parameters |
|---|---|
<TeamComponent> |
ShowScopeTooltip (default true), ShowScopeOverrides, ShowRoles |
<ApiKeyView> |
ShowScopeTooltip (true), ShowScopeOverrides, ShowRoles, ShowLastUsed (true), ShowExpiryDatePicker, ShowTags (bool?, null=auto), ChipTagKeys, ShowAuditLogButton |
<SystemApiKeyView> |
ShowScopeTooltip (true), ShowScopeOverrides (true), ShowLastUsed (true), ShowExpiryDatePicker, ShowAuditLogButton |
Access to manage keys is gated on apikey:manage; the audit log on audit:read. (The former per-component
CrossTeamRoles / RequiredScopes parameters were removed — grant cross-team access via the role→system-scope
mapping instead.)
Step 8: Audit Logging
Adds audit logging for service calls, authorization events, and data changes. Logs can be stored in the application logger, MongoDB, or both.
Requires: Step 4
Program.cs
builder.Services.AddThargaAuditLogging();
Options
builder.Services.AddThargaAuditLogging(o =>
{
o.StorageMode = AuditStorageMode.Logger; // default: Logger — options: Logger, MongoDB, Logger | MongoDB
o.CallerFilter = AuditCallerFilter.Api | AuditCallerFilter.Web; // default: Api | Web
o.EventFilter = AuditEventFilter.All; // default: All
o.ExcludedActions = new[] { "read", "list" }; // default: empty — skip noisy read operations
o.ExcludedEndpoints = Array.Empty<string>(); // default: empty
o.RetentionDays = 90; // default: 90
o.BatchSize = 100; // default: 100 — for MongoDB batch writes
o.FlushIntervalSeconds = 5; // default: 5 — for MongoDB flush interval
});
Note: To use
AuditStorageMode.MongoDB, you need MongoDB configured (from Step 4).
What becomes available
| Component | Description |
|---|---|
<AuditLogView /> |
Audit log viewer with charts and filtering |
IAuditLogger |
Injectable service for custom audit entries |
Audit entry fields
Each audit entry captures: timestamp, correlation ID, event type, feature/action, caller identity, team key, access level, scope check results, duration, and custom metadata.
Event types
| Type | When logged |
|---|---|
ServiceCall |
Any proxied service method call |
AuthSuccess |
Successful authentication |
AuthFailure |
Failed authentication |
ScopeDenial |
Scope check denied |
DataChange |
Data modification |
RateLimit |
Rate limit hit |
Verification
Perform some actions, then view the audit log via <AuditLogView />. Entries should appear with correct caller identity and scope information.
Quick reference: Registration order in Program.cs
using Microsoft.AspNetCore.Authentication;
using Tharga.Team;
using Tharga.Team.Blazor.Features.Authentication;
using Tharga.Team.Blazor.Framework;
using Tharga.Team.Service;
// Step 1: Radzen + Blazor foundation
builder.Services.AddRadzenComponents();
builder.Services.AddThargaBlazor(o => o.Title = "My App");
// Step 2: Authentication
builder.AddThargaAuth();
// Step 3: Controllers
builder.Services.AddThargaControllers();
// Step 4: Team management
builder.Services.AddThargaTeamBlazor(o =>
{
o.Title = "My App";
o.RegisterTeamService<MyTeamService, MyUserService>();
o.RegisterApiKeyAdministrationService<MyApiKeyService>(); // Step 5
o.ShowMemberRoles = true; // Step 7
o.ShowScopeOverrides = true; // Step 6
});
builder.Services.AddThargaTeamRepository(o =>
{
o.RegisterUserRepository<UserEntity>();
o.RegisterTeamRepository<TeamEntity, TeamMember>();
});
// Step 5: API key auth
builder.Services.AddThargaApiKeys();
builder.Services.AddAuthentication()
.AddThargaApiKeyAuthentication();
// Step 6: Scopes
builder.Services.AddThargaScopes(scopes =>
{
scopes.Register("feature:read", AccessLevel.Viewer);
scopes.Register("feature:write", AccessLevel.User);
});
builder.Services.AddScopedWithScopes<IMyService, MyService>();
// Step 7: Tenant roles
builder.Services.AddThargaTenantRoles(roles =>
{
roles.Register("Editor", new[] { "feature:read", "feature:write" });
});
// Step 8: Audit
builder.Services.AddThargaAuditLogging();
var app = builder.Build();
// Step 2: Auth endpoints
app.UseThargaAuth();
// Step 3: Controllers
app.UseThargaControllers();
Quick reference: _Imports.razor
@* Step 1: UI Foundation *@
@using Radzen
@using Radzen.Blazor
@using Tharga.Blazor
@using Tharga.Blazor.Framework
@using Tharga.Blazor.Framework.Buttons
@using Tharga.Blazor.Features.BreadCrumbs
@* Step 2: Authentication *@
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.Authorization
@using Tharga.Team.Blazor.Features.Authentication
@using Tharga.Team.Blazor.Framework
@* Step 4: Team management *@
@using Tharga.Team.Blazor.Features.Team
Package summary
| Package | Added in | Purpose |
|---|---|---|
Tharga.Blazor |
Step 1 | Generic UI components (Radzen, buttons, breadcrumbs) |
Tharga.Team.Blazor |
Step 2 | Authentication, team UI, claims augmentation |
Tharga.Team.Service |
Step 3 | API controllers, API key auth, scopes, audit |
Tharga.Team.MongoDB |
Step 4 | MongoDB persistence for teams and users |
Tharga.Team |
(transitive) | Domain models, authorization primitives |