Security

Comprehensive security guide for ShipKit applications covering authentication, data protection, API security, and infrastructure security

Security

This guide covers security best practices and implementations for ShipKit applications, including authentication, data protection, API security, and infrastructure security.

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'
import { env } from '@/env.mjs'

export const authConfig: NextAuthConfig = {
  adapter: PrismaAdapter(db),
  session: {
    strategy: 'jwt',
    maxAge: 30 * 24 * 60 * 60, // 30 days
  },
  pages: {
    signIn: '/auth/signin',
    error: '/auth/error',
    verifyRequest: '/auth/verify',
  },
  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, account, profile }) {
      if (user) {
        token.id = user.id
        token.role = user.role
      }
      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
    },
  },
  providers: [], // Configured in auth.ts
}

Role-Based Access Control (RBAC)

// src/lib/auth/rbac.ts
import { getServerSession } from 'next-auth'
import { redirect } from 'next/navigation'

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

export const permissions = {
  [Role.USER]: ['read:own_data', 'write:own_data'],
  [Role.ADMIN]: ['read:all_data', 'write:all_data', 'manage:users'],
  [Role.SUPER_ADMIN]: ['read:all_data', 'write:all_data', 'manage:users', 'manage:system'],
} as const

export async function checkPermission(permission: string) {
  const session = await getServerSession()
  if (!session?.user) {
    redirect('/auth/signin')
  }

  const userRole = session.user.role as Role
  const userPermissions = permissions[userRole] || []

  return userPermissions.includes(permission)
}

export function withPermission(permission: string) {
  return function (
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    const originalMethod = descriptor.value

    descriptor.value = async function (...args: any[]) {
      const hasPermission = await checkPermission(permission)
      if (!hasPermission) {
        throw new Error('Unauthorized')
      }
      return originalMethod.apply(this, args)
    }

    return descriptor
  }
}

Password Security

// src/lib/auth/password.ts
import { hash, compare } from 'bcryptjs'
import { z } from 'zod'

const passwordSchema = z
  .string()
  .min(8)
  .max(100)
  .regex(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/)

export async function hashPassword(password: string): Promise<string> {
  if (!passwordSchema.safeParse(password).success) {
    throw new Error(
      'Password must be 8-100 characters and include uppercase, lowercase, number, and special character'
    )
  }
  return hash(password, 12)
}

export async function verifyPassword(
  password: string,
  hashedPassword: string
): Promise<boolean> {
  return compare(password, hashedPassword)
}

export function validatePassword(password: string): boolean {
  return passwordSchema.safeParse(password).success
}

Data Protection

Encryption at Rest

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

const algorithm = 'aes-256-gcm'
const keyLength = 32
const ivLength = 16
const authTagLength = 16

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

export function encrypt(text: string): EncryptedData {
  const iv = randomBytes(ivLength)
  const cipher = createCipheriv(algorithm, Buffer.from(env.ENCRYPTION_KEY, 'hex'), 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,
    Buffer.from(env.ENCRYPTION_KEY, 'hex'),
    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
}

Data Sanitization

// src/lib/security/sanitization.ts
import { z } from 'zod'
import DOMPurify from 'isomorphic-dompurify'

export function sanitizeHtml(html: string): string {
  return DOMPurify.sanitize(html, {
    ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a'],
    ALLOWED_ATTR: ['href'],
  })
}

export function sanitizeFilename(filename: string): string {
  return filename.replace(/[^a-zA-Z0-9.-]/g, '_')
}

export const userInputSchema = z.object({
  email: z.string().email(),
  name: z.string().min(2).max(50).regex(/^[a-zA-Z0-9\s-]*$/),
  bio: z.string().max(500).transform(sanitizeHtml),
})

export function validateAndSanitizeInput<T>(
  schema: z.ZodSchema<T>,
  data: unknown
): T {
  return schema.parse(data)
}

API Security

Rate Limiting

// src/lib/security/rate-limit.ts
import { Redis } from '@upstash/redis'
import { Ratelimit } from '@upstash/ratelimit'
import { headers } from 'next/headers'
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'),
  analytics: true,
})

export async function checkRateLimit() {
  const headersList = headers()
  const ip = headersList.get('x-forwarded-for') ?? 'anonymous'

  const { success, limit, reset, remaining } = await rateLimiter.limit(ip)

  if (!success) {
    throw new Error(
      `Rate limit exceeded. Try again in ${Math.ceil(
        (reset - Date.now()) / 1000
      )} seconds`
    )
  }

  return { limit, remaining, reset }
}

API Authentication

// src/lib/security/api-auth.ts
import { type NextRequest } from 'next/server'
import { getToken } from 'next-auth/jwt'
import { env } from '@/env.mjs'

export async function authenticateApiRequest(req: NextRequest) {
  // Check for API key
  const apiKey = req.headers.get('x-api-key')
  if (apiKey === env.API_SECRET_KEY) {
    return { type: 'api-key' as const }
  }

  // Check for JWT token
  const token = await getToken({ req })
  if (token) {
    return { type: 'jwt' as const, token }
  }

  throw new Error('Unauthorized')
}

