License Security
Cryptographic license integrity, tenant provisioning, HMAC-SHA256 tamper detection, and the license data model.
The license is the foundational security and provisioning primitive. Creating a license provisions a new tenant: it seeds the admin role, head office branch, and base currency in a single atomic transaction. Every authenticated request validates the tenant's license for integrity, expiry, and active status.
License Data Model
Identity
| Field | Type | Notes |
|---|---|---|
id | String | Format: XXXX-YYYY-ZZZZ (3 segments, 4 chars each). Characters from set ABCDEFGHJKLMNPQRSTUVWXYZ23456789 (excludes ambiguous 0, 1, O, I). Generated with crypto.randomBytes. |
companyName | String | 2–100 chars |
companyAbbreviation | String | Unique. Auto-generated 3–4 char CNA from company name. |
slug | String | Unique. Auto-generated from companyName via slugify, or user override. |
logo | String? | URL |
Contact Information (all optional)
companyAddress, companyContactEmail, companyContactPhone, contactPersonName, contactPersonEmail, contactPersonPhone
Contract Terms (signed fields)
These fields are protected by the HMAC-SHA256 signature. Changing any of them requires re-signing by a System Admin.
| Field | Type | Notes |
|---|---|---|
licenseCode | String | Unique. HMAC-SHA256 signature (base64) — the integrity seal. |
licenseExpiryDate | Date | Evaluated at end-of-day in the tenant's timezone. |
modules | String[] | Array of module codes. Currently only CORE. |
timezone | String | IANA timezone (e.g., Africa/Nairobi). |
Financial Configuration
| Field | Default | Notes |
|---|---|---|
financialYearCycle | 20251231M1231 | Format: ccyymmddMnndd |
accountingType | ACCRUAL | Enum: CASH or ACCRUAL |
Password & Session Policies
| Field | Default | Range |
|---|---|---|
passwordMinLength | 8 | 8–128 |
passwordMinNumber | 1 | 0–10 |
passwordMinUppercase | 1 | 0–10 |
passwordMinLowercase | 1 | 0–10 |
passwordMinSpecialChar | 1 | 0–10 |
passwordChangeInterval | 90 days | 0–365 |
inactivityTimeout | 600 seconds | 60–86,400 |
System Fields
| Field | Type | Notes |
|---|---|---|
customFields | JSON? | GIN-indexed |
version | Int | Incremented on every update (optimistic concurrency) |
isActive | Boolean | Default true |
deletedAt | DateTime? | Soft delete marker |
Cryptographic Integrity
Signing
Payload: {slug}|{expiryDate}|{sortedModules}|{timezone}
Example: acme-bank|2026-12-31|CORE|Africa/Nairobi
Algorithm: HMAC-SHA256
Key: LICENSE_SECRET_KEY (env, minimum 32 chars)
Encoding: Base64
Stored in: licenseCode field (unique constraint)Only the contract terms (slug, expiry, modules, timezone) are signed. Configuration fields (logo, contacts, password policies, financial config) can be changed by tenant admins without re-signing.
Verification
On every license access:
- Recompute HMAC-SHA256 from current DB fields
- Compare with stored
licenseCodeusingcrypto.timingSafeEqual(constant-time, prevents timing attacks) - If buffer lengths differ, comparison fails safely (caught via try/catch)
Tamper Response
On integrity mismatch:
- Redis key
license:blocked:{id}set to"1"with 5-minute TTL - All subsequent requests for that license get immediate 403 LICENSE_TAMPERED (block checked before cache or DB)
- HIGH sensitivity audit event logged
- Block is cleared only when a System Admin performs a legitimate contract update (re-signing)
Redis Caching Strategy
| Key | TTL | Purpose |
|---|---|---|
license:data:{id} | Dynamic: min(3600, max(60, secondsUntilExpiry)) | Cached license data (avoids DB hit per request) |
license:blocked:{id} | 300 seconds (5 minutes) | Tamper block flag (checked first, always) |
Cache invalidation: Both keys are cleared on contract updates and deletion. Config updates only clear the data cache (no re-signing involved).
API Endpoints
All routes under /api/v1/licenses. All require authentication.
| Method | Path | Access | Description |
|---|---|---|---|
| GET | / | System Admin (L0) | Paginated list with filters (search, isActive, expiresWithinDays, module). Returns summary stats: { total, active, inactive, expiringSoon }. No integrity checks per record. |
| POST | / | System Admin (L0) | Full tenant provisioning (see below). |
| GET | /:id | System Admin or own tenant | View license with full integrity check. Expiry validation disabled for viewing (admins can see expired licenses). Tenant isolation: non-L0 users can only access their own tenant. |
| PATCH | /:id/contract | System Admin (L0) | Update contract terms (expiry, modules, isActive). Triggers re-signing. Clears cache and block keys. |
| PATCH | /:id/config | System Admin or Tenant Admin (update_config) | Update operational config (logo, contacts, password policies, financial config, custom fields). Clears cache only. |
| DELETE | /:id | System Admin (L0) | Soft delete: sets isActive=false, deletedAt=now(), increments version. Clears all Redis keys. |
List Query Parameters
search (companyName/slug/abbreviation), isActive, expiresWithinDays, module, sortBy (createdAt/licenseExpiryDate/companyName), sortOrder, page, limit (1–100, default 20). Also supports custom field filter passthrough.
Tenant Provisioning
Creating a license (POST /) triggers a multi-step atomic transaction:
- Validate reference data — checks that the head office country exists in
Nationand base currency exists inCurrency - Generate unique slug — from company name (or user override), with counter suffix on collision
- Generate CNA — 3–4 character Company Name Abbreviation, checked for uniqueness (with race-condition guard inside the transaction)
- Generate license ID —
XXXX-YYYY-ZZZZformat usingcrypto.randomBytes, retry loop (max 3) for collisions - Sign license data — HMAC-SHA256 over contract terms
Within the transaction (all-or-nothing):
- Create the License record
- Seed the base currency as a
TenantCurrencywith 1:1 exchange rates - Seed the head office branch (code format:
{CNA}-{alpha3}-000-0000, type:LEAD) - Seed the TENANT_ADMIN role with full permissions from the
TENANT_ADMIN_DEFINITIONblueprint (creates Role + PermissionMatrix + PermissionAction entries for all 37 non-license entities, plusread/update_configfor license) - Create audit log entries for each provisioned entity
Returns: sanitized license (no licenseCode), tenant admin role, head office branch, base currency.
CNA Generation Algorithm
The Company Name Abbreviation is auto-generated as a unique 3–4 character code:
- 1 word: First 3 consonants (consonant-start) or first letter + 2 consonants (vowel-start), with fallbacks up to 4 chars
- 2 words: AA+B, fallbacks A+BB, AA+BB
- 3+ words: First letter of each word, fallbacks with extra chars
- Strips corporate suffixes: Ltd, Limited, Inc, Corp, PLC, LLC, etc.
- Multiple candidates generated and checked against DB for first available
Access Control
System Administrator (Layer 0)
- Cross-tenant license management
- Create, delete, and update contract terms (triggers re-signing)
- Config updates on any tenant
- Full list visibility across all tenants
Tenant Administrator (Layer 1)
- Isolated to own license only (
user.tenantId === license.id) - Can update operational config (logo, contacts, password policies, financial settings)
- Read-only access to contract terms
- No cross-tenant visibility
Security Event Classification
| Error Code | HTTP | Trigger | Response |
|---|---|---|---|
LICENSE_TAMPERED | 403 | HMAC signature mismatch | 5-minute block + HIGH audit event |
LICENSE_EXPIRED | 402 | Past end-of-day in tenant timezone | Access denied with renewal guidance |
LICENSE_INACTIVE | 403 | isActive === false | Access denied |