# API Keys & Tokens

> API token authentication for programmatic access with organization-scoped security

## 📋 Overview

API Tokens provide token-based authentication for programmatic access to Izri APIs, enabling CI/CD integration, automation scripts, and third-party applications. Tokens are now **organization-scoped**, providing better security isolation and role-based access control.

## 🚀 What's New (v2.0)

### Organization-Scoped Tokens

- **Previously**: Tokens were user-scoped (one user, one set of tokens)
- **Now**: Tokens are org-scoped (per organization, with role-based permissions)
- **Breaking Change**: Existing user-scoped tokens are no longer valid. Create org-scoped tokens instead.

### Enhanced Security

- Tokens are isolated by organization
- Scope enforcement based on user's role within the organization
- Audit logging for all token operations
- Rate limiting per organization
- Token rotation support

### Features

✅ Create tokens with specific scopes  
✅ Organization-based isolation  
✅ Role-based scope enforcement  
✅ Rate limiting per organization  
✅ Audit logging with full context  
✅ Token rotation (revoke old, create new atomically)  
✅ Token expiration support  

## 🏗️ Database Schema

**apiTokens table** (organization-scoped):

```typescript
{
  id: string                    // Unique token ID
  organizationId: string        // Organization ownership
  name: string                  // Descriptive name
  hash: string                  // SHA-256 hashed token
  scopes: string[]              // Permissions array
  lastUsedAt: Date?             // Last validation timestamp
  expiresAt: Date?              // Expiration date (optional)
  revokedAt: Date?              // Revocation date (soft delete)
  createdAt: Date               // Creation timestamp
  createdBy: string             // User who created token
}
```

**Indexes**:

```sql
CREATE UNIQUE INDEX api_tokens_hash_key ON api_tokens(hash);
CREATE INDEX api_tokens_org_idx ON api_tokens(organization_id);
CREATE INDEX api_tokens_revoked_idx ON api_tokens(revoked_at);
```

## 🔐 Token Format

### Token Structure

Tokens follow a simple, secure format:

```
izri_[64-char-hex-random-id]

Example:
izri_a7f8c2e91d5b4a9c3e6f7a2b8d0c1e5f9a8b7c6d5e4f3a2b1c0d9e8f7a6b5c
```

**Components**:
- Prefix: `izri_` (identifies tokens in logs and environments)
- Payload: 64 hex characters (cryptographically secure random)

### Token Generation

```typescript
import crypto from 'node:crypto'

function generateToken(): { raw: string; hash: string } {
  // Generate 32 random bytes
  const random = crypto.randomBytes(32).toString('hex')
  const raw = `izri_${random}`
  
  // Hash for storage (SHA-256)
  const hash = crypto.createHash('sha256').update(raw).digest('hex')
  
  return { raw, hash }
}
```

### Token Hashing (Storage)

```typescript
// Tokens are hashed before database storage
const { raw, hash } = generateToken()

await db.insert(apiTokens).values({
  hash,  // Store hash, not raw token
  organizationId,
  scopes,
  // ...
})

// Client stores raw token
// Server validates by hashing incoming token and comparing with stored hash
```

## 🔑 API Endpoints (tRPC Router)

### Create Token

**Mutation**: `tokens.createToken`

```typescript
const result = await trpc.tokens.createToken.mutate({
  organizationId: 'org_123',
  name: 'CI/CD Pipeline',
  scopes: ['projects:read', 'tests:run'],
  expiresIn: 604800  // 7 days in seconds (optional)
})

// Response
{
  token: {
    id: 'token_abc123',
    organizationId: 'org_123',
    name: 'CI/CD Pipeline',
    createdAt: '2024-01-15T10:30:00Z',
    expiresAt: '2024-01-22T10:30:00Z',
    scopes: ['projects:read', 'tests:run']
  },
  rawToken: 'izri_...',  // ⚠️ Only shown once!
  message: 'Token created. Save the token now — it will not be shown again.'
}
```

**Important**: The raw token is only displayed once during creation. Store it securely immediately!

### List Tokens

**Query**: `tokens.listTokens`

```typescript
const result = await trpc.tokens.listTokens.query({
  organizationId: 'org_123'
})

// Response
{
  tokens: [
    {
      id: 'token_abc123',
      organizationId: 'org_123',
      name: 'CI/CD Pipeline',
      scopes: ['projects:read', 'tests:run'],
      lastUsedAt: '2024-01-15T11:45:00Z',
      expiresAt: '2024-01-22T10:30:00Z',
      createdAt: '2024-01-15T10:30:00Z'
    }
  ]
}
```

