Authentication & Security

Authentication patterns, role-based access control, and security best practices

Authentication & Security

Implement secure authentication and authorization patterns using Supabase Auth and role-based access control.

Role-Based Access Control

Permission System

// lib/auth/permissions.ts
export type UserRole = 'admin' | 'manager' | 'employee' | 'viewer'

export const PERMISSIONS = {
  admin: [
    'users.create',
    'users.read',
    'users.update',
    'users.delete',
    'products.create',
    'products.read',
    'products.update',
    'products.delete',
    'inventory.create',
    'inventory.read',
    'inventory.update',
    'inventory.delete',
    'analytics.read',
    'settings.update',
  ],
  manager: [
    'products.create',
    'products.read',
    'products.update',
    'inventory.create',
    'inventory.read',
    'inventory.update',
    'analytics.read',
  ],
  employee: [
    'products.read',
    'inventory.read',
    'inventory.update',
  ],
  viewer: [
    'products.read',
    'inventory.read',
  ],
} as const

export function hasPermission(userRole: UserRole, permission: string): boolean {
  return PERMISSIONS[userRole]?.includes(permission as any) || false
}

Server-Side Authentication

User Management

// lib/auth/server.ts
import { createClient } from '@/lib/supabase/server'

export async function getCurrentUser() {
  const supabase = await createClient()
  
  try {
    const { data: { user }, error } = await supabase.auth.getUser()
    
    if (error || !user) {
      return null
    }
    
    // Get user profile with role
    const { data: profile } = await supabase
      .from('users')
      .select('id, email, role, full_name')
      .eq('id', user.id)
      .single()
    
    return {
      ...user,
      profile
    }
  } catch (error) {
    console.error('Error getting current user:', error)
    return null
  }
}

export async function requireAuth() {
  const user = await getCurrentUser()
  
  if (!user) {
    throw new Error('Authentication required')
  }
  
  return user
}

export async function requireRole(requiredRole: UserRole) {
  const user = await requireAuth()
  
  if (!hasPermission(user.profile?.role, 'admin') && user.profile?.role !== requiredRole) {
    throw new Error('Insufficient permissions')
  }
  
  return user
}

Client-Side Authentication

Auth Hook

// hooks/use-current-user.ts
'use client'

import { useEffect, useState } from 'react'
import { createClient } from '@/lib/supabase/client'

export function useCurrentUser() {
  const [user, setUser] = useState(null)
  const [loading, setLoading] = useState(true)
  
  useEffect(() => {
    const supabase = createClient()
    
    // Get initial session
    supabase.auth.getSession().then(({ data: { session } }) => {
      setUser(session?.user ?? null)
      setLoading(false)
    })
    
    // Listen for auth changes
    const { data: { subscription } } = supabase.auth.onAuthStateChange(
      (_event, session) => {
        setUser(session?.user ?? null)
        setLoading(false)
      }
    )
    
    return () => subscription.unsubscribe()
  }, [])
  
  return { user, loading }
}

Protected Route Component

// components/auth/protected-route.tsx
'use client'

import { useCurrentUser } from '@/hooks/use-current-user'
import { redirect } from 'next/navigation'
import { useEffect } from 'react'

interface ProtectedRouteProps {
  children: React.ReactNode
  requiredRole?: UserRole
  fallback?: React.ReactNode
}

export function ProtectedRoute({ 
  children, 
  requiredRole, 
  fallback = <div>Loading...</div> 
}: ProtectedRouteProps) {
  const { user, loading } = useCurrentUser()

  useEffect(() => {
    if (!loading && !user) {
      redirect('/auth/login')
    }
  }, [user, loading])

  if (loading) {
    return fallback
  }

  if (!user) {
    return null
  }

  if (requiredRole && !hasPermission(user.profile?.role, requiredRole)) {
    return <div>Access denied. Insufficient permissions.</div>
  }

  return <>{children}</>
}

Security Best Practices

Input Validation

Always validate and sanitize user input:

import { z } from 'zod'

const productSchema = z.object({
  name: z.string().min(1).max(100),
  sku: z.string().regex(/^[A-Z0-9-]+$/),
  price: z.number().positive(),
})

export async function createProduct(formData: FormData) {
  const rawData = {
    name: formData.get('name'),
    sku: formData.get('sku'),
    price: Number(formData.get('price')),
  }

  const validatedData = productSchema.parse(rawData)
  
  // Proceed with validated data
}

Environment Variables

Secure sensitive data with environment variables:

# .env.local
NEXT_PUBLIC_SUPABASE_URL=your-supabase-url
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
SUPABASE_SERVICE_ROLE_KEY=your-service-key

Row Level Security

Enable RLS policies in Supabase:

-- Enable RLS
ALTER TABLE products ENABLE ROW LEVEL SECURITY;

-- Allow authenticated users to read products
CREATE POLICY "Users can read products" 
ON products FOR SELECT 
TO authenticated 
USING (true);

-- Allow managers to modify products
CREATE POLICY "Managers can modify products" 
ON products FOR ALL 
TO authenticated 
USING (
  EXISTS (
    SELECT 1 FROM users 
    WHERE users.id = auth.uid() 
    AND users.role IN ('admin', 'manager')
  )
);

API Route Protection

Protect API routes with middleware:

// middleware.ts
import { createMiddlewareClient } from '@supabase/auth-helpers-nextjs'
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export async function middleware(req: NextRequest) {
  const res = NextResponse.next()
  const supabase = createMiddlewareClient({ req, res })

  const {
    data: { session },
  } = await supabase.auth.getSession()

  // Protect API routes
  if (req.nextUrl.pathname.startsWith('/api/protected') && !session) {
    return NextResponse.redirect(new URL('/auth/login', req.url))
  }

  return res
}

export const config = {
  matcher: ['/api/protected/:path*', '/dashboard/:path*']
}

Authentication Flows

Login/Logout

// lib/auth/actions.ts
'use server'

import { createClient } from '@/lib/supabase/server'
import { redirect } from 'next/navigation'

export async function signIn(formData: FormData) {
  const email = formData.get('email') as string
  const password = formData.get('password') as string

  const supabase = await createClient()

  const { error } = await supabase.auth.signInWithPassword({
    email,
    password,
  })

  if (error) {
    redirect('/auth/login?error=Invalid credentials')
  }

  redirect('/dashboard')
}

export async function signOut() {
  const supabase = await createClient()
  await supabase.auth.signOut()
  redirect('/auth/login')
}

Session Management

Sessions are automatically managed by Supabase Auth. Use the provided hooks and utilities to check authentication state and manage user sessions across your application.