9830ac29a5
Behebt das Pentest-Finding „JWT in localStorage (MITTEL)": bei XSS war
der Token JS-erreichbar → Angreifer könnte alle Anbieter-Credentials
abrufen. Branchenstandard-Lösung für SPAs jetzt umgesetzt.
Architektur:
- Access-Token: 15 min Lifetime, lebt NUR im JavaScript-Memory
(api.ts tokenStore + AuthContext). Kein localStorage mehr.
- Refresh-Token: 7 Tage, im httpOnly-Cookie (Secure bei HTTPS_ENABLED,
SameSite=Strict, Path=/api/auth). JavaScript hat keinen Zugriff →
XSS klaut max. einen 15-min-Access-Token.
Backend:
- signAccessToken/signRefreshToken mit `type`-Claim
- Auth-Middleware verweigert Tokens mit type=refresh
- POST /api/auth/login + /customer-login: setzt refresh_token-Cookie,
gibt access-Token im Body
- POST /api/auth/refresh: liest Cookie, rotiert ihn, gibt neuen Access
aus. Prüft tokenInvalidatedAt (Logout/Rollenänderung = sofortige
Invalidation auch des Refresh-Tokens)
- POST /api/auth/logout: löscht Cookie + setzt tokenInvalidatedAt
- cookie-parser als neue Dependency
Frontend:
- api.ts: in-memory tokenStore (kein localStorage); withCredentials=true
für Cookie-Roundtrip; axios-Response-Interceptor mit
Single-Flight-Refresh-Retry bei 401 (Original-Request wird
transparent retried mit neuem Token)
- AuthContext: beim App-Start /auth/refresh aufrufen → wenn Cookie
noch gültig, ist der User automatisch eingeloggt. Tab-Reload
funktioniert weiterhin obwohl Access-Token nur in memory ist.
- 9 alte `localStorage.getItem('token')`-Stellen migriert auf
`getAccessToken()` (PDF-Preview-iframe, Audit-Log-CSV-Export,
DB-Backup-Download, File-Download-URLs, Portal-PDF-Link)
Live verifiziert:
- Login setzt Cookie (httpOnly, SameSite=Strict, Path=/api/auth) + Bearer
- API mit Bearer: 200; ohne: 401
- Refresh mit Cookie: rotiert sauber + neuer Access-Token im Body
- Refresh-Token als Bearer abgewiesen: 401 ("Falscher Token-Typ")
- Logout: Cookie gelöscht, danach /refresh → 401
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
64 lines
1.8 KiB
JSON
64 lines
1.8 KiB
JSON
{
|
|
"name": "opencrm-backend",
|
|
"version": "1.1.0",
|
|
"description": "OpenCRM Backend API",
|
|
"main": "dist/index.js",
|
|
"prisma": {
|
|
"seed": "npx tsx prisma/seed.ts"
|
|
},
|
|
"scripts": {
|
|
"dev": "tsx watch src/index.ts",
|
|
"build": "tsc",
|
|
"start": "node dist/index.js",
|
|
"db:migrate": "prisma migrate dev",
|
|
"db:push": "prisma db push",
|
|
"schema:sync": "prisma migrate dev --name auto_$(date +%Y%m%d_%H%M%S)",
|
|
"db:seed": "tsx prisma/seed.ts",
|
|
"db:studio": "prisma studio",
|
|
"db:backup": "tsx prisma/backup-data.ts",
|
|
"db:restore": "tsx prisma/restore-data.ts",
|
|
"seed:defaults": "tsx scripts/seed-factory-defaults.ts"
|
|
},
|
|
"dependencies": {
|
|
"@prisma/client": "^5.22.0",
|
|
"@types/cookie-parser": "^1.4.10",
|
|
"adm-zip": "^0.5.16",
|
|
"archiver": "^7.0.1",
|
|
"bcryptjs": "^2.4.3",
|
|
"cookie-parser": "^1.4.7",
|
|
"cors": "^2.8.5",
|
|
"dotenv": "^16.4.5",
|
|
"dotenv-expand": "^13.0.0",
|
|
"express": "^4.21.1",
|
|
"express-rate-limit": "^8.4.0",
|
|
"express-validator": "^7.2.0",
|
|
"helmet": "^8.1.0",
|
|
"imapflow": "^1.2.8",
|
|
"jsonwebtoken": "^9.0.2",
|
|
"mailparser": "^3.9.3",
|
|
"multer": "^1.4.5-lts.1",
|
|
"node-cron": "^4.2.1",
|
|
"nodemailer": "^7.0.13",
|
|
"pdf-lib": "^1.17.1",
|
|
"pdfkit": "^0.17.2",
|
|
"tsx": "^4.19.2",
|
|
"undici": "^6.23.0"
|
|
},
|
|
"devDependencies": {
|
|
"@types/adm-zip": "^0.5.7",
|
|
"@types/archiver": "^7.0.0",
|
|
"@types/bcryptjs": "^2.4.6",
|
|
"@types/cors": "^2.8.17",
|
|
"@types/express": "^4.17.25",
|
|
"@types/jsonwebtoken": "^9.0.7",
|
|
"@types/mailparser": "^3.4.6",
|
|
"@types/multer": "^1.4.12",
|
|
"@types/node": "^22.9.0",
|
|
"@types/node-cron": "^3.0.11",
|
|
"@types/nodemailer": "^7.0.9",
|
|
"@types/pdfkit": "^0.17.4",
|
|
"prisma": "^5.22.0",
|
|
"typescript": "^5.6.3"
|
|
}
|
|
}
|