|
| 1 | +# API Key Authentication |
| 2 | + |
| 3 | +## Overview |
| 4 | + |
| 5 | +The Eryxon Flow API uses API keys for external integrations (ERP systems, automation, etc.). Keys follow the format `ery_live_xxx` or `ery_test_xxx`. |
| 6 | + |
| 7 | +## How It Works |
| 8 | + |
| 9 | +### Key Generation |
| 10 | +1. Admin creates API key via dashboard (Admin > API Keys) |
| 11 | +2. System generates random key: `ery_live_<32-random-chars>` |
| 12 | +3. Key is hashed using **SHA-256** and stored in `api_keys` table |
| 13 | +4. Plaintext key shown once to user (never stored) |
| 14 | + |
| 15 | +### Key Validation |
| 16 | +1. Client sends request with `Authorization: Bearer ery_live_xxx` |
| 17 | +2. System extracts key prefix (first 12 chars) for efficient lookup |
| 18 | +3. Looks up candidate keys by prefix |
| 19 | +4. Hashes provided key with **SHA-256** and compares to stored hash |
| 20 | +5. On match: sets tenant context and allows request |
| 21 | + |
| 22 | +## Architecture |
| 23 | + |
| 24 | +``` |
| 25 | +┌─────────────────────┐ ┌──────────────────────┐ |
| 26 | +│ Dashboard User │ │ External System │ |
| 27 | +│ (JWT Auth) │ │ (API Key Auth) │ |
| 28 | +└─────────┬───────────┘ └──────────┬───────────┘ |
| 29 | + │ │ |
| 30 | + ▼ ▼ |
| 31 | +┌─────────────────────┐ ┌──────────────────────┐ |
| 32 | +│ api-key-generate │ │ api-jobs, api-parts │ |
| 33 | +│ (creates keys) │ │ (validates keys) │ |
| 34 | +└─────────┬───────────┘ └──────────┬───────────┘ |
| 35 | + │ │ |
| 36 | + │ SHA-256 hash │ SHA-256 hash |
| 37 | + ▼ ▼ |
| 38 | +┌──────────────────────────────────────────────────┐ |
| 39 | +│ api_keys table │ |
| 40 | +│ key_prefix | key_hash | tenant_id | active │ |
| 41 | +└──────────────────────────────────────────────────┘ |
| 42 | +``` |
| 43 | + |
| 44 | +## Shared Auth Module |
| 45 | + |
| 46 | +All API endpoints use the shared authentication module: |
| 47 | + |
| 48 | +```typescript |
| 49 | +// supabase/functions/_shared/auth.ts |
| 50 | +import { authenticateAndSetContext } from "../_shared/auth.ts"; |
| 51 | + |
| 52 | +// In your endpoint: |
| 53 | +const { tenantId } = await authenticateAndSetContext(req, supabase); |
| 54 | +``` |
| 55 | + |
| 56 | +This module: |
| 57 | +- Extracts Bearer token from Authorization header |
| 58 | +- Validates key format (`ery_live_*` or `ery_test_*`) |
| 59 | +- Looks up key by prefix (efficient query) |
| 60 | +- Hashes and compares using SHA-256 |
| 61 | +- Sets tenant context for Row-Level Security |
| 62 | +- Updates `last_used_at` timestamp (async) |
| 63 | + |
| 64 | +## Two Auth Patterns |
| 65 | + |
| 66 | +| Pattern | Endpoints | Token Type | Use Case | |
| 67 | +|---------|-----------|------------|----------| |
| 68 | +| **API Key** | `api-jobs`, `api-parts`, etc. | `ery_live_xxx` | External integrations | |
| 69 | +| **JWT** | `api-key-generate`, `api-integrations` | Supabase session | Dashboard users | |
| 70 | + |
| 71 | +## Key Format |
| 72 | + |
| 73 | +``` |
| 74 | +ery_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6 |
| 75 | +└─┬─┘└┬─┘└──────────────┬────────────────┘ |
| 76 | + │ │ │ |
| 77 | + │ │ └── 32 random chars |
| 78 | + │ └── Environment (live/test) |
| 79 | + └── Prefix identifier |
| 80 | +``` |
| 81 | + |
| 82 | +## Database Schema |
| 83 | + |
| 84 | +```sql |
| 85 | +CREATE TABLE api_keys ( |
| 86 | + id UUID PRIMARY KEY, |
| 87 | + tenant_id UUID NOT NULL REFERENCES tenants(id), |
| 88 | + name TEXT NOT NULL, |
| 89 | + key_prefix TEXT NOT NULL, -- First 12 chars for lookup |
| 90 | + key_hash TEXT NOT NULL, -- SHA-256 hash of full key |
| 91 | + active BOOLEAN DEFAULT true, |
| 92 | + last_used_at TIMESTAMPTZ, |
| 93 | + created_at TIMESTAMPTZ DEFAULT now(), |
| 94 | + created_by UUID REFERENCES profiles(id) |
| 95 | +); |
| 96 | + |
| 97 | +-- Index for efficient prefix lookup |
| 98 | +CREATE INDEX idx_api_keys_prefix ON api_keys(key_prefix) WHERE active = true; |
| 99 | +``` |
| 100 | + |
| 101 | +## Security Considerations |
| 102 | + |
| 103 | +1. **Keys are never stored** - Only SHA-256 hash is persisted |
| 104 | +2. **Prefix lookup** - Avoids comparing all keys (O(1) vs O(n)) |
| 105 | +3. **Constant-time comparison** - SHA-256 comparison prevents timing attacks |
| 106 | +4. **Async last_used update** - Doesn't block request response |
| 107 | +5. **Soft delete** - Keys are deactivated, not deleted (audit trail) |
| 108 | + |
| 109 | +## Error Responses |
| 110 | + |
| 111 | +| Error | HTTP Status | Cause | |
| 112 | +|-------|-------------|-------| |
| 113 | +| `Missing or invalid authorization header` | 401 | No Bearer token | |
| 114 | +| `Invalid API key format` | 401 | Key doesn't match `ery_*` pattern | |
| 115 | +| `Invalid API key` | 401 | Key not found or hash mismatch | |
| 116 | + |
| 117 | +## Testing |
| 118 | + |
| 119 | +```bash |
| 120 | +# Test with valid key |
| 121 | +curl -H "Authorization: Bearer ery_live_yourkey" \ |
| 122 | + https://yourproject.supabase.co/functions/v1/api-jobs |
| 123 | + |
| 124 | +# Expected: 200 OK with jobs data |
| 125 | + |
| 126 | +# Test with invalid key |
| 127 | +curl -H "Authorization: Bearer invalid_key" \ |
| 128 | + https://yourproject.supabase.co/functions/v1/api-jobs |
| 129 | + |
| 130 | +# Expected: 401 Unauthorized |
| 131 | +``` |
| 132 | + |
| 133 | +## Historical Note |
| 134 | + |
| 135 | +**Issue Fixed (Dec 2024):** API endpoints were using bcrypt for key validation while key generation used SHA-256. This caused all API key authentication to fail. Fixed by standardizing all endpoints on the shared auth module with SHA-256. |
0 commit comments