Files
Charon/docs/plans/current_spec.md

35 KiB

Login Page Issues Fix - Comprehensive Implementation Plan (REVISED)

Date: December 21, 2025 Revision: Post-Supervisor Review - Critical Implementation Flaws Addressed Target: Login page at http://100.98.12.109:8080/login Issues Identified: 3


Executive Summary

Three issues have been identified on the login page that need resolution:

  1. 401 Unauthorized from /api/v1/auth/me - Expected behavior during initialization
  2. Cross-Origin-Opener-Policy (COOP) header warning - Browser warning on non-localhost HTTP
  3. Missing autocomplete attribute on password input - Accessibility/DOM warning

This plan analyzes each issue, determines if it's a bug or expected behavior, and provides actionable fixes with specific file locations and implementation details.

🔴 CRITICAL REVISION NOTICE

Supervisor Review Identified Critical Implementation Flaw:

The original plan proposed checking c.Request.TLS != nil to detect HTTPS connections. This approach is fundamentally broken in reverse proxy architectures:

  • Problem: Caddy terminates TLS before forwarding requests to the backend
  • Result: c.Request.TLS is ALWAYS nil, making HTTPS detection impossible
  • Impact: COOP header would never be set, even in production HTTPS

Corrected Approaches:

  1. Option A: Check X-Forwarded-Proto header (requires Caddy configuration verification)
  2. Option B: Set COOP only when NOT in development mode (simpler, recommended)

Additional Requirements Added:

  • Verify Caddy forwards X-Forwarded-Proto header in reverse proxy config
  • Add integration tests for proxy header propagation
  • Document mixed content warnings for production HTTPS requirements
  • Add autocomplete compliance considerations for regulated industries

This revision ensures the implementation will work correctly in production reverse proxy deployments.


Issue 1: GET /api/v1/auth/me Returns 401 (Unauthorized)

Status: EXPECTED BEHAVIOR (Minor Enhancement Possible)

Root Cause Analysis

File: frontend/src/context/AuthContext.tsx (lines 10-24)

The AuthProvider component runs a checkAuth() function on mount that:

useEffect(() => {
  const checkAuth = async () => {
    try {
      const stored = localStorage.getItem('charon_auth_token');
      if (stored) {
        setAuthToken(stored);
      }
      const response = await client.get('/auth/me');  // Line 16
      setUser(response.data);
    } catch {
      setAuthToken(null);
      setUser(null);
    } finally {
      setIsLoading(false);
    }
  };

  checkAuth();
}, []);

Why This Happens:

  • On first load (before login), no auth token exists in localStorage
  • The /auth/me call is made to check if the user has a valid session
  • The backend correctly returns 401 because no valid authentication exists
  • This is expected behavior - the error is caught silently and doesn't affect UX

Backend Authentication Flow:

File: backend/internal/api/routes/routes.go (line 154)

protected.GET("/auth/me", authHandler.Me)

File: backend/internal/api/middleware/auth.go (lines 12-35)

  • Checks Authorization header first
  • Falls back to auth_token cookie
  • Falls back to token query parameter (deprecated)
  • Returns 401 if no valid token found

Assessment

Is this a bug? No - this is expected behavior for an unauthenticated user.

User Impact: None - the error is silently caught and doesn't display to the user.

Browser Console Impact: Minimal - developers see a 401 in Network tab, but this is normal for auth checks.

If we want to eliminate the 401 from appearing in dev tools, we can optimize the auth check:

Option A: Skip /auth/me call if no token in localStorage

File: frontend/src/context/AuthContext.tsx

useEffect(() => {
  const checkAuth = async () => {
    try {
      const stored = localStorage.getItem('charon_auth_token');
      if (!stored) {
        // No token stored, skip API call
        setIsLoading(false);
        return;
      }

      setAuthToken(stored);
      const response = await client.get('/auth/me');
      setUser(response.data);
    } catch {
      setAuthToken(null);
      setUser(null);
    } finally {
      setIsLoading(false);
    }
  };

  checkAuth();
}, []);

Option B: Add a dedicated "check session" endpoint that returns 200 with authenticated: false instead of 401

Backend File: backend/internal/api/handlers/auth_handler.go (lines 271-304)

The VerifyStatus handler already exists and returns:

