Security

Guide for implementing security best practices in ShipKit applications, including authentication, authorization, data protection, and security measures

Security Guide

This guide covers security best practices and implementation details for ShipKit applications.

Authentication

NextAuth.js Configuration

// src/lib/auth/config.ts
import { NextAuthConfig } from 'next-auth'
import { PrismaAdapter } from '@auth/prisma-adapter'
import { db } from '@/server/db'

export const authConfig: NextAuthConfig = {
  adapter: PrismaAdapter(db),
  session: {
    strategy: 'jwt',
    maxAge: 30 * 24 * 60 * 60, // 30 days
  },
  cookies: {
    sessionToken: {
      name: '__Secure-next-auth.session-token',
      options: {
        httpOnly: true,
        sameSite: 'lax',
        path: '/',
        secure: true,
      },
    },
  },
  callbacks: {
    authorized({ auth, request: { nextUrl } }) {
      const isLoggedIn = !!auth?.user
      const isOnDashboard = nextUrl.pathname.startsWith('/dashboard')

      if (isOnDashboard) {
        if (isLoggedIn) return true
        return false
      } else if (isLoggedIn) {
        return Response.redirect(new URL('/dashboard', nextUrl))
      }
      return true
    },
    jwt({ token, user }) {
      if (user) {
        token.role = user.role
        token.id = user.id
      }
      return token
    },
    session({ session, token }) {
      if (token && session.user) {
        session.user.id = token.id as string
        session.user.role = token.role as string
      }
      return session
    },
  },
}

Middleware Protection

// src/middleware.ts
import { NextResponse } from 'next/server'
import { auth } from '@/lib/auth'

export default auth((req) => {
  const isLoggedIn = !!req.auth
  const isOnDashboard = req.nextUrl.pathname.startsWith('/dashboard')
  const isOnAdmin = req.nextUrl.pathname.startsWith('/admin')

  // Protect dashboard routes
  if (isOnDashboard && !isLoggedIn) {
    return Response.redirect(new URL('/login', req.nextUrl))
  }

  // Protect admin routes
  if (isOnAdmin && req.auth?.user?.role !== 'ADMIN') {
    return Response.redirect(new URL('/', req.nextUrl))
  }

  // Add security headers
  const response = NextResponse.next()
  response.headers.set(
    'Strict-Transport-Security',
    'max-age=31536000; includeSubDomains'
  )
  response.headers.set('X-Frame-Options', 'DENY')
  response.headers.set('X-Content-Type-Options', 'nosniff')
  response.headers.set(
    'Content-Security-Policy',
    "default-src 'self'; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline';"
  )
  response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin')
  response.headers.set('Permissions-Policy', 'camera=(), microphone=(), geolocation=()')

  return response
})

export const config = {
  matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
}

Authorization

Role-Based Access Control

// src/lib/auth/rbac.ts
import { type Session } from 'next-auth'

export enum Role {
  USER = 'USER',
  ADMIN = 'ADMIN',
  SUPER_ADMIN = 'SUPER_ADMIN',
}

export enum Permission {
  READ_USERS = 'READ_USERS',
  WRITE_USERS = 'WRITE_USERS',
  DELETE_USERS = 'DELETE_USERS',
  MANAGE_SETTINGS = 'MANAGE_SETTINGS',
}

const rolePermissions: Record<Role, Permission[]> = {
  [Role.USER]: [],
  [Role.ADMIN]: [Permission.READ_USERS, Permission.WRITE_USERS],
  [Role.SUPER_ADMIN]: [
    Permission.READ_USERS,
    Permission.WRITE_USERS,
    Permission.DELETE_USERS,
    Permission.MANAGE_SETTINGS,
  ],
}

export function hasPermission(
  session: Session | null,
  permission: Permission
): boolean {
  if (!session?.user?.role) return false
  const role = session.user.role as Role
  return rolePermissions[role].includes(permission)
}

// Usage in components
export function PermissionGate({
  permission,
  children,
}: {
  permission: Permission
  children: React.ReactNode
}) {
  const session = useSession()
  if (!hasPermission(session.data, permission)) return null
  return <>{children}</>
}

API Route Protection

// src/lib/auth/protect-api.ts
import { type NextApiHandler } from 'next'
import { getServerSession } from 'next-auth'
import { authConfig } from './config'
import { Permission } from './rbac'

