feat: SSO from GTI Gerenciador and the Gerenciador visual identity
Collaborators logged into GTI Gerenciador open GTI Fiscal without a second password. The panel signs a 60 s JWT (Ed25519; the Hub holds only the public key); the Hub checks signature, iss/aud/exp/nbf, maximum lifetime, nonce against a one-time state cookie set when the flow starts at GET /login (login-CSRF protection) and a single-use jti (sso_used_assertions). Collaborators are provisioned in admin_users with auth_source=GTI_GERENCIADOR and no password; a CHECK constraint makes a passwordless local account or a federated account with a password impossible (migration 0021). Sessions last FEDERATED_SESSION_SECONDS (1 h) and are renewed silently through the panel. The local admin stays as break-glass at /login/local, audited. Logout ends only the Hub session. Every SSO success/rejection (stable reason code), logout and local login is audited with the real client IP, never the token. verify_password now fails closed on a missing hash instead of raising. Visual identity: gti-theme.css is a verbatim copy of the panel's design system (tokens, sidebar, light/dark theme) plus its tema.js, sidebar.js and logo; control-panel.css keeps only Hub components, all colours from the panel tokens. Page structure and features unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -26,7 +26,10 @@ def hash_password(password: str) -> str:
|
||||
return "pbkdf2_sha256$240000$%s$%s" % (base64.urlsafe_b64encode(salt).decode(), base64.urlsafe_b64encode(encoded).decode())
|
||||
|
||||
|
||||
def verify_password(password: str, stored: str) -> bool:
|
||||
def verify_password(password: str, stored: str | None) -> bool:
|
||||
# Federated (SSO) accounts have no password: always refuse, never raise.
|
||||
if not isinstance(stored, str) or not isinstance(password, str):
|
||||
return False
|
||||
try:
|
||||
algorithm, rounds, salt, digest = stored.split("$", 3)
|
||||
candidate = hashlib.pbkdf2_hmac(algorithm.removeprefix("pbkdf2_"), password.encode(), base64.urlsafe_b64decode(salt), int(rounds))
|
||||
@@ -49,11 +52,12 @@ def create_admin(session, username: str, password: str):
|
||||
session.add(admin); session.commit(); return admin
|
||||
|
||||
|
||||
def create_session(session, admin: AdminUser, request: Request):
|
||||
def create_session(session, admin: AdminUser, request: Request, seconds: int | None = None):
|
||||
raw_token = secrets.token_urlsafe(32)
|
||||
raw_csrf = secrets.token_urlsafe(32)
|
||||
now = datetime.now(timezone.utc)
|
||||
row = WebSession(admin_user_id=admin.id, token_hash=_digest(raw_token), csrf_hash=_digest(raw_csrf), expires_at=now + timedelta(seconds=settings.web_session_seconds), user_agent=request.headers.get("user-agent", "")[:512], remote_addr=request.client.host if request.client else None)
|
||||
lifetime = seconds if seconds is not None else settings.web_session_seconds
|
||||
row = WebSession(admin_user_id=admin.id, token_hash=_digest(raw_token), csrf_hash=_digest(raw_csrf), expires_at=now + timedelta(seconds=lifetime),user_agent=request.headers.get("user-agent", "")[:512], remote_addr=request.client.host if request.client else None)
|
||||
session.add(row); admin.last_login_at = now; session.commit()
|
||||
return raw_token, raw_csrf
|
||||
|
||||
|
||||
Reference in New Issue
Block a user