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

357 lines
8.3 KiB
Markdown

# CORS and Vault Client Implementation
## ❌ Why Your Changes Won't Work
You tried to fix CORS by adding these changes:
```typescript
// ❌ 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:**
```typescript
// 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:
```hcl
# 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:
```yaml
# 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:
```bash
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:
```typescript
// ✅ 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)
```typescript
// ❌ 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)
```typescript
// ✅ Clean, maintainable, type-safe
const client = new VaultClient(options);
const data = await client.read(path); // Simple!
```
### Benefits of VaultClient
1. **Error Handling**
```typescript
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**
```typescript
const client = new VaultClient({
...options,
timeout: 5000 // 5 seconds
});
// Request will abort after 5 seconds
```
4. **Path Normalization**
```typescript
// 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**
```typescript
// 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
// 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)
```bash
curl -X GET \
-H "X-Vault-Token: your-token" \
http://localhost:8200/v1/secret/data/test
```
### 2. Test from browser console
```javascript
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
```typescript
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
```typescript
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
```typescript
// 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!