Skip to main content

AI Chatbot Security & API Contract

Summary (3–6 bullets)

  • Defines authentication, RBAC, and tenant/agent data-scoping rules for chatbot queries.
  • Documents fail-closed downline-aware authorization policy and enforcement points.
  • Specifies endpoint contract, error behavior, rate limits, and audit logging coverage.
  • Captures production posture (demo mode disabled) and frontend integration guardrails.
  • Includes troubleshooting and monitoring guidance for secure operations.

When to use this (3–6 bullets)

  • When implementing or reviewing chatbot auth/RBAC behavior.
  • During security incident triage for 401/403 or suspected data-scope violations.
  • Before deploying frontend/backend changes that touch chatbot routing or auth headers.
  • When validating production hardening and legacy-path compatibility behavior.

What you’ll walk away with (2–5 bullets)

  • A complete security contract for chatbot request handling.
  • Clear role-based access expectations (admin vs agent/ceo) and denial semantics.
  • A deploy/debug checklist for endpoint correctness and safe client behavior.

Authentication

  • Include Authorization: Bearer <JWT> header
  • JWT contains: tenant_id, role, agent_name (if agent)
  • Queries automatically scoped to user’s tenant and agent

Demo Mode (Fallback) - DISABLED IN PRODUCTION

  • Production: Demo mode disabled - authentication required (returns 401 if no token)
  • Non-Production: Demo mode allowed with restricted viewer role (no agent-specific data)
  • No Authorization header provided (non-prod only)
  • Restricted keywords blocked: commission, my, agent, downline
  • Response prefixed with “[DEMO MODE - CEO View Only]“

Rate Limits

  • Demo mode: 30 requests/minute (per IP)
  • Authenticated: 60 requests/minute (per user)
  • 429 Response: “Too many requests. Please wait…”

Data Scoping

All queries filtered by:
  • tenant_id (from JWT)
  • agent_id (from JWT, canonical identifier)
  • Role-based access control (see RBAC Policy below)

Downline-Aware RBAC Policy

Policy Overview

The AI chatbot enforces downline-aware Role-Based Access Control (RBAC) to prevent cross-agent data disclosure:
  • role=admin: Can query any agent within tenant (but MUST provide explicit target for agent-scoped requests; no “all agents”)
  • role=agent or role=ceo: Can query ONLY self + downline hierarchy (including self) within tenant
  • Fail-closed: Missing role, missing agent_id (for agent/ceo), unresolvable target, or non-downline access => HTTP 403

Enforcement Points

  1. Server-side RBAC check at ai_query_public() entrypoint (before any sensitive BigQuery or LLM calls)
  2. Downline check via is_in_downline() using hierarchy traversal (auth-only query allowed)
  3. Query layer filters by authorized_target_agent_id (enforced at entrypoint)

Authorization Flow

  1. Extract requested_target from question (agent name or ID)
  2. Resolve requested_target to agent_id via dim_agent_hierarchy lookup (tenant-scoped)
  3. For agent/ceo roles: Check if resolved agent_id is in downline using is_in_downline()
  4. For admin role: Allow if target resolves within tenant (no downline check)
  5. If authorized: Pass authorized_target_agent_id to data queries
  6. If denied: Return HTTP 403 immediately (no sensitive data queries executed)

Security Guarantees

  • Server-side enforcement: RBAC checks happen before any sensitive BigQuery query, preventing prompt injection bypass
  • Downline-aware: AGENT/CEO can only access self + downline (not arbitrary agents)
  • Tenant isolation: All queries filtered by tenant_id from JWT
  • Fail-closed: Missing agent_id, unknown role, or non-downline access defaults to deny
  • Auth queries allowed: Small auth-only queries (is_in_downline, resolve_target_to_agent_id) are allowed, but sensitive data queries must be blocked on deny
  • LLM call prevention: RBAC denial happens before any LLM call - this is the real prompt injection defense
  • Depth cap: Downline traversal limited to 10 levels to prevent infinite loops
  • Cycle guards: Prevents self-referential parent relationships

