Skip to content

Security Architecture

Last Updated: 2025-11-22

Overview

The AccessALI Customer Portal implements multiple layers of security to protect user data, prevent unauthorized access, and maintain compliance with security best practices. This document outlines the security architecture, authentication mechanisms, and security controls implemented in the application.

Security First

Security is a critical aspect of the AccessALI portal. All developers must follow these security guidelines when implementing features.

Security Layers

graph TB
    subgraph "Network Security"
        HTTPS[HTTPS/TLS 1.3]
        CDN[Vercel Edge Network]
        WAF[Web Application Firewall]
    end

    subgraph "Application Security"
        Auth[NextAuth.js Authentication]
        Middleware[Auth Middleware]
        RBAC[Role-Based Access Control]
        RateLimit[Rate Limiting]
        CSRF[CSRF Protection]
    end

    subgraph "Input Security"
        Validation[Zod Input Validation]
        Sanitization[Data Sanitization]
        XSS[XSS Prevention]
    end

    subgraph "Data Security"
        Encryption[Data Encryption]
        Hashing[Password Hashing]
        SQLInjection[SQL Injection Prevention]
    end

    HTTPS --> Auth
    CDN --> Middleware
    WAF --> Auth

    Auth --> RBAC
    Middleware --> RateLimit
    Middleware --> CSRF

    RBAC --> Validation
    RateLimit --> Sanitization
    CSRF --> XSS

    Validation --> Encryption
    Sanitization --> Hashing
    XSS --> SQLInjection

Authentication & Authorization

NextAuth.js v5 Implementation

AccessALI uses NextAuth.js v5 (beta) for robust authentication and session management.

Supported Authentication Methods

Method Provider Status Use Case
Email/Password Credentials Provider ✅ Implemented Standard login
Google OAuth Google Provider ✅ Implemented Social login
Facebook OAuth Facebook Provider ✅ Implemented Social login
Magic Link Email Provider 🔄 Planned Passwordless login

Authentication Flow

sequenceDiagram
    participant User
    participant NextAuth
    participant Database
    participant OAuth

    alt Email/Password Login
        User->>NextAuth: Submit credentials
        NextAuth->>Database: Verify user
        Database-->>NextAuth: User data
        NextAuth->>NextAuth: Verify password (bcrypt)
        NextAuth-->>User: Session token
    else OAuth Login
        User->>NextAuth: Click "Sign in with Google"
        NextAuth->>OAuth: Redirect to OAuth provider
        OAuth-->>User: OAuth consent screen
        User->>OAuth: Grant permissions
        OAuth-->>NextAuth: OAuth tokens
        NextAuth->>Database: Create/update user & account
        NextAuth-->>User: Session token
    end

    User->>NextAuth: Access protected page
    NextAuth->>Database: Verify session
    Database-->>NextAuth: Session valid
    NextAuth-->>User: Authorized access

Role-Based Access Control (RBAC)

AccessALI implements a role-based permission system:

enum UserRole {
  BUYER   // Default role for property buyers
  ADMIN   // Full system access
  SUPPORT // Customer support access
}

Permission Matrix

Resource BUYER SUPPORT ADMIN
View own properties
View all properties
Make payments
Upload documents
View all users
Manage users
System settings

Implementing Authorization

Server Actions:

'use server'

import { auth } from '@/lib/auth'
import { UnauthorizedError } from '@/lib/repositories/errors'

export async function adminOnlyAction() {
  const session = await auth()

  // Check authentication
  if (!session?.user?.id) {
    throw new UnauthorizedError('Must be logged in')
  }

  // Check authorization
  if (session.user.role !== 'ADMIN') {
    throw new UnauthorizedError('Admin access required')
  }

  // Perform admin action
  return doAdminStuff()
}

Middleware Protection:

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

export default auth((req) => {
  const { pathname } = req.nextUrl

  // Protect admin routes
  if (pathname.startsWith('/admin')) {
    if (req.auth?.user?.role !== 'ADMIN') {
      return NextResponse.redirect(new URL('/unauthorized', req.url))
    }
  }

  // Protect all dashboard routes
  if (pathname.startsWith('/dashboard')) {
    if (!req.auth) {
      return NextResponse.redirect(new URL('/login', req.url))
    }
  }

  return NextResponse.next()
})

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

Session Management

Session Strategy:

  • Type: JWT (JSON Web Tokens)
  • Storage: HTTP-only cookies (client-side), Database (server-side)
  • Expiration: 30 days (configurable)
  • Refresh: Automatic on activity

Session Security:

// JWT Configuration
{
  secret: process.env.NEXTAUTH_SECRET,    // Strong secret (min 32 chars)
  maxAge: 30 * 24 * 60 * 60,              // 30 days
  updateAge: 24 * 60 * 60,                 // Refresh daily
  encryption: true,                        // Encrypted JWTs
  sameSite: 'lax',                        // CSRF protection
  httpOnly: true,                         // No JavaScript access
  secure: process.env.NODE_ENV === 'production' // HTTPS only in prod
}

