fix(auth): JWT-Secret härten und Session-Laufzeit verkürzen
- Gemeinsames Secret-Modul (jwt-secret.ts): bricht in Produktion hart ab, wenn NEXTAUTH_SECRET fehlt oder <32 Zeichen – kein öffentlicher Fallback mehr - auth.ts und middleware.ts nutzen dieselbe Secret-Quelle (kein Divergieren) - Schwacher Compose-Fallback entfernt: Deployment ohne Secret bricht ab (:?) - "Angemeldet bleiben" 30 → 14 Tage; Laufzeiten zentralisiert (SESSION_MAX_AGE_*) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -90,7 +90,7 @@ services:
|
||||
environment:
|
||||
DATABASE_URL: postgresql://${POSTGRES_USER:-lageplan}:${POSTGRES_PASSWORD:-lageplan_secret}@db:5432/${POSTGRES_DB:-lageplan}
|
||||
NEXTAUTH_URL: ${NEXTAUTH_URL:-https://localhost:3000}
|
||||
NEXTAUTH_SECRET: ${NEXTAUTH_SECRET:-super-secret-key-change-in-production}
|
||||
NEXTAUTH_SECRET: ${NEXTAUTH_SECRET:?NEXTAUTH_SECRET muss gesetzt sein (mind. 32 Zeichen) - z.B. via stack.env}
|
||||
MINIO_ENDPOINT: minio
|
||||
MINIO_PORT: "9000"
|
||||
MINIO_ACCESS_KEY: ${MINIO_ROOT_USER:-minioadmin}
|
||||
|
||||
@@ -66,7 +66,7 @@ services:
|
||||
environment:
|
||||
DATABASE_URL: postgresql://${POSTGRES_USER:-lageplan}:${POSTGRES_PASSWORD:-lageplan_secret}@db:5432/${POSTGRES_DB:-lageplan}
|
||||
NEXTAUTH_URL: ${NEXTAUTH_URL:-http://localhost:3000}
|
||||
NEXTAUTH_SECRET: ${NEXTAUTH_SECRET:-super-secret-key-change-in-production}
|
||||
NEXTAUTH_SECRET: ${NEXTAUTH_SECRET:?NEXTAUTH_SECRET muss gesetzt sein (mind. 32 Zeichen) - z.B. via .env oder stack.env}
|
||||
MINIO_ENDPOINT: minio
|
||||
MINIO_PORT: 9000
|
||||
MINIO_ACCESS_KEY: ${MINIO_ROOT_USER:-minioadmin}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { cookies } from 'next/headers'
|
||||
import { login, createToken } from '@/lib/auth'
|
||||
import { login, createToken, SESSION_MAX_AGE_SECONDS, SESSION_MAX_AGE_REMEMBER_SECONDS } from '@/lib/auth'
|
||||
import { loginSchema } from '@/lib/validations'
|
||||
import { prisma } from '@/lib/db'
|
||||
import { loginLimiter, getClientIp, rateLimitResponse } from '@/lib/rate-limit'
|
||||
@@ -50,7 +50,7 @@ export async function POST(request: NextRequest) {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'lax',
|
||||
maxAge: rememberMe ? 60 * 60 * 24 * 30 : 60 * 60 * 24, // 30 days or 24 hours
|
||||
maxAge: rememberMe ? SESSION_MAX_AGE_REMEMBER_SECONDS : SESSION_MAX_AGE_SECONDS, // 14 Tage oder 24 Stunden
|
||||
path: '/',
|
||||
})
|
||||
|
||||
|
||||
@@ -2,14 +2,7 @@ import { SignJWT, jwtVerify } from 'jose'
|
||||
import { cookies } from 'next/headers'
|
||||
import { prisma } from './db'
|
||||
import bcrypt from 'bcryptjs'
|
||||
|
||||
const secretValue = process.env.NEXTAUTH_SECRET
|
||||
if (!secretValue || secretValue.length < 32) {
|
||||
console.warn('[AUTH] WARNING: NEXTAUTH_SECRET is missing or too short (<32 chars). Set a strong secret in production!')
|
||||
}
|
||||
const JWT_SECRET = new TextEncoder().encode(
|
||||
secretValue || 'dev-only-fallback-do-not-use-in-production-' + Date.now()
|
||||
)
|
||||
import { JWT_SECRET } from './jwt-secret'
|
||||
|
||||
export interface UserPayload {
|
||||
id: string
|
||||
@@ -21,11 +14,17 @@ export interface UserPayload {
|
||||
emailVerified?: boolean
|
||||
}
|
||||
|
||||
// Zentrale Session-Laufzeiten (Sekunden) – EINE Quelle für Token-Ablauf UND Cookie-maxAge,
|
||||
// damit beide nie auseinanderdriften.
|
||||
export const SESSION_MAX_AGE_SECONDS = 60 * 60 * 24 // 24 Stunden (Standard)
|
||||
export const SESSION_MAX_AGE_REMEMBER_SECONDS = 60 * 60 * 24 * 14 // 14 Tage ("angemeldet bleiben")
|
||||
|
||||
export async function createToken(user: UserPayload, rememberMe = false): Promise<string> {
|
||||
const maxAge = rememberMe ? SESSION_MAX_AGE_REMEMBER_SECONDS : SESSION_MAX_AGE_SECONDS
|
||||
return await new SignJWT({ user })
|
||||
.setProtectedHeader({ alg: 'HS256' })
|
||||
.setIssuedAt()
|
||||
.setExpirationTime(rememberMe ? '30d' : '24h')
|
||||
.setExpirationTime(`${maxAge}s`)
|
||||
.sign(JWT_SECRET)
|
||||
}
|
||||
|
||||
|
||||
34
src/lib/jwt-secret.ts
Normal file
34
src/lib/jwt-secret.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Zentrale, Edge-sichere Quelle für das JWT-Signaturgeheimnis.
|
||||
*
|
||||
* Wird sowohl von der API (src/lib/auth.ts, Node-Runtime) als auch von der
|
||||
* Middleware (Edge-Runtime) importiert. Deshalb hier KEINE Node-/Prisma-Imports.
|
||||
*
|
||||
* Sicherheit: In Produktion wird hart abgebrochen, wenn kein starkes Geheimnis
|
||||
* gesetzt ist – so kann niemals still mit einem öffentlich bekannten Fallback
|
||||
* signiert werden (Token-Fälschung).
|
||||
*/
|
||||
|
||||
const secretValue = process.env.NEXTAUTH_SECRET
|
||||
|
||||
// Fester Dev-Fallback (>= 32 Zeichen), damit API und Middleware in der lokalen
|
||||
// Entwicklung ohne gesetztes Secret DIESELBEN Tokens verifizieren können.
|
||||
const DEV_FALLBACK = 'dev-only-fallback-do-not-use-in-production-change-me'
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
if (!secretValue || secretValue.length < 32) {
|
||||
throw new Error(
|
||||
'[AUTH] NEXTAUTH_SECRET fehlt oder ist kürzer als 32 Zeichen. ' +
|
||||
'In Produktion ist ein starkes Geheimnis zwingend erforderlich.'
|
||||
)
|
||||
}
|
||||
} else if (!secretValue || secretValue.length < 32) {
|
||||
console.warn(
|
||||
'[AUTH] WARNUNG: NEXTAUTH_SECRET fehlt/zu kurz – nutze unsicheren Dev-Fallback. ' +
|
||||
'Für Produktion ein starkes Geheimnis (>= 32 Zeichen) setzen!'
|
||||
)
|
||||
}
|
||||
|
||||
export const JWT_SECRET = new TextEncoder().encode(
|
||||
secretValue && secretValue.length >= 32 ? secretValue : DEV_FALLBACK
|
||||
)
|
||||
@@ -1,9 +1,6 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { jwtVerify } from 'jose'
|
||||
|
||||
const JWT_SECRET = new TextEncoder().encode(
|
||||
process.env.NEXTAUTH_SECRET || 'dev-only-fallback-do-not-use-in-production'
|
||||
)
|
||||
import { JWT_SECRET } from './lib/jwt-secret'
|
||||
|
||||
// Routes that require authentication
|
||||
const PROTECTED_ROUTES = ['/app', '/settings', '/admin']
|
||||
|
||||
Reference in New Issue
Block a user