Skip to content

Granit.Security

Granit.Security provides the authentication and authorization stack for the framework. JWT Bearer validation, claims transformation for Keycloak, Entra ID, and AWS Cognito, dynamic RBAC permissions with per-role caching, back-channel logout — all wired through the module system.

  • DirectoryGranit.Security/ Base abstractions (ICurrentUserService, ActorKind)
    • DirectoryGranit.Authentication.JwtBearer/ Generic JWT Bearer, back-channel logout
      • Granit.Authentication.Keycloak Keycloak claims transformation
      • Granit.Authentication.EntraId Entra ID roles parsing
      • Granit.Authentication.Cognito Cognito groups → roles
    • DirectoryGranit.Authorization/ Dynamic RBAC, permission definitions, policy provider
      • Granit.Authorization.EntityFrameworkCore EF Core permission grant store
PackageRoleDepends on
Granit.SecurityICurrentUserService, ActorKindGranit.Core
Granit.Authentication.JwtBearerJWT Bearer middleware, back-channel logoutGranit.Security
Granit.Authentication.KeycloakKeycloak claims transformationGranit.Authentication.JwtBearer
Granit.Authentication.EntraIdEntra ID roles parsingGranit.Authentication.JwtBearer
Granit.Authentication.CognitoCognito groups → rolesGranit.Authentication.JwtBearer
Granit.AuthorizationRBAC permissions, dynamic policy providerGranit.Security, Granit.Caching
Granit.Authorization.EntityFrameworkCoreEF Core permission grant storeGranit.Authorization, Granit.Persistence
graph TD
    S[Granit.Security] --> C[Granit.Core]
    JWT[Granit.Authentication.JwtBearer] --> S
    KC[Granit.Authentication.Keycloak] --> JWT
    EID[Granit.Authentication.EntraId] --> JWT
    CG[Granit.Authentication.Cognito] --> JWT
    AZ[Granit.Authorization] --> S
    AZ --> CA[Granit.Caching]
    AZEF[Granit.Authorization.EntityFrameworkCore] --> AZ
    AZEF --> P[Granit.Persistence]
[DependsOn(typeof(GranitAuthenticationKeycloakModule))]
[DependsOn(typeof(GranitAuthorizationModule))]
public class AppModule : GranitModule { }
{
"Authentication": {
"Authority": "https://keycloak.example.com/realms/my-realm",
"Audience": "my-client"
},
"Keycloak": {
"ClientId": "my-client",
"AdminRole": "admin",
"RoleClaimsSource": "realm_access"
}
}

The core abstraction for accessing the authenticated actor. Injected everywhere audit fields, tenant resolution, or authorization checks need to know “who is calling.”

public interface ICurrentUserService
{
string? UserId { get; }
string? UserName { get; }
string? Email { get; }
string? FirstName { get; }
string? LastName { get; }
bool IsAuthenticated { get; }
ActorKind ActorKind { get; }
bool IsMachine { get; }
string? ApiKeyId { get; }
IReadOnlyList<string> GetRoles();
bool IsInRole(string role);
}

ActorKind distinguishes between User (human), ExternalSystem (API key / service account), and System (background jobs, scheduled tasks).

GranitJwtBearerModule registers:

  • ASP.NET Core JWT Bearer authentication
  • CurrentUserServiceICurrentUserService implementation extracting claims from HttpContext
  • IRevokedSessionStore — distributed cache-backed session revocation
{
"Authentication": {
"Authority": "https://idp.example.com/realms/my-realm",
"Audience": "my-client",
"RequireHttpsMetadata": true,
"NameClaimType": "sub",
"BackChannelLogout": {
"Enabled": true,
"EndpointPath": "/auth/back-channel-logout",
"SessionRevocationTtl": "01:00:00"
}
}
}
PropertyDefaultDescription
AuthorityOIDC issuer URL (required)
AudienceExpected aud claim (required)
RequireHttpsMetadatatrueEnforce HTTPS for metadata endpoint
NameClaimType"sub"Claim used as user identifier
BackChannelLogout.EnabledfalseEnable OIDC back-channel logout
BackChannelLogout.EndpointPath"/auth/back-channel-logout"Endpoint path
BackChannelLogout.SessionRevocationTtl"01:00:00"How long revoked sessions are remembered

Provider-agnostic implementation of the OIDC Back-Channel Logout specification. When the IdP sends a logout token, the session is revoked in distributed cache.

// In OnApplicationInitialization
app.MapBackChannelLogout(); // POST /auth/back-channel-logout (anonymous)

The endpoint validates the logout token signature against the IdP’s JWKS, extracts the sid claim, and stores it in IDistributedCache with key granit:revoked-session:{sid}.

Subsequent requests with a revoked sid are rejected by the JWT Bearer events handler.

GranitAuthenticationKeycloakModule post-configures JWT Bearer with Keycloak-specific behavior:

  • Extracts roles from realm_access.roles or resource_access.{clientId}.roles
  • Maps them to standard ClaimTypes.Role claims
  • Registers an "Admin" authorization policy
// Keycloak JWT payload (simplified)
{
"realm_access": {
"roles": ["admin", "doctor"]
},
"resource_access": {
"my-client": {
"roles": ["manage-patients"]
}
}
}
// After transformation → ClaimTypes.Role: "admin", "doctor", "manage-patients"

GranitAuthenticationEntraIdModule post-configures JWT Bearer with Entra ID-specific behavior:

  • Extracts roles from the v1.0 roles claim and the v2.0 wids claim
  • Maps them to standard ClaimTypes.Role claims

GranitAuthenticationCognitoModule post-configures JWT Bearer with Cognito-specific behavior:

  • Extracts groups from the cognito:groups claim (multiple claims with same type)
  • Maps them to standard ClaimTypes.Role claims
