Skip to content

Organization Authorization ​

Since Keycloak 26, Organizations provide built-in multi-tenancy support. Users can belong to one or more organizations, and membership is included as an organization claim in JWT tokens.

Keycloak.AuthServices.Authorization provides first-class support for enforcing organization membership — declaratively via endpoint metadata or imperatively via IAuthorizationService.

Table of Contents:

Token Claim Format ​

Keycloak produces two claim formats depending on the Organization Membership Mapper configuration:

Default (string array) — when no ID or attributes are enabled:

json
{
  "organization": ["acme-corp", "startup-co"]
}

Rich JSON (map) — when addOrganizationId or addOrganizationAttributes is enabled:

json
{
  "organization": {
    "acme-corp": { "id": "a56bea03-..." },
    "startup-co": { "id": "uuid-2", "department": ["engineering"] }
  }
}

Both formats are handled transparently by the library.

TIP

To include organization membership in tokens, request the organization:* scope. The plain organization scope alone does not include the claim.

Setup ​

Register organization authorization in your service collection:

csharp
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddKeycloakWebApiAuthentication(builder.Configuration);
builder.Services.AddKeycloakAuthorization();

var app = builder.Build();

app.UseAuthentication();
app.UseAuthorization();

AddKeycloakAuthorization() registers the OrganizationRequirementHandler and the built-in parameter resolvers (RouteParameterResolver, HeaderParameterResolver, QueryParameterResolver).

Declarative Authorization ​

Use RequireOrganizationMembership() on endpoints — it works like RequireProtectedResource() but for organization membership.

Any Organization ​

Require the user to belong to any organization:

csharp
app.MapGet("/orgs", (ClaimsPrincipal user) =>
{
    var orgs = user.GetOrganizations();
    return Results.Ok(orgs.Select(o => new { o.Alias, o.Id }));
})
.RequireOrganizationMembership();

Route-Based (Dynamic) ​

Resolve the organization from a route parameter:

csharp
app.MapGet("/orgs/{orgId}/projects", (string orgId) =>
    Results.Ok(new { Organization = orgId, Projects = new[] { "project-alpha" } })
)
.RequireOrganizationMembership("{orgId}");

The {orgId} template is resolved at evaluation time using the default RouteParameterResolver.

Static Organization ​

Require membership in a specific, hardcoded organization:

csharp
app.MapGet("/acme/settings", () =>
    Results.Ok(new { Theme = "dark", Tier = "enterprise" })
)
.RequireOrganizationMembership("acme-corp");

Header-Based (Custom Resolver) ​

Resolve the organization from a request header using a custom IParameterResolver:

csharp
app.MapGet("/tenant/projects", () =>
    Results.Ok(new { Projects = new[] { "tenant-project" } })
)
.RequireOrganizationMembership<RouteHandlerBuilder, HeaderParameterResolver>(
    "{X-Organization}"
);

You can also use the runtime Type overload:

csharp
.RequireOrganizationMembership("{X-Organization}", typeof(HeaderParameterResolver));

Policy Builder ​

Compose organization membership with other requirements using the policy builder:

csharp
builder.Services
    .AddKeycloakAuthorization()
    .AddAuthorizationBuilder()
    .AddPolicy("AcmeAdmin", policy => policy
        .RequireOrganizationMembership("acme-corp")
        .RequireRealmRoles("admin")
    );

// Use the policy
app.MapGet("/acme/admin", () => Results.Ok(new { Message = "Acme admin area" }))
    .RequireAuthorization("AcmeAdmin");

Imperative Authorization ​

Single Check ​

Use IAuthorizationService to check membership programmatically:

csharp
app.MapGet("/check/{orgId}", async (
    string orgId,
    ClaimsPrincipal user,
    IAuthorizationService authorizationService) =>
{
    var result = await authorizationService.AuthorizeAsync(
        user, null, new OrganizationRequirement(orgId)
    );

    return result.Succeeded
        ? Results.Ok(new { OrgId = orgId, Access = true })
        : Results.Forbid();
});