c.JSON(http.StatusOK, gin.H{
  "authenticated": false,
})

Frontend Change: Use /auth/verify-status instead of /auth/me in checkAuth

Priority: LOW (Cosmetic Enhancement)


Issue 2: Cross-Origin-Opener-Policy Header Warning

Status: EXPECTED FOR HTTP DEV ENVIRONMENT (Documentation Needed)

Root Cause Analysis

File: backend/internal/api/middleware/security.go (lines 61-62)

// Cross-Origin-Opener-Policy: Isolate browsing context
c.Header("Cross-Origin-Opener-Policy", "same-origin")

Why This Happens:

  • The COOP header same-origin is a security feature that isolates the browsing context
  • Browser warns about COOP on HTTP (non-HTTPS) connections on non-localhost IPs
  • The warning states: "Cross-Origin-Opener-Policy policy would block the window.closed call"

COOP Header Purpose:

  • Prevents other origins from accessing the window object
  • Protects against Spectre-like attacks
  • Required for using SharedArrayBuffer and high-resolution timers

Current Behavior:

  • Header is applied globally to all responses
  • No conditional logic for development vs production
  • Same header value for HTTP and HTTPS

File: backend/internal/api/routes/routes.go (lines 36-40)

securityHeadersCfg := middleware.SecurityHeadersConfig{
  IsDevelopment: cfg.Environment == "development",
}
router.Use(middleware.SecurityHeaders(securityHeadersCfg))

The IsDevelopment flag is passed but currently only affects CSP directives, not COOP.

Assessment

Is this a bug? No - this is expected behavior when accessing the app via HTTP on a non-localhost IP.

User Impact:

  • Visual warning in browser console (Chrome/Edge DevTools)
  • No functional impact on the application
  • COOP doesn't break any existing functionality

Security Impact:

  • COOP is a security enhancement and should remain in production (HTTPS)
  • Can be relaxed for local development HTTP

Phase 1: Make COOP conditional on HTTPS

File: backend/internal/api/middleware/security.go

Current Implementation: (lines 61-62)

// Cross-Origin-Opener-Policy: Isolate browsing context
c.Header("Cross-Origin-Opener-Policy", "same-origin")

ORIGINAL APPROACH (FLAWED):

// CRITICAL FLAW: c.Request.TLS will ALWAYS be nil behind a reverse proxy!
// Caddy terminates TLS before forwarding to the backend.
if c.Request.TLS != nil {  // ⚠️ THIS WILL NEVER BE TRUE
  c.Header("Cross-Origin-Opener-Policy", "same-origin")
}

CORRECT APPROACH (Option A - Check X-Forwarded-Proto):

// Cross-Origin-Opener-Policy: Isolate browsing context
// Only set on HTTPS to avoid browser warnings on HTTP development
// Reference: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Opener-Policy
//
// IMPORTANT: Behind reverse proxy (Caddy), TLS is terminated at proxy level.
// Must check X-Forwarded-Proto header instead of c.Request.TLS
isHTTPS := c.GetHeader("X-Forwarded-Proto") == "https"

if isHTTPS {
  c.Header("Cross-Origin-Opener-Policy", "same-origin")
}

CORRECT APPROACH (Option B - Simpler for Dev/Prod Split):

// Cross-Origin-Opener-Policy: Isolate browsing context
// Skip in development mode to avoid browser warnings on HTTP
// In production, Caddy always uses HTTPS, so safe to set unconditionally
if !cfg.IsDevelopment {
  c.Header("Cross-Origin-Opener-Policy", "same-origin")
}

Recommended Implementation: Option B (simpler, avoids header dependency)

Rationale:

  • Development mode = always HTTP → skip COOP to avoid warnings
  • Production mode = always HTTPS (enforced by load balancer/Caddy) → always set COOP
  • Eliminates need to parse X-Forwarded-Proto header
  • Fails safe: if misconfigured, production gets COOP anyway

Phase 2: Verify Proxy Header Configuration

⚠️ CRITICAL: Ensure Caddy forwards X-Forwarded-Proto header

File: backend/internal/caddy/config.go (verify line ~1216)

Required Configuration:

