Skip to content

bugfix/AB#32738 - ABP Cached Permission Checking with Keycloak OIDC (ARCH-SPIKE - DO NOT MERGE)#2370

Draft
plavoie-BC wants to merge 8 commits into
devfrom
bugfix/AB#32738-SPIKE-abp-cached-permissions
Draft

bugfix/AB#32738 - ABP Cached Permission Checking with Keycloak OIDC (ARCH-SPIKE - DO NOT MERGE)#2370
plavoie-BC wants to merge 8 commits into
devfrom
bugfix/AB#32738-SPIKE-abp-cached-permissions

Conversation

@plavoie-BC
Copy link
Copy Markdown
Contributor

@plavoie-BC plavoie-BC commented Apr 30, 2026

Pull request overview

This PR is an architecture spike to migrate Unity’s authorization from “permissions stamped into the auth cookie as claims” to ABP’s standard server-side permission evaluation via IPermissionChecker/IPermissionStore (Redis-cached), reducing cookie size and aligning with ABP permission patterns.

Changes:

  • Removes permission-claim stamping at login and deletes the custom claim-based PermissionChecker.
  • Simplifies authorization policy registration to rely on ABP’s permission policy provider, while adding custom composite handlers for “role OR permission” and “any-of permissions” scenarios.
  • Adds/updates seed logic and tests to support ABP-standard claim types (notably AbpClaimTypes.UserId and AbpClaimTypes.Role) and composite authorization behaviors.

Migration Plan: ABP Cached Permission Checking with Keycloak OIDC

Problem

The ASP.NET authentication cookie is ~14KB (4 chunks) because ~100+ Permission claims are stamped on the ClaimsPrincipal at login time. The custom PermissionChecker reads these claims directly from the cookie, bypassing ABP's built-in Redis-cached permission store.

Goal

Use ABP's default PermissionChecker which resolves permissions server-side via IPermissionStore (Redis-cached), eliminating permission claims from the cookie while keeping Keycloak as the external OIDC identity provider.

Architecture (Current vs Proposed)

Current Flow

Keycloak → OIDC → OnTokenValidated
  → Find ABP IdentityUser by OidcSub
  → Load ALL permissions from DB → stamp as "Permission" claims on principal
  → Cookie stores full ClaimsPrincipal (~14KB, 146 claims)
  → Runtime: Custom PermissionChecker reads "Permission" claims from cookie (zero DB calls)

Proposed Flow

Keycloak → OIDC → OnTokenValidated
  → Find ABP IdentityUser by OidcSub
  → Add AbpClaimTypes.UserId + AbpClaimTypes.Role to principal
  → Cookie stores only: UserId, TenantId, Roles, DisplayName (~2-3KB, 1 chunk)
  → Runtime: ABP PermissionChecker queries Redis by UserId/Role (~1ms per check)

Steps

Step 1: Add ABP-Standard Claims in Login Handlers

In IdentityProfileLoginBase.AssignDefaultClaims:

  • Change principal.AddClaim("UserId", userId.ToString())principal.AddClaim(AbpClaimTypes.UserId, userId.ToString())

In IdentityProfileLoginUserHandler, when adding roles:

  • Add principal.AddClaim(AbpClaimTypes.Role, dbRole.Name) alongside the existing UnityClaimsTypes.Role claim

Step 2: Remove Permission-Stamping Code

In IdentityProfileLoginUserHandler.Handle():

  • Remove the block that calls PermissionManager.GetAllForUserAsync() and adds "Permission" claims
  • Remove calls to AssignDefaultPermissions() and AssignITOperationsPermissions()

In IdentityProfileLoginAdminHandler.Handle():

  • Remove AssignAdminHostPermissions() call and the _adminPermissions array

Remove or deprecate:

  • IdentityExtensionMethods.AddPermission()
  • IdentityExtensionMethods.AddPermissions()

Step 3: Remove Custom PermissionChecker

Delete src/Unity.GrantManager.Web/Identity/PermissionChecker.cs — ABP's built-in PermissionChecker will automatically take over (it uses IPermissionValueProviderManager with UserPermissionValueProvider and RolePermissionValueProvider).

Step 4: Update Custom CurrentUser

In src/Unity.GrantManager.Web/Identity/CurrentUser.cs:

  • Change FindUserId() to read AbpClaimTypes.UserId instead of the custom "UserId" claim type

Step 5: Verify ABP Role-Based Permission Grants Exist