export function protectApiRoute(
  handler: NextApiHandler,
  requiredPermission?: Permission
): NextApiHandler {
  return async (req, res) => {
    const session = await getServerSession(req, res, authConfig)

    if (!session) {
      return res.status(401).json({ error: 'Unauthorized' })
    }

    if (requiredPermission && !hasPermission(session, requiredPermission)) {
      return res.status(403).json({ error: 'Forbidden' })
    }

    return handler(req, res)
  }
}

// Usage in API routes
export default protectApiRoute(
  async function handler(req, res) {
    // Protected route logic
  },
  Permission.MANAGE_SETTINGS
)

Data Protection

Encryption

// src/lib/encryption.ts
import { createCipheriv, createDecipheriv, randomBytes } from 'crypto'
import { env } from '@/env.mjs'

const algorithm = 'aes-256-gcm'
const keyBuffer = Buffer.from(env.ENCRYPTION_KEY, 'base64')

interface EncryptedData {
  encrypted: string
  iv: string
  authTag: string
}

export function encrypt(text: string): EncryptedData {
  const iv = randomBytes(12)
  const cipher = createCipheriv(algorithm, keyBuffer, iv)

  let encrypted = cipher.update(text, 'utf8', 'hex')
  encrypted += cipher.final('hex')

  return {
    encrypted,
    iv: iv.toString('hex'),
    authTag: cipher.getAuthTag().toString('hex'),
  }
}

export function decrypt(data: EncryptedData): string {
  const decipher = createDecipheriv(
    algorithm,
    keyBuffer,
    Buffer.from(data.iv, 'hex')
  )

  decipher.setAuthTag(Buffer.from(data.authTag, 'hex'))

  let decrypted = decipher.update(data.encrypted, 'hex', 'utf8')
  decrypted += decipher.final('utf8')

  return decrypted
}

// Usage with Prisma
model User {
  id        String   @id @default(cuid())
  email     String   @unique
  // Encrypted fields
  ssn       Json?    // Stores EncryptedData
}

// In application code
async function storeSensitiveData(userId: string, ssn: string) {
  const encrypted = encrypt(ssn)
  await db.user.update({
    where: { id: userId },
    data: { ssn: encrypted },
  })
}

async function retrieveSensitiveData(userId: string) {
  const user = await db.user.findUnique({
    where: { id: userId },
    select: { ssn: true },
  })

  if (!user?.ssn) return null
  return decrypt(user.ssn as EncryptedData)
}

Password Handling

// src/lib/auth/password.ts
import { hash, verify } from 'argon2'

export async function hashPassword(password: string): Promise<string> {
  return hash(password, {
    type: 2, // argon2id
    memoryCost: 65536, // 64MB
    timeCost: 3, // 3 iterations
    parallelism: 1,
  })
}

export async function verifyPassword(
  hashedPassword: string,
  password: string
): Promise<boolean> {
  try {
    return await verify(hashedPassword, password)
  } catch {
    return false
  }
}

// Usage in authentication
async function signIn(email: string, password: string) {
  const user = await db.user.findUnique({
    where: { email },
    select: { id: true, password: true },
  })

  if (!user) return null

  const isValid = await verifyPassword(user.password, password)
  if (!isValid) return null

  return user
}

Input Validation

API Validation

// src/lib/validation.ts
import { z } from 'zod'
import { TRPCError } from '@trpc/server'

export const UserSchema = z.object({
  email: z.string().email(),
  password: z
    .string()
    .min(8)
    .regex(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/),
  name: z.string().min(2).max(100),
})

export const PostSchema = z.object({
  title: z.string().min(1).max(100),
  content: z.string().min(1).max(10000),
  published: z.boolean().optional(),
})

// Usage in API routes
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  try {
    const data = UserSchema.parse(req.body)
    // Process validated data
  } catch (error) {
    if (error instanceof z.ZodError) {
      return res.status(400).json({ errors: error.errors })
    }
    return res.status(500).json({ error: 'Internal Server Error' })
  }
}

Form Validation

// src/components/forms/user-form.tsx
'use client'

import { zodResolver } from '@hookform/resolvers/zod'
import { useForm } from 'react-hook-form'
import { UserSchema } from '@/lib/validation'
import { type z } from 'zod'

type FormData = z.infer<typeof UserSchema>

