CalDAV: Support Basic auth; trim+validate UUID; deprecate query token via headers; ICS end time helper; docs+instructions updated

This commit is contained in:
2025-10-06 17:25:25 +02:00
parent 90029f4b6a
commit 31b007d145
29 changed files with 2311 additions and 321 deletions

121
docs/caldav-setup.md Normal file
View File

@@ -0,0 +1,121 @@
# CalDAV-Kalender Einrichtung
## Übersicht
Die App bietet einen CalDAV-Endpunkt, um Buchungen in externe Kalender-Apps zu synchronisieren. Aus Sicherheitsgründen erfolgt die Authentifizierung per Header. Unterstützt werden:
- Authorization: Bearer <TOKEN>
- Basic Auth, wobei der Token als Benutzername übergeben wird (Passwort leer/optional)
## Token generieren
1. Als Admin einloggen
2. Im Admin-Bereich den CalDAV-Token generieren
3. Token und URL werden angezeigt
**Wichtig:** Der Token ist 24 Stunden gültig. Danach muss ein neuer Token generiert werden.
## Endpunkt
```
GET /caldav/calendar/events.ics
Authorization: Bearer <TOKEN>
oder
Authorization: Basic <base64(token:)> # Token als Benutzername, Passwort leer
```
## Unterstützte Kalender-Apps
### ✅ Thunderbird (empfohlen)
1. Kalender → Neuer Kalender → Im Netzwerk
2. Format: CalDAV
3. Standort: CalDAV-URL eingeben
4. Benutzername: Den generierten Token eingeben (Basic Auth)
5. Passwort: Leer lassen
### ✅ Outlook (mit Einschränkungen)
1. Datei → Kontoeinstellungen → Internetkalender
2. URL eingeben
3. Erweiterte Einstellungen → Benutzerdefinierte Header:
```
Authorization: Bearer <TOKEN>
```
**Hinweis:** Nicht alle Outlook-Versionen unterstützen benutzerdefinierte Header.
### ⚠️ Apple Calendar (eingeschränkt)
Apple Calendar unterstützt keine Authorization-Header für Kalenderabonnements.
**Alternativen:**
- Manuelle ICS-Datei importieren (nicht automatisch aktualisiert)
- CalDAV-Bridge verwenden (z.B. über Proxy)
### ⚠️ Google Calendar (eingeschränkt)
Google Calendar unterstützt keine Authorization-Header für URL-Abonnements.
**Alternativen:**
- Google Apps Script als Bridge verwenden
- Manuelle ICS-Datei importieren
## Testen mit cURL
```bash
# Bearer
curl -H "Authorization: Bearer <DEIN_TOKEN>" \
https://deine-domain.de/caldav/calendar/events.ics
# Basic (Token als Benutzername, Passwort leer)
curl -H "Authorization: Basic $(printf "%s" "<DEIN_TOKEN>:" | base64)" \
https://deine-domain.de/caldav/calendar/events.ics
```
Erwartete Antwort: ICS-Datei mit allen bestätigten und ausstehenden Buchungen.
## Sicherheit
### Warum Authorization-Header?
- **Query-Parameter** (`?token=...`) werden in Browser-History, Server-Logs und Referrer-Headers gespeichert
- **Authorization-Header** werden nicht geloggt und nicht in der URL sichtbar
- Folgt REST-API Best Practices
### Token-Verwaltung
- Token sind 24 Stunden gültig
- Abgelaufene Token werden automatisch beim nächsten Zugriff gelöscht
- Bei Bedarf neuen Token generieren (alte Token werden nicht automatisch invalidiert)
### Backward Compatibility
Die alte Methode mit Query-Parameter (`?token=...`) wird noch unterstützt, aber als **deprecated** markiert. Der Server sendet zusätzlich Response-Header (`Deprecation: true` und `Warning: 299 ...`). Eine Warnung wird im Server-Log ausgegeben.
## Troubleshooting
### "Unauthorized - Token required"
- Prüfe, ob der Authorization-Header korrekt gesetzt ist
- Format: `Authorization: Bearer <TOKEN>` (mit Leerzeichen nach "Bearer")
### "Unauthorized - Invalid or expired token"
- Token ist abgelaufen (24h Gültigkeit)
- Generiere einen neuen Token im Admin-Bereich
### Kalender zeigt keine Termine
- Prüfe, ob Buchungen mit Status "confirmed" oder "pending" existieren
- Teste den Endpunkt mit cURL
- Prüfe Server-Logs auf Fehler
## Zukünftige Verbesserungen
- [ ] Langlebige Token mit Refresh-Mechanismus
- [ ] Token-Revocation-Endpoint
- [ ] CalDAV-Bridge für Apple Calendar und Google Calendar
- [ ] Webhook-basierte Push-Notifications statt Polling

