← All articles

Org Tree Claims and ASP.NET Core Policies: Technical Reference

· Jeff Zuerlein
Org Tree Claims and ASP.NET Core Policies: Technical Reference

This article explains how AuthorizationHub converts an organizational tree into ASP.NET Core identity claims, and how to write authorization policies against those claims. It is the technical companion to "Your Permissions Are a Photo of Your Org Chart", which covers the concept and motivation. This article covers implementation.

Summary

  • AuthorizationHub models an organization as a tree of four party types: Tenant (root), Organization, Role, and Person.
  • When a user makes a request, AuthorizationHub generates one claim for every party the user is related to, up the tree — not just their immediate group or role.
  • Each claim has a type (Organization or Role) and a JSON value containing PartyId, DisplayName, and ExternalId.
  • Policies are written by adding pre-built AllowedOrganizations...Requirement or AllowedRoles...Requirement objects to an ASP.NET Core authorization policy, matching on DisplayName, ExternalId, or PartyId.
  • No custom claims-transformation code is required. AuthorizationHub adds the claims inside the ASP.NET Core pipeline automatically once configured.

Key Terms

TermDefinition
TenantThe root of an organizational tree. Represents a top-level organization (e.g., a company).
OrganizationA group node inside a Tenant (e.g., a department or team). Organizations can be nested.
RoleA role node inside a Tenant, distinct from Organization. Represents a job function.
PersonA user, placed at one or more points in the tree.
PartyThe general term for any node in the tree — Tenant, Organization, Role, or Person.
PartyIdThe integer identifier AuthorizationHub assigns to a party.
DisplayNameThe human-readable name of a party. Must be unique per Tenant for Organizations and Roles.
ExternalIdAn optional identifier used to link a party to an external system (e.g., an identity provider). Must be unique if set.
ClaimAn ASP.NET Core identity claim AuthorizationHub adds to a user's identity, representing one relationship to a party in the tree.

Q: What is the org tree made of?

A: Four party types, always structured the same way:

  • Tenant — the root. Every tree starts with one.
  • Organization — a group node under a Tenant. Organizations can be nested inside other Organizations.
  • Role — a role node under a Tenant, kept distinct from Organization so job function and group membership can be modeled separately.
  • Person — a user, placed at one or more Organization or Role nodes in the tree.

A Person can belong to multiple Organizations, multiple Roles, and multiple Tenants at once.

Q: How does a tree relationship become a claim?

A: AuthorizationHub generates a claim for every party above a Person in the tree, not just the party they're directly assigned to.

Example: a user who is a member of the "Marketing Administrator" Role, where that Role sits under a "Marketing" Organization, which sits under an "Acme Corporation" Tenant, receives three claims — one for the Role, one for the Organization, and one for the Tenant. This is what lets a policy check "is this user anywhere under Marketing" without needing to enumerate every Role inside Marketing individually.

This generation happens automatically inside the ASP.NET Core request pipeline once AuthorizationHub is configured — no manual claims-transformation code is required.

Q: What does a claim actually look like?

A: Each claim has a type (Organization or Role) and a value that is a JSON document containing:

  • PartyId — AuthorizationHub's integer identifier for the party
  • DisplayName — the party's human-readable name
  • ExternalId — the optional external identifier, if one was set

Two appsettings.json options change the exact shape:

  • UseMultiTenantRequirements: true — claim values are JSON objects containing a list of tenants the claim applies to. Use this in multi-tenant applications, where a MultiTenant requirement type compares that tenant list against the tenant relevant to the current request.
  • UseMultiTenantRequirements not set — three separate claims are generated per relationship instead: one each for DisplayName, ExternalId, and PartyId.
  • ReplaceGroupClaimsWithRoleClaims: true — converts Organization (group) claims into Role claims. Useful when retrofitting an existing app that already uses [Authorize(Roles = "Admin")]-style attributes and doesn't distinguish groups from roles.

Q: How do you add AuthorizationHub to a project?

A: Install packages and register services in Program.cs.

bash
dotnet new web -n MyWebApp
cd MyWebApp

dotnet add package AuthorizationHub
dotnet add package AuthorizationHub.UI
dotnet add package AuthorizationHub.Data.SqlServer   # or .Postgres / .Sqlite
csharp
using AuthorizationHub;
using AuthorizationHub.Data.SqlServer;
using AuthorizationHub.UI.Extensions;

