> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sunschool.xyz/llms.txt
> Use this file to discover all available pages before exploring further.

# Security Configuration

> Authentication, authorization, and security best practices

## Authentication System

Sunschool uses JWT (JSON Web Token) authentication with scrypt password hashing. From `server/middleware/auth.ts`:

```typescript theme={null}
import jwt from 'jsonwebtoken';
import { scrypt, randomBytes, timingSafeEqual } from 'crypto';
import { promisify } from 'util';

const scryptAsync = promisify(scrypt);
const JWT_SECRET = process.env.JWT_SECRET || 'sunschool-secure-jwt-secret-for-development';
const JWT_EXPIRES_IN = '7d'; // 7 days
```

### JWT Configuration

<Info>
  JWT tokens provide stateless authentication, allowing the server to verify user identity without session storage.
</Info>

#### Token Generation

From `server/middleware/auth.ts`:

```typescript theme={null}
export function generateToken(user: { id: string | number, role: string }): string {
  const payload: JwtPayload = {
    userId: String(user.id),
    role: user.role
  };
  
  try {
    const token = jwt.sign(payload, JWT_SECRET, { expiresIn: JWT_EXPIRES_IN });
    return token;
  } catch (error) {
    console.error('Failed to generate token:', error);
    throw error;
  }
}
```

**Configuration:**

```env theme={null}
JWT_SECRET="your-secure-random-secret-min-32-chars"
JWT_EXPIRY=7d  # Optional: 7d, 30d, 1h, etc.
```

From `server/config/env.ts`:

```typescript theme={null}
export const JWT_SECRET = getEnv('JWT_SECRET', SESSION_SECRET);
export const JWT_EXPIRY = getEnv('JWT_EXPIRY', '7d');
```

#### Token Verification

From `server/middleware/auth.ts`:

```typescript theme={null}
export async function authenticateJwt(req: AuthRequest, res: Response, next: NextFunction): Promise<void> {
  let token: string | undefined;

  // Try Authorization header first (most common)
  const authHeader = req.headers.authorization;
  if (authHeader) {
    const parts = authHeader.split(' ');
    if (parts.length === 2 && parts[0] === 'Bearer') {
      token = parts[1];
    }
  }

  // Check for custom header (for sunschool.xyz domain)
  if (!token && req.headers['x-sunschool-auth-token']) {
    token = req.headers['x-sunschool-auth-token'] as string;
  }

  // Check cookies
  if (!token && req.cookies && req.cookies.token) {
    token = req.cookies.token;
  }
  
  if (!token) {
    res.status(401).json({ error: 'No authorization token provided' });
    return;
  }
  
  try {
    const payload = verifyToken(token);
    const user = await storage.getUser(payload.userId);
    
    if (!user) {
      res.status(401).json({ error: 'User not found' });
      return;
    }
    
    req.user = user;
    next();
  } catch (error) {
    if (error instanceof jwt.TokenExpiredError) {
      res.status(401).json({ error: 'Token expired' });
    } else {
      res.status(401).json({ error: 'Invalid token' });
    }
  }
}
```

### Generate Secure Secrets

<Warning>
  **Never use default secrets in production.** Generate cryptographically secure random values.
</Warning>

**Generate JWT secret:**

```bash theme={null}
# Using OpenSSL (recommended)
openssl rand -base64 32
# Output: XwK9vL2mN8pQ5rT6uY7zA1bC3dE4fG5hIjK6lM7nO8p

# Using Node.js
node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"
# Output: A1bC2dE3fG4hI5jK6lM7nO8pQ9rS0tU1vW2xY3zA4b

# Using Python
python3 -c "import secrets; print(secrets.token_urlsafe(32))"
```

**Add to `.env`:**

```env theme={null}
JWT_SECRET="XwK9vL2mN8pQ5rT6uY7zA1bC3dE4fG5hIjK6lM7nO8pQ9rS0tU1vW2xY3zA4b"
SESSION_SECRET="A1bC2dE3fG4hI5jK6lM7nO8pQ9rS0tU1vW2xY3zA4bC5dE6fG7hI8jK9lM0n"
```

<Info>
  If `JWT_SECRET` is not set, it defaults to `SESSION_SECRET`. Set both separately for better security.
</Info>

## Password Security

