192 lines
4.9 KiB
Markdown
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
|