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:
- JIT (Just-In-Time) Access Control — temporary, audited privilege elevation with automatic expiry
- Field-Level Encryption — AES-256-GCM encryption for PII/PCI fields at rest and in transit
- 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
| Task | File | Priority |
|---|---|---|
JITAccessRequest DB table | packages/db/src/schema/core.ts | P0 |
| JIT request API endpoint | apps/web/src/app/api/jit/request/route.ts | P0 |
| JIT approval webhook | apps/web/src/app/api/jit/approve/route.ts | P0 |
| Redis TTL-based auto-expiry | packages/queue/src/workers/jit-expiry.worker.ts | P1 |
| Saathi AI risk scoring for JIT | senior_assistant_agent.py — _handle_jit_risk_review | P1 |
| Audit log integration | Extend audit_logs table with jit_request_id FK | P1 |
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)
| Table | Field | Compliance Driver |
|---|---|---|
core_user | phone | DPDP, GDPR |
contacts | phone, email | DPDP, GDPR |
bank_connections | account_number, routing_number | PCI-DSS |
invoices | billing_address | DPDP |
connector_secrets | secret_value | SOC 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
| Task | Priority |
|---|---|
webauthn_credentials DB table | P0 |
Registration endpoint /api/auth/webauthn/register | P0 |
Verification endpoint /api/auth/webauthn/verify | P0 |
| Better-auth WebAuthn plugin integration | P0 |
| Admin portal passkey management UI | P1 |
| Saathi AI senior briefing gated behind WebAuthn | P1 |
| Fallback TOTP if WebAuthn device not present | P1 |
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
| Milestone | Target | Status |
|---|---|---|
| JIT schema + API | Q1 2027 Week 1-2 | 📋 Planned |
| Field-level encryption library | Q1 2027 Week 2-3 | 📋 Planned |
| Drizzle encrypted column integration | Q1 2027 Week 3 | 📋 Planned |
| WebAuthn registration/verification | Q1 2027 Week 3-4 | 📋 Planned |
| Admin UI: passkey management | Q1 2027 Week 4 | 📋 Planned |
| Saathi AI JIT risk scoring | Q2 2027 Week 1 | 📋 Planned |
| Security audit + pen test | Q2 2027 Week 2 | 📋 Planned |
| Phase 16 GA release | Q2 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
.envfiles — use Infisical HSM exclusively.