export function validateApiKey(apiKey: string): boolean {
  return apiKey === env.API_SECRET_KEY
}

CORS Configuration

// src/middleware.ts
import { type NextRequest } from 'next/server'
import { corsHeaders } from './lib/security/cors'

export function middleware(request: NextRequest) {
  // Check if request is for API
  if (request.nextUrl.pathname.startsWith('/api/')) {
    // Handle preflight requests
    if (request.method === 'OPTIONS') {
      return new Response(null, {
        status: 204,
        headers: corsHeaders,
      })
    }

    // Add CORS headers to response
    const response = NextResponse.next()
    Object.entries(corsHeaders).forEach(([key, value]) => {
      response.headers.set(key, value)
    })

    return response
  }

  return NextResponse.next()
}

export const config = {
  matcher: '/api/:path*',
}

Infrastructure Security

Headers Configuration

// next.config.mjs
const securityHeaders = [
  {
    key: 'X-DNS-Prefetch-Control',
    value: 'on'
  },
  {
    key: 'Strict-Transport-Security',
    value: 'max-age=63072000; includeSubDomains; preload'
  },
  {
    key: 'X-Frame-Options',
    value: 'SAMEORIGIN'
  },
  {
    key: 'X-Content-Type-Options',
    value: 'nosniff'
  },
  {
    key: 'Referrer-Policy',
    value: 'origin-when-cross-origin'
  },
  {
    key: 'Permissions-Policy',
    value: 'camera=(), microphone=(), geolocation=()'
  },
  {
    key: 'Content-Security-Policy',
    value: ContentSecurityPolicy.replace(/\s{2,}/g, ' ').trim()
  }
]

const ContentSecurityPolicy = `
  default-src 'self';
  script-src 'self' 'unsafe-eval' 'unsafe-inline' *.vercel-insights.com;
  style-src 'self' 'unsafe-inline';
  img-src * blob: data:;
  media-src 'none';
  connect-src *;
  font-src 'self';
`

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

Environment Variables

// src/env.mjs
import { createEnv } from '@t3-oss/env-nextjs'
import { z } from 'zod'

export const env = createEnv({
  server: {
    DATABASE_URL: z.string().url(),
    NEXTAUTH_URL: z.string().url(),
    NEXTAUTH_SECRET: z.string().min(32),
    ENCRYPTION_KEY: z.string().length(64), // 32 bytes in hex
    API_SECRET_KEY: z.string().min(32),
    UPSTASH_REDIS_URL: z.string().url(),
    UPSTASH_REDIS_TOKEN: z.string(),
  },
  client: {
    NEXT_PUBLIC_APP_URL: z.string().url(),
  },
  runtimeEnv: process.env,
})

File Upload Security

// src/lib/security/upload.ts
import { type File } from '@web-std/file'
import { z } from 'zod'

const MAX_FILE_SIZE = 5 * 1024 * 1024 // 5MB
const ALLOWED_FILE_TYPES = [
  'image/jpeg',
  'image/png',
  'image/webp',
  'application/pdf',
]

export const uploadSchema = z.object({
  file: z
    .instanceof(File)
    .refine((file) => file.size <= MAX_FILE_SIZE, 'File size must be 5MB or less')
    .refine(
      (file) => ALLOWED_FILE_TYPES.includes(file.type),
      'File type not supported'
    ),
})

export async function scanFile(file: File): Promise<boolean> {
  // Implement virus scanning here
  // This is a placeholder
  return true
}

export function sanitizeFile(file: File): Promise<File> {
  // Implement file sanitization here
  // This is a placeholder
  return Promise.resolve(file)
}

Security Best Practices

  1. Authentication

    • Use secure session management
    • Implement proper password policies
    • Enable multi-factor authentication
    • Use secure token handling
  2. Data Protection

    • Encrypt sensitive data
    • Implement proper access controls
    • Sanitize user input
    • Use secure data transmission
  3. API Security

    • Implement rate limiting
    • Use proper authentication
    • Enable CORS protection
    • Validate all inputs
  4. Infrastructure

    • Configure security headers
    • Use secure environment variables
    • Implement file upload security
    • Enable SSL/TLS

Security Checklist

  1. Authentication

    • [ ] Configure NextAuth.js
    • [ ] Set up RBAC
    • [ ] Implement password policies
    • [ ] Enable MFA
  2. Data Protection

    • [ ] Set up encryption
    • [ ] Configure access controls
    • [ ] Implement sanitization
    • [ ] Secure data storage
  3. API Security

    • [ ] Enable rate limiting
    • [ ] Set up API authentication
    • [ ] Configure CORS
    • [ ] Validate requests
  4. Infrastructure

    • [ ] Set security headers
    • [ ] Secure environment variables
    • [ ] Configure file upload security
    • [ ] Enable monitoring