TCM.InAppAuth 1.0.23

dotnet add package TCM.InAppAuth --version 1.0.23
                    
NuGet\Install-Package TCM.InAppAuth -Version 1.0.23
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="TCM.InAppAuth" Version="1.0.23" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="TCM.InAppAuth" Version="1.0.23" />
                    
Directory.Packages.props
<PackageReference Include="TCM.InAppAuth" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add TCM.InAppAuth --version 1.0.23
                    
#r "nuget: TCM.InAppAuth, 1.0.23"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package TCM.InAppAuth@1.0.23
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=TCM.InAppAuth&version=1.0.23
                    
Install as a Cake Addin
#tool nuget:?package=TCM.InAppAuth&version=1.0.23
                    
Install as a Cake Tool

TCM.InAppAuth

Shared library for in-app RBAC (feature permissions) and data scope (org tree + row-level filtering) across TCM services. Identity comes from Keycloak SSO; roles, org structure, and permission matrices are managed in the InAppAuth database.

Features

  • Dynamic permission matrix: Role × Resource slug × Action ([AppPermission])
  • Multi-app isolation via AppId in resource slug (pre-planning:buyer)
  • Org unit tree (admin CRUD, user assignment, merge)
  • Data scope per role/resource (IDataScopeService) with kill switch per resource
  • GET /permissions/me for client-side menu guards (server still enforces everything)
  • Permission audit log for sensitive admin changes

Installation

<ProjectReference Include="..\..\shareds\TCM.InAppAuth\TCM.InAppAuth.csproj" />

Configuration

Environment / appsettings

Key Description
IN_APP_AUTH_CONNTECTION_STRING SQL Server connection string (Development: appsettings key of the same name)
IN_APP_AUTH_ENABLED true to enforce [AppPermission] (default off in Development unless set)
IN_APP_AUTH_APP_ID App id for resource slug prefix (e.g. PrePlanningpre-planning:controller)

Example appsettings.Development.json in the host API:

{
  "IN_APP_AUTH_CONNTECTION_STRING": "Server=...;Database=InAppAuth;...",
  "IN_APP_AUTH_ENABLED": true,
  "IN_APP_AUTH_APP_ID": "PrePlanning"
}

Service registration

In Program.cs:

using TCM.InAppAuth.Extensions;

builder.Services.AddInAppAuthentication(builder.Configuration);

// If the host already calls AddControllers():
builder.Services.AddControllers()
    .AddInAppAuthControllers();

// Pipeline (same as other TCM APIs)
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();

AddInAppAuthentication registers DbContext, MediatR, AutoMapper, IPermissionService, IDataScopeService, IOrgTreeService, repositories, and InAppAuth API controllers.

Database migration

From the TCM.InAppAuth project folder:

dotnet ef database update --context InAppAuthDbContext

Latest migration adds: OrgUnit, AppUserOrgUnit, OrgTreeMeta, RoleResourceDataScope, ResourceDataScopeConfig, PermissionAuditLog, and AppUser.PermissionVersion.

Feature permissions (RBAC)

Controller

[AppPermission]                           // auto: GET list → index, GET {id} → show, etc.
[AppPermission("import-excel")]           // explicit action
[AppPermission("PrePlanning", "sync")]    // app-scoped slug + action (if not using env AppId)

Admin realm role admin in Keycloak JWT bypasses all checks. Internal role slug admin in InAppAuth DB also bypasses.

Sync resources from host assembly

Call POST /api/v1/Resource/sync (with appropriate permission) after deploy so new controllers appear in the admin matrix.

Data scope (row-level)

Concepts

Scope type Meaning
All All rows (admin / reporting)
Own CreatedUserId == current user (Guid from JWT sub)
PrimaryOrgUnit Rows with OrgUnitId = user's primary assignment
AssignedOrgUnits Union of all assigned org units
OrgUnitAndDescendants Assigned/primary units + subtree (by OrgUnit.Path)
ExplicitOrgUnits Fixed list in ExplicitOrgUnitIds JSON

Union across roles: any role with All wins; otherwise org ids and Own are unioned. Rows with OrgUnitId and CreatedUserId both null are visible only to All.

Domain entity contract (TCM.Core)

public class Order : BaseGuidEntity, IScopedEntity
{
    public Guid? OrgUnitId { get; set; }
    // CreatedUserId inherited — same Keycloak subject as AppUser.UserId
}

Enforcement modes (ResourceDataScopeConfig)

Mode Behavior
Off (default) No filtering; IDataScopeService returns AllowAll
LogOnly Resolve scope but do not filter (for impact analysis)
Enforce Apply ApplyFilter / CanAccess rules

Set per resource slug via PUT /api/v1/DataScope/resources/Enforcement.

Handler integration pattern

