Skip to main content

Phase 16: JIT Access Control, Field-Level Encryption & WebAuthn

Status: 📋 Planned — Q1 2027
Owner: Platform Security Team
Prerequisites: Phase 14 (Saathi AI), Phase 15 (WhatsApp Delivery)


Overview

Phase 16 establishes the security hardening layer required for enterprise and regulated-industry clients (QuantTrade, financial advisors, law firms) to adopt BizOSaaS at scale. Three pillars:

  1. JIT (Just-In-Time) Access Control — temporary, audited privilege elevation with automatic expiry
  2. Field-Level Encryption — AES-256-GCM encryption for PII/PCI fields at rest and in transit
  3. WebAuthn / Passkey Authentication — hardware-backed, phishing-resistant login for admin and senior roles

Pillar 1: JIT Access Control

Problem

Current RBAC is static — a partner or admin either has elevated privileges permanently or not at all. This is a moat-limiting risk for financial clients.

Architecture

Request (partner/admin)


JIT Access Request API

├─→ HITL Approval (Saathi AI review + human gate)

├─→ Time-bounded JWT with scoped claims issued
│ expires_at: NOW() + 30min
│ scope: ["read:financial", "write:reports"]

├─→ Audit trail logged → platform_audit_log

└─→ Auto-revoke on expiry via Redis TTL + background job

Implementation Plan

TaskFilePriority
JITAccessRequest DB tablepackages/db/src/schema/core.tsP0
JIT request API endpointapps/web/src/app/api/jit/request/route.tsP0
JIT approval webhookapps/web/src/app/api/jit/approve/route.tsP0
Redis TTL-based auto-expirypackages/queue/src/workers/jit-expiry.worker.tsP1
Saathi AI risk scoring for JITsenior_assistant_agent.py_handle_jit_risk_reviewP1
Audit log integrationExtend audit_logs table with jit_request_id FKP1

Database Schema (Phase 16)

CREATE TABLE jit_access_requests (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id),
requestor_id UUID NOT NULL REFERENCES core_user(id),
approver_id UUID REFERENCES core_user(id),
scope TEXT[] NOT NULL, -- ["read:financial", "write:invoices"]
reason TEXT NOT NULL,
risk_score INTEGER, -- Saathi AI risk assessment 0-100
status TEXT DEFAULT 'pending', -- pending | approved | rejected | expired
issued_token TEXT, -- JWT token (stored hashed)
expires_at TIMESTAMPTZ,
approved_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

Pillar 2: Field-Level Encryption

Problem

PII/PCI fields (phone, bank_account, tax_id, credit_card) are stored in plaintext. Required for DPDP (India), PCI-DSS compliance.

Architecture

Write path:  plaintext → AES-256-GCM(key: tenant_KEK) → ciphertext stored in DB
Read path: ciphertext → AES-256-GCM decrypt(key: tenant_KEK) → plaintext returned
Key path: tenant_KEK → encrypted with platform_MEK → stored in Infisical HSM

Encryption Key Hierarchy

Platform Master Encryption Key (MEK)
└── Tenant Key Encryption Key (KEK) [one per tenant, rotatable]
└── Data Encryption Key (DEK) [one per record type, derived]
└── Field ciphertext

Fields to Encrypt (P0 scope)

TableFieldCompliance Driver
core_userphoneDPDP, GDPR
contactsphone, emailDPDP, GDPR
bank_connectionsaccount_number, routing_numberPCI-DSS
invoicesbilling_addressDPDP
connector_secretssecret_valueSOC 2

Implementation

// packages/db/src/encryption/field-encryption.ts
import { createCipheriv, createDecipheriv, randomBytes } from 'crypto';

export function encryptField(plaintext: string, tenantKEK: Buffer): string {
const iv = randomBytes(12);
const cipher = createCipheriv('aes-256-gcm', tenantKEK, iv);
const enc = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
const tag = cipher.getAuthTag();
return `v1:${Buffer.concat([iv, tag, enc]).toString('base64')}`;
}

export function decryptField(ciphertext: string, tenantKEK: Buffer): string {
const buf = Buffer.from(ciphertext.replace('v1:', ''), 'base64');
const iv = buf.subarray(0, 12);
const tag = buf.subarray(12, 28);
const enc = buf.subarray(28);
const decipher = createDecipheriv('aes-256-gcm', tenantKEK, iv);
decipher.setAuthTag(tag);
return Buffer.concat([decipher.update(enc), decipher.final()]).toString('utf8');
}

