Saltar a contenido

Middleware

The Hub's middleware (apps/hub/src/middleware.ts) runs on every server-rendered request. It handles authentication gating, security headers, CSRF protection, CORS, locale detection, and routing redirects.

Middleware Pipeline

flowchart TD
    A[Incoming Request] --> B{Static asset?}
    B -->|Yes| C[Skip middleware]
    B -->|No| D{Prerendered route?}
    D -->|Yes| C
    D -->|No| E{Coming-soon redirect?}
    E -->|Yes| F[302 Redirect to /coming-soon]
    E -->|No| G{Protected route?}
    G -->|Yes| H{Auth cookie present?}
    H -->|No| I[302 Redirect to /auth/signin]
    H -->|Yes| J[Continue]
    G -->|No| J
    J --> K{API + state-changing method?}
    K -->|Yes| L{CSRF exempt?}
    L -->|No| M{CSRF token valid?}
    M -->|No| N[403 CSRF mismatch]
    M -->|Yes| O[Continue]
    L -->|Yes| O
    K -->|No| O
    O --> P[Detect locale]
    P --> Q[Execute route handler]
    Q --> R[Apply security headers]
    R --> S{API route?}
    S -->|Yes| T[Apply CORS + privacy headers]
    S -->|No| U{CSRF cookie set?}
    T --> V[Return response]
    U -->|No| W[Set CSRF cookie]
    U -->|Yes| V
    W --> V

Static Asset Skipping

Requests for static assets bypass all middleware processing. Assets are identified by file extension:

.css, .js, .png, .jpg, .jpeg, .gif, .svg, .ico, .woff, .woff2, .ttf, .eot, .map

Prerendered Route Handling

Routes that use export const prerender = true in their Astro frontmatter are skipped by the middleware. This prevents request.headers access warnings that occur at build time when Astro pre-renders static pages.

Prerendered routes:

Route Page
/ Landing page
/coming-soon Coming-soon placeholder

Auth Gating

Protected Routes

The middleware checks for the cybereco-auth-token httpOnly cookie on these route prefixes:

Prefix Description
/dashboard Main user dashboard
/apps Ecosystem app directory and management
/my-data Personal data management and export
/profile User profile editing
/privacy Privacy and consent settings
/settings Account settings
/billing Subscription and billing management
/security Security settings (password, 2FA, sessions, devices)
/admin Administrative panel

If the cookie is missing or empty, the middleware returns a 302 redirect to /auth/signin with a returnUrl query parameter so the user can be redirected back after login:

/auth/signin?returnUrl=%2Fdashboard

Public Routes

These route prefixes never require authentication:

Prefix Reason
/auth Login, signup, and password reset pages
/api API routes handle their own auth checks internally
/coming-soon Static placeholder pages

Security Headers

Every server-rendered response receives the following security headers:

Content Security Policy (CSP)

The CSP policy restricts resource loading to trusted origins:

Directive Value Purpose
default-src 'self' Default fallback
script-src 'self' 'unsafe-inline' https://apis.google.com https://www.gstatic.com Scripts (+ 'unsafe-eval' in dev)
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com Stylesheets
font-src 'self' https://fonts.gstatic.com Web fonts
img-src 'self' data: https: blob: Images
connect-src 'self' + Firebase/Google API domains XHR/fetch/WebSocket
frame-src 'self' https://*.firebaseapp.com Iframes (Firebase Auth popup)
object-src 'none' Block plugins
base-uri 'self' Restrict <base> tag
form-action 'self' Restrict form submissions
frame-ancestors 'none' Prevent framing (clickjacking)

In production, upgrade-insecure-requests is also included.

Development Mode

In development (import.meta.env.DEV), the CSP is relaxed to allow localhost connections, WebSocket hot-reload, and 'unsafe-eval' for Vite.

Other Security Headers

Header Value Purpose
Strict-Transport-Security max-age=31536000; includeSubDomains; preload Force HTTPS for 1 year
X-Frame-Options DENY Prevent clickjacking (legacy)
X-Content-Type-Options nosniff Prevent MIME type sniffing
X-XSS-Protection 1; mode=block XSS filter (legacy browsers)
Referrer-Policy strict-origin-when-cross-origin Limit referrer information
Cross-Origin-Opener-Policy same-origin-allow-popups Allow Firebase Auth popup flows
Permissions-Policy Disable: accelerometer, camera, geolocation, gyroscope, magnetometer, microphone, payment, usb Restrict browser features

