Skip to content

Claims-Based Identity / RBAC

The Claims-Based Identity pattern represents a user’s identity as claims (key-value pairs) extracted from a JWT token. RBAC (Role-Based Access Control) restricts access by verifying that the user’s role holds the required permissions.

Granit combines JWT Keycloak + strict RBAC (permissions on roles only, never per user) with a dynamic policy system.

sequenceDiagram
    participant C as Client
    participant KC as Keycloak
    participant API as Granit API
    participant CT as ClaimsTransformation
    participant PC as PermissionChecker
    participant PS as PermissionGrantStore

    C->>KC: Authentication
    KC-->>C: JWT (access_token)
    C->>API: Request + Bearer token

    API->>CT: KeycloakClaimsTransformation
    CT->>CT: Extract realm_access.roles from JWT
    CT->>CT: Add roles as claims

    API->>PC: Check permission "Patients.Create"
    PC->>PS: GetGrantedPermissionsAsync(roles, tenantId)
    PS-->>PC: Permission list
    alt Permission granted
        PC-->>API: true
    else Permission denied
        PC-->>API: false -- 403 Forbidden
    end
ComponentFileRole
ICurrentUserServicesrc/Granit.Security/ICurrentUserService.csUserId, UserName, Email, GetRoles(), IsInRole()
KeycloakClaimsTransformationsrc/Granit.Authentication.Keycloak/Authentication/KeycloakClaimsTransformation.csExtracts realm_access.roles from the Keycloak JWT
WolverineCurrentUserServicesrc/Granit.Wolverine/Internal/WolverineCurrentUserService.csAsyncLocal fallback for background handlers
ComponentFileRole
DynamicPermissionPolicyProvidersrc/Granit.Authorization/Authorization/DynamicPermissionPolicyProvider.csCreates AuthorizationPolicy on the fly from permission names
PermissionCheckersrc/Granit.Authorization/Services/PermissionChecker.csEvaluates permissions against role grants
IPermissionDefinitionProvidersrc/Granit.Authorization/Definitions/IPermissionDefinitionProvider.csPermission declaration (code-first)
EfCorePermissionGrantStoresrc/Granit.Authorization.EntityFrameworkCore/Stores/EfCorePermissionGrantStore.csEF Core grant persistence
  • Permissions are assigned to roles, never to individual users
  • Cache is per role (not per user) for performance
  • AdminRoles bootstraps roles with all permissions at startup
ProblemSolution
Keycloak returns roles in a custom format (realm_access)KeycloakClaimsTransformation normalizes to standard claims
Background handlers have no HttpContextWolverineCurrentUserService maintains user via AsyncLocal
Creating one policy per permission would be explosiveDynamicPermissionPolicyProvider creates policies on demand
Per-user grants do not scale (10K users x 100 permissions)Strict RBAC: grants on roles (10 roles x 100 permissions)
// Declare permissions (code-first)
public sealed class PatientPermissionDefinitionProvider : IPermissionDefinitionProvider
{
public void DefinePermissions(IPermissionDefinitionContext context)
{
PermissionGroup group = context.AddGroup("Patients");
group.AddPermission("Patients.Create");
group.AddPermission("Patients.Read");
group.AddPermission("Patients.Delete");
}
}

Localized displayName values are optional in this simplified example. See the full authorization documentation for adding LocalizableString.

// Protect an endpoint
app.MapPost("/api/patients", CreatePatientEndpoint.Handle)
.RequireAuthorization("Patients.Create");
// Programmatic check
public static class DischargePatientHandler
{
public static async Task Handle(
DischargePatientCommand cmd,
IPermissionChecker permissionChecker,
CancellationToken cancellationToken)
{
bool canDischarge = await permissionChecker.IsGrantedAsync("Patients.Discharge", ct);
if (!canDischarge)
throw new ForbiddenException();
}
}