Session Best Practices

  • Always use HTTPS in production
  • Set appropriate session expiration
  • Implement logout on all devices
  • Monitor for suspicious session activity

Input Validation & Sanitization

Zod Schema Validation

All user inputs are validated using Zod schemas before processing.

import { z } from 'zod'

// Define schema
export const createPaymentSchema = z.object({
  propertyId: z.string().cuid(),
  amount: z.number().positive().max(100000000),
  paymentMethod: z.enum(['CREDIT_CARD', 'BANK_TRANSFER', 'CASH']),
  notes: z.string().max(500).optional()
})

// Validate in Server Action
export async function createPayment(data: unknown) {
  // Validate input
  const validated = createPaymentSchema.parse(data) // Throws if invalid

  // Safe to use validated data
  return await paymentRepository.create(validated)
}

XSS Prevention

React Automatic Escaping:

React automatically escapes all values rendered in JSX, preventing XSS attacks.

// ✅ Safe: React escapes user input
<div>{user.name}</div>

// ❌ Dangerous: Bypasses React escaping
<div dangerouslySetInnerHTML={{ __html: user.bio }} />

Content Security Policy (CSP):

// next.config.mjs
const cspHeader = `
  default-src 'self';
  script-src 'self' 'unsafe-eval' 'unsafe-inline';
  style-src 'self' 'unsafe-inline';
  img-src 'self' blob: data: https:;
  font-src 'self';
  object-src 'none';
  base-uri 'self';
  form-action 'self';
  frame-ancestors 'none';
  upgrade-insecure-requests;
`

export default {
  async headers() {
    return [
      {
        source: '/(.*)',
        headers: [
          {
            key: 'Content-Security-Policy',
            value: cspHeader.replace(/\n/g, ''),
          },
        ],
      },
    ]
  },
}

SQL Injection Prevention

Prisma ORM Protection:

Prisma uses parameterized queries, preventing SQL injection:

// ✅ Safe: Prisma parameterizes inputs
await prisma.user.findMany({
  where: {
    email: userInput // Automatically sanitized
  }
})

// ❌ Never use raw SQL with user input
await prisma.$executeRaw`SELECT * FROM users WHERE email = ${userInput}`

// ✅ If raw SQL needed, use parameterized queries
await prisma.$executeRaw`SELECT * FROM users WHERE email = ${Prisma.sql`${userInput}`}`

CSRF Protection

Next.js Built-in CSRF Protection

Next.js Server Actions include automatic CSRF protection:

// Form with CSRF token
<form action={serverAction}>
  <input type="text" name="data" />
  <button type="submit">Submit</button>
  {/* CSRF token automatically included */}
</form>

API Routes CSRF Protection

// API route with CSRF check
import { NextRequest } from 'next/server'

export async function POST(req: NextRequest) {
  // Verify origin header
  const origin = req.headers.get('origin')
  const host = req.headers.get('host')

  if (origin !== `https://${host}`) {
    return new Response('Invalid origin', { status: 403 })
  }

  // Process request
  return Response.json({ success: true })
}

Rate Limiting

Redis-Based Rate Limiting

import { Ratelimit } from '@upstash/ratelimit'
import { Redis } from '@upstash/redis'

// Create rate limiter
const ratelimit = new Ratelimit({
  redis: Redis.fromEnv(),
  limiter: Ratelimit.slidingWindow(10, '10 s'), // 10 requests per 10 seconds
  analytics: true,
})

// Apply in middleware
export async function middleware(request: NextRequest) {
  const ip = request.ip ?? '127.0.0.1'

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

  if (!success) {
    return new Response('Too Many Requests', {
      status: 429,
      headers: {
        'X-RateLimit-Limit': limit.toString(),
        'X-RateLimit-Remaining': remaining.toString(),
        'X-RateLimit-Reset': reset.toString(),
      },
    })
  }

  return NextResponse.next()
}

Rate Limit Tiers