Filtering Resources ​

Filter a collection based on organization membership:

csharp
app.MapGet("/workspaces", async (
    ClaimsPrincipal user,
    IAuthorizationService authorizationService) =>
{
    var allWorkspaces = new[]
    {
        new { Id = 1, Name = "Acme Platform", Org = "acme-corp" },
        new { Id = 2, Name = "Partner Portal", Org = "partner-inc" },
        new { Id = 3, Name = "Startup MVP", Org = "startup-co" },
    };

    var accessible = new List<object>();
    foreach (var workspace in allWorkspaces)
    {
        var result = await authorizationService.AuthorizeAsync(
            user, null, new OrganizationRequirement(workspace.Org)
        );

        if (result.Succeeded)
            accessible.Add(workspace);
    }

    return Results.Ok(accessible);
});

TIP

For simple cases, you can also use the ClaimsPrincipal extension methods directly — user.IsMemberOf("acme-corp") or user.GetOrganizations() — without going through the authorization service.

Claim Parsing Extensions ​

The ClaimsPrincipal extension methods are available in Keycloak.AuthServices.Common.Claims:

csharp
using Keycloak.AuthServices.Common.Claims;

// Get all organizations
IReadOnlyList<OrganizationMembership> orgs = user.GetOrganizations();

// Check membership by alias
bool isMember = user.IsMemberOf("acme-corp");

// Check membership by ID (rich JSON format only)
bool isMemberById = user.IsMemberOfById("a56bea03-...");

The OrganizationMembership model:

csharp
public class OrganizationMembership
{
    public string Alias { get; set; }
    public string? Id { get; set; }
    public IDictionary<string, string[]>? Attributes { get; set; }
}

Multi-Tenancy Patterns ​

Keycloak Organizations are a multi-tenancy primitive — each organization represents a tenant, customer, or company. The library handles the authorization gate ("can this user access this tenant?"), while the application handles data isolation and tenant context.

Library vs. Application Responsibility ​

ConcernWho Handles
User belongs to tenant?Library (RequireOrganizationMembership)
Which tenant is this request for?App (from route, header, or claim)
Isolate data per tenantApp (EF Core filters, DB schemas)
Tenant-specific configurationApp (IOptionsSnapshot per org)
Org-scoped rolesNot yet in Keycloak (keycloak#30180)

Tenant Context Middleware ​

Extract the current tenant from the organization claim and make it available to the rest of the pipeline:

csharp
app.Use(async (context, next) =>
{
    var orgs = context.User.GetOrganizations();
    if (orgs.Count > 0)
    {
        context.Features.Set(new TenantContext(orgs.First()));
    }
    await next();
});

Data Isolation with EF Core ​

Use a global query filter to scope all queries to the current tenant:

csharp
// In DbContext.OnModelCreating
builder.Entity<Project>()
    .HasQueryFilter(e => e.TenantId == _currentTenant.Id);

Tenant Switching ​

Users who belong to multiple organizations can switch between them. Keycloak supports requesting a token scoped to a single organization using the organization:<alias> scope:

scope=openid organization:acme-corp

This produces a token with only that organization in the claim, which is useful when your application needs a single active tenant context.

B2B Identity Federation ​

Each organization in Keycloak can be linked to an external identity provider (e.g., customer's Azure AD or Okta). Users from that org automatically federate through their own IdP — enabling B2B SSO without application-level changes.

Keycloak Setup ​

Organizations must be enabled in Keycloak (KC 26+). See the OrganizationAuthorization sample for a complete Docker Compose setup with realm import and seed script.

Key steps:

  1. Enable organizations on the realm (organizationsEnabled: true)
  2. Create organizations via Admin API or UI
  3. Assign users to organizations
  4. Ensure the organization client scope is assigned to your client
  5. Request organization:* scope when obtaining tokens