**Note**: Actual token hash is never returned after creation

### Revoke Token

**Mutation**: `tokens.revokeToken`

```typescript
await trpc.tokens.revokeToken.mutate({
  organizationId: 'org_123',
  tokenId: 'token_abc123'
})

// Response
{ success: true }
```

**Effects**:
- Token immediately becomes invalid
- Cannot be restored
- All requests using token will be rejected
- Audit logged

### Rotate Token

**Mutation**: `tokens.rotateToken`

```typescript
const result = await trpc.tokens.rotateToken.mutate({
  organizationId: 'org_123',
  tokenId: 'token_abc123',
  newName: 'Updated CI/CD Token',  // optional
  expiresIn: 604800,               // optional
  scopes: ['projects:read']        // optional
})

// Response
{
  token: {
    id: 'token_def456',  // New token ID
    organizationId: 'org_123',
    name: 'Updated CI/CD Token',
    createdAt: '2024-01-15T12:00:00Z',
    expiresAt: '2024-01-22T12:00:00Z',
    scopes: ['projects:read']
  },
  rawToken: 'izri_...',  // New raw token (shown once)
  message: 'Token rotated. Save the new token now — it will not be shown again.'
}
```

**Behavior**:
- Atomically creates new token and revokes old one
- Can update metadata (name, expiration, scopes) in one operation
- Old token ID cannot be used again
- Both operations are audit logged

## 🔒 Scopes & Permissions

### Scope Vocabulary

Scopes control what operations a token can perform. Scopes are hierarchical and role-aware.

```typescript
// Read permissions
'projects:read'      // Read projects
'tests:read'         // Read test results and history
'costs:read'         // Read cost metrics

// Write permissions
'tests:run'          // Execute tests
'tests:write'        // Modify test configurations
'projects:write'     // Create/modify projects

// Admin permissions
'org:read'           // Read organization metadata
'org:write'          // Modify organization settings
'team:manage'        // Manage team members
'billing:read'       // Read billing information

// Full access
'*'                  // All scopes (owner-only)
```

### Role-Based Scopes

Tokens inherit maximum scopes based on the creator's role:

```typescript
// OWNER - Full access
['*']

// ADMIN - Broad access (excludes billing)
['projects:read', 'projects:write', 'tests:read', 'tests:run', 'tests:write',
 'costs:read', 'org:read', 'org:write', 'team:manage']

// MEMBER - Standard access
['projects:read', 'tests:read', 'tests:run']

// VIEWER - Read-only access
['projects:read', 'tests:read', 'costs:read']
```

### Scope Validation

```typescript
// Check if token has required scope
function hasScope(tokenScopes: string[], required: string): boolean {
  return tokenScopes.includes(required) || tokenScopes.includes('*')
}

// Validate scope is real
function isValidScope(scope: string): boolean {
  // Returns true only for defined scopes
  return ['projects:read', 'tests:run', '*', /* ... */].includes(scope)
}
```

### Enforcement Examples

```typescript
// ✅ ADMIN can create token with tests:run
const token = await trpc.tokens.createToken.mutate({
  organizationId: 'org_123',
  scopes: ['tests:run']  // Admin has this scope
})

// ❌ MEMBER cannot create token with team:manage
try {
  const token = await trpc.tokens.createToken.mutate({
    organizationId: 'org_123',
    scopes: ['team:manage']  // Member doesn't have this scope
  })
} catch (e) {
  // TRPCError: FORBIDDEN - "Your role (member) does not permit scope: team:manage"
}
```

## 📊 Rate Limiting

Tokens operations are rate-limited per organization:

```typescript
// TOKEN_CREATE: 10 tokens per minute per org
// TOKEN_ROTATE: 20 rotations per minute per org
// TOKEN_LIST: 100 list requests per minute per org
// TOKEN_REVOKE: 50 revokes per minute per org
```

Rate limit headers in responses:

```
X-RateLimit-Limit: 10
X-RateLimit-Remaining: 8
X-RateLimit-Reset: 2024-01-15T10:31:00Z
```

Exceeding limits:

```typescript
{
  code: 'TOO_MANY_REQUESTS',
  message: 'Rate limit exceeded. Try again after 2024-01-15T10:31:00Z'
}
```