Ensure that permissions are properly assigned to ABP roles in the AbpPermissionGrants table (provider = "R", providerKey = roleName). The RolePermissionValueProvider will query these.

User-level grants (provider = "U", providerKey = userId) are already being created by the existing PermissionManager usage in the admin UI.

Step 6: Update JavaScript Permission Checks

Verify that abp.auth.isGranted() still works — it calls the /api/abp/application-configuration endpoint which uses IPermissionChecker server-side. This should work automatically once the custom checker is removed.

Trade-offs

Aspect Current (claims in cookie) Proposed (ABP default + Redis)
Cookie size ~14KB (4 chunks) ~2-3KB (1 chunk)
Permission changes Require re-login Effective immediately (cache TTL)
Runtime latency Zero (read from cookie) ~1ms Redis hit per unique permission
Server dependency None Redis must be available
Complexity Custom code to maintain Standard ABP pattern

Files to Modify

  • src/Unity.GrantManager.Web/Identity/LoginHandlers/IdentityProfileLoginBase.cs
  • src/Unity.GrantManager.Web/Identity/LoginHandlers/IdentityProfileLoginUserHandler.cs
  • src/Unity.GrantManager.Web/Identity/LoginHandlers/IdentityProfileLoginAdminHandler.cs
  • src/Unity.GrantManager.Web/Identity/CurrentUser.cs
  • src/Unity.GrantManager.Web/Identity/IdentityExtensionMethods.cs

Files to Delete

  • src/Unity.GrantManager.Web/Identity/PermissionChecker.cs

Risks & Considerations

  1. Redis availability — If Redis is down, permission checks will fall back to DB queries (slower but functional)
  2. Existing client_roles usage — The custom CurrentUser.Roles reads "client_roles" claims; need to verify nothing else depends on this claim type for authorization
  3. Tag helpers / Razor — The ZoneRequirementType.PermissionOnly tag helper likely calls IPermissionChecker — verify it still works
  4. Multi-tenant — ABP's PermissionStore respects CurrentTenant automatically; no change needed
  5. Test coverage — Update Unity.GrantManager.Web.Tests to remove reliance on permission claims in test principals

Co-authored-by: Copilot <copilot@github.com>
@plavoie-BC
Copy link
Copy Markdown
Contributor Author

Here's a summary of what was done:

Changes Made

Files deleted:

  • Identity/PermissionChecker.cs — custom claims-based checker removed; ABP's default (Redis-cached) takes over
  • Identity/IdentityExtensionMethods.cs — AddPermission()/AddPermissions() no longer needed

Files created:

  • Identity/Authorization/PermissionOrAuthorizationHandler.cs — handles OR-style policies via IPermissionChecker
  • Identity/Authorization/RoleOrPermissionAuthorizationHandler.cs — handles IT Admin/Operations role-or-permission policies

Files modified:

  • Identity/LoginHandlers/IdentityProfileLoginBase.cs — uses AbpClaimTypes.UserId instead of custom "UserId" claim; removed PermissionManager dependency
  • Identity/LoginHandlers/IdentityProfileLoginUserHandler.cs — removed all permission stamping; adds AbpClaimTypes.Role alongside client_roles
  • Identity/LoginHandlers/IdentityProfileLoginAdminHandler.cs — removed _adminPermissions array and AssignAdminHostPermissions()
  • Identity/CurrentUser.cs — reads AbpClaimTypes.UserId instead of "UserId"
  • Identity/PolicyRegistrant.cs — removed all RequireClaim("Permission", ...) policies (ABP handles them automatically); kept only the 4 custom composite policies using the new handlers

Estimated cookie reduction: ~14KB → ~2-3KB (from 146 claims to ~20 claims)

Key prerequisite: Ensure permissions are correctly assigned to roles in the AbpPermissionGrants table (provider "R", providerKey = role name). The RolePermissionValueProvider will now query these via Redis cache at runtime.

@plavoie-BC plavoie-BC requested a review from AndreGAot April 30, 2026 17:12
@github-actions
Copy link
Copy Markdown

🧪 Unit Test Results (Parallel Execution)

Tests

📊 Summary

Result Count
✅ Passed 631
❌ Failed 0
⚠️ Skipped 0

📄 HTML Reports

  • Merged Tests (HTML): Included in artifacts
    Generated automatically by CI.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR is an architecture spike to migrate permission checking from “permissions stamped into the auth cookie” to ABP’s default server-side permission resolution (Redis-cached IPermissionStore) while keeping Keycloak OIDC authentication.