// In reverse_proxy directive
reverseProxy := map[string]interface{}{
  "handler": "reverse_proxy",
  "upstreams": upstreams,
  "headers": map[string]interface{}{
    "request": map[string]interface{}{
      "set": map[string][]string{
        "X-Forwarded-Proto": ["{http.request.scheme}"],
        "X-Forwarded-Host":  ["{http.request.host}"],
        "X-Real-IP":         ["{http.request.remote.host}"],
      },
    },
  },
}

Verification Steps:

  1. Check that Caddy config includes X-Forwarded-Proto header
  2. Add integration test to verify header propagation
  3. Test both HTTP (development) and HTTPS (production) scenarios

Phase 3: Update Documentation

File: docs/security.md (create new section: "Production Deployment Considerations")

Add a section explaining:

  • Why COOP warning appears on HTTP development
  • That it's expected and safe to ignore in local dev
  • That COOP is enforced in production HTTPS
  • ⚠️ CRITICAL WARNING: All production endpoints MUST use HTTPS
  • Mixed HTTP/HTTPS content will break COOP and secure cookies
  • How to test with HTTPS locally (self-signed cert or mkcert)

Add Mixed Content Warning:

### ⚠️ Production HTTPS Requirements

**All production deployments MUST enforce HTTPS for the following reasons:**

1. **Security Headers:** COOP (`Cross-Origin-Opener-Policy`) should only be set over HTTPS
2. **Secure Cookies:** `auth_token` cookie uses `Secure` flag, requires HTTPS
3. **Mixed Content:** Mixing HTTP and HTTPS will cause browser warnings and broken functionality
4. **Load Balancer Configuration:** Ensure your load balancer/CDN:
   - Terminates TLS with valid certificates
   - Forwards `X-Forwarded-Proto: https` header to backend
   - Redirects HTTP → HTTPS (301 permanent redirect)

**Consequences of HTTP in Production:**

