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.
Package structure
Section titled “Package structure”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
| Package | Role | Depends on |
|---|---|---|
Granit.Security | ICurrentUserService, ActorKind | Granit.Core |
Granit.Authentication.JwtBearer | JWT Bearer middleware, back-channel logout | Granit.Security |
Granit.Authentication.Keycloak | Keycloak claims transformation | Granit.Authentication.JwtBearer |
Granit.Authentication.EntraId | Entra ID roles parsing | Granit.Authentication.JwtBearer |
Granit.Authentication.Cognito | Cognito groups → roles | Granit.Authentication.JwtBearer |
Granit.Authorization | RBAC permissions, dynamic policy provider | Granit.Security, Granit.Caching |
Granit.Authorization.EntityFrameworkCore | EF Core permission grant store | Granit.Authorization, Granit.Persistence |
Dependency graph
Section titled “Dependency graph”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" }}[DependsOn(typeof(GranitAuthenticationEntraIdModule))][DependsOn(typeof(GranitAuthorizationModule))]public class AppModule : GranitModule { }{ "Authentication": { "Authority": "https://login.microsoftonline.com/{tenant-id}/v2.0", "Audience": "api://{client-id}" }}[DependsOn(typeof(GranitAuthenticationCognitoModule))][DependsOn(typeof(GranitAuthorizationModule))]public class AppModule : GranitModule { }{ "Authentication": { "Authority": "https://cognito-idp.{region}.amazonaws.com/{userPoolId}", "Audience": "{clientId}" }, "Cognito": { "UserPoolId": "eu-west-1_XXXXXXXXX", "ClientId": "my-client-id", "Region": "eu-west-1" }}[DependsOn(typeof(GranitJwtBearerModule))][DependsOn(typeof(GranitAuthorizationModule))]public class AppModule : GranitModule { }{ "Authentication": { "Authority": "https://idp.example.com", "Audience": "my-api", "RequireHttpsMetadata": true, "NameClaimType": "sub" }}[DependsOn(typeof(GranitAuthenticationKeycloakModule))][DependsOn(typeof(GranitAuthorizationEntityFrameworkCoreModule))]public class AppModule : GranitModule{ public override void ConfigureServices(ServiceConfigurationContext context) { context.Services.AddGranitAuthorizationEntityFrameworkCore<AppDbContext>(); }}ICurrentUserService
Section titled “ICurrentUserService”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).
Authentication
Section titled “Authentication”JWT Bearer
Section titled “JWT Bearer”GranitJwtBearerModule registers:
- ASP.NET Core JWT Bearer authentication
CurrentUserService—ICurrentUserServiceimplementation extracting claims fromHttpContextIRevokedSessionStore— distributed cache-backed session revocation
Configuration
Section titled “Configuration”{ "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" } }}| Property | Default | Description |
|---|---|---|
Authority | — | OIDC issuer URL (required) |
Audience | — | Expected aud claim (required) |
RequireHttpsMetadata | true | Enforce HTTPS for metadata endpoint |
NameClaimType | "sub" | Claim used as user identifier |
BackChannelLogout.Enabled | false | Enable OIDC back-channel logout |
BackChannelLogout.EndpointPath | "/auth/back-channel-logout" | Endpoint path |
BackChannelLogout.SessionRevocationTtl | "01:00:00" | How long revoked sessions are remembered |
Back-channel logout
Section titled “Back-channel logout”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 OnApplicationInitializationapp.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.
Keycloak claims transformation
Section titled “Keycloak claims transformation”GranitAuthenticationKeycloakModule post-configures JWT Bearer with Keycloak-specific behavior:
- Extracts roles from
realm_access.rolesorresource_access.{clientId}.roles - Maps them to standard
ClaimTypes.Roleclaims - 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"Entra ID claims transformation
Section titled “Entra ID claims transformation”GranitAuthenticationEntraIdModule post-configures JWT Bearer with Entra ID-specific behavior:
- Extracts roles from the v1.0
rolesclaim and the v2.0widsclaim - Maps them to standard
ClaimTypes.Roleclaims
Cognito claims transformation
Section titled “Cognito claims transformation”GranitAuthenticationCognitoModule post-configures JWT Bearer with Cognito-specific behavior:
- Extracts groups from the
cognito:groupsclaim (multiple claims with same type) - Maps them to standard
ClaimTypes.Roleclaims
// Cognito JWT payload — groups appear as repeated claims// "cognito:groups": "admin"// "cognito:groups": "doctors"// After transformation → ClaimTypes.Role: "admin", "doctors"Authorization
Section titled “Authorization”Permission definitions
Section titled “Permission definitions”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).
Permission checking pipeline
Section titled “Permission checking pipeline”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:
AlwaysAllowoption (development only)- Admin role bypass — users with any role in
AdminRolesget all permissions - Per-role cache lookup (key:
perm:{tenantId}:{roleName}:{permissionName}) IPermissionGrantStorefallback (default:NullPermissionGrantStore→ always denied)
Using permissions
Section titled “Using permissions”// Option 1: Attribute on endpointapp.MapGet("/invoices", [Permission("Invoices.Invoices.Read")] async ( AppDbContext db, CancellationToken cancellationToken) =>{ return await db.Invoices .ToListAsync(cancellationToken) .ConfigureAwait(false);});
// Option 2: Imperative checkpublic 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"); } // ... }}Configuration
Section titled “Configuration”{ "Authorization": { "AdminRoles": ["admin"], "CacheDuration": "00:05:00", "AlwaysAllow": false }}| Property | Default | Description |
|---|---|---|
AdminRoles | ["admin"] | Roles that bypass all permission checks |
CacheDuration | 00:05:00 | Per-role permission cache TTL |
AlwaysAllow | false | Skip all checks (development only) |
EF Core permission store
Section titled “EF Core permission store”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).
Public API summary
Section titled “Public API summary”| Category | Key types | Package |
|---|---|---|
| Module | GranitSecurityModule, GranitJwtBearerModule, GranitAuthenticationKeycloakModule, GranitAuthenticationEntraIdModule, GranitAuthenticationCognitoModule, GranitAuthorizationModule, GranitAuthorizationEntityFrameworkCoreModule | — |
| Abstractions | ICurrentUserService, ActorKind | Granit.Security |
| Authentication | CurrentUserService, IRevokedSessionStore, BackChannelLogoutTokenValidator | Granit.Authentication.JwtBearer |
| Claims | KeycloakClaimsTransformation | Granit.Authentication.Keycloak |
| Claims | EntraIdClaimsTransformation | Granit.Authentication.EntraId |
| Claims | CognitoClaimsTransformation | Granit.Authentication.Cognito |
| Authorization | IPermissionDefinitionProvider, IPermissionDefinitionManager, IPermissionChecker, IPermissionGrantStore, PermissionAttribute | Granit.Authorization |
| Authorization options | GranitAuthorizationOptions, JwtBearerAuthOptions, KeycloakOptions, CognitoOptions | — |
| EF Core | PermissionGrant, IPermissionGrantDbContext, IPermissionManagerReader, IPermissionManagerWriter | Granit.Authorization.EntityFrameworkCore |
| Extensions | AddGranitJwtBearer(), AddGranitKeycloak(), AddGranitCognito(), AddGranitAuthorization(), AddGranitAuthorizationEntityFrameworkCore<T>(), MapBackChannelLogout() | — |
See also
Section titled “See also”- Identity module — User management, Keycloak Admin API, user cache
- Privacy module — GDPR compliance, cookies, CORS
- Core module —
ICurrentTenant, exception hierarchy - Persistence module —
AuditedEntitybase class, interceptors - API Reference (auto-generated from XML docs)