feat(auth): Add session event logging for login attempts

- Added session callback mechanism in secubox_core.auth
- Login events (success/failure) now emit to registered callback
- secubox-auth module records sessions in JSON file
- Fixed login.html endpoint URLs and JSON parsing

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
CyberMind-FR 2026-05-09 06:17:49 +02:00
parent 676630ca71
commit fb5c33eadc
3 changed files with 54 additions and 17 deletions

View File

@ -22,6 +22,22 @@ log = get_logger("auth")
# auto_error=False allows us to handle missing token gracefully
_bearer = HTTPBearer(auto_error=False)
# Session event callback (set by auth module to record sessions)
_session_callback = None
def set_session_callback(callback):
"""Set callback function for session events: callback(event, username, details)"""
global _session_callback
_session_callback = callback
def _emit_session_event(event: str, username: str, details: dict = None):
"""Emit session event to callback if set."""
if _session_callback:
try:
_session_callback(event, username, details or {})
except Exception as e:
log.warning("Session callback error: %s", e)
# ── JWT helpers ────────────────────────────────────────────────────
def _secret() -> str:
@ -111,10 +127,12 @@ async def login(req: LoginRequest):
"""Endpoint de login — retourne un JWT."""
if not _check_password(req.username, req.password):
log.warning("Échec login: %s", req.username)
_emit_session_event("login_failed", req.username, {"reason": "invalid_credentials"})
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Identifiants incorrects",
)
token = create_token(req.username)
log.info("Login OK: %s", req.username)
_emit_session_event("login_success", req.username, {"expires_in": 86400})
return TokenResponse(access_token=token)

View File

@ -1,7 +1,7 @@
"""SecuBox Auth API - OAuth2 + Vouchers + Sessions with Enhanced Monitoring"""
from fastapi import FastAPI, APIRouter, Depends, HTTPException, BackgroundTasks
from pydantic import BaseModel, Field, field_validator
from secubox_core.auth import router as auth_router, require_jwt, create_token
from secubox_core.auth import router as auth_router, require_jwt, create_token, set_session_callback
from secubox_core.config import get_config
import json
import secrets
@ -194,11 +194,29 @@ async def _periodic_cleanup():
await asyncio.sleep(300) # Every 5 minutes
def _handle_session_event(event: str, username: str, details: dict):
"""Handle session events from secubox_core.auth login."""
_record_event(event, {"username": username, **details})
# Also create session record for successful logins
if event == "login_success":
sessions = _load(SESSIONS_FILE, [])
sessions.append({
"id": secrets.token_hex(8),
"username": username,
"created": datetime.now().isoformat(),
"expires": int(time.time()) + details.get("expires_in", 86400),
"type": "jwt"
})
_save(SESSIONS_FILE, sessions)
@app.on_event("startup")
async def startup():
"""Start background cleanup."""
"""Start background cleanup and register session callback."""
global _cleanup_task
_cleanup_task = asyncio.create_task(_periodic_cleanup())
# Register callback for login events
set_session_callback(_handle_session_event)
@app.on_event("shutdown")

View File

@ -234,7 +234,7 @@
submitBtn.textContent = 'Authenticating...';
try {
const res = await fetch('/api/v1/hub/auth/login', {
const res = await fetch('/api/v1/auth/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
@ -268,25 +268,26 @@
// Load auth mode and version info (public endpoint)
async function loadPublicInfo() {
try {
const res = await fetch('/api/v1/hub/public/info');
const res = await fetch('/api/v1/hub/health');
if (res.ok) {
const data = await res.json();
// Update auth mode
const authModeEl = document.getElementById('auth-mode');
const authModeText = document.getElementById('auth-mode-text');
if (data.auth_mode) {
authModeText.textContent = data.auth_mode;
authModeEl.className = 'auth-mode ' + (data.auth_mode.toLowerCase() === 'zkp' ? 'zkp' : 'standard');
}
// Update version
if (data.version) {
document.getElementById('version-badge').textContent = 'SecuBox v' + data.version;
const text = await res.text();
// Only parse if it looks like JSON
if (text.startsWith('{')) {
const data = JSON.parse(text);
// Update version from health endpoint
if (data.version) {
document.getElementById('version-badge').textContent = 'SecuBox v' + data.version;
}
// Check for ZKP auth mode
const authModeEl = document.getElementById('auth-mode');
const authModeText = document.getElementById('auth-mode-text');
authModeText.textContent = data.auth_mode || 'ZKP';
authModeEl.className = 'auth-mode zkp';
}
}
} catch (e) {
// Silently fail - use defaults
console.log('[Login] Info fetch skipped:', e.message);
}
}