Security Controls
Authentication, encryption, network isolation, key management, audit controls, and compliance posture for REFRACT PDF. For CISO, security teams, and pen-test scoping.
Last updated: 2026-05-31. Version: 1.0.
For architecture context see Architecture & Data Flow.
1. Authentication and Authorisation
API Authentication
| Endpoint | Method | Notes |
|---|---|---|
| Dispatcher render/workflow/gateway endpoints | Microsoft Entra ID Bearer JWT | Anonymous at the Azure Functions level - there is no function key on any route. aud, iss, tid, and exp are validated inside the handler on every request. |
| Licensing API | Tenant identifier, license key, and identity token | Key stored in Azure Key Vault; retrieved via MSI at startup |
| eSign Portal - session links | Session and group identifiers in the URL | Validated against the session record in the portal's own store, not an HKDF-derived token |
| REFRACT Workflow - portal token | HMAC-signed portal token | 4-hour TTL, reissued on each valid access; the initial invite token is capped at 1 day |
| SharePoint Graph calls | REFRACT SharePoint Access app registration | Microsoft Graph Sites.ReadWrite.All; its client secret is held in your Azure Key Vault (sp-secrets). The managed identity's Sites.ReadWrite.All (granted by Grant-SpAccess.ps1) is used only when sp-secrets holds no client secret. |
| Microsoft Entra group membership checks | Managed Identity (MSI) | Microsoft Graph GroupMember.Read.All, granted by Grant-SpAccess.ps1 |
Authentication Flow
flowchart LR
subgraph Callers["External Callers"]
SPFx["SPFx App<br/>(SharePoint)"]
AdminTool["Admin / Audit Tool"]
Approver["Approver Browser"]
ESignPortal["eSign Portal<br/>(Refract Logic-operated)"]
end
subgraph Dispatcher["The Dispatcher - func-dispatcher (your Azure subscription)"]
RenderAPI["Render / Workflow<br/>Endpoints"]
AdminAPI["Admin / Audit<br/>Endpoints"]
TokenVerify["eSign Token<br/>Verify Endpoint"]
end
subgraph AzureServices["Azure Services - Managed Identity Only (no passwords)"]
SPO["SharePoint<br/>Graph API"]
KV["Azure Key Vault"]
SB["Azure Service Bus queues"]
end
subgraph Processor["The REFRACT Engine - func-processor (your Azure subscription)"]
SBTrigger["Service Bus<br/>Trigger (MSI)"]
end
SPFx -->|"Entra ID Bearer JWT"| RenderAPI
AdminTool -->|"Azure AD Bearer JWT (exp checked every hop)"| AdminAPI
Approver -->|"Workflow portal token - 4-hour TTL"| TokenVerify
ESignPortal -->|"Session/group identifiers · HTTPS"| TokenVerify
RenderAPI -->|"MSI"| SPO
RenderAPI -->|"MSI"| KV
RenderAPI -->|"MSI"| SB
SB -->|"MSI trigger"| SBTrigger
SBTrigger -->|"MSI"| SPO
SBTrigger -->|"MSI"| KV
Authorisation
- Feature flags are enforced at render time via license-gated feature gates (
licenseFeatures.ts); theNODE_ENV !== 'production'guard prevents override in production deployments. - Multi-stage workflow approvals use per-step SHA-256 approver-email hashing with
crypto.timingSafeEqualcomparison (prevents timing oracles). - Audit-event API requires valid Bearer JWT; tenant scope is enforced on every query.
Licensing Integrity and Enforcement Model
REFRACT PDF implements a tiered licensing enforcement model to reduce runtime dependency on external systems while maintaining tamper-evident integrity.
Entitlement Verification
- All license entitlements are cryptographically signed by REFRACT PDF using JWS ES256 (ECDSA P-256).
- REFRACT PDF holds the private signing key. Your Dispatcher Function holds only the public verification key - it can verify but never forge entitlements.
- The JWS signature is validated before every use in runtime decision-making.
- Any modification to the cached
license.jsonfile causes signature verification to fail; the file is discarded and the tier cascade falls through to Tier 3 (Licensing API).
Runtime Enforcement
- The Dispatcher evaluates a 4-tier cascade: in-memory cache (24 h TTL) → SharePoint
license.json→ Licensing API → 48-hour grace (API unavailability only). - Feature gating is enforced locally via the cached entitlement state. No external call is required when the in-memory cache (Tier 1) or SharePoint file (Tier 2) is valid.
- When both local tiers miss, the Dispatcher calls the Licensing API directly at
license.refractplatform.com(Tier 3);POST /v1/validatecarries your tenant identifier, license key, an identity token, and your deployment address - no document content.
Sync Security Controls
- Licensing sync and Tier 3 runtime calls use HTTPS (TLS 1.2+).
- Authentication uses a REFRACT PDF-issued identity token (JWS ES256) bound to your
licenseKeyandtenantId; no user credentials or document content are transmitted. - The signed entitlement payload includes a tenant ID binding (
tidclaim) verified before the file is written to SharePoint; atidmismatch causes the response to be rejected.
Resilience and Failure Behaviour
| Scenario | Outcome |
|---|---|
| Licensing API temporarily unreachable | An instance that is already licensed serves from its Tier 1 cache, from license.json on a plan other than trial, or under the 48-hour grace period from its last valid answer. A new instance (after a cold start, scale-out, deployment, or platform move) has no grace period: on a trial plan, or without a valid license.json, it tries the API once more, then refuses the request with 503 and code LICENSE_UNAVAILABLE, "retryable": true, and Retry-After: 30, until the API answers. See Licensing Architecture. Independently, the REFRACT Notification rate-limit cache is fail-open - it keeps serving the last-known limits for up to 24 h and never blocks a send when the API is unreachable. No impact to notifications. |
| Grace period expires (API still unreachable) | Requests are refused with 503 and code LICENSE_UNAVAILABLE, as on a new instance. System resumes automatically when API is reachable. |
| License revoked or expired (API responds) | Nightly sync deletes license.json; in-memory cache cleared. Next request is refused with 403 and code LICENSE_INVALID. Grace period does not apply to genuine revocations or expired licenses. |
| REFRACT Notification service temporarily unavailable | Notification requests are held on a durable Azure Service Bus queue and processed when the service recovers - emails are delayed, not dropped. Document generation, rendering, and signing are unaffected because notifications are a downstream, asynchronous step. |
| eSign Portal temporarily unavailable | The signing UI itself is hosted by the portal; the document and signed output are served from your own Dispatcher function. New sessions pause until the portal recovers; documents already accessible remain so. |
2. Data Encryption
In Transit
- All HTTP endpoints require HTTPS (TLS 1.2+). The Dispatcher function app has
httpsOnlyenforced in Bicep. - Azure Service Bus queues connections use AMQP over TLS.
- Internal Azure backbone traffic (Isolated Azure Function - Azure Blob Storage, Azure Key Vault) uses TLS enforced by Azure.
At Rest
| Data | Encryption | Key location |
|---|---|---|
| Azure Blob Storage (outputs, audit, archives) | AES-256 (Azure Storage Service Encryption) | Microsoft-managed (customer-managed key option available via Azure Key Vault) |
| Azure Key Vault secrets | AES-256 (standard Key Vault SKU, not HSM-backed) | Azure - your tenant |
| Approver email addresses in workflow state | AES-256-GCM + random IV | Your Azure Key Vault (ESIGN_ENCRYPTION_KEY) |
| Portal tokens | HKDF-derived kEnc (AES-GCM) + kMac (HMAC-SHA256) | Your Azure Key Vault |
.raudit audit archives | AES-256 (Azure Storage SSE); the manifest records a SHA-256 hash of each component (no HMAC) | Microsoft-managed (Storage SSE) |
3. Network Controls
REFRACT PDF uses a tiered network model so each deployment matches its environment and compliance needs. In all tiers, storage access is governed by Microsoft Entra ID RBAC (managed-identity only) and allowSharedKeyAccess: false (no shared-key or anonymous access). Write-once (WORM) protection on the audit container is available in every tier but is deployed unlocked by default; it becomes an enforced guarantee only where the immutability policy has been applied and locked for the storage account (see the -LockImmutability deployment flag below).
Development / Demo deployments
Storage accounts run with defaultAction: Allow, secured by RBAC + no-shared-key; write-once (WORM) protection is available on the audit container but, as in every tier, is enforced only where the immutability policy has been applied and locked for the storage account. This avoids VNet provisioning for non-production environments while keeping identity-based access control fully enforced. No document data is exposed: every account still requires a valid managed-identity role to read or write.
Production - Tier 1 (Standard, recommended)
Deployed on the Azure Functions Flex Consumption plan with VNet integration. All four storage accounts (stDisp, stProc, stData, stOut) use networkAcls.defaultAction: Deny. Function App access to storage traverses the VNet via subnet service endpoints; external internet access to the data plane is blocked at the network ACL layer. This is the recommended production posture and is enabled by supplying the vnetSubnetId deployment parameter - no plan upgrade required.
Production - Tier 2 (Dedicated / Regulated)
For customers requiring dedicated compute or specific regulatory isolation (e.g. FedRAMP High, air-gapped, or internal policies mandating isolated infrastructure), Tier 2 extends Tier 1 with additional isolation. Tier 2 capabilities are at two different levels of readiness today - stated plainly here rather than bundled as a single offering:
Available today, as a deployment parameter: Service Bus network isolation (serviceBusSku=Premium, default: Standard). Namespace-level IP/VNet firewall rules (and Private Endpoints) for Service Bus are a Premium-tier-only Azure capability - Tier 1's Standard-tier namespace is RBAC-only (managed-identity access, no shared keys) with no network ACL option available at that tier at all; a deployment attempting one on Standard fails outright rather than silently no-op'ing. Premium costs materially more (~$677/month per namespace vs. Standard's ~$10/month) and is deliberately restricted to production deployments - a non-production deployment requesting it is blocked at deploy time - so it is only ever chosen as a deliberate, informed decision made with the customer at onboarding, never a default or an accidental selection.
Planning note: moving an existing customer from Standard to Premium later is a namespace migration requiring a scheduled maintenance window, not a live configuration toggle - Azure provisions a new Premium namespace and migrates entities into it (the DNS name is preserved, so client connection strings are unaffected, but messages in flight at the moment of cutover are not carried over). This is why the tier is decided once, at onboarding, rather than deferred.
Not yet implemented - roadmap, requires an engineering scoping conversation: a dedicated (Elastic Premium) compute plan for the Function Apps and Private Endpoints for storage/Key Vault. These are not currently exposed as deployment parameters. A customer whose compliance requirement depends on either of these should raise it during onboarding scoping, not assume it is available at go-live.
Choosing Standard vs. Premium
Most customers, including most compliance-driven ones, belong on Tier 1 / Standard:
- Your requirement is that the document data plane (storage) be network-isolated - Tier 1 already delivers this via
networkAcls.defaultAction: Denyand VNet service endpoints on all four storage accounts. - You don't have a specific citation requiring the messaging/queueing layer itself to be network-restricted, as distinct from the data it carries.
- Managed-identity RBAC (no shared keys, no connection strings) is an acceptable control for Service Bus on its own - reasonable here because Service Bus carries workflow metadata (job signals, delivery events), not document content or file bytes.
- Cost efficiency matters: Standard Service Bus is ~$10/month; Premium is ~$677/month per customer namespace, recurring.
Consider Tier 2 / Premium Service Bus when:
- A named regulatory framework or internal security policy explicitly requires network-level isolation of all backend Azure services - not just the storage data plane. FedRAMP High system boundaries are the common example.
- Your organization operates under a blanket policy that any Azure resource with a public endpoint is out of bounds regardless of auth model, even one gated by RBAC-only access.
- You can commit to the decision at onboarding - Premium is not something to add later without a scheduled maintenance window, so "we might need it eventually" should be resolved before go-live, not deferred.
If your requirement extends to dedicated compute or Private Endpoints for storage/Key Vault (beyond Service Bus), flag that separately during onboarding scoping - unlike Service Bus tier, those are not yet a turnkey parameter.
Application endpoints (all tiers)
The Dispatcher's HTTP endpoints are reachable over the public internet by design - they serve SPFx (browser) clients, external signers, and integration callers (Power Platform, the eSign Portal). These endpoints are secured by layered authentication rather than network restriction: Entra ID JWT bearer enforcement (no route uses an Azure Functions key), HMAC-SHA256 signatures on portal callbacks, and opaque signed tokens on signer and delivery routes. The data plane (storage) is network-isolated in Tier 1 and Tier 2 even though the application endpoints remain publicly reachable.
REFRACT PDF's outbound calls to the Licensing API and eSign Portal use HTTPS/TLS 1.2+. No inbound firewall rules need to be opened for these calls.
4. Key Management
- All secrets are stored in your Azure Key Vault. Refract Logic staff have no standing access.
- HKDF root key derives
kEncandkMacper-token viaHKDF(root, tokenId, purpose). enablePurgeProtection: trueon Azure Key Vault (set by Bicep; irreversible); 30-day soft-delete retention.- Key rotation is an operator action via
az keyvault secret set. Approver-email ciphertext stores akeyIdfield to support future re-encryption on key roll.
Key Management Flow
flowchart TD
KV["🔐 Azure Key Vault<br/>(your subscription)<br/>Refract Logic staff have NO standing access"]
KV -->|"HKDF root key (retrieved via MSI)"| HKDF
subgraph HKDF["HKDF Derivation - per token"]
direction LR
kEnc["kEnc<br/>AES-GCM encrypt<br/>portal token payload"]
kMac["kMac<br/>HMAC-SHA256 sign<br/>portal token"]
end
KV -->|"Approver-email AES-256-GCM key (retrieved via MSI)"| AES["AES-256-GCM<br/>Encrypt approver PII<br/>in Azure Table Storage"]
KV -->|"ACS email key (optional) - BYO email only"| ACS["Azure Communication Services<br/>BYO outbound email"]
AppSettings["Azure App Settings<br/>(your Function App config)"] -->|"AUDIT_REPORT_HMAC_KEY"| HMACSign["HMAC-SHA256<br/>Sign verification report PDFs<br/>+ upload .sig.json companion"]
style KV fill:#e8f5e9,stroke:#388e3c
5. Audit and Tamper-Resistance
| Control | Status |
|---|---|
| Append-only audit JSONL blob | Each operation appends; no overwrites |
| Sequential event ordering (hash chain) - eSign and Workflow audit trails only | Each event includes previousHash; verifier API checks chain integrity |
| HMAC-signed verification reports | AUDIT_REPORT_HMAC_KEY signs each verify-report PDF; companion .sig.json uploaded |
| Write-once (WORM) storage containers | Deploy with -LockImmutability flag (irreversible; enables WORM for audit containers) |
| Storage geo-redundancy | Standard_GRS for audit and output storage (Bicep default) |
| Azure Key Vault purge protection | enablePurgeProtection: true; 30-day soft-delete retention |
| Signed audit tip (daily) | Planned - daily signed tip for long-gap tamper detection |
6. Input Validation and Injection Prevention
| Attack vector | Control |
|---|---|
| OData injection (licensing queries) | odata tagged-template literal; single-quote doubling; no raw string concatenation |
| SSRF (external asset fetch) | DNS resolution mandatory; resolved IP validated against RFC1918 blocklist; IP pinned for duration of fetch |
ZIP slip (.raudit archive extraction) | assertSafeArchiveEntryName rejects traversal, absolute, UNC, drive, and control-char paths before any extraction |
| Condition engine code injection | Zero eval/Function usage; BLOCKED_PROTOTYPE_KEYS guard; MAX_EXPRESSION_LENGTH 4096; MAX_TOKEN_COUNT 1024; MAX_EVAL_DEPTH 200; 19 security tests in CI |
| Duplicate seal (replay) | Service Bus messageId = {correlationId}-workflow-seal; requiresDuplicateDetection: true on queue |
| Concurrent approval race | Optimistic concurrency: If-Match ETag on all 4 intermediate updateWorkflowState calls; 412 - HTTP 409 to caller |
| Schema substitution mid-workflow | loadSchema verifies SHA-256 of fetched YAML against schemaSha256 stored at workflow start; throws on mismatch |
7. Dependency and Supply Chain
pnpmlockfile with exact version pinning; CI validates lockfile integrity.- SBOM generation via CycloneDX from pnpm lockfile - integration in progress.
- No
evaland no dynamicrequire()in production code paths. - ZIP-slip path validation enforced in archive extraction; third-party dependencies tracked via Dependabot.
8. Rate Limiting and Abuse Prevention
- Licensing API rate limiter uses Azure Table Storage and ETag optimistic concurrency; scale-safe across multiple Isolated Azure Function instances.
- SPFx command set has per-item in-flight debounce (
_inFlight = new Set<string>()); duplicate clicks are dropped. - Dashboard polling uses a 60-second base interval with adaptive backoff on 429 (doubles up to 8x, maximum approximately 8 minutes).
9. Certifications
Refract Logic holds no independent certifications today - no SOC 2, no ISO 27001, no HIPAA BAA, and no FedRAMP authorization. Document rendering deploys into your own Azure subscription and region; see the Sub-Processor List for what does and does not transit shared services. PDF/UA-1 structural tagging (MarkInfo, StructTreeRoot, Lang, ViewerPreferences/DisplayDocTitle) is implemented; see Section 508 for what the accessibility check reports and does not enforce.
Pen-Test Scoping and Security Questionnaires
Contact our security team for pen-test scoping, architecture review, and compliance mapping.