Drizzle ORM Integration (custom column type)

// packages/db/src/encryption/encrypted-column.ts
import { customType } from 'drizzle-orm/pg-core';
import { encryptField, decryptField } from './field-encryption';

export const encryptedText = (fieldName: string) =>
customType<{ data: string; driverData: string }>({
dataType() { return 'text'; },
toDriver(value: string) {
const kek = getActiveTenantKEK(); // resolved from Infisical
return encryptField(value, kek);
},
fromDriver(value: string) {
const kek = getActiveTenantKEK();
return decryptField(value, kek);
},
})(fieldName);

Pillar 3: WebAuthn / Passkey Authentication

Problem

Admin and partner accounts use password-based auth with optional TOTP. This is insufficient for QuantTrade and financial service clients requiring phishing-resistant MFA.

Architecture

Registration flow:
User → Browser (WebAuthn.create) → Authenticator (hardware key / FaceID / TouchID)
→ Platform verifies attestation → credential stored in `webauthn_credentials`

Authentication flow:
User → Browser (WebAuthn.get) → Authenticator signs challenge
→ Platform verifies assertion → better-auth session issued

Implementation Plan

TaskPriority
webauthn_credentials DB tableP0
Registration endpoint /api/auth/webauthn/registerP0
Verification endpoint /api/auth/webauthn/verifyP0
Better-auth WebAuthn plugin integrationP0
Admin portal passkey management UIP1
Saathi AI senior briefing gated behind WebAuthnP1
Fallback TOTP if WebAuthn device not presentP1

Database Schema

CREATE TABLE webauthn_credentials (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES core_user(id) ON DELETE CASCADE,
credential_id BYTEA NOT NULL UNIQUE,
public_key BYTEA NOT NULL,
counter BIGINT NOT NULL DEFAULT 0,
device_type TEXT, -- 'singleDevice' | 'multiDevice'
backed_up BOOLEAN DEFAULT FALSE,
transports TEXT[], -- ['usb', 'nfc', 'ble', 'hybrid', 'internal']
aaguid UUID,
friendly_name TEXT,
last_used_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

Library Selection

@simplewebauthn/server  (Node.js server-side verification)
@simplewebauthn/browser (client-side WebAuthn API wrapper)
better-auth webauthn plugin (session integration)

Security Hardening — Supporting Measures

Content Security Policy (CSP) Hardening

Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{CSP_NONCE}';
connect-src 'self' https://api.bizoholic.com https://app.posthog.com;
img-src 'self' data: https:;
frame-ancestors 'none';

Rate Limiting (upgrade)

  • JIT request endpoint: 5 requests / 15 min per user
  • WebAuthn registration: 3 attempts / 10 min per user
  • Senior briefing access: 20 requests / hour per tenant

Security Audit Checklist (Phase 16 gate)

  • OWASP Top 10 audit on all new endpoints
  • Penetration test: JIT token elevation bypass attempts
  • Encryption key rotation drill (MEK rotation)
  • WebAuthn attestation format validation (FIDO2 Level 2)
  • Infisical secret scanning CI pipeline integration

Timeline

MilestoneTargetStatus
JIT schema + APIQ1 2027 Week 1-2📋 Planned
Field-level encryption libraryQ1 2027 Week 2-3📋 Planned
Drizzle encrypted column integrationQ1 2027 Week 3📋 Planned
WebAuthn registration/verificationQ1 2027 Week 3-4📋 Planned
Admin UI: passkey managementQ1 2027 Week 4📋 Planned
Saathi AI JIT risk scoringQ2 2027 Week 1📋 Planned
Security audit + pen testQ2 2027 Week 2📋 Planned
Phase 16 GA releaseQ2 2027 Week 3📋 Planned

Dependencies

  • Infisical — MEK and KEK storage (already integrated in Phase 3)
  • Redis — JIT token TTL enforcement (already in platform)
  • better-auth — WebAuthn plugin (needs ^1.6.0)
  • Saathi AI (Phase 14) — Risk scoring for JIT approvals
  • HITL Framework — Human approval gate for JIT requests

⚠️ Security Note: All Phase 16 features must pass a dedicated security review before production deployment. JIT tokens must never be logged in plaintext. Encryption keys must never be stored in application code or .env files — use Infisical HSM exclusively.