using TCM.Core.Abstractions;
using TCM.Core.Services;
using TCM.InAppAuth.Extensions;
using TCM.InAppAuth.Models.DataScope;
using TCM.InAppAuth.Services;

public class GetOrderQueryHandler
{
    private readonly IDataScopeService _dataScope;
    private readonly IAuthenticatedUserService _auth;
    private readonly IOrderRepository _repo;

    public async Task<OrderDto?> Handle(Guid id, CancellationToken ct)
    {
        var slug = AppPermissionAttribute.BuildResourceSlug(_appId, "order");
        var scope = HttpContext?.GetDataScope()
            ?? await _dataScope.ResolveAsync(slug, PermissionAction.Show, cancellationToken: ct);

        var order = await _repo.GetByIdAsync(id);
        if (order == null || !_dataScope.CanAccess(order, scope, _auth.UserId))
            return null; // return 404 from controller — do not leak existence

        return Map(order);
    }

    public async Task<PagedList<OrderDto>> List(CancellationToken ct)
    {
        var slug = AppPermissionAttribute.BuildResourceSlug(_appId, "order");
        var scope = await _dataScope.ResolveAsync(slug, PermissionAction.Index, cancellationToken: ct);

        var query = _dataScope.ApplyFilter(_repo.Query(), scope, _auth.UserId);
        return await query.ToPagedListAsync(ct);
    }
}

After [AppPermission], scope is also in HttpContext.Items under IDataScopeService.HttpContextItemKey — use HttpContext.GetDataScope() extension.

Create

Default: set OrgUnitId from user's primary org. If user has no primary org and scope is not All, return 400 with a clear message. Frontend can use GET /api/v1/AppUser/me/org-units (MissingPrimaryOrgWarning flag).

Bulk update/delete

Always call ApplyFilter on IQueryable before ExecuteUpdate / ExecuteDelete.

Cross-resource FKs

Scope-check only the root entity for the API resource. Do not re-check FK entities in the same response (use projection if you need to hide fields).

Domain-specific scope augmenter

IDataScopeAugmenter<T> is an extension hook for services whose access rules go beyond OrgUnitId + CreatedUserId — typically workflow approval (assignee, approvers, delegates) or any rule computed from domain-specific data InAppAuth does not own.

Augmenter predicates are OR-combined with the base scope. They run only when the base scope is actually filtering (skipped when Off, when AllowAll = true, and when LogOnly forces full access). Return null from BuildPredicate when no rule applies for the current user/context.

Contract

using System.Linq.Expressions;
using TCM.Core.Abstractions;
using TCM.InAppAuth.Models.DataScope;
using TCM.InAppAuth.Services;

public interface IDataScopeAugmenter<T> where T : class, IScopedEntity
{
    Expression<Func<T, bool>>? BuildPredicate(DataScopeContext context, Guid? currentUserId);
    bool CanAccess(T record, DataScopeContext context, Guid? currentUserId);
}

Example: workflow participant table

public class PurchaseRequestWorkflowAugmenter : IDataScopeAugmenter<PurchaseRequest>
{
    public Expression<Func<PurchaseRequest, bool>>? BuildPredicate(
        DataScopeContext context, Guid? currentUserId)
    {
        if (currentUserId == null) return null;
        var userId = currentUserId.Value;
        return pr => pr.Participants.Any(p => p.UserId == userId && p.IsActive);
    }

    public bool CanAccess(PurchaseRequest record, DataScopeContext context, Guid? currentUserId)
        => currentUserId != null
           && record.Participants.Any(p => p.UserId == currentUserId.Value && p.IsActive);
}

Registration

builder.Services.AddInAppAuthentication(builder.Configuration);
builder.Services.AddDataScopeAugmenter<PurchaseRequest, PurchaseRequestWorkflowAugmenter>();

Constraints

  • BuildPredicate must be EF Core translatable — no client-side method calls inside the expression.
  • Return null (not _ => false) when the augmenter has no rule to contribute.
  • Multiple augmenters per entity are allowed; predicates are OR-combined in registration order.
  • Augmenters are not consulted when scope is Off / AllowAll = true / LogOnly; use this only to expand the base scope, not to restrict it.

See documents/inappauth-workflow-participant-scope-guide.md for patterns (participant table, workflow engine join, derived field, snapshot list), checklist, and pitfalls.

Admin APIs (InAppAuth host)