export function UserForm() {
  const form = useForm<FormData>({
    resolver: zodResolver(UserSchema),
  })

  return (
    <form onSubmit={form.handleSubmit(onSubmit)}>
      <input
        {...form.register('email')}
        type="email"
        className={cn(
          'input',
          form.formState.errors.email && 'input--error'
        )}
      />
      {form.formState.errors.email && (
        <p className="text-red-500">
          {form.formState.errors.email.message}
        </p>
      )}

      <input
        {...form.register('password')}
        type="password"
        className={cn(
          'input',
          form.formState.errors.password && 'input--error'
        )}
      />
      {form.formState.errors.password && (
        <p className="text-red-500">
          {form.formState.errors.password.message}
        </p>
      )}

      <button type="submit">Submit</button>
    </form>
  )
}

Security Headers

CSP Configuration

// next.config.mjs
const securityHeaders = [
  {
    key: 'Content-Security-Policy',
    value: [
      "default-src 'self'",
      "script-src 'self' 'unsafe-eval' 'unsafe-inline'",
      "style-src 'self' 'unsafe-inline'",
      "img-src 'self' blob: data:",
      "font-src 'self'",
      "object-src 'none'",
      "base-uri 'self'",
      "form-action 'self'",
      "frame-ancestors 'none'",
      "block-all-mixed-content",
      "upgrade-insecure-requests",
    ].join('; '),
  },
  {
    key: 'X-Frame-Options',
    value: 'DENY',
  },
  {
    key: 'X-Content-Type-Options',
    value: 'nosniff',
  },
  {
    key: 'Referrer-Policy',
    value: 'strict-origin-when-cross-origin',
  },
  {
    key: 'Permissions-Policy',
    value: 'camera=(), microphone=(), geolocation=()',
  },
]

export default {
  async headers() {
    return [
      {
        source: '/:path*',
        headers: securityHeaders,
      },
    ]
  },
}

Rate Limiting

API Rate Limiting

// src/lib/rate-limit.ts
import { Ratelimit } from '@upstash/ratelimit'
import { Redis } from '@upstash/redis'
import { env } from '@/env.mjs'

const redis = new Redis({
  url: env.UPSTASH_REDIS_URL,
  token: env.UPSTASH_REDIS_TOKEN,
})

export const rateLimiter = new Ratelimit({
  redis,
  limiter: Ratelimit.slidingWindow(10, '10 s'),
})

// Middleware usage
export async function rateLimit(req: NextApiRequest) {
  const ip = req.headers['x-forwarded-for'] || 'anonymous'
  const { success, limit, reset, remaining } = await rateLimiter.limit(
    ip.toString()
  )

  return {
    success,
    headers: {
      'X-RateLimit-Limit': limit.toString(),
      'X-RateLimit-Remaining': remaining.toString(),
      'X-RateLimit-Reset': reset.toString(),
    },
  }
}

// Usage in API routes
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  const rateLimit = await rateLimit(req)

  if (!rateLimit.success) {
    return res.status(429).json({
      error: 'Too Many Requests',
    })
  }

  // Set rate limit headers
  Object.entries(rateLimit.headers).forEach(([key, value]) => {
    res.setHeader(key, value)
  })

  // Handle request
}

Security Best Practices

  1. Authentication

    • Use secure session management
    • Implement MFA where possible
    • Enforce strong passwords
    • Handle session expiration
  2. Authorization

    • Implement RBAC
    • Use principle of least privilege
    • Validate permissions
    • Audit access logs
  3. Data Protection

    • Encrypt sensitive data
    • Use secure protocols
    • Implement backup strategies
    • Handle data deletion
  4. Input Validation

    • Validate all inputs
    • Sanitize user data
    • Prevent injection attacks
    • Handle file uploads securely

Security Checklist

  1. Authentication

    • [ ] Secure password storage
    • [ ] MFA implementation
    • [ ] Session management
    • [ ] Password policies
  2. Authorization

    • [ ] Role-based access
    • [ ] Permission checks
    • [ ] API protection
    • [ ] Audit logging
  3. Data Security

    • [ ] Encryption at rest
    • [ ] Secure transmission
    • [ ] Data backups
    • [ ] Access controls
  4. Monitoring

    • [ ] Security logging
    • [ ] Intrusion detection
    • [ ] Rate limiting
    • [ ] Error handling