feat: bind the GTI Fiscal SSO assertion to the browser (state/nonce)

/sso/fiscal now requires the state that GTI Fiscal creates when the flow
starts and signs it as the "nonce" claim; GTI Fiscal checks it against
its own one-time cookie. Without a valid state the panel sends the
browser to GTI Fiscal's /login, so the flow always starts there. This
prevents someone from pushing an assertion for their own account into
another person's browser (login CSRF).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Gustavo
2026-09-23 12:21:27 -03:00
co-authored by Claude Opus 5.5
parent 9139a1f6f3
commit 161da90e02
3 changed files with 50 additions and 20 deletions
+10 -5
View File
@@ -34,29 +34,34 @@ def _pagina_erro(mensagem: str, status_code: int) -> HTMLResponse:
@router.get("/sso/fiscal")
def iniciar_sso_fiscal(request: Request, next: str | None = None):
def iniciar_sso_fiscal(request: Request, next: str | None = None, state: str | None = None):
destino = sso.proximo_seguro(next)
if not sso.configurado():
return _pagina_erro("O acesso ao GTI Fiscal não está configurado neste painel.", 503)
# Sem state o fluxo não começou no Fiscal (ex.: clique na sidebar): manda
# para o login do Fiscal, que cria o state e devolve para cá.
if not sso.estado_valido(state):
return RedirectResponse(sso.url_login_fiscal(destino), status_code=303)
parametros = urlencode({"state": state, "next": destino})
if request.url.scheme != "https":
publica = os.environ.get("SSO_GERENCIADOR_URL_PUBLICA", "").rstrip("/")
if publica.startswith("https://"):
return RedirectResponse(f"{publica}/sso/fiscal?{urlencode({'next': destino})}", status_code=303)
return RedirectResponse(f"{publica}/sso/fiscal?{parametros}", status_code=303)
return _pagina_erro("O acesso ao GTI Fiscal só funciona pelo endereço HTTPS do painel.", 400)
try:
usuario = exigir_login(request)
except NaoAutenticado:
volta = f"/sso/fiscal?{urlencode({'next': destino})}"
return RedirectResponse(f"/login?{urlencode({'next': volta})}", status_code=303)
return RedirectResponse(f"/login?{urlencode({'next': f'/sso/fiscal?{parametros}'})}", status_code=303)
funcionario = db_funcionarios.obter_por_login(usuario.login)
if funcionario is None:
db_log.registrar(str(usuario), "sso_fiscal", "funcionario", usuario.login, False, "conta de bootstrap não usa SSO")
return _pagina_erro("A conta de bootstrap não acessa o GTI Fiscal — entre com seu usuário de funcionário.", 403)
assercao = sso.emitir_assercao(funcionario)
assercao = sso.emitir_assercao(funcionario, nonce=state)
db_log.registrar(str(usuario), "sso_fiscal", "funcionario", str(funcionario["ID"]), True)
resposta = templates.TemplateResponse(
request, "sso_fiscal.html", {"acao": sso.url_callback(), "assercao": assercao, "proximo": destino}