The middleware also removes X-Powered-By and Server headers to reduce fingerprinting surface.

CSRF Protection

The middleware implements the double-submit cookie pattern for CSRF protection.

How It Works

  1. Cookie creation: On every non-API page response, the middleware checks for a cybereco-csrf-token cookie. If absent, it generates a 32-byte random hex token and sets it as a cookie:

    cookies.set('cybereco-csrf-token', csrfToken, {
      path: '/',
      maxAge: 86400,      // 24 hours
      sameSite: 'lax',
      httpOnly: false,    // JavaScript must read this
      secure: !isDev,
    });
    

    Why not httpOnly?

    The CSRF cookie has httpOnly: false because client-side JavaScript must read it to include the value in request headers.

  2. Header validation: For state-changing API requests (POST, PUT, DELETE, PATCH), the middleware compares the cybereco-csrf-token cookie value against the X-CSRF-Token request header:

    const csrfCookie = cookies.get('cybereco-csrf-token')?.value;
    const csrfHeader = request.headers.get('X-CSRF-Token');
    
    if (!csrfCookie || !csrfHeader || csrfCookie !== csrfHeader) {
      return new Response(
        JSON.stringify({ error: 'CSRF token mismatch' }),
        { status: 403 }
      );
    }
    
  3. Client usage: React islands read the cookie and include it in API requests:

    const csrfToken = document.cookie
      .split('; ')
      .find(row => row.startsWith('cybereco-csrf-token='))
      ?.split('=')[1];
    
    await fetch('/api/privacy/settings', {
      method: 'PUT',
      headers: {
        'Content-Type': 'application/json',
        'X-CSRF-Token': csrfToken,
      },
      body: JSON.stringify(settings),
    });
    

Exempt Routes

These routes are exempt from CSRF validation:

Route Reason
/api/csp-report Browser-initiated CSP reports have no user state
/api/auth/login Initial authentication (no session exists yet)
/api/auth/set-cookie Part of the auth flow (called immediately after Firebase login)

CORS Headers

API routes (/api/*) receive CORS headers based on the request origin:

Allowed origins:

  • https://hub.cybere.co
  • https://cybere.co
  • https://justsplit.cybere.co
  • http://localhost:4321
  • http://localhost:40000
  • http://localhost:40001
  • http://localhost:40002

CORS response headers:

Header Value
Access-Control-Allow-Origin Matched origin (if on allowlist)
Access-Control-Allow-Methods GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers Content-Type, Authorization
Access-Control-Allow-Credentials true

Privacy Headers

API routes also receive privacy-specific cache headers to prevent sensitive data from being cached:

Header Value
Cache-Control no-store, no-cache, must-revalidate, private
Pragma no-cache
X-Content-Type-Options nosniff

Locale Detection

The middleware detects the user's preferred language and stores it in Astro.locals.locale for use by pages and layouts.

Detection order:

  1. Cookie: Check for cybereco-language cookie value (en or es)
  2. Accept-Language header: Check if the header contains es
  3. Default: Fall back to en
function detectLocale(request: Request): string {
  // 1. Cookie
  const cookieMatch = request.headers.get('cookie')?.match(/cybereco-language=(\w+)/);
  if (cookieMatch && ['en', 'es'].includes(cookieMatch[1])) {
    return cookieMatch[1];
  }

  // 2. Accept-Language
  if (request.headers.get('accept-language')?.includes('es')) {
    return 'es';
  }

  // 3. Default
  return 'en';
}

Coming-Soon Redirects

Ecosystem apps that are not yet launched redirect to the /coming-soon page with the app name as a query parameter:

Route Redirect
/app/somos /coming-soon?app=somos
/app/demos /coming-soon?app=demos
/app/plantopia /coming-soon?app=plantopia

Source File

The complete middleware implementation is in:

apps/hub/src/middleware.ts