Example Scenarios

Scenario 1: Agent asks about self
  • User: Kenny Young (agent)
  • Question: “How much did I make this month?”
  • Result: ✅ Allowed (defaults to self)
Scenario 2: Agent asks about downline agent
  • User: Kenny Young (agent)
  • Question: “How much did [downline agent] make?”
  • Result: ✅ Allowed (if agent is in Kenny’s downline)
Scenario 3: Agent asks about non-downline agent
  • User: Kenny Young (agent)
  • Question: “How much did Tommy Dang make?” (Tommy not in Kenny’s downline)
  • Result: ❌ HTTP 403 Forbidden (no sensitive query executed)
Scenario 4: Admin asks about any agent
  • User: Admin
  • Question: “How much did Tommy Dang make?”
  • Result: ✅ Allowed (admin can query any agent in tenant)
Scenario 5: Admin asks without target
  • User: Admin
  • Question: “Show me all agent commissions” (no specific target)
  • Result: ❌ HTTP 403 Forbidden (explicit deny - admin must provide target)

Error Responses

  • 401 Unauthorized:
    • Token expired/invalid
    • Production: No Authorization header (demo mode disabled)
    • Bearer token present but invalid (no fallback to demo)
  • 403 Forbidden:
    • No access to requested data
    • Agent/CEO querying non-downline agent
    • Missing authorized_agent_id for agent-scoped queries
  • 429 Rate Limited: Too many requests
  • 500 Internal Error: Server error

Audit Logging

Every request logged with:
  • timestamp, tenant_id, user_email, role, agent_name
  • question_length, latency_ms, is_demo

Security Features

JWT Authentication

  • Real user extracted from token
  • Automatic tenant and agent scoping
  • Token validation with proper error handling

Demo Mode Restrictions

  • CEO view only, no agent data
  • Keyword filtering for sensitive queries
  • Clear indication of demo mode

Rate Limiting

  • Per-IP caps prevent abuse
  • Graceful degradation on limits
  • Clear error messages

Audit Logging

  • Every query logged with user context
  • Performance metrics tracked
  • Security events captured

Safe Error Handling

  • No logout loops on API errors
  • User-friendly error messages
  • Graceful fallback to demo mode

API Endpoints

POST /api/v1/ai/query-public

Request Headers:
Content-Type: application/json
Authorization: `Bearer <JWT_TOKEN>` (optional)
Request Body:
{
  "question": "Show me my commission breakdown for September 2025"
}
Response (Authenticated):
{
  "answer": "Based on your commission data for September 2025...",
  "meta": {
    "is_demo": false
  }
}
Response (Demo Mode):
{
  "answer": "[DEMO MODE - CEO View Only]\n\nBased on CEO metrics for September 2025...",
  "meta": {
    "is_demo": true
  }
}
Response (Restricted in Demo):
{
  "answer": "🔒 Demo mode only shows CEO metrics. Please sign in to see agent-specific data.",
  "meta": {
    "is_demo": true,
    "restricted": true
  }
}

Frontend Integration

Configuration