Method Path Purpose
GET /api/v1/OrgUnit/tree Org tree for UI
POST /api/v1/OrgUnit Create node
PUT /api/v1/OrgUnit/{id} Update name/sort/active
PATCH /api/v1/OrgUnit/{id}/move Change parent (updates paths)
POST /api/v1/OrgUnit/merge Merge assignments into target (domain rows unchanged)
POST /api/v1/OrgUnit/split Split source into new sibling nodes + move selected users
GET /api/v1/OrgUnit/{id}/users List users in org unit (paginated; optional includeDescendants)
POST /api/v1/OrgUnit/{id}/users Add multiple users to an org unit (skips already assigned)
PUT /api/v1/AppUser/{id}/org-units Replace user org assignments
GET /api/v1/AppUser/me/org-units Current user's orgs + warning flag
GET /api/v1/Permission/me Permissions + dataScope + org list
POST /api/v1/DataScope/roles/Get Role data scopes
PUT /api/v1/DataScope/roles/Update Replace role data scopes
GET /api/v1/DataScope/resources/Enforcement List enforcement configs
PUT /api/v1/DataScope/resources/Enforcement Kill switch per resource
POST /api/v1/DataScope/MigrateAllowOwnOnly Copy AllowOwnOnly permissions → RoleResourceDataScope (Own)
GET /api/v1/DataScope/admin/audit-logs Permission change audit

Client login flow

  1. User authenticates with Keycloak.
  2. POST /api/v1/AppUser/sync — upsert AppUser from token.
  3. GET /api/v1/Permission/me — cache matrix + org units + MissingPrimaryOrgWarning.
  4. On each API call, server runs [AppPermission] then optional data scope in handlers.

Example /permissions/me fragment:

{
  "isAdmin": false,
  "missingPrimaryOrgWarning": true,
  "orgUnits": [{ "id": "...", "name": "Sales HCM", "isPrimary": true }],
  "permissions": [
    {
      "resourceSlug": "pre-planning:buyer",
      "action": "show",
      "allowOwnOnly": false,
      "dataScope": "OrgUnitAndDescendants"
    }
  ]
}

Caching

  • Org tree version: OrgTreeMeta.LastTreeChangeAt (bumped on tree changes).
  • Per user: AppUser.PermissionVersion (bumped on role/org assignment changes).
  • Include these in cache keys when caching resolved org ids or scope outside a request.

Migrating from AllowOwnOnly

  1. Configure RoleResourceDataScope with ScopeType = Own for the same role/resource rows.
  2. Keep feature Permission rows granted as today.
  3. Set ResourceDataScopeConfig.EnforcementMode to LogOnly, then Enforce when ready.
  4. HttpContext.Items["OwnOnly"] remains set when scope includes Own (backward compatible).

Keycloak scope

  • Used: SSO user + realm role admin.
  • Not used: Keycloak groups for data scope (AuthorizePath / ViewPath / Groups are obsolete — use IDataScopeService).

Project layout

TCM.InAppAuth/
├── Attributes/AppPermissionAttribute.cs
├── Controllers/v1/          # AppUser, Role, Permission, Resource, OrgUnit, DataScope
├── Entities/                # AppUser, OrgUnit, RoleResourceDataScope, ...
├── Services/                # PermissionService, DataScopeService, OrgTreeService
├── Extensions/              # AddInAppAuthentication, ApplyDataScope, HttpContext helpers
└── Migrations/

Pilot: TCM.Tasks

ScheduleTask implements IScopedEntity (OrgUnitId). Host must register both AddInAppAuthentication and AddTCMTasks, with matching IN_APP_AUTH_APP_ID / task AppId.

Resource slug: {app-id-kebab}:schedule-task (e.g. tasks:schedule-task).

After deploy:

  1. POST /Resource/sync — registers schedule-task actions including run-now.
  2. Grant role permissions + optional RoleResourceDataScope.
  3. POST /DataScope/MigrateAllowOwnOnly if migrating from legacy AllowOwnOnly.
  4. Set ResourceDataScopeConfig to LogOnly then Enforce when ready.

See TCM.Tasks/README.md for task-specific setup.

Further reading

Architecture decisions and phase 5 (HR sync): documents/inappauth-data-permission-and-org-structure-plan.md in the shareds repo.

Product Compatible and additional computed target framework versions.
.NET net7.0 is compatible.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on TCM.InAppAuth:

Package Downloads
TCM.Tasks

Task scheduling library for TCM projects, built on Quartz.NET.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.23 0 6/8/2026
1.0.22 46 6/6/2026
1.0.21 122 6/2/2026
1.0.20 112 6/2/2026
1.0.19 93 5/28/2026
1.0.18 92 5/28/2026
1.0.17 96 5/26/2026
1.0.16 126 5/26/2026
1.0.15 111 5/24/2026
1.0.14 88 5/24/2026
1.0.13 85 5/24/2026
1.0.12 90 5/24/2026
1.0.11 90 5/24/2026
1.0.10 98 5/24/2026
1.0.9 103 5/23/2026
1.0.8 98 5/23/2026
1.0.7 98 5/23/2026
1.0.6 99 5/19/2026
1.0.5 90 5/18/2026
1.0.4 92 5/16/2026
Loading failed