Files
v2-Docker/lizenzserver/services/auth/app.py
Claude Project Manager 0d7d888502 Initial commit
2025-07-05 17:51:16 +02:00

279 Zeilen
8.3 KiB
Python

import os
import sys
from flask import Flask, request, jsonify
from flask_cors import CORS
import jwt
from datetime import datetime, timedelta
import logging
from functools import wraps
from prometheus_flask_exporter import PrometheusMetrics
# Add parent directory to path for imports
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
from config import get_config
from repositories.base import BaseRepository
# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# Initialize Flask app
app = Flask(__name__)
config = get_config()
app.config.from_object(config)
CORS(app)
# Initialize Prometheus metrics
metrics = PrometheusMetrics(app)
metrics.info('auth_service_info', 'Auth Service Information', version='1.0.0')
# Initialize repository
db_repo = BaseRepository(config.DATABASE_URL)
def create_token(payload: dict, expires_delta: timedelta) -> str:
"""Create JWT token"""
to_encode = payload.copy()
expire = datetime.utcnow() + expires_delta
to_encode.update({"exp": expire, "iat": datetime.utcnow()})
return jwt.encode(
to_encode,
config.JWT_SECRET,
algorithm=config.JWT_ALGORITHM
)
def decode_token(token: str) -> dict:
"""Decode and validate JWT token"""
try:
payload = jwt.decode(
token,
config.JWT_SECRET,
algorithms=[config.JWT_ALGORITHM]
)
return payload
except jwt.ExpiredSignatureError:
raise ValueError("Token has expired")
except jwt.InvalidTokenError:
raise ValueError("Invalid token")
def require_api_key(f):
"""Decorator to require API key"""
@wraps(f)
def decorated_function(*args, **kwargs):
api_key = request.headers.get('X-API-Key')
if not api_key:
return jsonify({"error": "Missing API key"}), 401
# Validate API key
query = """
SELECT id, client_name, allowed_endpoints
FROM api_clients
WHERE api_key = %s AND is_active = true
"""
client = db_repo.execute_one(query, (api_key,))
if not client:
return jsonify({"error": "Invalid API key"}), 401
# Check if endpoint is allowed
endpoint = request.endpoint
allowed = client.get('allowed_endpoints', [])
if allowed and endpoint not in allowed:
return jsonify({"error": "Endpoint not allowed"}), 403
# Add client info to request
request.api_client = client
return f(*args, **kwargs)
return decorated_function
@app.route('/health', methods=['GET'])
def health_check():
"""Health check endpoint"""
return jsonify({
"status": "healthy",
"service": "auth",
"timestamp": datetime.utcnow().isoformat()
})
@app.route('/api/v1/auth/token', methods=['POST'])
@require_api_key
def create_access_token():
"""Create access token for license validation"""
data = request.get_json()
if not data or 'license_id' not in data:
return jsonify({"error": "Missing license_id"}), 400
license_id = data['license_id']
hardware_id = data.get('hardware_id')
# Verify license exists and is active
query = """
SELECT id, is_active, max_devices
FROM licenses
WHERE id = %s
"""
license = db_repo.execute_one(query, (license_id,))
if not license:
return jsonify({"error": "License not found"}), 404
if not license['is_active']:
return jsonify({"error": "License is not active"}), 403
# Create token payload
payload = {
"sub": license_id,
"hwid": hardware_id,
"client_id": request.api_client['id'],
"type": "access"
}
# Add features and limits based on license
payload["features"] = data.get('features', [])
payload["limits"] = {
"api_calls": config.DEFAULT_RATE_LIMIT_PER_HOUR,
"concurrent_sessions": config.MAX_CONCURRENT_SESSIONS
}
# Create tokens
access_token = create_token(payload, config.JWT_ACCESS_TOKEN_EXPIRES)
# Create refresh token
refresh_payload = {
"sub": license_id,
"client_id": request.api_client['id'],
"type": "refresh"
}
refresh_token = create_token(refresh_payload, config.JWT_REFRESH_TOKEN_EXPIRES)
return jsonify({
"access_token": access_token,
"refresh_token": refresh_token,
"token_type": "Bearer",
"expires_in": int(config.JWT_ACCESS_TOKEN_EXPIRES.total_seconds())
})
@app.route('/api/v1/auth/refresh', methods=['POST'])
def refresh_access_token():
"""Refresh access token"""
data = request.get_json()
if not data or 'refresh_token' not in data:
return jsonify({"error": "Missing refresh_token"}), 400
try:
# Decode refresh token
payload = decode_token(data['refresh_token'])
if payload.get('type') != 'refresh':
return jsonify({"error": "Invalid token type"}), 400
license_id = payload['sub']
# Verify license still active
query = "SELECT is_active FROM licenses WHERE id = %s"
license = db_repo.execute_one(query, (license_id,))
if not license or not license['is_active']:
return jsonify({"error": "License is not active"}), 403
# Create new access token
access_payload = {
"sub": license_id,
"client_id": payload['client_id'],
"type": "access"
}
access_token = create_token(access_payload, config.JWT_ACCESS_TOKEN_EXPIRES)
return jsonify({
"access_token": access_token,
"token_type": "Bearer",
"expires_in": int(config.JWT_ACCESS_TOKEN_EXPIRES.total_seconds())
})
except ValueError as e:
return jsonify({"error": str(e)}), 401
@app.route('/api/v1/auth/verify', methods=['POST'])
def verify_token():
"""Verify token validity"""
auth_header = request.headers.get('Authorization')
if not auth_header or not auth_header.startswith('Bearer '):
return jsonify({"error": "Missing or invalid authorization header"}), 401
token = auth_header.split(' ')[1]
try:
payload = decode_token(token)
return jsonify({
"valid": True,
"license_id": payload['sub'],
"expires_at": datetime.fromtimestamp(payload['exp']).isoformat()
})
except ValueError as e:
return jsonify({
"valid": False,
"error": str(e)
}), 401
@app.route('/api/v1/auth/api-key', methods=['POST'])
def create_api_key():
"""Create new API key (admin only)"""
# This endpoint should be protected by admin authentication
# For now, we'll use a simple secret header
admin_secret = request.headers.get('X-Admin-Secret')
if admin_secret != os.getenv('ADMIN_SECRET', 'change-this-admin-secret'):
return jsonify({"error": "Unauthorized"}), 401
data = request.get_json()
if not data or 'client_name' not in data:
return jsonify({"error": "Missing client_name"}), 400
import secrets
api_key = f"sk_{secrets.token_urlsafe(32)}"
secret_key = secrets.token_urlsafe(64)
query = """
INSERT INTO api_clients (client_name, api_key, secret_key, allowed_endpoints)
VALUES (%s, %s, %s, %s)
RETURNING id
"""
allowed_endpoints = data.get('allowed_endpoints', [])
client_id = db_repo.execute_insert(
query,
(data['client_name'], api_key, secret_key, allowed_endpoints)
)
if not client_id:
return jsonify({"error": "Failed to create API key"}), 500
return jsonify({
"client_id": client_id,
"api_key": api_key,
"secret_key": secret_key,
"client_name": data['client_name']
}), 201
@app.errorhandler(404)
def not_found(error):
return jsonify({"error": "Not found"}), 404
@app.errorhandler(500)
def internal_error(error):
logger.error(f"Internal error: {error}")
return jsonify({"error": "Internal server error"}), 500
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5001, debug=True)