var builder = WebApplication.CreateBuilder(args);

// Sets up DI and needed authorization policies.
builder.Services.AddAuthorizationHub();

// Configures AuthorizationHub to use SQL Server.
builder.Services.AddSQLServerToAuthorizationHub();

var app = builder.Build();

app.UseRouting();
app.UseAuthorization();

// Adds routes for the management UI and REST endpoints.
app.UseAuthorizationHubUI();

app.Run();

Connection string and administrator setup live in appsettings.json, under an AuthorizationHubOptions section:

json
"AuthorizationHubOptions": {
  "SqlServerConnection": "Server=localhost;Database=AuthorizationHub;User ID=;Password=;MultipleActiveResultSets=true;TrustServerCertificate=True",
  "Administrators": [
    "2e0c5e52-31d3-405f-b880-619e92c66047"
  ]
}

Administrators is a list of NameIdentifier claim values (for ASP.NET Core Identity, this is the AspNetUsers.Id value) that are allowed to edit the org tree before any tree data exists — this solves the bootstrapping problem of needing an admin before the app has ever run.

Q: How do you build the org tree?

A: Through the AuthorizationHub UI, at /authorizationhub-ui on your application's root.

  1. Create a Tenant first — every tree requires a root.
  2. Add Organizations and Roles underneath the Tenant to mirror the structure your application actually needs (not necessarily your HR chart or Active Directory).
  3. Add People and place them at the Organization/Role nodes that reflect their actual position.

DisplayName is required and must be unique per Tenant for Organizations and Roles. ExternalId is optional but must be unique if provided — this is the field to populate if you want to link a tree node back to an identity provider or another system of record.

Q: How do you write a policy that checks group (Organization) membership?

A: Use one of the three AllowedOrganizations...Requirement types, depending on which identifier you want to match against.

By DisplayName:

csharp
builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("SalaryInfoDisplay", policyBuilder => policyBuilder.AddRequirements(
        new AllowedOrganizationsByDisplayNameRequirement(["Accounting", "Administrators", "SuperUsers"])
        ));
});

By ExternalId:

csharp
new AllowedOrganizationsByExternalIdRequirement(["90f7989e-c10d-4b86-840b-0be0ecdd8043"])

By PartyId:

csharp
new AllowedOrganizationsByPartyIdRequirement(["41", "103", "22"])

All three check the same thing — is the user's claim set for Organizations a match against the provided list — they just differ in which identifier they compare.

Q: How do you write a policy that checks Role membership?

A: Same pattern as Organizations, using the AllowedRoles...Requirement types.

By DisplayName:

csharp
builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("SalaryInfoDisplay", policyBuilder => policyBuilder.AddRequirements(
        new AllowedRolesByDisplayNameRequirement(["Accounting Manager", "CFO", "CEO"])
        ));
});

By ExternalId:

csharp
new AllowedRolesByExternalIdRequirement(["90f7989e-c10d-4b86-840b-0be0ecdd8043"])

By PartyId:

csharp
new AllowedRolesByPartyIdRequirement(["42", "104", "23"])

Q: Which identifier should a policy use — DisplayName, ExternalId, or PartyId?

A: Depends on where the policy's source of truth lives:

  • DisplayName — simplest to read and write directly in code. Best when the policy is being authored by hand and the tree's naming is stable. Risk: if someone renames the Organization or Role in the tree, the policy silently stops matching.
  • ExternalId — best when the party is linked to an external system (an identity provider group, an HR system record) and you want the policy tied to that external identifier rather than whatever the tree happens to call it internally.
  • PartyId — most stable against renames, since it's AuthorizationHub's own internal identifier, but least human-readable in code — a reviewer can't tell what "41" means without checking the tree.

There is no requirement to pick one identifier type globally — different policies in the same application can use different requirement types as appropriate.

Q: How does this avoid the "stale group" problem?

A: Because claims are generated from the live tree on every request, not from a copy maintained separately. Moving a Person to a different Role or Organization changes their claims on their next request — there is no second data store to update, and no manual step that can be skipped or forgotten. For the conceptual case behind this design, see "Your Permissions Are a Photo of Your Org Chart".

© 2025 AuthorizationHub LLC. All rights reserved.