feat: Add cookie-based JWT authentication for user containers - secure access control
This commit is contained in:
parent
45bd329e13
commit
436b1c0b0e
57
api.py
57
api.py
|
|
@ -1,4 +1,4 @@
|
|||
from flask import Blueprint, jsonify, request, current_app, redirect
|
||||
from flask import Blueprint, jsonify, request, current_app, redirect, make_response
|
||||
from flask_jwt_extended import (
|
||||
create_access_token,
|
||||
jwt_required,
|
||||
|
|
@ -23,6 +23,34 @@ api_bp = Blueprint('api', __name__, url_prefix='/api')
|
|||
token_blacklist = set()
|
||||
|
||||
|
||||
def create_auth_response(access_token, user_data, expires_in):
|
||||
"""Erstellt eine JSON-Response mit JWT-Token als HttpOnly Cookie"""
|
||||
response_data = {
|
||||
'access_token': access_token,
|
||||
'token_type': 'Bearer',
|
||||
'expires_in': expires_in,
|
||||
'user': user_data
|
||||
}
|
||||
|
||||
response = make_response(jsonify(response_data))
|
||||
|
||||
# Setze JWT als HttpOnly Cookie
|
||||
# HttpOnly verhindert JavaScript-Zugriff
|
||||
# Secure: nur über HTTPS
|
||||
# SameSite: CSRF-Schutz
|
||||
response.set_cookie(
|
||||
'spawner_token',
|
||||
access_token,
|
||||
max_age=expires_in,
|
||||
httponly=True,
|
||||
secure=True, # Nur über HTTPS
|
||||
samesite='Lax', # CSRF-Schutz
|
||||
path='/' # Für alle Pfade verfügbar
|
||||
)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
@api_bp.route('/auth/login', methods=['POST'])
|
||||
def api_login():
|
||||
"""API-Login mit Magic Link (Passwordless)"""
|
||||
|
|
@ -252,11 +280,7 @@ def api_verify_signup():
|
|||
|
||||
current_app.logger.info(f"[SIGNUP] User {user.email} erfolgreich registriert")
|
||||
|
||||
return jsonify({
|
||||
'access_token': access_token,
|
||||
'token_type': 'Bearer',
|
||||
'expires_in': int(expires.total_seconds()),
|
||||
'user': {
|
||||
user_data = {
|
||||
'id': user.id,
|
||||
'email': user.email,
|
||||
'slug': user.slug,
|
||||
|
|
@ -264,7 +288,8 @@ def api_verify_signup():
|
|||
'state': user.state,
|
||||
'container_id': user.container_id
|
||||
}
|
||||
}), 200
|
||||
|
||||
return create_auth_response(access_token, user_data, int(expires.total_seconds())), 200
|
||||
|
||||
|
||||
@api_bp.route('/auth/verify-login', methods=['GET'])
|
||||
|
|
@ -351,11 +376,7 @@ def api_verify_login():
|
|||
|
||||
current_app.logger.info(f"[LOGIN] User {user.email} erfolgreich eingeloggt")
|
||||
|
||||
return jsonify({
|
||||
'access_token': access_token,
|
||||
'token_type': 'Bearer',
|
||||
'expires_in': int(expires.total_seconds()),
|
||||
'user': {
|
||||
user_data = {
|
||||
'id': user.id,
|
||||
'email': user.email,
|
||||
'slug': user.slug,
|
||||
|
|
@ -363,16 +384,22 @@ def api_verify_login():
|
|||
'state': user.state,
|
||||
'container_id': user.container_id
|
||||
}
|
||||
}), 200
|
||||
|
||||
return create_auth_response(access_token, user_data, int(expires.total_seconds())), 200
|
||||
|
||||
|
||||
@api_bp.route('/auth/logout', methods=['POST'])
|
||||
@jwt_required()
|
||||
def api_logout():
|
||||
"""API-Logout - invalidiert Token"""
|
||||
"""API-Logout - invalidiert Token und löscht Cookie"""
|
||||
jti = get_jwt()['jti']
|
||||
token_blacklist.add(jti)
|
||||
return jsonify({'message': 'Erfolgreich abgemeldet'}), 200
|
||||
|
||||
# Erstelle Response und lösche Cookie
|
||||
response = make_response(jsonify({'message': 'Erfolgreich abgemeldet'}))
|
||||
response.delete_cookie('spawner_token', path='/')
|
||||
|
||||
return response, 200
|
||||
|
||||
|
||||
@api_bp.route('/user/me', methods=['GET'])
|
||||
|
|
|
|||
|
|
@ -69,7 +69,8 @@ class ContainerManager:
|
|||
labels=labels,
|
||||
environment={
|
||||
'USER_ID': str(user_id),
|
||||
'USER_SLUG': slug
|
||||
'USER_SLUG': slug,
|
||||
'JWT_SECRET': Config.SECRET_KEY # Für Token-Validierung im Container
|
||||
},
|
||||
restart_policy={'Name': 'unless-stopped'},
|
||||
mem_limit=Config.DEFAULT_MEMORY_LIMIT,
|
||||
|
|
@ -243,7 +244,8 @@ class ContainerManager:
|
|||
environment={
|
||||
'USER_ID': str(user_id),
|
||||
'USER_SLUG': slug,
|
||||
'CONTAINER_TYPE': container_type
|
||||
'CONTAINER_TYPE': container_type,
|
||||
'JWT_SECRET': Config.SECRET_KEY # Für Token-Validierung im Container
|
||||
},
|
||||
restart_policy={'Name': 'unless-stopped'},
|
||||
mem_limit=Config.DEFAULT_MEMORY_LIMIT,
|
||||
|
|
|
|||
|
|
@ -3,11 +3,12 @@ Persönliches Wörterbuch - Flask Backend mit SQLite
|
|||
Speichert Wörter und Bedeutungen in einer persistenten Datenbank pro Benutzer
|
||||
"""
|
||||
|
||||
from flask import Flask, render_template, request, jsonify
|
||||
from flask import Flask, render_template, request, jsonify, g
|
||||
from datetime import datetime
|
||||
import sqlite3
|
||||
import os
|
||||
import logging
|
||||
import jwt
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
|
|
@ -59,6 +60,49 @@ def init_db():
|
|||
conn.close()
|
||||
|
||||
|
||||
# ============================================================
|
||||
# JWT-Token Validierung
|
||||
# ============================================================
|
||||
JWT_SECRET = os.getenv('JWT_SECRET', 'secret-key-from-spawner') # Sollte vom Spawner gesetzt werden
|
||||
|
||||
def validate_jwt_token():
|
||||
"""Validiere JWT-Token aus Cookie - wird vor jedem Request ausgeführt"""
|
||||
# GET / ist öffentlich (index.html laden)
|
||||
if request.path == '/' and request.method == 'GET':
|
||||
return
|
||||
|
||||
# GET /health ist öffentlich (Health Check)
|
||||
if request.path == '/health' and request.method == 'GET':
|
||||
return
|
||||
|
||||
# Alle anderen Endpoints brauchen gültigen JWT-Token im Cookie
|
||||
token = request.cookies.get('spawner_token')
|
||||
|
||||
if not token:
|
||||
return jsonify({'error': 'Authentifizierung erforderlich - kein Token'}), 401
|
||||
|
||||
try:
|
||||
# Dekodiere und validiere JWT
|
||||
# Hinweis: Der Secret-Key muss mit dem Spawner synchron sein!
|
||||
payload = jwt.decode(token, JWT_SECRET, algorithms=['HS256'])
|
||||
# Speichere User-ID im g-Object für API-Endpunkte
|
||||
g.user_id = payload.get('sub') # 'sub' ist die Standard-Claim für User-ID
|
||||
logger.info(f"[DICTIONARY] Token validiert für User {g.user_id}")
|
||||
except jwt.ExpiredSignatureError:
|
||||
logger.warning("[DICTIONARY] JWT-Token abgelaufen")
|
||||
return jsonify({'error': 'Token abgelaufen - bitte neu anmelden'}), 401
|
||||
except jwt.InvalidTokenError as e:
|
||||
logger.warning(f"[DICTIONARY] Ungültiger JWT-Token: {str(e)}")
|
||||
return jsonify({'error': 'Ungültiger Token - authentifizieren erforderlich'}), 401
|
||||
except Exception as e:
|
||||
logger.error(f"[DICTIONARY] Token-Validierungsfehler: {str(e)}")
|
||||
return jsonify({'error': 'Authentifizierungsfehler'}), 500
|
||||
|
||||
|
||||
# Registriere before_request Handler
|
||||
app.before_request(validate_jwt_token)
|
||||
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
"""Hauptseite mit HTML-Interface"""
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
Flask==3.0.0
|
||||
Werkzeug==3.1.6
|
||||
requests==2.31.0
|
||||
PyJWT==2.8.1
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user