Endpoint Limit Window Use Case
/api/auth/* 5 requests 15 minutes Login attempts
/api/upload 10 requests 1 hour File uploads
/api/* (general) 100 requests 15 minutes API calls
Server Actions 60 requests 1 minute Form submissions

Password Security

Password Hashing (bcrypt)

import bcrypt from 'bcryptjs'

// Hash password on registration
export async function hashPassword(password: string): Promise<string> {
  const saltRounds = 12 // Cost factor
  return bcrypt.hash(password, saltRounds)
}

// Verify password on login
export async function verifyPassword(
  password: string,
  hashedPassword: string
): Promise<boolean> {
  return bcrypt.compare(password, hashedPassword)
}

Password Requirements

const passwordSchema = z.string()
  .min(8, 'Password must be at least 8 characters')
  .max(100, 'Password must be less than 100 characters')
  .regex(/[A-Z]/, 'Password must contain at least one uppercase letter')
  .regex(/[a-z]/, 'Password must contain at least one lowercase letter')
  .regex(/[0-9]/, 'Password must contain at least one number')
  .regex(/[^A-Za-z0-9]/, 'Password must contain at least one special character')

Never Log Passwords

Never log passwords or password hashes. Use [REDACTED] in logs.

Data Encryption

At-Rest Encryption

  • Database: PostgreSQL with encryption at rest (Neon)
  • Backups: Encrypted backups
  • File Storage: Vercel Blob Storage (encrypted by default)

In-Transit Encryption

  • HTTPS/TLS 1.3: All production traffic
  • Certificate: Automatic SSL via Vercel
  • HSTS: HTTP Strict Transport Security enabled
// Security headers
{
  key: 'Strict-Transport-Security',
  value: 'max-age=63072000; includeSubDomains; preload'
}

Secure Headers

// next.config.mjs
async headers() {
  return [
    {
      source: '/:path*',
      headers: [
        {
          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: 'X-XSS-Protection',
          value: '1; mode=block'
        },
        {
          key: 'Referrer-Policy',
          value: 'origin-when-cross-origin'
        },
        {
          key: 'Permissions-Policy',
          value: 'camera=(), microphone=(), geolocation=()'
        }
      ]
    }
  ]
}

Error Handling Security

Secure Error Messages

// ❌ Bad: Exposes internal details
throw new Error(`Database connection failed: ${dbError.message}`)

// ✅ Good: Generic message to client, detailed logging
export class DatabaseError extends Error {
  constructor(operation: string, cause?: unknown) {
    super(`Database operation failed: ${operation}`)
    this.name = 'DatabaseError'
    this.cause = cause
  }
}

// Log detailed error server-side
console.error('Database error:', { operation, cause: dbError })

// Return generic error to client
return { error: 'An error occurred. Please try again.' }

Never Expose Stack Traces

Never return stack traces or internal error details to clients in production. Log them server-side only.

Secrets Management

Environment Variables

# ✅ Store in .env.local (never committed)
DATABASE_URL="postgresql://..."
NEXTAUTH_SECRET="strong-random-secret"
GOOGLE_CLIENT_ID="..."
GOOGLE_CLIENT_SECRET="..."

# ❌ Never hardcode secrets
const apiKey = "sk-1234567890" // Don't do this!

Production Secrets

  • Store in Vercel Environment Variables
  • Rotate secrets regularly
  • Use different secrets per environment
  • Never log secret values

Security Monitoring

Logging

import winston from 'winston'

const logger = winston.createLogger({
  level: 'info',
  format: winston.format.json(),
  transports: [
    new winston.transports.File({ filename: 'error.log', level: 'error' }),
    new winston.transports.File({ filename: 'combined.log' })
  ]
})

// Log security events
logger.warn('Failed login attempt', {
  email: '[REDACTED]',
  ip: request.ip,
  timestamp: new Date().toISOString()
})

Security Events to Monitor

  • Failed login attempts (> 5 in 15 min)
  • Password reset requests
  • Role changes
  • Unusual access patterns
  • Rate limit violations
  • Invalid tokens

Security Checklist

Development

  • All inputs validated with Zod
  • SQL injection prevented (Prisma ORM)
  • XSS prevented (React escaping)
  • CSRF protection enabled
  • Authentication required for protected routes
  • Authorization checks in place
  • Passwords hashed with bcrypt
  • Secrets in environment variables
  • Security headers configured
  • Rate limiting implemented

Pre-Production

  • Security audit completed
  • Dependency vulnerabilities fixed
  • HTTPS enforced
  • CSP headers configured
  • Error messages sanitized
  • Logging configured
  • Secrets rotated
  • Backup strategy in place

Production

  • HTTPS/TLS enabled
  • Security monitoring active
  • Regular security updates
  • Incident response plan
  • Regular backups
  • Access logs reviewed

Common Vulnerabilities & Prevention

OWASP Top 10

Vulnerability Prevention Implementation
Injection Parameterized queries Prisma ORM
Broken Authentication Strong session management NextAuth.js
Sensitive Data Exposure Encryption & secure storage TLS, env vars
XML External Entities (XXE) Disable XML parsing N/A (no XML)
Broken Access Control RBAC & authorization checks Middleware
Security Misconfiguration Secure headers next.config.mjs
XSS Input validation & escaping React, Zod
Insecure Deserialization Validate deserialized data Zod schemas
Using Components with Known Vulnerabilities Regular updates Dependabot
Insufficient Logging & Monitoring Comprehensive logging Winston

Next Steps