View File

@@ -37,17 +37,38 @@ Das System verwendet ein Rate-Limiting, um Spam und Missbrauch des Buchungsformu
- **Zeitfenster:** 10 Minuten
- **Verhalten:** Nach 5 Anfragen muss der Nutzer 10 Minuten warten
### Login (IP-basiert)
- **Limit:** 5 fehlgeschlagene Login-Versuche pro IP-Adresse
- **Zeitfenster:** 15 Minuten
- **Verhalten:** Das Limit zählt nur fehlgeschlagene Versuche. Nach erfolgreichem Login wird der Zähler zurückgesetzt. Bei Überschreitung muss der Nutzer 15 Minuten warten.
- **Zweck:** Schutz vor Brute-Force-Angriffen auf Admin-Accounts
### Admin-Operationen (Benutzer-basiert)
- **Limit:** 30 Anfragen pro Admin-Benutzer
- **Zeitfenster:** 5 Minuten
- **Verhalten:** Nach 30 Anfragen muss der Admin 5 Minuten warten
- **Zweck:** Verhindert versehentliches Spam durch Admin-UI (z.B. Doppelklicks, fehlerhafte Skripte)
- **Betroffene Endpoints:** Treatments (create/update/remove), Bookings (manualCreate/updateStatus/remove), Recurring Availability (alle Schreiboperationen), Gallery (alle Schreiboperationen), Reviews (approve/reject/delete)
### Admin-Operationen (IP-basiert)
- **Limit:** 50 Anfragen pro IP-Adresse
- **Zeitfenster:** 5 Minuten
- **Verhalten:** Nach 50 Anfragen muss 5 Minuten gewartet werden
- **Zweck:** Zusätzlicher Schutz gegen IP-basierte Angriffe auf Admin-Endpoints
## Wie es funktioniert
Das Rate-Limiting prüft **beide** Kriterien:
Das Rate-Limiting prüft die passenden Kriterien je Endpoint. Für Admin-Operationen werden **beide** Limits geprüft:
1. **E-Mail-Adresse:** Verhindert, dass dieselbe Person mit derselben E-Mail zu viele Anfragen stellt
2. **IP-Adresse:** Verhindert, dass jemand mit verschiedenen E-Mail-Adressen von derselben IP aus spammt
3. **Benutzer-basiert (Admin):** Limitiert Anfragen je Admin-Benutzer
4. **IP-basiert (Admin):** Limitiert Anfragen je IP zusätzlich
Wenn eines der Limits überschritten wird, erhält der Nutzer eine Fehlermeldung mit Angabe der Wartezeit.
## IP-Erkennung
Das System erkennt die Client-IP auch hinter Proxies und Load Balancern durch folgende Headers:
Das System erkennt die Client-IP auch hinter Proxies und Load Balancern durch folgende Headers (unterstützt `Headers`-API und einfache Record-Objekte):
- `x-forwarded-for`
- `x-real-ip`
- `cf-connecting-ip` (Cloudflare)
@@ -60,7 +81,7 @@ Das System erkennt die Client-IP auch hinter Proxies und Load Balancern durch fo
## Anpassung
Die Limits können in `src/server/lib/rate-limiter.ts` in der Funktion `checkBookingRateLimit()` angepasst werden:
Die Limits können in `src/server/lib/rate-limiter.ts` angepasst werden. Beispiele:
```typescript
// E-Mail-Limit anpassen
@@ -74,6 +95,23 @@ const ipConfig: RateLimitConfig = {
maxRequests: 5, // Anzahl der Anfragen
windowMs: 10 * 60 * 1000, // Zeitfenster in Millisekunden
};
// Login-Bruteforce-Schutz
const loginConfig: RateLimitConfig = {
maxRequests: 5,
windowMs: 15 * 60 * 1000,
};
// Admin-Operationen
const adminUserConfig: RateLimitConfig = {
maxRequests: 30,
windowMs: 5 * 60 * 1000,
};
const adminIpConfig: RateLimitConfig = {
maxRequests: 50,
windowMs: 5 * 60 * 1000,
};
```
## Fehlermeldungen
@@ -91,3 +129,5 @@ Für Produktionsumgebungen empfehlen sich:
- ✅ Whitelist für vertrauenswürdige IPs (z.B. Admin-Zugang)
- ✅ Anpassung der Limits basierend auf tatsächlichem Nutzungsverhalten
Siehe auch `docs/redis-migration.md` für Hinweise zur Migration auf Redis in Multi-Instance-Setups.