- COOP header triggers browser warnings
- Secure cookies are not sent by browser
- Authentication breaks (users can't login)
- WebSocket connections fail
- Password managers may not save credentials

**How to Verify:**

```bash
# Check that load balancer forwards X-Forwarded-Proto
curl -H "X-Forwarded-Proto: https" https://your-domain.com/api/v1/health

# Response headers should include:
# Cross-Origin-Opener-Policy: same-origin
# Strict-Transport-Security: max-age=31536000; includeSubDomains

### Tests to Update

**File:** `backend/internal/api/middleware/security_test.go`

**⚠️ CRITICAL: Old tests using `req.TLS` are INVALID for reverse proxy scenario**

Add these test cases:

```go
// Test development mode - COOP should NOT be set
func TestSecurityHeaders_COOP_DevelopmentMode(t *testing.T) {
  gin.SetMode(gin.TestMode)
  router := gin.New()
  cfg := SecurityHeadersConfig{IsDevelopment: true}
  router.Use(SecurityHeaders(cfg))
  router.GET("/test", func(c *gin.Context) {
    c.Status(http.StatusOK)
  })

  req := httptest.NewRequest("GET", "/test", nil)
  resp := httptest.NewRecorder()
  router.ServeHTTP(resp, req)

  // COOP should NOT be set in development mode
  assert.Empty(t, resp.Header().Get("Cross-Origin-Opener-Policy"),
    "COOP header should not be set in development mode")
}

// Test production mode - COOP SHOULD be set
func TestSecurityHeaders_COOP_ProductionMode(t *testing.T) {
  gin.SetMode(gin.TestMode)
  router := gin.New()
  cfg := SecurityHeadersConfig{IsDevelopment: false}
  router.Use(SecurityHeaders(cfg))
  router.GET("/test", func(c *gin.Context) {
    c.Status(http.StatusOK)
  })

  req := httptest.NewRequest("GET", "/test", nil)
  resp := httptest.NewRecorder()
  router.ServeHTTP(resp, req)

  // COOP SHOULD be set in production mode
  assert.Equal(t, "same-origin", resp.Header().Get("Cross-Origin-Opener-Policy"),
    "COOP header must be set in production mode")
}

// ALTERNATIVE: If implementing Option A (X-Forwarded-Proto check)
// Test HTTP via proxy - COOP should NOT be set
func TestSecurityHeaders_COOP_HTTPViaProxy(t *testing.T) {
  gin.SetMode(gin.TestMode)
  router := gin.New()
  cfg := SecurityHeadersConfig{IsDevelopment: false}
  router.Use(SecurityHeaders(cfg))
  router.GET("/test", func(c *gin.Context) {
    c.Status(http.StatusOK)
  })

  req := httptest.NewRequest("GET", "/test", nil)
  req.Header.Set("X-Forwarded-Proto", "http")
  resp := httptest.NewRecorder()
  router.ServeHTTP(resp, req)

  // COOP should NOT be set when X-Forwarded-Proto is http
  assert.Empty(t, resp.Header().Get("Cross-Origin-Opener-Policy"),
    "COOP should not be set for HTTP requests (even in production)")
}

// Test HTTPS via proxy - COOP SHOULD be set
func TestSecurityHeaders_COOP_HTTPSViaProxy(t *testing.T) {
  gin.SetMode(gin.TestMode)
  router := gin.New()
  cfg := SecurityHeadersConfig{IsDevelopment: false}
  router.Use(SecurityHeaders(cfg))
  router.GET("/test", func(c *gin.Context) {
    c.Status(http.StatusOK)
  })

  req := httptest.NewRequest("GET", "/test", nil)
  req.Header.Set("X-Forwarded-Proto", "https")
  resp := httptest.NewRecorder()
  router.ServeHTTP(resp, req)

  // COOP SHOULD be set when X-Forwarded-Proto is https
  assert.Equal(t, "same-origin", resp.Header().Get("Cross-Origin-Opener-Policy"),
    "COOP must be set for HTTPS requests")
}

Integration Test for Proxy Headers:

File: backend/integration/proxy_headers_test.go (new file)

package integration

import (
  "net/http"
  "testing"
  "github.com/stretchr/testify/assert"
  "github.com/stretchr/testify/require"
)

// TestProxyHeaderPropagation verifies that Caddy forwards X-Forwarded-Proto
func TestProxyHeaderPropagation(t *testing.T) {
  // This test requires the full stack (Caddy + Backend) to be running
  if testing.Short() {
    t.Skip("Skipping integration test in short mode")
  }

  tests := []struct {
    name           string
    requestScheme  string
    expectCOOP     bool
  }{
    {
      name:          "HTTP request should not have COOP",
      requestScheme: "http",
      expectCOOP:    false,
    },
    {
      name:          "HTTPS request should have COOP",
      requestScheme: "https",
      expectCOOP:    true,
    },
  }

  for _, tt := range tests {
    t.Run(tt.name, func(t *testing.T) {
      // Make request through Caddy proxy
      req, err := http.NewRequest("GET", "http://localhost:8080/api/v1/health", nil)
      require.NoError(t, err)

      // Simulate load balancer setting X-Forwarded-Proto
      req.Header.Set("X-Forwarded-Proto", tt.requestScheme)

      client := &http.Client{}
      resp, err := client.Do(req)
      require.NoError(t, err)
      defer resp.Body.Close()

      coopHeader := resp.Header.Get("Cross-Origin-Opener-Policy")

      if tt.expectCOOP {
        assert.Equal(t, "same-origin", coopHeader,
          "COOP header should be present for HTTPS")
      } else {
        assert.Empty(t, coopHeader,
          "COOP header should not be present for HTTP")
      }
    })
  }
}

Priority: MEDIUM (User-facing warning, but not breaking)


Issue 3: Missing Autocomplete Attribute on Password Input

Status: ACCESSIBILITY BUG (MUST FIX)

Root Cause Analysis

File: frontend/src/pages/Login.tsx (lines 93-100)

<Input
  label={t('auth.password')}
  type="password"
  value={password}
  onChange={e => setPassword(e.target.value)}
  required
  placeholder="••••••••"
  disabled={loading}
/>

Missing: autoComplete attribute

Why This Matters:

  1. Accessibility: Password managers rely on autocomplete to identify password fields
  2. User Experience: Browsers can't offer to save/fill passwords without proper attributes
  3. DOM Standards: HTML5 spec recommends autocomplete for all input fields
  4. Security: Modern password managers use autocomplete to prevent phishing

Component Implementation:

File: frontend/src/components/ui/Input.tsx (lines 1-95)

The Input component is a controlled component that forwards all props to the native <input> element:

export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
  // Custom props...
}

<input
  ref={ref}
  type={isPassword ? (showPassword ? 'text' : 'password') : type}
  disabled={disabled}
  className={...}
  {...props}  // Line 64 - All other props are spread here