Changes:

  • Removes the custom cookie-claim-based IPermissionChecker implementation and the permission-claim stamping during login.
  • Switches login claim stamping to ABP-standard claim types (AbpClaimTypes.UserId, adds AbpClaimTypes.Role).
  • Simplifies policy registration to rely on ABP’s permission policy provider, keeping only custom composite policies via new authorization requirements/handlers.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/PolicyRegistrant.cs Removes per-permission policy registrations; registers only composite/custom policies using new requirements.
applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/PermissionChecker.cs Deletes the custom claim-based permission checker so ABP’s default checker can take over.
applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/LoginHandlers/IdentityProfileLoginUserHandler.cs Stops stamping permissions onto principals; adds AbpClaimTypes.Role alongside the existing role claim type.
applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/LoginHandlers/IdentityProfileLoginBase.cs Changes stamped user-id claim from legacy "UserId" to AbpClaimTypes.UserId.
applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/LoginHandlers/IdentityProfileLoginAdminHandler.cs Removes host-admin permission stamping during login.
applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/IdentityExtensionMethods.cs Removes extension methods used for adding permission claims.
applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/CurrentUser.cs Updates custom current-user lookup to read AbpClaimTypes.UserId.
applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/Authorization/RoleOrPermissionAuthorizationHandler.cs Adds requirement/handler for “role OR permission” composite policies.
applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/Authorization/PermissionOrAuthorizationHandler.cs Adds requirement/handler for “any-of these permissions” composite policies.
Comments suppressed due to low confidence (1)

applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/LoginHandlers/IdentityProfileLoginUserHandler.cs:76

  • This handler no longer stamps any baseline permissions (e.g., the previously hard-coded IdentityPermissions.UserLookup.Default). If the equivalent grants are not present in AbpPermissionGrants for the user’s roles, calls to identity user lookup (used by assignee selection and other UI flows via IIdentityUserIntegrationService) will start failing with authorization errors. Ensure the needed permissions are granted via role/user permission seeding before relying on ABP’s server-side permission checks.
            using (CurrentTenant.Change(userTenantAccount.TenantId))
            {
                var userRoles = await IdentityUserRepository.GetRolesAsync(userTenantAccount.Id);

                if (userRoles != null)
                {
                    foreach (var role in userRoles)
                    {
                        var dbRole = await IdentityRoleManager.GetByIdAsync(role.Id);
                        principal.AddClaim(UnityClaimsTypes.Role, dbRole.Name);
                        principal.AddClaim(AbpClaimTypes.Role, dbRole.Name);
                    }
                }
            }

            AssignDefaultClaims(validatedTokenContext.Principal!, userTenantAccount.DisplayName ?? string.Empty, userTenantAccount.Id);

            validatedTokenContext.Principal!.AddClaim(AbpClaimTypes.TenantId, userTenantAccount.TenantId?.ToString() ?? Guid.Empty.ToString());

Comment on lines 28 to 33
protected static void AssignDefaultClaims(ClaimsPrincipal claimsPrinicipal, string displayName, Guid userId)
{
claimsPrinicipal.AddClaim("DisplayName", displayName);
claimsPrinicipal.AddClaim("UserId", userId.ToString());
claimsPrinicipal.AddClaim(AbpClaimTypes.UserId, userId.ToString());
claimsPrinicipal.AddClaim("Badge", Utils.CreateUserBadge(displayName));
}
Comment on lines 20 to 33
UserTenantAccountDto userTenantAccount;
if (!AdminHasUserAccount(userTenantAccounts))
{
// Create Host Account
userTenantAccount = await CreateAdminAccountAsync(validatedTokenContext, idp);
}
else
{
userTenantAccount = userTenantAccounts.First(s => s.TenantId == null);
}

