Authentication

Authentication methods, multi-factor authentication, and secure user verification.

Authentication

Smart Shelf implements robust authentication mechanisms to ensure secure user access and identity verification. Our authentication system supports multiple methods while maintaining the highest security standards.

Authentication Methods

Primary Authentication (Supabase Auth)

Smart Shelf uses Supabase Auth as the primary authentication provider, offering enterprise-grade security with ease of implementation:

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

const supabase = createClient()

// Email/Password Authentication
export async function signUpWithEmail(email: string, password: string, userData: any) {
  const { data, error } = await supabase.auth.signUp({
    email,
    password,
    options: {
      data: {
        full_name: userData.fullName,
        role: userData.role || 'employee',
        department: userData.department
      }
    }
  })
  
  if (error) throw error
  return data
}

export async function signInWithEmail(email: string, password: string) {
  const { data, error } = await supabase.auth.signInWithPassword({
    email,
    password
  })
  
  if (error) throw error
  return data
}

export async function signOut() {
  const { error } = await supabase.auth.signOut()
  if (error) throw error
}

Password Security Requirements

Strong password policies are enforced to protect user accounts:

// lib/auth/password-validation.ts
export const passwordRequirements = {
  minLength: 12,
  requireUppercase: true,
  requireLowercase: true,
  requireNumbers: true,
  requireSpecialChars: true,
  maxAge: 90, // days
  historyCheck: 12, // previous passwords to check
  noCommonPasswords: true
}

