Files
zclaw_openfang/admin-v2/src/router/AuthGuard.tsx
iven e3b93ff96d fix(security): implement all 15 security fixes from penetration test V1
Security audit (2026-03-31): 5 HIGH + 10 MEDIUM issues, all fixed.

HIGH:
- H1: JWT password_version mechanism (pwv in Claims, middleware verification,
  auto-increment on password change)
- H2: Docker saas port bound to 127.0.0.1
- H3: TOTP encryption key decoupled from JWT secret (production bailout)
- H4+H5: Tauri CSP hardened (removed unsafe-inline, restricted connect-src)

MEDIUM:
- M1: Persistent rate limiting (PostgreSQL rate_limit_events table)
- M2: Account lockout (5 failures -> 15min lock)
- M3: RFC 5322 email validation with regex
- M4: Device registration typed struct with length limits
- M5: Provider URL validation on create/update (SSRF prevention)
- M6: Legacy TOTP secret migration (fixed nonce -> random nonce)
- M7: Legacy frontend crypto migration (static salt -> random salt)
- M8+M9: Admin frontend: removed JS token storage, HttpOnly cookie only
- M10: Pipeline debug log sanitization (keys only, 100-char truncation)

Also: fixed CLAUDE.md Section 12 (was corrupted), added title.rs middleware
skeleton, fixed RegisterDeviceRequest visibility.
2026-04-01 08:38:37 +08:00

64 lines
2.1 KiB
TypeScript

// ============================================================
// ZCLAW Admin V2 — Auth Guard with session restore
// ============================================================
//
// Auth strategy:
// 1. If Zustand has isAuthenticated=true (normal flow after login) -> authenticated
// 2. If isAuthenticated=false but account in localStorage -> call GET /auth/me
// to validate HttpOnly cookie and restore session
// 3. If cookie invalid -> clean up and redirect to /login
import { useEffect, useRef, useState } from 'react'
import { Navigate, useLocation } from 'react-router-dom'
import { Spin } from 'antd'
import { useAuthStore } from '@/stores/authStore'
import { authService } from '@/services/auth'
export function AuthGuard({ children }: { children: React.ReactNode }) {
const isAuthenticated = useAuthStore((s) => s.isAuthenticated)
const account = useAuthStore((s) => s.account)
const login = useAuthStore((s) => s.login)
const logout = useAuthStore((s) => s.logout)
const location = useLocation()
// Track restore attempt to avoid double-calling
const restoreAttempted = useRef(false)
const [restoring, setRestoring] = useState(false)
useEffect(() => {
if (restoreAttempted.current) return
restoreAttempted.current = true
// If not authenticated but account exists in localStorage,
// try to validate the HttpOnly cookie via /auth/me
if (!isAuthenticated && account) {
setRestoring(true)
authService.me()
.then((meAccount) => {
// Cookie is valid — restore session
login(meAccount)
setRestoring(false)
})
.catch(() => {
// Cookie expired or invalid — clean up stale data
logout()
setRestoring(false)
})
}
}, []) // eslint-disable-line react-hooks/exhaustive-deps
if (restoring) {
return (
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100vh' }}>
<Spin size="large" />
</div>
)
}
if (!isAuthenticated) {
return <Navigate to="/login" state={{ from: location }} replace />
}
return <>{children}</>
}