### scrypt Hashing

Sunschool uses **scrypt** for password hashing - a memory-hard function resistant to brute-force attacks.

From `server/middleware/auth.ts`:

```typescript theme={null}
export async function hashPassword(password: string): Promise<string> {
  const salt = randomBytes(16).toString("hex");
  const buf = (await scryptAsync(password, salt, 64)) as Buffer;
  return `${buf.toString("hex")}.${salt}`;
}

export async function comparePasswords(supplied: string, stored: string): Promise<boolean> {
  if (!stored || !stored.includes('.')) {
    console.error('Invalid stored password format');
    return false;
  }
  const [hashed, salt] = stored.split(".");
  const hashedBuf = Buffer.from(hashed, "hex");
  const suppliedBuf = (await scryptAsync(supplied, salt, 64)) as Buffer;
  return timingSafeEqual(hashedBuf, suppliedBuf);
}
```

**Security features:**

* **Random 16-byte salt** per password
* **64-byte derived key** using scrypt
* **Timing-safe comparison** to prevent timing attacks
* **Format:** `{hash_hex}.{salt_hex}`

### Password Requirements

<Warning>
  Implement password requirements in your client application. The server accepts any non-empty password.
</Warning>

**Recommended client-side validation:**

```javascript theme={null}
function validatePassword(password) {
  return (
    password.length >= 8 &&
    /[A-Z]/.test(password) &&  // Uppercase
    /[a-z]/.test(password) &&  // Lowercase
    /[0-9]/.test(password)     // Digit
  );
}
```

## Role-Based Access Control

From `shared/schema.ts`:

```typescript theme={null}
export const userRoleEnum = pgEnum("user_role", ["ADMIN", "PARENT", "LEARNER"]);
```

### User Roles

<Tabs>
  <Tab title="ADMIN">
    **Permissions:**

    * Full system access
    * View all users and data
    * Manage parents and learners
    * Access admin-only endpoints
    * Export all data

    **Use case:** System administrators, school administrators

    From `server/auth.ts`:

    ```typescript theme={null}
    // First user auto-promoted to ADMIN
    const userCountResult = await db.select({ count: count() }).from(users);
    const isFirstUser = userCountResult[0].count === 0;
    const effectiveRole = isFirstUser ? "ADMIN" : role;
    ```
  </Tab>

  <Tab title="PARENT">
    **Permissions:**

    * Manage own children (learners)
    * View children's progress and lessons
    * Create/delete learner accounts
    * Configure rewards and goals
    * Export own family data

    **Restrictions:**

    * Cannot access other families' data
    * Cannot promote users to admin

    From `server/routes.ts`:

    ```typescript theme={null}
    // Parents scoped to their children
    if (req.user?.role === "PARENT") {
      const children = await storage.getUsersByParentId(req.user.id);
      if (!children.some(child => child.id.toString() === learnerId.toString())) {
        return res.status(403).json({ error: "Forbidden" });
      }
    }
    ```
  </Tab>

  <Tab title="LEARNER">
    **Permissions:**

    * Access own lessons and quizzes
    * View own progress and achievements
    * Redeem rewards (with parent approval)
    * Update own profile (limited fields)

    **Restrictions:**

    * Cannot access other learners' data
    * Cannot manage users
    * Cannot configure system settings

    **Note:** Learners typically don't have passwords (parent-managed accounts).
  </Tab>
</Tabs>

### Role Middleware

From `server/middleware/auth.ts`:

```typescript theme={null}
export function hasRoleMiddleware(roles: string[]) {
  return (req: AuthRequest, res: Response, next: NextFunction): void => {
    if (!req.user) {
      res.status(401).json({ error: 'Unauthorized' });
      return;
    }
    
    if (!roles.includes(req.user.role)) {
      res.status(403).json({ error: 'Forbidden' });
      return;
    }
    
    next();
  };
}
```

**Usage in routes:**

```typescript theme={null}
// Admin-only endpoint
app.get("/api/parents", hasRole(["ADMIN"]), asyncHandler(async (req, res) => {
  const parents = await storage.getAllParents();
  res.json(parents);
}));

// Parent or Admin
app.get("/api/learners", hasRole(["PARENT", "ADMIN"]), asyncHandler(async (req, res) => {
  // ...
}));

// Any authenticated user
app.get("/api/lessons/active", isAuthenticated, asyncHandler(async (req, res) => {
  // ...
}));
```