AssignAdminHostPermissions(validatedTokenContext.Principal!);
AssignDefaultClaims(validatedTokenContext.Principal!, userTenantAccount.DisplayName ?? string.Empty, userTenantAccount.Id);
return userTenantAccount;
}
Comment on lines +27 to +38
protected override async Task HandleRequirementAsync(
AuthorizationHandlerContext context,
PermissionOrRequirement requirement)
{
foreach (var permission in requirement.Permissions)
{
if (await _permissionChecker.IsGrantedAsync(context.User, permission))
{
context.Succeed(requirement);
return;
}
}
@plavoie-BC
Copy link
Copy Markdown
Contributor Author

Remediation Plan: ITAdministrator and ITOperations - Seed Host Admin Permission Grants

Extend the existing PermissionGrantsDataSeeder to seed host-level (tenantId=null) permission grants for the ITAdministrator and ITOperations roles. These permissions were previously stamped directly onto the authentication cookie by AssignAdminHostPermissions. After migrating to ABP's cached permission store, they must exist in AbpPermissionGrants for ABP's RolePermissionValueProvider to resolve them.


Steps

Phase 1 — Domain (PermissionGrantsDataSeeder)

  1. Extend SeedAsync in PermissionGrantsDataSeeder.cs — Add a host-level seeding branch gated on context.TenantId == null. Currently all seeds pass context.TenantId (tenant-scoped). Add a block that handles host seeding and return early.

  2. Seed ITAdministrator role permissions (host-level)_permissionDataSeeder.SeedAsync(RolePermissionValueProvider.ProviderName, "ITAdministrator", [...], tenantId: null) with:

    • TenantManagementPermissions.Tenants.Default
    • TenantManagementPermissions.Tenants.Create
    • TenantManagementPermissions.Tenants.Update
    • TenantManagementPermissions.Tenants.Delete
    • TenantManagementPermissions.Tenants.ManageFeatures
    • TenantManagementPermissions.Tenants.ManageConnectionStrings
    • IdentitySeedPermissions.Users.Create
    • IdentitySeedPermissions.UserLookup.Default
    • IdentityConsts.ITAdminPermissionName
  3. Seed ITOperations role permissions (host-level) — same pattern with:

    • GrantManagerPermissions.Endpoints.ManageEndpoints
    • IdentityConsts.ITOperationsPermissionName

Phase 2 — Admin Login Handler (parallel)

  1. Add role claim to admin principal in IdentityProfileLoginAdminHandler.cs — After AssignDefaultClaims, add principal.AddClaim(AbpClaimTypes.Role, IdentityConsts.ITAdminRoleName) so ABP's RolePermissionValueProvider can resolve granted permissions by role name during the session.

Phase 3 — Verification

  1. dotnet build Domain project and Web project
  2. Manual/integration: Admin login → Tenant Management menu visible → CRUD accessible

Key facts:

  • No EF migration needed (data-only change to existing AbpPermissionGrants table)
  • IPermissionDataSeeder.SeedAsync is idempotent — inserts if absent, no-ops if exists
  • ITAdmin/ITOperations roles are NOT created in host AbpRoles — they're resolved from Keycloak JWT role claims. ABP's RolePermissionValueProvider looks up grants by role name string
  • The existing seeder already has the exact same calling pattern for tenant roles — just replicate for host

Risks:

  • PermissionGrantsDataSeeder must actually be invoked in a host context. Verify DataSeedContext with TenantId == null reaches this seeder (DbMigrator runs at both host and tenant levels)

Ready for your review. Should I proceed with implementation?

plavoie-BC and others added 2 commits April 30, 2026 10:54
Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <copilot@github.com>
@github-actions
Copy link
Copy Markdown

🧪 Unit Test Results (Parallel Execution)

Tests

📊 Summary

Result Count
✅ Passed 631
❌ Failed 0
⚠️ Skipped 0

📄 HTML Reports

  • Merged Tests (HTML): Included in artifacts
    Generated automatically by CI.

@plavoie-BC plavoie-BC changed the title [ARCH-SPIKE] ABP Cached Permission Checking with Keycloak OIDC (DO NOT MERGE) bugfix/AB#32738 - ABP Cached Permission Checking with Keycloak OIDC (ARCH-SPIKE - DO NOT MERGE) Apr 30, 2026
@github-actions
Copy link
Copy Markdown

🧪 Unit Test Results (Parallel Execution)

Tests

📊 Summary

Result Count
✅ Passed 651
❌ Failed 0
⚠️ Skipped 0

📄 HTML Reports

  • Merged Tests (HTML): Included in artifacts
    Generated automatically by CI.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR is an architecture spike to migrate Unity’s authorization from “permissions stamped into the auth cookie as claims” to ABP’s standard server-side permission evaluation via IPermissionChecker/IPermissionStore (Redis-cached), reducing cookie size and aligning with ABP permission patterns.

Changes:

  • Removes permission-claim stamping at login and deletes the custom claim-based PermissionChecker.
  • Simplifies authorization policy registration to rely on ABP’s permission policy provider, while adding custom composite handlers for “role OR permission” and “any-of permissions” scenarios.
  • Adds/updates seed logic and tests to support ABP-standard claim types (notably AbpClaimTypes.UserId and AbpClaimTypes.Role) and composite authorization behaviors.

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/PolicyRegistrant.cs Drops explicit claim-based permission policies and registers only composite policies using new requirements/handlers.
applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/Authorization/RoleOrPermissionAuthorizationHandler.cs Adds an authorization requirement/handler for role-or-permission policies.
applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/Authorization/PermissionOrAuthorizationHandler.cs Adds an authorization requirement/handler for “any granted permission in set” policies (batch API).
applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/LoginHandlers/IdentityProfileLoginBase.cs Switches to AbpClaimTypes.UserId while retaining legacy UserId claim.
applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/LoginHandlers/IdentityProfileLoginUserHandler.cs Stops loading/stamping permissions into the principal; adds ABP role claims.
applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/LoginHandlers/IdentityProfileLoginAdminHandler.cs Stops stamping admin host permissions; adds ABP role claim for IT admin.
applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/CurrentUser.cs Updates user id resolution to read AbpClaimTypes.UserId.
applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Permissions/PermissionGrantsDataSeeder.cs Adds host-level permission seeding intended to replace login-time permission stamping.
applications/Unity.GrantManager/src/Unity.GrantManager.Application/Assessments/AssessmentAuthorizationHandler.cs Updates user id claim lookup to prefer AbpClaimTypes.UserId with legacy fallback.
applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/PermissionChecker.cs Deletes the custom claim-based IPermissionChecker implementation.
applications/Unity.GrantManager/src/Unity.GrantManager.Web/Identity/IdentityExtensionMethods.cs Deletes permission-claim helper extensions no longer used.
applications/Unity.GrantManager/test/Unity.GrantManager.Web.Tests/Identity/Authorization/RoleOrPermissionAuthorizationHandlerTests.cs Adds unit tests for role-or-permission handler behavior.
applications/Unity.GrantManager/test/Unity.GrantManager.Web.Tests/Identity/Authorization/PermissionOrAuthorizationHandlerTests.cs Adds unit tests for “any-of” permission handler using batch permission checks.
applications/Unity.GrantManager/test/Unity.GrantManager.Application.Tests/Assessments/FindUserIdirIdTests.cs Adds tests covering new/legacy claim fallback behavior for user id extraction.

// ITOperations host-level permissions (previously stamped at login)
await _permissionDataSeeder.SeedAsync(RolePermissionValueProvider.ProviderName, IdentityConsts.ITOperationsRoleName,
[
GrantManagerPermissions.Endpoints.ManageEndpoints,
Comment on lines +13 to +15
// Simple permission-based policies (e.g., [Authorize("PermissionName")]) are handled
// automatically by ABP's AbpAuthorizationPolicyProvider + PermissionRequirementHandler.
// Only register custom composite policies here.
Comment on lines +35 to +42
var identity = new ClaimsIdentity("TestAuth");
identity.AddClaim(new Claim(ClaimTypes.Role, roleName));
return new ClaimsPrincipal(identity);
}

private static ClaimsPrincipal CreateUserWithoutRole()
{
var identity = new ClaimsIdentity("TestAuth");
Comment on lines +348 to +353
"UnityTenantManagement.Tenants",
"UnityTenantManagement.Tenants.Create",
"UnityTenantManagement.Tenants.Update",
"UnityTenantManagement.Tenants.Delete",
"AbpTenantManagement.Tenants.ManageFeatures",
"UnityTenantManagement.Tenants.ManageConnectionStrings",
@github-actions
Copy link
Copy Markdown

🧪 Unit Test Results (Parallel Execution)

Tests

📊 Summary

Result Count
✅ Passed 651
❌ Failed 0
⚠️ Skipped 0

📄 HTML Reports

  • Merged Tests (HTML): Included in artifacts
    Generated automatically by CI.

@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud Bot commented May 7, 2026

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 7, 2026

🧪 Unit Test Results (Parallel Execution)

Tests

📊 Summary

Result Count
✅ Passed 652
❌ Failed 0
⚠️ Skipped 0

📄 HTML Reports

  • Merged Tests (HTML): Included in artifacts
    Generated automatically by CI.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants