Appearance
JWT Claims Reference
Section: 14 — Auth
Last Updated: 2026-05-30
Scope: Custom Keycloak claims, claim mapping SPIs, usage in services
Overview
Microtec ERP extends standard OIDC claims with custom claims added by two Keycloak protocol mapper SPIs. These claims are present in the Keycloak JWT and are read by the Auth Service when generating the ERP Access Context Token.
Standard OIDC Claims (Always Present)
| Claim | Type | Description | Example |
|---|---|---|---|
sub | string | Keycloak user UUID | "a1b2c3d4-..." |
email | string | User email | "ahmed@acme.sa" |
email_verified | bool | Email verified flag | true |
preferred_username | string | Username | "ahmed.ali" |
given_name | string | First name | "Ahmed" |
family_name | string | Last name | "Ali" |
iat | number | Issued at (Unix timestamp) | 1748560000 |
exp | number | Expiry (Unix timestamp) | 1748560900 |
iss | string | Issuer (realm URL) | "https://auth.../realms/microtec" |
aud | string/array | Audience | "erp-api" |
realm_access.roles | string[] | Keycloak realm roles | ["erp-user", "offline_access"] |
Custom ERP Claims
These claims are added by custom Keycloak protocol mapper SPIs deployed from the KeycloakProviders/ project.
| Claim | Type | Added By SPI | Description | Example |
|---|---|---|---|---|
tenant | string | erp-tenant-mapper | Tenant identifier (subdomain-based) | "acme-corp" |
subdomain | string | erp-tenant-mapper | Tenant subdomain | "acme" |
erp_id | string (UUID) | erp-user-mapper | Internal ERP user ID (not Keycloak sub) | "550e8400-..." |
companyid | string (UUID) | company-branch-mapper | Currently selected company GUID | "6ba7b810-..." |
branchId | string (UUID) | company-branch-mapper | Currently selected branch GUID | "7c9e6679-..." |
erp_policies | string | erp-policy-mapper | Comma-separated module:action permissions | "accounting:read,hr:write" |
account_ids | string[] | multi-account-switcher | All company IDs accessible to this user | ["6ba7b810-...", "550e8400-..."] |
Claim Details
tenant
The tenant identifier derived from the user's registered subdomain. This is the primary multi-tenancy discriminator.
json
{ "tenant": "acme-corp" }Mapping: Keycloak user attribute tenantId → JWT claim tenant.
subdomain
Short form of the tenant, used for URL construction and domain routing.
json
{ "subdomain": "acme" }erp_id
The internal ERP user ID (different from the Keycloak sub). This is used as the CreatedBy / UpdatedBy value in the AuditInterceptor.
json
{ "erp_id": "550e8400-e29b-41d4-a716-446655440000" }IMPORTANT
Services must use erp_id for audit trail attribution, not sub. The Keycloak sub is an IdP-internal identifier; erp_id is the business domain identifier.
companyid / branchId
The company and branch the user selected at login (or via context switch). These are baked into the Keycloak JWT via the company-branch-mapper SPI.
json
{
"companyid": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
"branchId": "7c9e6679-f89b-12d3-a456-426655440000"
}NOTE
companyid uses lowercase id (inconsistent casing — legacy). branchId uses camelCase. This is intentional and matches the SPI implementation. Do not "fix" casing in consumers.
erp_policies
A comma-separated list of module:action pairs representing the user's active permissions.
json
{ "erp_policies": "accounting:read,accounting:write,hr:read,inventory:read,inventory:write,inventory:export" }Parsed by ICurrentUserService.HasPermission(module, action):
csharp
if (!_currentUser.HasPermission("accounting", "write"))
return Result.Forbidden("You do not have permission to create journal entries");Possible Actions
| Action | Description |
|---|---|
read | View records |
write | Create and update records |
delete | Delete records |
export | Export data to Excel/PDF |
approve | Approve workflows |
admin | Module administration |
account_ids
All company IDs accessible to the user (for the multi-account switcher):
json
{ "account_ids": ["6ba7b810-...", "550e8400-...", "7c9e6679-..."] }Claim Mapping SPIs
company-branch-mapper
Location: KeycloakProviders/src/CompanyBranchMapper/
Trigger: Runs during token issuance and token refresh.
Logic:
- Reads
companyIdfrom the user's last session (stored in Keycloak user attributes) - Reads
branchIdfrom the user's last session - Validates company/branch belong to the user's tenant
- Injects
companyidandbranchIdclaims
Configuration (in Keycloak Admin):
Mapper type: Custom Script Mapper (Java SPI)
Token claim name: companyid
Claim JSON type: String
Add to ID token: ON
Add to access token: ONerp-policy-mapper
Location: KeycloakProviders/src/ErpPolicyMapper/
Trigger: Runs during token issuance.
Logic:
- Calls ERP internal API:
GET /api/v1/users/{erpId}/policies?companyId={companyId} - Concatenates
module:actionpairs - Injects as
erp_policiesstring claim
WARNING
This SPI makes a synchronous HTTP call during token issuance. Ensure the ERP policy service is healthy before Keycloak starts, or login will fail. The SPI has a 3-second timeout and falls back to empty policies on timeout.
erp-tenant-mapper
Location: KeycloakProviders/src/TenantMapper/
Logic: Reads tenantId and subdomain from Keycloak user attributes (set during user provisioning) and injects as claims.
Sample Decoded JWT
json
{
"exp": 1748560900,
"iat": 1748560000,
"jti": "ab12cd34-...",
"iss": "https://auth.microtec-test.com/realms/microtec",
"aud": ["erp-api", "account"],
"sub": "keycloak-user-uuid",
"typ": "Bearer",
"azp": "erp-angular",
"session_state": "...",
"acr": "1",
"realm_access": {
"roles": ["erp-user", "offline_access", "uma_authorization"]
},
"scope": "openid email profile",
"sid": "...",
"email_verified": true,
"tenant": "acme-corp",
"subdomain": "acme",
"erp_id": "550e8400-e29b-41d4-a716-446655440000",
"companyid": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
"branchId": "7c9e6679-f89b-12d3-a456-426655440000",
"erp_policies": "accounting:read,accounting:write,hr:read,inventory:read",
"account_ids": ["6ba7b810-...", "550e8400-..."],
"name": "Ahmed Ali",
"preferred_username": "ahmed.ali",
"given_name": "Ahmed",
"family_name": "Ali",
"email": "ahmed@acme.sa"
}Consuming Claims in Services
Via ICurrentUserService
csharp
// Injected automatically by Microtec.Web.Core
public class CreateInvoiceHandler
{
private readonly ICurrentUserService _currentUser;
public async Task<Result> Handle(CreateInvoiceCommand cmd)
{
// Access claims
var tenantId = _currentUser.TenantId; // from 'tenant' claim
var companyId = _currentUser.CompanyId; // from 'companyid' claim
var branchId = _currentUser.BranchId; // from 'branchId' claim
var userId = _currentUser.UserId; // from 'erp_id' claim
// Permission check
if (!_currentUser.HasPermission("accounting", "write"))
return Result.Forbidden();
// ...
}
}Via ClaimsPrincipal (direct)
csharp
// Accessing claims directly when ICurrentUserService is not available
var tenant = User.FindFirstValue("tenant");
var erpId = User.FindFirstValue("erp_id");Known Issues and Quirks
| Quirk | Description |
|---|---|
companyid casing | Lowercase id — not companyId. Legacy from original SPI. |
erp_policies timeout | Policy mapper times out if ERP service is slow; results in empty permissions |
| Refresh token claims | account_ids is only added to the access token, not the refresh token. Re-issue access token to get updated company list. |