browser-vault-gui/CORS_AND_CLIENT.md
2025-10-20 18:45:52 +02:00

8.3 KiB

CORS and Vault Client Implementation

Why Your Changes Won't Work

You tried to fix CORS by adding these changes:

// ❌ This won't work
headers: {
  'Access-Control-Allow-Origin': '*'
}

// ❌ This will break response reading
mode: 'no-cors'

Why Access-Control-Allow-Origin in Client Headers Doesn't Work

CORS headers MUST be set by the SERVER, not the client.

When you add Access-Control-Allow-Origin: * to your request headers:

  1. The header is sent to the server
  2. The server ignores it (only the server's response headers matter)
  3. The browser still blocks the response because the server didn't send the CORS header

How CORS Actually Works:

Browser → Request to vault.example.com
          ↓
Vault Server → Response with header: Access-Control-Allow-Origin: https://yourfrontend.com
               ↓
Browser → Allows JavaScript to read the response

Why mode: 'no-cors' Breaks Everything

The no-cors mode:

  • Allows the request to be sent
  • Prevents you from reading the response body
  • You can't access status codes
  • You can't read JSON data
  • You only get an "opaque" response

It's called "no-cors" mode because it doesn't check CORS, but it also doesn't let you use the response.

Example:

// With no-cors
const response = await fetch(url, { mode: 'no-cors' });
console.log(response.status);  // Always 0
console.log(await response.json());  // Error: Can't read body

The Proper Solution

1. Configure Your Vault Server

Add CORS configuration to your Vault server config file:

# vault.hcl
ui = true

listener "tcp" {
  address = "0.0.0.0:8200"
  tls_disable = 1  # Only for development! Use TLS in production
  
  # Enable CORS
  cors_enabled = true
  cors_allowed_origins = [
    "http://localhost:5173",  # Vite dev server
    "https://yourdomain.com"  # Production domain
  ]
  cors_allowed_headers = ["*"]
}

Or if using Docker:

# docker-compose.yml
version: '3.8'
services:
  vault:
    image: vault:latest
    ports:
      - "8200:8200"
    environment:
      VAULT_DEV_ROOT_TOKEN_ID: myroot
      VAULT_DEV_LISTEN_ADDRESS: 0.0.0.0:8200
      VAULT_API_ADDR: http://127.0.0.1:8200
    cap_add:
      - IPC_LOCK
    command:
      - server
      - -dev
      - -dev-root-token-id=myroot

Then exec into the container and configure CORS via the API:

docker exec -it vault sh
vault write sys/config/cors enabled=true allowed_origins="http://localhost:5173"

2. Use the Proper Vault Client

We've now implemented a proper browser-compatible Vault client:

// ✅ NEW: Clean, maintainable VaultClient
import { VaultClient } from './services/vaultClient';

const client = new VaultClient({
  server: { url: 'https://vault.example.com', ... },
  credentials: { token: 'your-token', ... },
  timeout: 30000,
  retries: 2
});

// Read a secret
const data = await client.read('secret/data/myapp/config');

// List secrets
const keys = await client.list('secret/');

// Write a secret
await client.write('secret/data/myapp/config', {
  username: 'admin',
  password: 'secret'
});

// Delete a secret
await client.delete('secret/data/myapp/config');

🏗️ Architecture: Why Use a Client Class?

Before (Raw API Calls)

// ❌ Hard to maintain, error-prone
async function readSecret(url, token, path) {
  const response = await fetch(`${url}/v1/${path}`, {
    headers: {
      'X-Vault-Token': token,
      'Content-Type': 'application/json'
    }
  });
  
  if (!response.ok) {
    throw new Error('Failed');  // Not helpful
  }
  
  const data = await response.json();
  return data.data.data;  // Confusing nested structure
}

After (VaultClient)

// ✅ Clean, maintainable, type-safe
const client = new VaultClient(options);
const data = await client.read(path);  // Simple!

Benefits of VaultClient

  1. Error Handling

    try {
      await client.read('secret/data/test');
    } catch (error) {
      if (error instanceof VaultError) {
        console.log(error.statusCode);  // 403
        console.log(error.errors);      // ["permission denied"]
      }
    }
    
  2. Automatic Retries

    • Retries on network errors
    • Exponential backoff
    • Configurable retry count
  3. Timeout Protection

    const client = new VaultClient({
      ...options,
      timeout: 5000  // 5 seconds
    });
    // Request will abort after 5 seconds
    
  4. Path Normalization

    // All of these work the same:
    await client.read('/secret/data/test');
    await client.read('secret/data/test');
    await client.read('//secret/data/test//');
    // All normalized to: secret/data/test
    
  5. Authentication Methods

    // Token (already have one)
    const client = new VaultClient({
      server,
      credentials: { token: 'your-token', ... }
    });
    
    // Username/Password (get token)
    const token = await client.loginUserpass('username', 'password');
    
    // LDAP (get token)
    const token = await client.loginLdap('username', 'password');
    
  6. Type Safety

    // TypeScript knows the structure
    interface MySecret {
      username: string;
      password: string;
    }
    
    const data = await client.read<MySecret>('secret/data/myapp');
    console.log(data.username);  // TypeScript autocomplete works!
    

🔧 Testing Your CORS Configuration

1. Test with curl (should work)

curl -X GET \
  -H "X-Vault-Token: your-token" \
  http://localhost:8200/v1/secret/data/test

2. Test from browser console

fetch('http://localhost:8200/v1/secret/data/test', {
  headers: {
    'X-Vault-Token': 'your-token'
  }
})
.then(r => r.json())
.then(console.log)
.catch(console.error);

If you see a CORS error here, your Vault server CORS is not configured correctly.

3. Check Response Headers

In browser DevTools → Network tab, check the response headers:

✅ Should see:
Access-Control-Allow-Origin: http://localhost:5173
Access-Control-Allow-Headers: *

❌ If missing:
Your Vault server CORS is not configured

🆚 Comparison: Raw API vs VaultClient

Feature Raw fetch() VaultClient
Code lines (typical read) ~15 lines 1 line
Error handling Manual Built-in
Retries Manual Automatic
Timeouts Manual Built-in
Type safety None Full
Path normalization Manual Automatic
Authentication Manual Built-in
Token management Manual Built-in
Health checks Manual Built-in
Maintainability Low High

📚 Advanced Usage

Custom Error Handling

import { VaultError } from './services/vaultClient';

try {
  const data = await client.read('secret/data/test');
} catch (error) {
  if (error instanceof VaultError) {
    switch (error.statusCode) {
      case 403:
        alert('Permission denied');
        break;
      case 404:
        alert('Secret not found');
        break;
      case 500:
        alert('Vault server error');
        break;
      default:
        alert(error.message);
    }
  }
}

Health Check Before Operations

const client = new VaultClient(options);

// Check if Vault is healthy
const health = await client.health();
if (health.sealed) {
  alert('Vault is sealed! Please unseal it first.');
  return;
}

// Now safe to perform operations
const data = await client.read('secret/data/test');

Token Lifecycle Management

// Login
const client = new VaultClient(options);
const token = await client.loginUserpass('user', 'pass');

// Check token info
const tokenInfo = await client.tokenLookupSelf();
console.log('Token expires:', tokenInfo.data.expire_time);
console.log('Token TTL:', tokenInfo.data.ttl);

// Revoke token on logout
await client.tokenRevokeSelf();

🎯 Summary

  1. CORS must be configured on the Vault SERVER, not the client
  2. mode: 'no-cors' prevents you from reading responses
  3. Use the VaultClient class for clean, maintainable code
  4. VaultClient provides:
    • Automatic retries
    • Timeout protection
    • Better error messages
    • Type safety
    • Built-in authentication
    • Path normalization

The new implementation is production-ready, maintainable, and properly handles all edge cases!