Files
2026-03-02 21:16:26 +01:00

192 lines
4.9 KiB
Markdown

# auth-session-management: Implement Secure Session Handling
## Priority: HIGH
## Explanation
Sessions maintain user authentication state across requests. Use HTTP-only cookies with secure settings to prevent XSS and CSRF attacks. Never store sensitive data in client-accessible storage.
## Bad Example
```tsx
// Storing auth in localStorage - vulnerable to XSS
function login(credentials: Credentials) {
const token = await authenticate(credentials)
localStorage.setItem('authToken', token) // XSS can steal this
}
// Non-HTTP-only cookie - JavaScript accessible
export const setSession = createServerFn({ method: 'POST' })
.handler(async ({ data }) => {
setResponseHeader('Set-Cookie', `session=${data.token}`) // Not secure
})
```
## Good Example: Secure Session Cookie
```tsx
// lib/session.server.ts
import { useSession } from '@tanstack/react-start/server'
// Configure session with secure defaults
export function getSession() {
return useSession({
password: process.env.SESSION_SECRET!, // At least 32 characters
cookie: {
name: '__session',
httpOnly: true, // Not accessible via JavaScript
secure: process.env.NODE_ENV === 'production', // HTTPS only in prod
sameSite: 'lax', // CSRF protection
maxAge: 60 * 60 * 24 * 7, // 7 days
},
})
}
// Usage in server function
export const login = createServerFn({ method: 'POST' })
.validator(loginSchema)
.handler(async ({ data }) => {
const session = await getSession()
// Verify credentials
const user = await verifyCredentials(data.email, data.password)
if (!user) {
throw new Error('Invalid credentials')
}
// Store only essential data in session
await session.update({
userId: user.id,
email: user.email,
createdAt: Date.now(),
})
return { success: true }
})
```
## Good Example: Full Authentication Flow
```tsx
// lib/auth.functions.ts
import { createServerFn } from '@tanstack/react-start'
import { redirect } from '@tanstack/react-router'
import { getSession } from './session.server'
import { hashPassword, verifyPassword } from './password.server'
// Login
export const login = createServerFn({ method: 'POST' })
.validator(z.object({
email: z.string().email(),
password: z.string().min(1),
}))
.handler(async ({ data }) => {
const user = await db.users.findUnique({
where: { email: data.email },
})
if (!user || !await verifyPassword(data.password, user.passwordHash)) {
throw new Error('Invalid email or password')
}
const session = await getSession()
await session.update({
userId: user.id,
email: user.email,
})
throw redirect({ to: '/dashboard' })
})
// Logout
export const logout = createServerFn({ method: 'POST' })
.handler(async () => {
const session = await getSession()
await session.clear()
throw redirect({ to: '/' })
})
// Get current user
export const getCurrentUser = createServerFn()
.handler(async () => {
const session = await getSession()
const data = await session.data
if (!data?.userId) {
return null
}
const user = await db.users.findUnique({
where: { id: data.userId },
select: {
id: true,
email: true,
name: true,
avatar: true,
// Don't include passwordHash!
},
})
return user
})
```
## Good Example: Session with Role-Based Access
```tsx
// lib/session.server.ts
interface SessionData {
userId: string
email: string
role: 'user' | 'admin'
createdAt: number
}
export async function getSessionData(): Promise<SessionData | null> {
const session = await getSession()
const data = await session.data
if (!data?.userId) return null
// Validate session age
const maxAge = 7 * 24 * 60 * 60 * 1000 // 7 days
if (Date.now() - data.createdAt > maxAge) {
await session.clear()
return null
}
return data as SessionData
}
// Middleware for admin-only routes
export const requireAdmin = createMiddleware()
.server(async ({ next }) => {
const session = await getSessionData()
if (!session || session.role !== 'admin') {
throw redirect({ to: '/unauthorized' })
}
return next({ context: { session } })
})
```
## Session Security Checklist
| Setting | Value | Purpose |
|---------|-------|---------|
| `httpOnly` | `true` | Prevents XSS from accessing cookie |
| `secure` | `true` in prod | Requires HTTPS |
| `sameSite` | `'lax'` or `'strict'` | CSRF protection |
| `maxAge` | Application-specific | Session duration |
| `password` | 32+ random chars | Encryption key |
## Context
- Always use HTTP-only cookies for session tokens
- Generate `SESSION_SECRET` with `openssl rand -base64 32`
- Store minimal data in session - fetch user details on demand
- Implement session rotation on privilege changes
- Consider session invalidation on password change
- Use `sameSite: 'strict'` for highest CSRF protection