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.