Skip to content

Security Requirements & Patterns

MANDATORY Security Principles

  • Security First: All content access is controlled by published status at the course level.
  • Defense in Depth: Multiple layers of security validation.
  • Least Privilege: Users only have access to what they need.
  • Input Validation: All user inputs must be validated and sanitized.

Published Content Filtering

Static Generation Security

  • generateStaticParams: MUST only include published courses.
  • Entry Visibility: All entries within a published course are visible. Content visibility is controlled at the course level, not at the entry level.
  • Runtime Verification: Always verify course published status before rendering content.
  • Access Control: Implement proper access control in both static and dynamic routes.

Authentication & Authorization

Authentication Requirements

  • Protected Routes: All LMS routes require an authenticated Auth.js (NextAuth) session.
  • Token Validation: Verify JWT tokens on all API routes.
  • Session Management: Handle session expiration gracefully.
  • Re-authentication: Provide re-authentication flow when needed.

Password Policy (Canonical)

  • New-password surfaces (sign-up, reset, invitation accept, admin user create, account password set/change) must enforce:
    • minimum length: 8
    • maximum length: 128
    • at least one lowercase letter ([a-z])
    • at least one uppercase letter ([A-Z])
    • at least one digit ([0-9])
  • Special characters are allowed but not required.
  • Use one shared schema in @open-learning-hub/auth-utils/password-policy (newPasswordSchema) and import it in both apps.
  • Sign-in validation remains min(1) + bcrypt verification; do not re-evaluate password complexity at login.
  • Dev/test seed credentials must use a policy-compliant password via SEED_DEMO_PASSWORD (and optional SEED_SUPERADMIN_PASSWORD override) and stay synchronized with e2e fixtures through shared seed credential modules.

Role-Based Access Control

  • student: Access to enrolled courses only.
  • course_admin: Access to dashboards for assigned courses only (per course_admin_assignments); cannot edit course content.
  • tenant_admin: Full management within their own tenant — users, courses, enrolments, course-admin assignments, tenant settings. Cannot cross tenants.
  • super_admin: Global; can manage tenants, search across tenants via withTenantOverride(), and impersonate tenant_admin (audit-logged).

See apps/lms/docs/reference/user-management.md for the full RBAC contract, permission matrix, and persona flows.

Input Validation & Sanitization

Client-Side Validation

  • Validate all form inputs before submission.
  • Provide immediate feedback on validation errors.
  • Prevent invalid data from being submitted.

Server-Side Validation

  • ALWAYS validate on server: Never trust client-side validation alone.
  • Validate all API route inputs.
  • Sanitize user inputs before processing.
  • Use Zod or similar for schema validation.

Environment Variables Security

Public Variables

  • Use NEXT_PUBLIC_ prefix only for truly public variables.
  • Never include sensitive data in public variables.
  • Public variables are exposed in client-side bundle.

Private Variables

  • Server-side variables MUST NOT use NEXT_PUBLIC_ prefix.
  • Store sensitive credentials in .env.local (never commit).
  • Use secure key management in production.
  • Rotate credentials regularly.

Variable Validation

// Example: Environment variable validation
function getRequiredEnv(name: string): string {
  const value = process.env[name];
  if (!value) {
    throw new Error(`Missing required environment variable: ${name}`);
  }
  return value;
}

const authSecret = getRequiredEnv("AUTH_SECRET");
const databaseUrl = getRequiredEnv("DATABASE_URL");

API Route Security

Authentication Checks

  • Verify authentication on all protected routes.
  • Check user permissions before processing requests.
  • Return appropriate status codes (401 Unauthorized, 403 Forbidden).

Rate Limiting

  • Implement rate limiting on API routes and server actions that create accounts, authenticate users, mutate enrollments, submit assessments, or trigger cache invalidation.
  • Prevent abuse and DoS attacks without exposing whether a specific account, course, or token exists.
  • Return 429 Too Many Requests with a user-safe error message and a Retry-After header when possible.
  • Use Upstash Redis (@upstash/ratelimit) in production. Use an in-memory fixed-window implementation for local development and tests; it must not add a production dependency or be trusted across multiple server instances.
  • Select the implementation through RATE_LIMIT_DRIVER (upstash in production, memory in dev/test). Production deployments using upstash must provide UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN.
  • Never key limits by raw email alone. Hash or normalize identifiers before composing keys, and include the action name to avoid cross-endpoint interference.
Endpoint Limit Window Key scope
POST /api/auth/callback/credentials 10 requests 15 min IP + normalized email
POST /learn/sign-up 5 requests 1 h IP + normalized email
POST .../enroll 10 requests 1 h authenticated user
POST .../submit-quiz 30 requests 1 h authenticated user
POST /api/cms/revalidate 60 requests 1 min IP
POST /api/csp-report 60 requests 1 min IP

The sign-up, enrollment, and quiz rows refer to route-owned server actions. Apply the same limits at the action boundary even when the action is invoked through React's server-action transport rather than a hand-authored /api/* route.

CORS Configuration

  • Configure CORS properly for API routes.
  • Only allow trusted origins.
  • Use environment-specific CORS settings.

XSS Prevention

Content Sanitization

  • Sanitize all user-generated content.
  • Use React's built-in XSS protection.
  • Escape HTML content when rendering.
  • Use Content Security Policy (CSP) headers.

Portable Text Rendering

  • Use @portabletext/react for content.
  • Sanitize Portable Text rawHtml blocks at the server CMS-fetch boundary (apps/lms/src/lib/cms/sanitize-cms-html.ts) before rendering.
  • Validate and sanitize custom block types.

CSRF Protection

Token-Based Protection

  • Use CSRF tokens for state-changing operations.
  • Validate tokens on server-side.
  • Implement SameSite cookie attributes.

Same-Origin Policy

  • Leverage browser's same-origin policy.
  • Use proper CORS configuration.
  • Validate request origins.

Data Protection

Sensitive Data Handling

  • Never log sensitive data (passwords, tokens, private keys).
  • Encrypt sensitive data at rest.
  • Use HTTPS for all data transmission.
  • Implement proper data retention policies.

Database Authorization

  • Enforce authorization in server components, server actions, and route handlers — there is no declarative database rule engine.
  • Resolve the current session with auth() from src/auth.ts before any sensitive query.
  • Validate every input with a Zod schema at the boundary, then pass the parsed values to Kysely queries.
  • Scope multi-tenant reads/writes by tenantId and userId columns explicitly in every Kysely query — never rely on the caller to filter.
  • Treat the database as the source of truth for roles; mirror them into the Auth.js session via the jwt/session callbacks but always re-check against the database for privileged actions.
  • Never rely solely on client-side validation.

Security Headers

// Example: Security headers in Next.js
export 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: "X-XSS-Protection",
    value: "1; mode=block",
  },
  {
    key: "Referrer-Policy",
    value: "origin-when-cross-origin",
  },
  {
    key: "Content-Security-Policy",
    value:
      "default-src 'self'; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline';",
  },
];

Security Best Practices

  • Regular Updates: Keep dependencies up-to-date.
  • Security Audits: Regular security audits and penetration testing.
  • Error Handling: Never expose sensitive information in error messages.
  • Logging: Log security events (failed auth attempts, permission denials).
  • Monitoring: Monitor for suspicious activity.
  • Documentation: Document security requirements and patterns.