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

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)