252 lines
7.8 KiB
Markdown
252 lines
7.8 KiB
Markdown
# 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)
|