355
docs/redis-migration.md Normal file
View File

@@ -0,0 +1,355 @@
# Redis Migration Guide
## Overview
This guide covers migrating from in-memory KV storage to Redis for multi-instance SaaS deployments. The current in-memory session storage works well for single-instance deployments but requires centralized storage for horizontal scaling.
### When to Migrate
**Current Setup (Sufficient For):**
- Single-instance deployments
- Development environments
- Small-scale production deployments
**Redis Required For:**
- Multiple server instances behind load balancer
- Container orchestration (Kubernetes, Docker Swarm)
- High-availability SaaS deployments
- Session persistence across server restarts
## What Needs Migration
### Critical Data (Must Migrate)
- **Sessions** (`sessionsKV`): Essential for authentication across instances
- **CSRF tokens**: Stored within session objects
- **Rate limiting data**: Currently in-memory Map in `rate-limiter.ts`
### Optional Data (Can Remain File-based)
- Bookings, treatments, reviews, gallery photos
- Can stay in file-based KV for now
- Migrate later if performance becomes an issue
## Redis Setup
### Installation Options
#### Self-hosted Redis
```bash
# Docker
docker run -d -p 6379:6379 redis:alpine
# Ubuntu/Debian
sudo apt update
sudo apt install redis-server
# macOS (Homebrew)
brew install redis
brew services start redis
```
#### Managed Services
- **Redis Cloud**: Managed Redis service
- **AWS ElastiCache**: Redis-compatible cache
- **Azure Cache for Redis**: Microsoft's managed service
- **DigitalOcean Managed Databases**: Redis option available
### Configuration
Add to `.env`:
```bash
# Redis Configuration (required for multi-instance deployments)
REDIS_URL=redis://localhost:6379
REDIS_TLS_ENABLED=false # Set to true for production
REDIS_PASSWORD=your_redis_password # Optional
```
## Code Changes
### 1. Install Redis Client
```bash
pnpm add ioredis
pnpm add -D @types/ioredis
```
### 2. Create Redis KV Adapter
Create `src/server/lib/create-redis-kv.ts`:
```typescript
import Redis from 'ioredis';
interface RedisKVStore<T> {
getItem(key: string): Promise<T | null>;
setItem(key: string, value: T): Promise<void>;
removeItem(key: string): Promise<void>;
getAllItems(): Promise<T[]>;
subscribe(): AsyncIterable<void>;
}
export function createRedisKV<T>(namespace: string): RedisKVStore<T> {
const redis = new Redis(process.env.REDIS_URL || 'redis://localhost:6379', {
retryDelayOnFailover: 100,
enableReadyCheck: false,
maxRetriesPerRequest: null,
lazyConnect: true,
});
const prefix = `${namespace}:`;
return {
async getItem(key: string): Promise<T | null> {
const value = await redis.get(`${prefix}${key}`);
return value ? JSON.parse(value) : null;
},
async setItem(key: string, value: T): Promise<void> {
await redis.set(`${prefix}${key}`, JSON.stringify(value));
},
async removeItem(key: string): Promise<void> {
await redis.del(`${prefix}${key}`);
},
async getAllItems(): Promise<T[]> {
const keys = await redis.keys(`${prefix}*`);
if (keys.length === 0) return [];
const values = await redis.mget(...keys);
return values
.filter(v => v !== null)
.map(v => JSON.parse(v!));
},
async* subscribe(): AsyncIterable<void> {
const pubsub = new Redis(process.env.REDIS_URL || 'redis://localhost:6379');
await pubsub.subscribe(`${namespace}:changes`);
for await (const message of pubsub) {
if (message[0] === 'message' && message[1] === `${namespace}:changes`) {
yield;
}
}
}
};
}
// Helper to notify subscribers of changes
export async function notifyRedisChanges(namespace: string): Promise<void> {
const redis = new Redis(process.env.REDIS_URL || 'redis://localhost:6379');
await redis.publish(`${namespace}:changes`, 'update');
await redis.quit();
}
```
### 3. Update Session Storage
In `src/server/lib/auth.ts`:
```typescript
import { createRedisKV } from './create-redis-kv.js';
// Replace this line:
// export const sessionsKV = createKV<Session>("sessions");
// With this:
export const sessionsKV = createRedisKV<Session>("sessions");
```
### 4. Update Rate Limiter
In `src/server/lib/rate-limiter.ts`:
```typescript
import Redis from 'ioredis';
const redis = new Redis(process.env.REDIS_URL || 'redis://localhost:6379', {
retryDelayOnFailover: 100,
enableReadyCheck: false,
maxRetriesPerRequest: null,
lazyConnect: true,
});
export async function checkBookingRateLimit(
email?: string,
ip?: string
): Promise<{ allowed: boolean; resetTime?: number }> {
const now = Date.now();
const windowMs = 15 * 60 * 1000; // 15 minutes
const maxRequests = 3;
const results = await Promise.all([
email ? checkRateLimit(`booking:email:${email}`, maxRequests, windowMs) : { allowed: true },
ip ? checkRateLimit(`booking:ip:${ip}`, maxRequests, windowMs) : { allowed: true }
]);
return {
allowed: results.every(r => r.allowed),
resetTime: results.find(r => !r.allowed)?.resetTime
};
}
async function checkRateLimit(
key: string,
maxRequests: number,
windowMs: number
): Promise<{ allowed: boolean; resetTime?: number }> {
const now = Date.now();
const windowStart = now - windowMs;
// Use Redis sorted set for sliding window
const pipeline = redis.pipeline();
// Remove old entries
pipeline.zremrangebyscore(key, 0, windowStart);
// Count current entries
pipeline.zcard(key);
// Add current request
pipeline.zadd(key, now, `${now}-${Math.random()}`);
// Set expiry
pipeline.expire(key, Math.ceil(windowMs / 1000));
const results = await pipeline.exec();
const currentCount = results?.[1]?.[1] as number || 0;
return {
allowed: currentCount < maxRequests,
resetTime: currentCount >= maxRequests ? windowStart + windowMs : undefined
};
}
```
### 5. Environment Configuration
Update `.env.example`:
```bash
# Redis Configuration (optional - required for multi-instance SaaS deployments)
# For single-instance deployments, the default in-memory storage is sufficient
# See docs/redis-migration.md for migration guide
REDIS_URL=redis://localhost:6379
REDIS_TLS_ENABLED=false # Enable for production
REDIS_PASSWORD=your_redis_password # Optional
```
## Migration Strategy
### Development Testing
1. **Start Redis locally**:
```bash
docker run -d -p 6379:6379 redis:alpine
```
2. **Test session functionality**:
- Login/logout
- Session validation
- CSRF token generation/validation
- Session rotation
3. **Test rate limiting**:
- Verify rate limit enforcement
- Check sliding window behavior
4. **Simulate multi-instance**:
- Run multiple server instances
- Verify session sharing works
### Staging Deployment
1. **Deploy Redis in staging**
2. **Run single instance first** to verify functionality
3. **Scale to multiple instances** and test session sharing
4. **Monitor Redis memory usage** and connection pool
### Production Rollout
#### Option A: Blue/Green Deployment
1. Deploy new version with Redis to green environment
2. Test thoroughly with production-like data
3. Switch traffic to green environment
4. **Note**: Existing sessions will be lost (users need to re-login)
#### Option B: Gradual Migration
1. Deploy Redis alongside existing system
2. Write to both stores temporarily (dual-write)
3. Read from Redis first, fallback to in-memory
4. After verification period, remove in-memory store
## Monitoring & Maintenance
### Key Metrics
- **Redis memory usage**: Monitor `used_memory` and `used_memory_peak`
- **Connection pool**: Track active connections and pool utilization
- **Session operations**: Monitor session creation/validation latency
- **Rate limit hits**: Track rate limit enforcement effectiveness
### Backup Strategy
- **Enable Redis persistence**: Configure RDB snapshots or AOF logging
- **Regular backups**: Backup Redis data regularly
- **Session data**: Ephemeral, but rate limit data should be backed up
### Scaling Considerations
- **Redis Cluster**: For horizontal scaling across multiple nodes
- **Redis Sentinel**: For high availability with automatic failover
- **Connection pooling**: Configure appropriate pool sizes
## Rollback Plan
If issues arise, you can revert to in-memory storage:
1. **Remove Redis imports** and revert to `createKV`
2. **Remove Redis environment variables**
3. **Redeploy** with in-memory storage
4. **Note**: All sessions will be lost (users need to re-login)
## Cost Considerations
### Redis Hosting Costs
- **Self-hosted**: Server costs + maintenance
- **Managed services**:
- Redis Cloud: ~$7/month for 30MB
- AWS ElastiCache: ~$15/month for t3.micro
- Azure Cache: ~$16/month for Basic tier
### Memory Requirements
**Session storage**: Minimal
- Example: 1000 concurrent users × 1KB per session = ~1MB
- CSRF tokens add minimal overhead
**Rate limiting**: Negligible
- Sliding window data is automatically cleaned up
- Minimal memory footprint per IP/email
## Alternative Solutions
### Sticky Sessions
- Load balancer routes user to same instance
- **Pros**: Simpler implementation
- **Cons**: Less resilient, harder to scale
### Database-backed Sessions
- Use PostgreSQL/MySQL instead of Redis
- **Pros**: No additional infrastructure
- **Cons**: Higher latency, more database load
### JWT Tokens
- Stateless authentication tokens
- **Pros**: No server-side session storage needed
- **Cons**: No server-side session invalidation, different security model
## References
- [Session Management Guide](session-management.md)
- [Rate Limiting Documentation](rate-limiting.md)
- [Redis Documentation](https://redis.io/documentation)
- [ioredis Library](https://github.com/luin/ioredis)
- [Redis Best Practices](https://redis.io/docs/manual/admin/)

251
docs/session-management.md Normal file
View File

@@ -0,0 +1,251 @@
# Session Management & CSRF Protection
## Overview
This application uses **HttpOnly cookie-based session management** with CSRF protection to provide secure authentication while protecting against common web vulnerabilities like XSS and CSRF attacks.
### Security Benefits
- **XSS Protection**: SessionId stored in HttpOnly cookies is not accessible to malicious JavaScript
- **CSRF Protection**: Double-submit cookie pattern prevents cross-site request forgery
- **Session Rotation**: New sessions created after login and password changes prevent session fixation
- **GDPR Compliance**: HttpOnly cookies provide better privacy protection than localStorage
## Architecture
### Session Storage
Sessions are stored in an in-memory KV store with the following structure:
```typescript
type Session = {
id: string;
userId: string;
expiresAt: string;
createdAt: string;
csrfToken?: string;
}
```
- **Expiration**: 24 hours
- **Storage**: In-memory KV store (single-instance deployment)
- **CSRF Token**: Cryptographically secure 64-character hex string
### Cookie Configuration
#### Session Cookie (`sessionId`)
- **Type**: HttpOnly, Secure (production), SameSite=Lax
- **Path**: `/`
- **MaxAge**: 86400 seconds (24 hours)
- **Purpose**: Authenticates user across requests
#### CSRF Cookie (`csrf-token`)
- **Type**: Non-HttpOnly, Secure (production), SameSite=Lax
- **Path**: `/`
- **MaxAge**: 86400 seconds (24 hours)
- **Purpose**: Provides CSRF token for JavaScript to include in requests
### CSRF Protection
The application uses the **double-submit cookie pattern**:
1. **Server-side**: CSRF token generated and stored in session
2. **Client-side**: Same token stored in non-HttpOnly cookie
3. **Validation**: Token from `X-CSRF-Token` header must match session token
4. **Timing-safe comparison**: Prevents timing attacks
### Session Rotation
Sessions are automatically rotated (new session created, old invalidated) after:
- Successful login
- Password changes
This prevents session fixation attacks.
## Implementation Details
### Server-side
#### Cookie Parsing Middleware (`src/server/routes/rpc.ts`)
```typescript
// Cookie parsing middleware - extracts sessionId from cookies
rpcApp.use("/*", async (c, next) => {
try {
const sessionId = getCookie(c, SESSION_COOKIE_NAME);
c.set('sessionId', sessionId || null);
await next();
} catch (error) {
console.error("Cookie parsing error:", error);
c.set('sessionId', null);
await next();
}
});
```
#### Authentication Helper (`src/server/lib/auth.ts`)
Key functions:
- `generateCSRFToken()`: Creates cryptographically secure token
- `getSessionFromCookies(c)`: Extracts and validates session from cookies
- `validateCSRFToken(c, sessionId)`: Validates CSRF token from header
- `assertOwner(c)`: Validates owner role with session and CSRF checks
- `rotateSession(oldSessionId, userId)`: Creates new session, invalidates old
#### RPC Handler Updates
All admin-only RPC handlers now:
- Accept Hono `context` parameter
- Use `assertOwner(context)` for authentication
- Remove `sessionId` from input schemas
- Automatically get session from cookies
### Client-side
#### RPC Client Configuration (`src/client/rpc-client.ts`)
```typescript
const link = new RPCLink({
url: `${window.location.origin}/rpc`,
headers: () => {
const csrfToken = getCSRFToken();
return csrfToken ? { 'X-CSRF-Token': csrfToken } : {};
},
fetch: (request, init) => {
return fetch(request, {
...init,
credentials: 'include' // Include cookies with all requests
});
}
});
```
#### AuthProvider Updates (`src/client/components/auth-provider.tsx`)
- Removed all localStorage usage
- Sessions managed entirely server-side
- No client-side sessionId storage
## Security Features
### XSS Protection
- SessionId not accessible to JavaScript (HttpOnly cookie)
- Malicious scripts cannot steal session tokens
### CSRF Protection
- Token validation on all state-changing operations (non-GET requests)
- Double-submit cookie pattern prevents CSRF attacks
- Timing-safe comparison prevents timing attacks
### Session Fixation Prevention
- Session rotation after authentication events
- Old sessions invalidated when new ones created
### Secure Defaults
- Secure flag enabled in production (requires HTTPS)
- SameSite=Lax prevents most CSRF attacks
- HttpOnly cookies prevent XSS token theft
## Development vs Production
### Development
- `secure: false` - allows cookies over HTTP (localhost)
- `NODE_ENV !== 'production'` detection
### Production
- `secure: true` - requires HTTPS
- All security flags enabled
## API Reference
### Key Functions (`src/server/lib/auth.ts`)
#### `generateCSRFToken(): string`
Creates a cryptographically secure random token using `crypto.randomBytes(32).toString('hex')`.
#### `getSessionFromCookies(c: Context): Promise<Session | null>`
Extracts sessionId from cookies, validates session exists and hasn't expired.
#### `validateCSRFToken(c: Context, sessionId: string): Promise<void>`
Validates CSRF token from `X-CSRF-Token` header against session token using timing-safe comparison.
#### `assertOwner(c: Context): Promise<void>`
Validates user has owner role and session is valid. Automatically validates CSRF token for non-GET requests.
#### `rotateSession(oldSessionId: string, userId: string): Promise<Session>`
Creates new session with new ID and CSRF token, deletes old session.
## Migration Guide
### Existing Password Hashes
- Base64 password hashes automatically migrated to bcrypt on server startup
- No manual intervention required
### Session Invalidation
- Old localStorage sessions will be invalidated
- Users need to re-login once after deployment
### Testing Migration
1. Deploy new version
2. Verify login creates cookies (check browser DevTools)
3. Test CSRF protection by manually calling API without token
4. Verify session rotation after password change
## Troubleshooting
### Cookies Not Being Sent
- Check `credentials: 'include'` in fetch configuration
- Verify CORS settings allow credentials
- Check cookie domain/path configuration
### CSRF Validation Failing
- Ensure `X-CSRF-Token` header is set
- Verify CSRF cookie is accessible to JavaScript
- Check token format (64-character hex string)
### Session Expired Errors
- Check cookie expiration settings
- Verify server time synchronization
- Check session cleanup logic
### Cross-Origin Issues
- Review CORS configuration for credentials
- Ensure domain configuration matches deployment
- Check SameSite cookie settings
## Future Scaling
For multi-instance deployments, see [Redis Migration Guide](redis-migration.md) for migrating to centralized session storage.
## Environment Configuration
### Required Environment Variables
```bash
# Domain Configuration
DOMAIN=localhost:5173 # For production: your-domain.com
# Note: Session cookies are scoped to this domain
# Server Configuration
NODE_ENV=development # Set to 'production' for production deployment
PORT=3000
```
### Optional Environment Variables
```bash
# Redis Configuration (for multi-instance deployments)
# See docs/redis-migration.md for migration guide
REDIS_URL=redis://localhost:6379
REDIS_TLS_ENABLED=false # Set to true for production
REDIS_PASSWORD=your_redis_password # Optional
```
### Cookie Behavior by Environment
- **Development** (`NODE_ENV !== 'production'`):
- `secure: false` - cookies work over HTTP
- `sameSite: 'Lax'` - allows cross-site navigation
- **Production** (`NODE_ENV === 'production'`):
- `secure: true` - requires HTTPS
- `sameSite: 'Lax'` - prevents most CSRF attacks
## References
- [OWASP CSRF Prevention](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html)
- [MDN HttpOnly Cookies](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#restrict_access_to_cookies)
- [RFC 6265 - HTTP State Management](https://tools.ietf.org/html/rfc6265)