## 🔌 Using API Tokens

### Authentication Header

```bash
# Add token to Authorization header
curl -H "Authorization: Bearer izri_..." \
  https://api.example.com/trpc/projects.list
```

### tRPC Client Configuration

```typescript
import { createTRPCProxyClient, httpBatchLink } from '@trpc/client'

const apiKey = process.env.IZRI_API_KEY
const orgId = process.env.IZRI_ORG_ID

const client = createTRPCProxyClient({
  links: [
    httpBatchLink({
      url: 'https://api.example.com/trpc',
      headers: {
        Authorization: `Bearer ${apiKey}`,
        'X-Organization-Id': orgId
      }
    })
  ]
})

// Use client
const projects = await client.projects.list.query()
```

### Direct HTTP Requests

```javascript
const response = await fetch('https://api.example.com/trpc/projects.list', {
  method: 'GET',
  headers: {
    'Authorization': `Bearer ${apiKey}`,
    'X-Organization-Id': orgId
  }
})
```

## 🔄 CI/CD Integration Examples

### GitHub Actions

```yaml
name: Run Tests
on: [push]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Set up environment
        run: |
          echo "IZRI_API_KEY=${{ secrets.IZRI_API_KEY }}" >> $GITHUB_ENV
          echo "IZRI_ORG_ID=${{ secrets.IZRI_ORG_ID }}" >> $GITHUB_ENV
      
      - name: Run Izri Tests
        run: |
          npm install @izri/client
          node run-tests.js
```

**run-tests.js**:

```javascript
import { createTRPCProxyClient, httpBatchLink } from '@trpc/client'

const client = createTRPCProxyClient({
  links: [
    httpBatchLink({
      url: 'https://api.example.com/trpc',
      headers: {
        'Authorization': `Bearer ${process.env.IZRI_API_KEY}`,
        'X-Organization-Id': process.env.IZRI_ORG_ID
      }
    })
  ]
})

// List projects
const projects = await client.projects.list.query()

// Run tests
for (const project of projects) {
  const result = await client.tests.run.mutate({
    projectId: project.id
  })
  console.log(`Project ${project.name}: ${result.status}`)
}
```

### GitLab CI

```yaml
stages:
  - test

run_tests:
  stage: test
  image: node:18
  script:
    - npm install
    - IZRI_API_KEY=$IZRI_API_KEY IZRI_ORG_ID=$IZRI_ORG_ID npm run test:remote
```

### Jenkins Pipeline

```groovy
pipeline {
  agent any
  
  environment {
    IZRI_API_KEY = credentials('izri-api-key')
    IZRI_ORG_ID = credentials('izri-org-id')
  }
  
  stages {
    stage('Test') {
      steps {
        sh '''
          npm install
          npm run test:remote
        '''
      }
    }
  }
}
```

## 🎨 Frontend Management

### Create Token Dialog

Users can create tokens from the organization's API Tokens page:

```
/organizations/{slug}/api-tokens
```

