feat: token de sessao assinado (app/session.py)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
361c2ca770
commit
95b1da7c10
@@ -0,0 +1,68 @@
|
||||
"""
|
||||
Token de sessão de login — cookie "painel_session" (ver app/auth.py e
|
||||
app/routers/auth.py). Stateless: nada gravado no banco, a validade inteira
|
||||
depende da assinatura HMAC-SHA256 com SESSION_SECRET (app/main.py define a
|
||||
variável de ambiente; se ausente, um valor aleatório é gerado em memória no
|
||||
start do processo — reiniciar sem SESSION_SECRET fixo derruba as sessões
|
||||
ativas, forçando login de novo, efeito colateral aceito).
|
||||
|
||||
Formato do token: "<payload em base64url>.<assinatura HMAC em hex>"
|
||||
Payload (antes de codificar): JSON com login, nome, perfil e validade
|
||||
("exp", timestamp Unix).
|
||||
|
||||
Nunca lança exceção para entrada inválida — ler_token() sempre devolve
|
||||
None nesse caso, para o chamador (app/auth.py) tratar como "não logado".
|
||||
"""
|
||||
import base64
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import os
|
||||
import secrets
|
||||
import time
|
||||
|
||||
SESSION_COOKIE_NAME = "painel_session"
|
||||
VALIDADE_SEGUNDOS = 12 * 60 * 60 # 12 horas
|
||||
|
||||
_SECRET_EM_MEMORIA = secrets.token_bytes(32)
|
||||
|
||||
|
||||
def _chave_secreta() -> bytes:
|
||||
configurada = os.environ.get("SESSION_SECRET", "")
|
||||
if configurada:
|
||||
return configurada.encode("utf-8")
|
||||
return _SECRET_EM_MEMORIA
|
||||
|
||||
|
||||
def _assinar(payload_b64: bytes) -> str:
|
||||
return hmac.new(_chave_secreta(), payload_b64, hashlib.sha256).hexdigest()
|
||||
|
||||
|
||||
def criar_token(login: str, nome: str, perfil: str, agora: float | None = None) -> str:
|
||||
momento = agora if agora is not None else time.time()
|
||||
payload = {"login": login, "nome": nome, "perfil": perfil, "exp": momento + VALIDADE_SEGUNDOS}
|
||||
payload_b64 = base64.urlsafe_b64encode(json.dumps(payload).encode("utf-8"))
|
||||
assinatura = _assinar(payload_b64)
|
||||
return payload_b64.decode("ascii") + "." + assinatura
|
||||
|
||||
|
||||
def ler_token(token: str | None, agora: float | None = None) -> dict | None:
|
||||
if not token or "." not in token:
|
||||
return None
|
||||
payload_b64_str, assinatura_recebida = token.rsplit(".", 1)
|
||||
payload_b64 = payload_b64_str.encode("ascii")
|
||||
|
||||
assinatura_esperada = _assinar(payload_b64)
|
||||
if not hmac.compare_digest(assinatura_recebida, assinatura_esperada):
|
||||
return None
|
||||
|
||||
try:
|
||||
payload = json.loads(base64.urlsafe_b64decode(payload_b64))
|
||||
except (ValueError, UnicodeDecodeError):
|
||||
return None
|
||||
|
||||
momento = agora if agora is not None else time.time()
|
||||
if momento > payload.get("exp", 0):
|
||||
return None
|
||||
|
||||
return {"login": payload["login"], "nome": payload["nome"], "perfil": payload["perfil"]}
|
||||
Reference in New Issue
Block a user