4.9 KiB
4.9 KiB
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
// 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
// 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
// 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
// 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_SECRETwithopenssl 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