**Features**:
- Choose token name
- Select scopes (limited to user's role)
- Set expiration date (optional)
- See raw token exactly once (with copy button)
- Automatic logout after token creation (for security)

### Token List

View all active tokens for the organization:

```
- Token name
- Scopes (shown as badges)
- Last used timestamp
- Expiration date
- Creation date
- Revoke button
- Rotate button
```

## 🔒 Security Best Practices

### 1. Use Minimum Scopes

```typescript
// ❌ Too broad
const token = await trpc.tokens.createToken.mutate({
  scopes: ['*']  // Full access
})

// ✅ Specific permissions
const token = await trpc.tokens.createToken.mutate({
  scopes: ['projects:read', 'tests:run']  // Just what's needed
})
```

### 2. Rotate Regularly

```typescript
// Create short-lived tokens
const token = await trpc.tokens.createToken.mutate({
  name: 'Temporary CI Token',
  expiresIn: 7 * 24 * 60 * 60  // 7 days
})

// Rotate before expiration
const newToken = await trpc.tokens.rotateToken.mutate({
  tokenId: token.id,
  expiresIn: 7 * 24 * 60 * 60
})
```

### 3. Monitor Usage

```typescript
// Check last used timestamp
const tokens = await trpc.tokens.listTokens.query({ organizationId })

for (const token of tokens) {
  if (!token.lastUsedAt) {
    console.log(`Token "${token.name}" has never been used`)
  }
  
  const daysUnused = (Date.now() - token.lastUsedAt) / (1000 * 60 * 60 * 24)
  if (daysUnused > 30) {
    console.log(`Token "${token.name}" unused for ${daysUnused} days`)
  }
}
```

### 4. Revoke Compromised Tokens

```typescript
// Immediately revoke if token is leaked
await trpc.tokens.revokeToken.mutate({
  organizationId: 'org_123',
  tokenId: 'token_abc123'
})

// Create new token
const newToken = await trpc.tokens.createToken.mutate({
  organizationId: 'org_123',
  name: 'Replacement Token',
  scopes: ['projects:read', 'tests:run']
})
```

### 5. Secure Storage

**Environment Variables**:
```bash
# .env.local (NOT committed)
IZRI_API_KEY=izri_...
IZRI_ORG_ID=org_...
```

**CI/CD Secrets**:
```yaml
# GitHub Actions secrets
secrets.IZRI_API_KEY
secrets.IZRI_ORG_ID
```

**Never**:
- ❌ Commit tokens to repository
- ❌ Log tokens in debug output
- ❌ Share tokens via email/chat
- ❌ Use one token for multiple purposes
- ❌ Grant tokens unnecessary scopes

## 🔄 Migration Guide (v1 → v2)

### What Changed

- **Tokens are now org-scoped** (not user-scoped)
- **Create endpoint location**: Same tRPC path, different input
- **New required field**: `organizationId`
- **Scope names**: Unchanged, but validation is stricter
- **Breaking**: Old user-scoped tokens no longer work

### Migration Steps

1. **Create new org-scoped tokens**:
   ```typescript
   // v1 (deprecated)
   const token = await trpc.apiKeys.create.mutate({
     name: 'My Token',
     scopes: ['projects:read']
   })

   // v2 (new)
   const token = await trpc.tokens.createToken.mutate({
     organizationId: 'org_123',
     name: 'My Token',
     scopes: ['projects:read']
   })
   ```

2. **Update environment variables**:
   ```bash
   # Add org ID
   IZRI_ORG_ID=org_123
   ```

3. **Update API calls**:
   ```typescript
   // Add org ID to headers
   headers: {
     'Authorization': `Bearer ${apiKey}`,
     'X-Organization-Id': orgId
   }
   ```

4. **Revoke old tokens** (if still in system):
   ```typescript
   // List to find old tokens
   const allTokens = await trpc.tokens.listTokens.query({
     organizationId
   })
   
   // Revoke if needed
   for (const token of allTokens) {
     await trpc.tokens.revokeToken.mutate({
       organizationId,
       tokenId: token.id
     })
   }
   ```

## 📋 Testing

Comprehensive tests cover:

```typescript
describe('API Token System', () => {
  // Organization scoping
  it('isolates tokens across organizations', ...)
  it('prevents cross-org access', ...)
  
  // Scope enforcement
  it('validates role-based scopes', ...)
  it('prevents invalid scope assignment', ...)
  
  // Rate limiting
  it('enforces per-organization limits', ...)
  it('provides reset times', ...)
  
  // Audit logging
  it('logs token creation', ...)
  it('logs token revocation', ...)
  it('logs token rotation', ...)
  
  // Integration
  it('creates/lists/revokes/rotates tokens', ...)
  it('works in CI/CD environments', ...)
})
```

Run tests:
```bash
npm run test -- tokens.test.ts
```

## 🔗 Related Documentation

- **Authentication**: [Backend Auth Overview](../backend/authentication-and-api.md)
- **API Reference**: [Full API Reference](../api-reference/authentication-api.md)
- **Webhooks**: [Webhook Authentication](./webhooks.md)

## ⚠️ Breaking Changes Summary

| Feature | v1 (Legacy) | v2 (New) | Impact |
|---------|------------|---------|--------|
| **Scoping** | User-scoped | Org-scoped | Must provide `organizationId` |
| **Route** | `apiKeys.*` | `tokens.*` | Update tRPC call paths |
| **Validation** | Loose | Strict role-based | May be rejected if scopes exceed role |
| **Rate Limits** | Per-user | Per-org | Higher limits but org-wide |
| **Audit Logging** | Limited | Comprehensive | All operations logged with context |

---

*Last updated: 2024-01-15*  
*Next: [Webhooks](./webhooks.md) →*