/>

The component already supports autoComplete through the spread operator, it just needs to be passed from the parent.

Phase 1: Fix Login Page

File: frontend/src/pages/Login.tsx

Email Input (lines 84-91):

<Input
  label={t('auth.email')}
  type="email"
  value={email}
  onChange={e => setEmail(e.target.value)}
  required
  placeholder="admin@example.com"
  disabled={loading}
  autoComplete="email"  // ADD THIS
/>

Password Input (lines 93-100):

<Input
  label={t('auth.password')}
  type="password"
  value={password}
  onChange={e => setPassword(e.target.value)}
  required
  placeholder="••••••••"
  disabled={loading}
  autoComplete="current-password"  // ADD THIS
/>

Phase 2: Fix Setup Page

File: frontend/src/pages/Setup.tsx

Email Input (lines 121-129):

<Input
  id="email"
  name="email"
  label={t('setup.emailLabel')}
  type="email"
  required
  placeholder={t('setup.emailPlaceholder')}
  value={formData.email}
  onChange={(e) => setFormData({ ...formData, email: e.target.value })}
  className={...}
  autoComplete="email"  // ADD THIS
/>

Password Input (lines 135-142):

<Input
  id="password"
  name="password"
  label={t('setup.passwordLabel')}
  type="password"
  required
  placeholder="••••••••"
  value={formData.password}
  onChange={(e) => setFormData({ ...formData, password: e.target.value })}
  autoComplete="new-password"  // ADD THIS - "new-password" for registration forms
/>

Phase 3: Fix Account Page (Password Change)

File: frontend/src/pages/Account.tsx

Current Password (lines 376-381):

<Input
  id="current-password"
  type="password"
  value={oldPassword}
  onChange={(e) => setOldPassword(e.target.value)}
  required
  autoComplete="current-password"  // ADD THIS
/>

New Password (lines 386-391):

<Input
  id="new-password"
  type="password"
  value={newPassword}
  onChange={(e) => setNewPassword(e.target.value)}
  required
  autoComplete="new-password"  // ADD THIS
/>

Confirm Password (lines 398-403):

<Input
  id="confirm-password"
  type="password"
  value={confirmPassword}
  onChange={(e) => setConfirmPassword(e.target.value)}
  required
  error={...}
  autoComplete="new-password"  // ADD THIS
/>

Phase 4: Fix SMTP Settings Page

File: frontend/src/pages/SMTPSettings.tsx

SMTP Username (lines 172-178):

<Input
  id="smtp-username"
  type="text"
  value={username}
  onChange={(e) => setUsername(e.target.value)}
  placeholder="your@email.com"
  autoComplete="username"  // ADD THIS
/>

SMTP Password (lines 182-188):

<Input
  id="smtp-password"
  type="password"
  value={password}
  onChange={(e) => setPassword(e.target.value)}
  placeholder="••••••••"
  helperText={t('smtp.passwordHelper')}
  autoComplete="current-password"  // ADD THIS
/>

Phase 5: Fix Accept Invite Page

File: frontend/src/pages/AcceptInvite.tsx

Password (lines 169-175):

<Input
  label={t('auth.password')}
  type="password"
  value={password}
  onChange={(e) => setPassword(e.target.value)}
  placeholder="••••••••"
  required
  autoComplete="new-password"  // ADD THIS - new account being created
/>

Confirm Password (lines 178-190):

<Input
  label={t('acceptInvite.confirmPassword')}
  type="password"
  value={confirmPassword}
  onChange={(e) => setConfirmPassword(e.target.value)}
  placeholder="••••••••"
  required
  error={...}
  autoComplete="new-password"  // ADD THIS
/>

Autocomplete Values Reference