export function validatePassword(password: string): PasswordValidationResult {
  const errors: string[] = []
  
  if (password.length < passwordRequirements.minLength) {
    errors.push(`Password must be at least ${passwordRequirements.minLength} characters long`)
  }
  
  if (passwordRequirements.requireUppercase && !/[A-Z]/.test(password)) {
    errors.push('Password must contain at least one uppercase letter')
  }
  
  if (passwordRequirements.requireLowercase && !/[a-z]/.test(password)) {
    errors.push('Password must contain at least one lowercase letter')
  }
  
  if (passwordRequirements.requireNumbers && !/\d/.test(password)) {
    errors.push('Password must contain at least one number')
  }
  
  if (passwordRequirements.requireSpecialChars && !/[!@#$%^&*(),.?":{}|<>]/.test(password)) {
    errors.push('Password must contain at least one special character')
  }
  
  // Check against common passwords database
  if (passwordRequirements.noCommonPasswords && isCommonPassword(password)) {
    errors.push('Password is too common, please choose a stronger password')
  }
  
  return {
    isValid: errors.length === 0,
    errors,
    strength: calculatePasswordStrength(password)
  }
}

function calculatePasswordStrength(password: string): 'weak' | 'medium' | 'strong' | 'very-strong' {
  let score = 0
  
  // Length scoring
  if (password.length >= 12) score += 2
  if (password.length >= 16) score += 1
  
  // Character variety scoring
  if (/[a-z]/.test(password)) score += 1
  if (/[A-Z]/.test(password)) score += 1
  if (/\d/.test(password)) score += 1
  if (/[!@#$%^&*(),.?":{}|<>]/.test(password)) score += 1
  
  // Pattern detection (reduce score for patterns)
  if (/(.)\1{2,}/.test(password)) score -= 1 // Repeated characters
  if (/123|abc|qwe/i.test(password)) score -= 1 // Sequential patterns
  
  if (score >= 7) return 'very-strong'
  if (score >= 5) return 'strong'
  if (score >= 3) return 'medium'
  return 'weak'
}

Multi-Factor Authentication (MFA)

TOTP (Time-based One-Time Password)

// lib/auth/mfa.ts
export async function enrollMFA(friendlyName: string = 'Smart Shelf TOTP') {
  try {
    const { data, error } = await supabase.auth.mfa.enroll({
      factorType: 'totp',
      friendlyName
    })
    
    if (error) throw error
    
    return {
      factorId: data.id,
      qrCode: data.totp.qr_code,
      secret: data.totp.secret,
      uri: data.totp.uri
    }
  } catch (error) {
    console.error('MFA enrollment failed:', error)
    throw error
  }
}

export async function verifyMFAEnrollment(factorId: string, code: string) {
  try {
    const { data, error } = await supabase.auth.mfa.verify({
      factorId,
      challengeId: '', // Will be provided by enrollment
      code
    })
    
    if (error) throw error
    return data
  } catch (error) {
    console.error('MFA verification failed:', error)
    throw error
  }
}

export async function challengeMFA(factorId: string) {
  try {
    const { data, error } = await supabase.auth.mfa.challenge({
      factorId
    })
    
    if (error) throw error
    return data.id // Challenge ID
  } catch (error) {
    console.error('MFA challenge failed:', error)
    throw error
  }
}

export async function verifyMFA(factorId: string, challengeId: string, code: string) {
  try {
    const { data, error } = await supabase.auth.mfa.verify({
      factorId,
      challengeId,
      code
    })
    
    if (error) throw error
    return data
  } catch (error) {
    console.error('MFA verification failed:', error)
    throw error
  }
}

SMS-based MFA (Optional)

// lib/auth/sms-mfa.ts
export async function sendSMSCode(phoneNumber: string) {
  try {
    const { data, error } = await supabase.auth.signInWithOtp({
      phone: phoneNumber
    })
    
    if (error) throw error
    return data
  } catch (error) {
    console.error('SMS MFA failed:', error)
    throw error
  }
}

export async function verifySMSCode(phoneNumber: string, code: string) {
  try {
    const { data, error } = await supabase.auth.verifyOtp({
      phone: phoneNumber,
      token: code,
      type: 'sms'
    })
    
    if (error) throw error
    return data
  } catch (error) {
    console.error('SMS verification failed:', error)
    throw error
  }
}

Social Authentication (OAuth)

Google OAuth Integration

// lib/auth/oauth.ts
export async function signInWithGoogle() {
  try {
    const { data, error } = await supabase.auth.signInWithOAuth({
      provider: 'google',
      options: {
        scopes: 'email profile',
        redirectTo: `${window.location.origin}/auth/callback`,
        queryParams: {
          access_type: 'offline',
          prompt: 'consent'
        }
      }
    })
    
    if (error) throw error
    return data
  } catch (error) {
    console.error('Google OAuth failed:', error)
    throw error
  }
}

export async function signInWithGitHub() {
  try {
    const { data, error } = await supabase.auth.signInWithOAuth({
      provider: 'github',
      options: {
        scopes: 'user:email',
        redirectTo: `${window.location.origin}/auth/callback`
      }
    })
    
    if (error) throw error
    return data
  } catch (error) {
    console.error('GitHub OAuth failed:', error)
    throw error
  }
}

export async function signInWithMicrosoft() {
  try {
    const { data, error } = await supabase.auth.signInWithOAuth({
      provider: 'azure',
      options: {
        scopes: 'email profile',
        redirectTo: `${window.location.origin}/auth/callback`
      }
    })
    
    if (error) throw error
    return data
  } catch (error) {
    console.error('Microsoft OAuth failed:', error)
    throw error
  }
}

OAuth Callback Handler

// app/auth/callback/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { createClient } from '@/lib/supabase/server'

export async function GET(request: NextRequest) {
  const { searchParams, origin } = new URL(request.url)
  const code = searchParams.get('code')
  const next = searchParams.get('next') ?? '/dashboard'

  if (code) {
    const supabase = createClient()
    
    try {
      const { data, error } = await supabase.auth.exchangeCodeForSession(code)
      
      if (!error && data.user) {
        // Log successful authentication
        await logAuthenticationEvent({
          user_id: data.user.id,
          event_type: 'oauth_login_success',
          provider: data.user.app_metadata?.provider,
          ip_address: request.headers.get('x-forwarded-for') || 'unknown',
          user_agent: request.headers.get('user-agent') || 'unknown'
        })
        
        // Check if user profile needs completion
        if (!data.user.user_metadata?.profile_completed) {
          return NextResponse.redirect(`${origin}/auth/complete-profile?next=${encodeURIComponent(next)}`)
        }
        
        return NextResponse.redirect(`${origin}${next}`)
      }
    } catch (error) {
      console.error('OAuth callback error:', error)
      
      // Log failed authentication
      await logAuthenticationEvent({
        event_type: 'oauth_login_failed',
        error: error.message,
        ip_address: request.headers.get('x-forwarded-for') || 'unknown',
        user_agent: request.headers.get('user-agent') || 'unknown'
      })
    }
  }

  // Return to sign-in page on error
  return NextResponse.redirect(`${origin}/auth/sign-in?error=oauth_callback_failed`)
}

Authentication Middleware

Protected Route Middleware

// middleware.ts
import { NextRequest, NextResponse } from 'next/server'
import { createClient } from '@/lib/supabase/middleware'

export async function middleware(request: NextRequest) {
  const { supabase, response } = createClient(request)
  
  // Protected routes that require authentication
  const protectedPaths = ['/dashboard', '/inventory', '/products', '/orders', '/settings']
  const publicPaths = ['/auth', '/api/auth', '/', '/about', '/contact']
  
  const path = request.nextUrl.pathname
  const isProtectedPath = protectedPaths.some(p => path.startsWith(p))
  const isPublicPath = publicPaths.some(p => path.startsWith(p))
  
  if (isProtectedPath) {
    const { data: { user }, error } = await supabase.auth.getUser()
    
    if (!user || error) {
      const redirectUrl = new URL('/auth/sign-in', request.url)
      redirectUrl.searchParams.set('redirectTo', path)
      return NextResponse.redirect(redirectUrl)
    }
    
    // Check for required profile completion
    if (!user.user_metadata?.profile_completed && path !== '/auth/complete-profile') {
      const completeProfileUrl = new URL('/auth/complete-profile', request.url)
      completeProfileUrl.searchParams.set('next', path)
      return NextResponse.redirect(completeProfileUrl)
    }
    
    // Check for required MFA setup
    if (!user.user_metadata?.mfa_enabled && user.user_metadata?.require_mfa && path !== '/auth/setup-mfa') {
      const setupMFAUrl = new URL('/auth/setup-mfa', request.url)
      setupMFAUrl.searchParams.set('next', path)
      return NextResponse.redirect(setupMFAUrl)
    }
  }
  
  // Redirect authenticated users away from auth pages
  if (path.startsWith('/auth/') && !path.includes('/auth/sign-out')) {
    const { data: { user } } = await supabase.auth.getUser()
    
    if (user) {
      return NextResponse.redirect(new URL('/dashboard', request.url))
    }
  }
  
  return response
}

export const config = {
  matcher: [
    '/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
  ],
}

Authentication Hooks

Client-side Authentication Hook

// hooks/use-auth.ts
import { useEffect, useState } from 'react'
import { User } from '@supabase/supabase-js'
import { createClient } from '@/lib/supabase/client'

const supabase = createClient()

export function useAuth() {
  const [user, setUser] = useState<User | null>(null)
  const [loading, setLoading] = useState(true)

  useEffect(() => {
    // Get initial session
    const getInitialSession = async () => {
      const { data: { session } } = await supabase.auth.getSession()
      setUser(session?.user ?? null)
      setLoading(false)
    }

    getInitialSession()

    // Listen for auth changes
    const { data: { subscription } } = supabase.auth.onAuthStateChange(
      async (event, session) => {
        setUser(session?.user ?? null)
        setLoading(false)
        
        // Log authentication events
        if (event === 'SIGNED_IN' && session?.user) {
          await logAuthenticationEvent({
            user_id: session.user.id,
            event_type: 'session_started',
            ip_address: await getClientIP(),
            user_agent: navigator.userAgent
          })
        } else if (event === 'SIGNED_OUT') {
          await logAuthenticationEvent({
            event_type: 'session_ended'
          })
        }
      }
    )

    return () => subscription.unsubscribe()
  }, [])

  return {
    user,
    loading,
    signOut: () => supabase.auth.signOut()
  }
}

async function getClientIP(): Promise<string> {
  try {
    const response = await fetch('/api/client-ip')
    const data = await response.json()
    return data.ip || 'unknown'
  } catch {
    return 'unknown'
  }
}

Authentication Security Features

Account Lockout Protection

// lib/auth/security.ts
const MAX_LOGIN_ATTEMPTS = 5
const LOCKOUT_DURATION = 15 * 60 * 1000 // 15 minutes

export async function checkAccountLockout(email: string): Promise<boolean> {
  const { data: attempts } = await supabase
    .from('failed_login_attempts')
    .select('*')
    .eq('email', email)
    .gte('created_at', new Date(Date.now() - LOCKOUT_DURATION).toISOString())
    .order('created_at', { ascending: false })

  return (attempts?.length || 0) >= MAX_LOGIN_ATTEMPTS
}

export async function recordFailedAttempt(email: string, ipAddress: string) {
  await supabase
    .from('failed_login_attempts')
    .insert({
      email,
      ip_address: ipAddress,
      created_at: new Date().toISOString()
    })
}

export async function clearFailedAttempts(email: string) {
  await supabase
    .from('failed_login_attempts')
    .delete()
    .eq('email', email)
}

Device Fingerprinting

// lib/auth/device-fingerprint.ts
export function generateDeviceFingerprint(): string {
  const canvas = document.createElement('canvas')
  const ctx = canvas.getContext('2d')
  ctx?.fillText('Device fingerprint', 10, 50)
  
  const fingerprint = {
    userAgent: navigator.userAgent,
    language: navigator.language,
    platform: navigator.platform,
    screen: `${screen.width}x${screen.height}`,
    timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
    canvas: canvas.toDataURL(),
    plugins: Array.from(navigator.plugins).map(p => p.name).join(',')
  }
  
  return btoa(JSON.stringify(fingerprint))
}

export async function verifyDeviceFingerprint(userId: string, fingerprint: string): Promise<boolean> {
  const { data: knownDevices } = await supabase
    .from('user_devices')
    .select('device_fingerprint')
    .eq('user_id', userId)
    .eq('is_trusted', true)

  return knownDevices?.some(device => device.device_fingerprint === fingerprint) || false
}

This authentication system provides comprehensive security while maintaining user experience. The multi-layered approach ensures that user identities are properly verified and protected throughout their interaction with Smart Shelf.