Environment Variable: NEXT_PUBLIC_AI_URL
  • Required: Yes (chatbot will fail to initialize if not set)
  • Purpose: Points to the Cloud Run service where RBAC fixes are deployed
  • Production Value: https://payroll-backend-prod-evndxpcirq-uc.a.run.app
  • Format: Must be origin-only (e.g., https://payroll-backend-prod-...run.app) - DO NOT include /api/v1 or any path components
  • Validation: Call-time validation (not module-init) prevents SSR/build crashes
  • Guardrails: Warning logged if env var contains /api/ paths (will be normalized to origin-only)
  • Important: Never hardcode Cloud Run URLs in aiClient.ts. Always use environment variables.
Canonical vs Legacy Endpoints:
  • Canonical Endpoint: /api/v1/ai/query-public
    • Frontend always calls this canonical path regardless of NEXT_PUBLIC_AI_URL format
    • Backend mounts router at /api/v1/ai (canonical)
    • This is the primary endpoint for all new deployments
  • Legacy Alias: /ai/query-public (temporary compatibility)
    • Backend also mounts router at /ai (legacy alias) for compatibility with stale bundles
    • Same router instance ensures identical RBAC/auth behavior
    • Removal Plan: Will be removed after stale bundles expire (monitor usage first)
    • See docs/AI_CHATBOT_INCIDENT_2025_12_24.md for details

ChatBubble.tsx

  • Automatically includes JWT when available
  • Safe error handling without logout
  • Clear user feedback for different error types

AISidebar.tsx

  • Same JWT and error handling pattern
  • Consistent user experience across components

Production Behavior (2025-12-24 Update)

Authentication Requirements

  • Production: Authentication required - no demo mode fallback
    • No Authorization header → 401 Unauthorized
    • Invalid Bearer token → 401 Unauthorized (no fallback to demo)
  • Non-Production: Demo mode allowed with restricted viewer role
    • No Authorization header → restricted demo mode (viewer role, no agent data)

RBAC Enforcement

  • All intent handlers enforce authorize_target_agent_id() before data fetch
  • Agent/CEO role: Can only query self + downline (enforced server-side)
  • Admin role: Can query any agent in tenant (explicit target required)
  • Fail-closed: Missing authorized_agent_id for agent-scoped queries → 403 Forbidden

BigQuery Hardening

  • ID-based filtering: Prefers authorized_agent_id over agent_name
  • Dedupe CTE: Prevents row multiplication from duplicate names
  • Exact equality: No LIKE/partial matching (prevents name collision leaks)
  • NULL guards: COALESCE and IS NOT NULL checks for correctness
See docs/AI_CHATBOT_INCIDENT_2025_12_24.md for complete fix summary.

Security Checklist

  • JWT token extracted and validated
  • Demo mode disabled in production (requires authentication)
  • Demo mode restricted to viewer role in non-prod (no agent data)
  • Bearer token present but invalid → 401 (no fallback)
  • All intent handlers enforce RBAC before data fetch
  • Agent/CEO role restricted to self + downline
  • BigQuery queries use ID-based filtering with dedupe guards
  • Rate limiting (30/min per IP)
  • Audit logging for all queries
  • Frontend passes JWT when available
  • Frontend error handling doesn’t trigger logout
  • 401/403/429 errors show helpful messages
  • Documentation created

Troubleshooting

Common Issues

  1. “Session expired” loop
    • Frontend is calling wrong endpoint
    • Check network tab for 401 responses
    • Verify JWT token is valid
  2. “Demo mode” for authenticated users
    • JWT token not being passed
    • Check Authorization header in network tab
    • Verify token format: Bearer <token>
  3. Rate limiting errors
    • Too many requests from same IP
    • Wait 1 minute before retrying
    • Check if multiple users sharing IP
  4. 403 Forbidden errors
    • User doesn’t have access to requested data
    • Check user role and agent_id in JWT
    • Verify tenant_id matches
    • For agent/ceo: Verify requested agent is in downline
    • For admin: Verify explicit target was provided
    • Known Issue: 403 for self-queries (see docs/AI_CHATBOT_DEBUG_NEXT.md)
      • Symptom: Agent gets 403 when asking about own data
      • Likely cause: agent_id enrichment missing or name→id resolution mismatch
      • Debug: Check Cloud Run logs for [RBAC-DEBUG] markers

Debug Steps

  1. Check browser network tab for request/response
  2. Verify JWT token in localStorage
  3. Check Cloud Run logs for audit entries
  4. Test with curl to isolate frontend vs backend issues

Monitoring

Cloud Run Logs

Look for [AI_AUDIT] entries to track:
  • User activity patterns
  • Performance metrics
  • Security events
  • Demo vs authenticated usage

Key Metrics

  • Request latency (latency_ms)
  • Demo mode usage (is_demo)
  • Error rates by status code
  • User distribution by role