According to HTML5 spec (https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#autofill):

  • email - Email address
  • username - Username or account name
  • current-password - Current password (for login)
  • new-password - New password (for registration or password change)

Tests to Add/Update

File: frontend/src/pages/__tests__/Login.test.tsx

it('has proper autocomplete attributes for password managers', () => {
  renderWithProviders(<Login />)

  const emailInput = screen.getByPlaceholderText(/admin@example.com/i)
  const passwordInput = screen.getByPlaceholderText(/••••••••/i)

  expect(emailInput).toHaveAttribute('autocomplete', 'email')
  expect(passwordInput).toHaveAttribute('autocomplete', 'current-password')
})

Autocomplete Security Considerations

⚠️ NOTE: Some regulated industries may require disabling autocomplete

OWASP/NIST Recommendation: Modern security guidelines recommend AGAINST disabling autocomplete:

  • Password managers improve security by enabling stronger, unique passwords
  • Users reuse weak passwords when managers are blocked
  • Disabling autocomplete reduces security, not improves it

References:

If compliance requires disabling autocomplete:

Implement as opt-in environment variable (not default):

# .env
DISABLE_PASSWORD_AUTOCOMPLETE=true  # Only for specific compliance requirements

Implementation:

// frontend/src/pages/Login.tsx
const disableAutocomplete = import.meta.env.VITE_DISABLE_PASSWORD_AUTOCOMPLETE === 'true'

<Input
  autoComplete={disableAutocomplete ? "off" : "current-password"}
  // ... other props
/>

Default Behavior: Autocomplete ENABLED (best practice)

Priority: HIGH (Accessibility and UX impact)


Configuration File Review

.gitignore

File: /projects/Charon/.gitignore

Current State: Well-structured and comprehensive

Recommended Changes: None - the file properly excludes:

  • Coverage artifacts (*.cover, *.html, coverage/)
  • Test outputs (test-results/, *.sarif)
  • Docker overrides (docker-compose.override.yml)
  • Temporary files at root (/caddy_*.json, /trivy-*.txt)

Verification:

  • Excludes test artifacts
  • Excludes build outputs
  • Excludes sensitive files (.env)
  • Excludes CodeQL/security scan results

codecov.yml

Status: File does not exist in repository

Finding: file_search and read_file both confirm no codecov.yml exists

Recommendation:

  • If using Codecov for coverage reporting, create a codecov.yml at root
  • If not using Codecov, no action needed
  • Current CI/CD workflows may use inline coverage settings

Suggested Content (if needed):

# codecov.yml - Code coverage configuration
coverage:
  status:
    project:
      default:
        target: 85%
        threshold: 1%
    patch:
      default:
        target: 80%
        threshold: 1%

comment:
  layout: "reach,diff,flags,files"
  behavior: default
  require_changes: false

ignore:
  - "**/__tests__/**"
  - "**/*.test.ts"
  - "**/*.test.tsx"
  - "**/test-*.ts"

Priority: LOW (Only if using Codecov service)

.dockerignore

File: /projects/Charon/.dockerignore

Current State: Well-maintained and comprehensive

Recommended Changes: None - the file properly excludes:

  • Build artifacts and coverage files
  • Test directories
  • Node modules
  • Git and CI/CD directories
  • Documentation (except key files)
  • CodeQL and security scan results

Verification:

  • Reduces Docker build context size
  • Excludes test artifacts
  • Keeps README and LICENSE
  • Excludes sensitive files

Dockerfile

File: /projects/Charon/Dockerfile

Current State: Multi-stage build with security best practices

Recommended Changes: None - the Dockerfile already implements:

  • Multi-stage builds (frontend, backend, Caddy, CrowdSec builders)
  • Non-root user (charon:charon with UID/GID 1000)
  • Healthcheck endpoint
  • Security labels (OCI image spec)
  • Minimal runtime dependencies
  • Recent base images (Alpine 3.23, Go 1.25, Node 24.12)

Security Verification:

  • Runs as non-root user (line 366: USER charon)
  • Includes security scanning (Trivy in CI/CD)
  • Uses HEALTHCHECK for monitoring
  • Proper volume permissions handled in entrypoint

Implementation Plan

Phase 1: Quick Wins (1-2 hours)

Priority: HIGH

  1. Add autocomplete attributes to all password/email inputs

    • Files: Login.tsx, Setup.tsx, Account.tsx, AcceptInvite.tsx, SMTPSettings.tsx
    • Impact: Immediate UX and accessibility improvement
    • Testing: Manual test with browser password manager
    • Add unit tests for autocomplete attributes
  2. Write tests for autocomplete attributes

    • File: frontend/src/pages/__tests__/Login.test.tsx
    • Verify email and password inputs have correct attributes

Phase 2: Documentation (1 hour)

Priority: MEDIUM

  1. Document COOP warning in development

    • Create or update: docs/getting-started.md or docs/security.md
    • Explain why the warning appears on HTTP
    • Provide context about COOP security benefits
    • Optional: Add instructions for local HTTPS testing
  2. Document auth flow in README or architecture docs

    • Explain that 401 on /auth/me during login is expected
    • Describe the three-tier authentication (header > cookie > query param)

Phase 3: Optional Enhancements (2-3 hours)

Priority: LOW

  1. Optimize AuthContext to skip /auth/me if no token

    • File: frontend/src/context/AuthContext.tsx
    • Reduces unnecessary 401 errors in console
    • Slightly faster initial load
  2. Make COOP header conditional on HTTPS

    • File: backend/internal/api/middleware/security.go
    • Add logic to skip COOP on HTTP development
    • Update tests to verify conditional behavior
    • Testing: Verify COOP is present on HTTPS, absent on HTTP dev

Testing Strategy

Unit Tests

Frontend:

  • frontend/src/pages/__tests__/Login.test.tsx - Add autocomplete verification
  • frontend/src/pages/__tests__/Setup.test.tsx - Verify autocomplete on setup form
  • frontend/src/components/ui/__tests__/Input.test.tsx - Verify autocomplete prop forwarding

Backend:

  • backend/internal/api/middleware/security_test.go - Add COOP conditional tests
  • backend/internal/api/middleware/auth_test.go - Verify existing auth flow (already comprehensive)

Integration Tests

  1. Manual Testing:

    • Navigate to login page without existing session
    • Verify browser DevTools shows autocomplete attributes
    • Test password manager save/fill functionality
    • Check browser console for COOP warning (should exist on HTTP, not on HTTPS)
  2. E2E Testing (if Playwright is set up):

    • Test login flow with password manager
    • Verify autocomplete suggestions appear

Acceptance Criteria

Issue 1 (401 Error):

  • Documented as expected behavior
  • Optional: AuthContext skips API call if no token

Issue 2 (COOP Warning):

  • COOP header conditional on HTTPS (or documented as expected)
  • Tests verify conditional behavior
  • Documentation explains the warning

Issue 3 (Autocomplete):

  • All password fields have autoComplete="current-password" or "new-password"
  • All email fields have autoComplete="email"
  • All username fields have autoComplete="username"
  • Tests verify autocomplete attributes
  • Password managers can save and fill credentials

Risk Assessment

Low Risk

  • Adding autocomplete attributes (native HTML feature)
  • Documentation updates

Medium Risk

  • Making COOP conditional (requires testing on multiple browsers)
  • Modifying AuthContext initialization (could affect auth flow)

Mitigation

  • Comprehensive unit and integration tests
  • Manual testing on Chrome, Firefox, Safari
  • Rollback plan: revert commits if issues arise

Files Requiring Changes

Frontend Files (High Priority)

  1. frontend/src/pages/Login.tsx - Add autocomplete to email/password inputs
  2. frontend/src/pages/Setup.tsx - Add autocomplete to registration form
  3. frontend/src/pages/Account.tsx - Add autocomplete to password change form
  4. frontend/src/pages/AcceptInvite.tsx - Add autocomplete to invite acceptance form
  5. frontend/src/pages/SMTPSettings.tsx - Add autocomplete to SMTP credentials
  6. frontend/src/pages/__tests__/Login.test.tsx - Add autocomplete attribute tests
  7. frontend/src/context/AuthContext.tsx - (Optional) Optimize checkAuth

Backend Files (Medium Priority)

  1. backend/internal/api/middleware/security.go - Make COOP conditional
  2. backend/internal/api/middleware/security_test.go - Add COOP conditional tests

Documentation Files (Medium Priority)

  1. docs/getting-started.md or docs/security.md - Document COOP warning
  2. README.md - (Optional) Add auth flow documentation

Configuration Files

  1. .gitignore - No changes needed
  2. .dockerignore - No changes needed
  3. Dockerfile - No changes needed
  4. codecov.yml - Does not exist (create only if using Codecov)

Summary

Issue Status Priority Effort Risk
401 on /auth/me Expected Behavior LOW 1h (optional optimization) Low
COOP Header Warning Expected on HTTP MEDIUM 2h Medium
Missing Autocomplete Bug HIGH 2h Low

Total Estimated Effort: 6-8 hours (includes proxy verification, testing, and documentation)

Time Breakdown:

  • Autocomplete fixes: 2 hours
  • COOP implementation fix: 2 hours
  • Proxy header verification: 1 hour
  • Integration tests: 2 hours
  • Documentation: 1-2 hours

Recommended Order:

  1. Fix autocomplete attributes (HIGH priority, LOW risk, immediate user benefit)
  2. Document COOP warning (MEDIUM priority, no code changes)
  3. Optionally make COOP conditional (MEDIUM priority, requires testing)
  4. Optionally optimize AuthContext (LOW priority, minor improvement)

Authentication Flow Components

  • frontend/src/context/AuthContext.tsx - Main auth state management
  • frontend/src/context/AuthContextValue.ts - Auth context type definitions
  • frontend/src/hooks/useAuth.ts - Auth hook for components
  • frontend/src/api/client.ts - Axios client with auth interceptor
  • frontend/src/components/RequireAuth.tsx - Route guard component
  • backend/internal/api/middleware/auth.go - Auth middleware
  • backend/internal/api/handlers/auth_handler.go - Auth endpoints
  • backend/internal/services/auth_service.go - Auth business logic

Security Headers Components

  • backend/internal/api/middleware/security.go - Security headers middleware
  • backend/internal/api/middleware/security_test.go - Security middleware tests
  • backend/internal/caddy/config.go - Caddy security header config (line 1216)

Form Components

  • frontend/src/components/ui/Input.tsx - Reusable input component
  • frontend/src/components/PasswordStrengthMeter.tsx - Password validation UI

Appendix B: Browser Compatibility

Autocomplete Attribute Support

Browser Version Support
Chrome 14+ Full support
Firefox 4+ Full support
Safari 6+ Full support
Edge 12+ Full support

COOP Header Support

Browser Version Support
Chrome 83+ Full support
Firefox 79+ Full support
Safari 15.2+ Full support
Edge 83+ Full support

Note: All modern browsers support both features. Legacy browser users (IE11 and below) will safely ignore these attributes/headers.



Appendix D: Reverse Proxy Architecture Considerations

Why c.Request.TLS Doesn't Work Behind a Reverse Proxy

Architecture Overview:

[Client Browser] --HTTPS--> [Load Balancer/Caddy] --HTTP--> [Backend (Go/Gin)]
                              ↑                              ↑
                         TLS terminates here          c.Request.TLS == nil

Key Points:

  1. TLS Termination: Load balancers and reverse proxies (Caddy, nginx, HAProxy) terminate TLS connections
  2. Backend Protocol: Communication between proxy and backend is typically plain HTTP over private network
  3. Request Object: Go's http.Request.TLS field is only populated for direct TLS connections
  4. Detection Method: Backend must rely on headers set by the proxy (X-Forwarded-Proto, X-Forwarded-For)

Common Pitfalls:

// ❌ WRONG: Will always be false behind reverse proxy
if c.Request.TLS != nil {
  // This code is never reached!
}

// ✅ CORRECT: Check forwarded protocol header
if c.GetHeader("X-Forwarded-Proto") == "https" {
  // This works correctly
}

// ✅ ALSO CORRECT: Trust deployment configuration
if !cfg.IsDevelopment {
  // Production = HTTPS enforced at load balancer
}

Security Implications:

  1. Trust Boundary: Backend must trust headers set by reverse proxy
  2. Header Spoofing: If backend is directly exposed (bypassing proxy), malicious clients could set X-Forwarded-Proto: https
  3. Mitigation: Ensure backend only accepts connections from trusted proxy (firewall rules, network policies)

Testing Considerations:

  • Unit tests cannot test c.Request.TLS behavior in proxy scenarios
  • Integration tests must include full proxy stack
  • Mock X-Forwarded-Proto header in tests to simulate proxy behavior

Production Deployment Checklist:

  • Verify load balancer/CDN forwards X-Forwarded-Proto header
  • Confirm backend firewall blocks direct public access
  • Test HTTPS redirect (HTTP → HTTPS 301)
  • Verify Strict-Transport-Security header is set
  • Check that secure cookies (Secure flag) work correctly
  • Validate COOP header is present on HTTPS responses
  • Test WebSocket connections over HTTPS

End of Plan