Files
findyourpilot/.agent/skills/tanstack-start-best-practices/rules/auth-session-management.md
2026-03-02 21:16:26 +01:00

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
  })
// 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_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