// Cognito JWT payload — groups appear as repeated claims
// "cognito:groups": "admin"
// "cognito:groups": "doctors"
// After transformation → ClaimTypes.Role: "admin", "doctors"

Modules declare their permissions via IPermissionDefinitionProvider:

public class InvoicePermissionDefinitionProvider : IPermissionDefinitionProvider
{
public void DefinePermissions(IPermissionDefinitionContext context)
{
var group = context.AddGroup("Invoices", "Invoice management");
group.AddPermission("Invoices.Invoices.Read", "View invoices");
group.AddPermission("Invoices.Invoices.Create", "Create invoices");
group.AddPermission("Invoices.Invoices.Update", "Edit invoices");
group.AddPermission("Invoices.Invoices.Delete", "Delete invoices");
}
}

Naming convention: [Module].[Resource].[Action]

Standard actions: Read, Create, Update, Delete, Manage (all CRUD), Execute (non-CRUD).

flowchart LR
    A[Request] --> B{AlwaysAllow?}
    B -->|yes| C[✓ Granted]
    B -->|no| D{AdminRole?}
    D -->|yes| C
    D -->|no| E{Cache hit?}
    E -->|yes| F{Granted?}
    E -->|no| G[IPermissionGrantStore]
    G --> H[Cache result]
    H --> F
    F -->|yes| C
    F -->|no| I[✗ Denied]

The PermissionChecker evaluates in order:

  1. AlwaysAllow option (development only)
  2. Admin role bypass — users with any role in AdminRoles get all permissions
  3. Per-role cache lookup (key: perm:{tenantId}:{roleName}:{permissionName})
  4. IPermissionGrantStore fallback (default: NullPermissionGrantStore → always denied)
// Option 1: Attribute on endpoint
app.MapGet("/invoices", [Permission("Invoices.Invoices.Read")] async (
AppDbContext db,
CancellationToken cancellationToken) =>
{
return await db.Invoices
.ToListAsync(cancellationToken)
.ConfigureAwait(false);
});
// Option 2: Imperative check
public class InvoiceService(IPermissionChecker permissionChecker)
{
public async Task DeleteAsync(Guid id, CancellationToken cancellationToken)
{
if (!await permissionChecker.IsGrantedAsync(
"Invoices.Invoices.Delete", cancellationToken)
.ConfigureAwait(false))
{
throw new ForbiddenException("Not authorized to delete invoices");
}
// ...
}
}
{
"Authorization": {
"AdminRoles": ["admin"],
"CacheDuration": "00:05:00",
"AlwaysAllow": false
}
}
PropertyDefaultDescription
AdminRoles["admin"]Roles that bypass all permission checks
CacheDuration00:05:00Per-role permission cache TTL
AlwaysAllowfalseSkip all checks (development only)

Granit.Authorization.EntityFrameworkCore replaces NullPermissionGrantStore with a real store backed by EF Core.

Entity:

public class PermissionGrant : AuditedEntity, IMultiTenant
{
public string Name { get; set; } = string.Empty; // Permission name
public string RoleName { get; set; } = string.Empty; // Role granted to
public Guid? TenantId { get; set; } // Tenant scope (null = global)
}
// Unique index: (TenantId, Name, RoleName)

DbContext integration:

public class AppDbContext : DbContext, IPermissionGrantDbContext
{
public DbSet<PermissionGrant> PermissionGrants => Set<PermissionGrant>();
}

Managing grants:

public class PermissionAdminService(
IPermissionManagerWriter writer,
IPermissionManagerReader reader)
{
public async Task GrantAsync(
string roleName, string permissionName, CancellationToken cancellationToken)
{
await writer.SetAsync(permissionName, roleName, tenantId: null,
isGranted: true, cancellationToken)
.ConfigureAwait(false);
}
public async Task<IReadOnlyList<string>> GetGrantedAsync(
string roleName, CancellationToken cancellationToken)
{
return await reader.GetGrantedPermissionsAsync(roleName, tenantId: null,
cancellationToken)
.ConfigureAwait(false);
}
}

All grant changes are audit-logged via AuditedEntity (ISO 27001 — 3-year retention).

CategoryKey typesPackage
ModuleGranitSecurityModule, GranitJwtBearerModule, GranitAuthenticationKeycloakModule, GranitAuthenticationEntraIdModule, GranitAuthenticationCognitoModule, GranitAuthorizationModule, GranitAuthorizationEntityFrameworkCoreModule
AbstractionsICurrentUserService, ActorKindGranit.Security
AuthenticationCurrentUserService, IRevokedSessionStore, BackChannelLogoutTokenValidatorGranit.Authentication.JwtBearer
ClaimsKeycloakClaimsTransformationGranit.Authentication.Keycloak
ClaimsEntraIdClaimsTransformationGranit.Authentication.EntraId
ClaimsCognitoClaimsTransformationGranit.Authentication.Cognito
AuthorizationIPermissionDefinitionProvider, IPermissionDefinitionManager, IPermissionChecker, IPermissionGrantStore, PermissionAttributeGranit.Authorization
Authorization optionsGranitAuthorizationOptions, JwtBearerAuthOptions, KeycloakOptions, CognitoOptions
EF CorePermissionGrant, IPermissionGrantDbContext, IPermissionManagerReader, IPermissionManagerWriterGranit.Authorization.EntityFrameworkCore
ExtensionsAddGranitJwtBearer(), AddGranitKeycloak(), AddGranitCognito(), AddGranitAuthorization(), AddGranitAuthorizationEntityFrameworkCore<T>(), MapBackChannelLogout()