## First User Admin Promotion

From `server/auth.ts` and `server/routes.ts`:

```typescript theme={null}
// Check if this is the first user being registered
const userCountResult = await db.select({ count: count() }).from(users);
const isFirstUser = userCountResult[0].count === 0;

// If this is the first user, make them an admin regardless of requested role
const effectiveRole = isFirstUser ? "ADMIN" : role;

const user = await storage.createUser({
  username,
  email,
  name,
  role: effectiveRole,
  password: hashedPassword,
});
```

<Info>
  **First registered user becomes ADMIN automatically.** This ensures you can bootstrap the system without manual database access.
</Info>

**Registration response includes promotion notice:**

```json theme={null}
{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "user": {
    "id": 1,
    "username": "admin",
    "role": "ADMIN"
  },
  "wasPromotedToAdmin": true
}
```

## Session Security

From `server/config/env.ts`:

```typescript theme={null}
export const SESSION_SECRET = getEnv('SESSION_SECRET', 'dev-secret-change-me');
```

**Session configuration:**

```env theme={null}
SESSION_SECRET="your-session-secret-different-from-jwt"
```

<Warning>
  Use different secrets for JWT and sessions to limit the impact of a potential key compromise.
</Warning>

### Session Storage

From `shared/schema.ts`, sessions are stored in PostgreSQL:

```typescript theme={null}
export const sessions = pgTable(
  "sessions",
  {
    sid: varchar("sid").primaryKey(),
    sess: jsonb("sess").notNull(),
    expire: timestamp("expire").notNull(),
  },
  (table) => [index("IDX_session_expire").on(table.expire)],
);
```

**Automatic cleanup:** Expired sessions are periodically removed via the `expire` index.

## CORS Configuration

From `server/auth.ts`:

```typescript theme={null}
const origin = req.headers.origin || req.headers.referer || 'unknown';
let isSunschool = false;
try {
  const parsedOrigin = new URL(origin);
  isSunschool = parsedOrigin.hostname === 'sunschool.xyz' || 
                parsedOrigin.hostname.endsWith('.sunschool.xyz');
} catch (_e) { /* ignore invalid origin URL */ }

if (isSunschool) {
  res.header('Access-Control-Allow-Origin', origin);
  res.header('Access-Control-Allow-Credentials', 'true');
  res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS');
  res.header('Access-Control-Allow-Headers', 'Content-Type,Authorization,X-Sunschool-Auth,X-Sunschool-Auth-Token');
}
```

<Warning>
  **SAST finding (MEDIUM priority):** CORS origin uses substring match. For production, tighten to exact domain match.
</Warning>

**Recommended production CORS:**

```typescript theme={null}
const allowedOrigins = [
  'https://sunschool.xyz',
  'https://www.sunschool.xyz',
  'https://app.sunschool.xyz'
];

if (allowedOrigins.includes(origin)) {
  res.header('Access-Control-Allow-Origin', origin);
  res.header('Access-Control-Allow-Credentials', 'true');
}
```

## API Security Best Practices

### Rate Limiting

<Info>
  **Not implemented by default.** Add rate limiting for production deployments.
</Info>

**Recommended: express-rate-limit**

```bash theme={null}
npm install express-rate-limit
```

```typescript theme={null}
import rateLimit from 'express-rate-limit';

const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100, // limit each IP to 100 requests per windowMs
  message: 'Too many requests from this IP'
});

app.use('/api/', limiter);
```

### Input Validation

**Current validation:**

```typescript theme={null}
if (!username || !password) {
  return res.status(400).json({ error: "Username and password are required" });
}

if (!gradeLevel || !learnerId) {
  return res.status(400).json({ error: "Missing required fields" });
}
```

**Recommended: Add schema validation with Zod**

```bash theme={null}
npm install zod
```

```typescript theme={null}
import { z } from 'zod';

const loginSchema = z.object({
  username: z.string().min(3).max(50),
  password: z.string().min(8)
});

try {
  const { username, password } = loginSchema.parse(req.body);
} catch (error) {
  return res.status(400).json({ error: error.errors });
}
```

### SQL Injection Protection

<Check>
  **Already protected.** Sunschool uses Drizzle ORM with parameterized queries.
</Check>

From `server/routes.ts`:

```typescript theme={null}
// Safe: Uses parameterized query via Drizzle
const user = await storage.getUserByUsername(username);

// Safe: Uses prepared statements
const result = await db.select().from(users).where(eq(users.id, userId));
```

**Avoid raw SQL unless necessary:**

```typescript theme={null}
// Unsafe
const query = `SELECT * FROM users WHERE username = '${username}'`;

// Safe (if raw SQL needed)
const query = await pool.query(
  'SELECT * FROM users WHERE username = $1',
  [username]
);
```

### XSS Protection

From `ENGINEERING.md`:

> SVG content is sanitized via a lightweight regex-based sanitizer in `server/services/svg-llm-service.ts`

**SVG sanitization:**

* Strips `<script>`, `<style>`, `<iframe>` tags
* Removes event handlers (`onclick`, `onload`, etc.)
* Blocks `javascript:` and `data:` URIs
* LLM prompts prohibit scripts and external references

<Warning>
  **SAST finding (LOW priority):** SVG innerHTML rendering. Server-side DOMPurify mitigates; consider client-side pass.
</Warning>

## Environment-Specific Security

### Development

```env theme={null}
NODE_ENV=development
DATABASE_SSL=false
JWT_SECRET="dev-jwt-secret-not-for-production"
SESSION_SECRET="dev-session-secret"
```

**Relaxed settings:**

* Detailed error messages
* No SSL for local database
* Shorter JWT expiry for testing

### Production

```env theme={null}
NODE_ENV=production
DATABASE_SSL=true
JWT_SECRET="<32+ char secure random string>"
SESSION_SECRET="<different 32+ char secure string>"
JWT_EXPIRY=7d
```

**Hardened settings:**

* Generic error messages
* SSL required for all connections
* Long random secrets
* Extended token expiry for user convenience

## Security Checklist

<Steps>
  <Step title="Generate Secure Secrets">
    ```bash theme={null}
    openssl rand -base64 32  # JWT_SECRET
    openssl rand -base64 32  # SESSION_SECRET
    ```
  </Step>

  <Step title="Enable SSL for Database">
    ```env theme={null}
    DATABASE_URL="postgresql://...?sslmode=require"
    DATABASE_SSL=true
    ```
  </Step>

  <Step title="Set Strong Password Policy">
    Implement client-side validation:

    * Minimum 8 characters
    * Mixed case, numbers, symbols
  </Step>

  <Step title="Configure CORS Properly">
    Use exact origin matching, not substrings
  </Step>

  <Step title="Add Rate Limiting">
    Install and configure `express-rate-limit`
  </Step>

  <Step title="Set NODE_ENV=production">
    ```env theme={null}
    NODE_ENV=production
    ```
  </Step>

  <Step title="Keep Dependencies Updated">
    ```bash theme={null}
    npm audit
    npm audit fix
    ```
  </Step>
</Steps>

## Audit Logging

<Info>
  **Not implemented by default.** Add audit logging for production.
</Info>

**Recommended approach:**

```typescript theme={null}
// Create audit_logs table
export const auditLogs = pgTable("audit_logs", {
  id: uuid("id").defaultRandom().primaryKey(),
  userId: integer("user_id").references(() => users.id),
  action: text("action").notNull(),
  resource: text("resource"),
  details: jsonb("details"),
  ipAddress: text("ip_address"),
  userAgent: text("user_agent"),
  createdAt: timestamp("created_at").defaultNow(),
});

// Log critical actions
await db.insert(auditLogs).values({
  userId: req.user.id,
  action: 'LOGIN',
  ipAddress: req.ip,
  userAgent: req.headers['user-agent']
});
```

## Next Steps

<CardGroup cols={2}>
  <Card title="Database Setup" icon="database" href="/self-hosting/database">
    Secure your database with SSL and access controls
  </Card>

  <Card title="Monitoring" icon="chart-line" href="/self-hosting/monitoring">
    Monitor authentication failures and security events
  </Card>

  <Card title="Troubleshooting" icon="bug" href="/self-hosting/troubleshooting">
    Debug authentication issues
  </Card>
</CardGroup>
