fix(auth): api_key condivisa fra postazioni, timing costante al login (#25)
login_user riusa l'api_key esistente invece di rigenerarla ad ogni
login: due postazioni collegate con lo stesso utente restavano
altrimenti disconnesse a vicenda. logout_user diventa no-op sulla key
a meno di all_sessions=True (nuovo parametro, passato da
POST /api/auth/logout?all_sessions=true nel router).
authenticate_user esegue comunque bcrypt.checkpw contro un hash fittizio
quando lo username non esiste, per non rivelare col tempo di risposta
se uno username è registrato.
PUT /api/users/{id}/password azzera l'api_key dell'utente dopo il
cambio, come già fa setup.py:634.
Non introdotto in questa issue: hashing o scadenza della api_key
(annotato come MANCA nel rapporto del lotto).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -27,6 +27,13 @@ def generate_api_key() -> str:
|
||||
return secrets.token_urlsafe(48) # 64 chars base64
|
||||
|
||||
|
||||
# A precomputed, unused bcrypt hash. When the username does not match any
|
||||
# user, authenticate_user still runs bcrypt against this instead of
|
||||
# returning immediately, so "unknown username" costs the same as "wrong
|
||||
# password" and a caller cannot use response time to enumerate usernames.
|
||||
_DUMMY_PASSWORD_HASH = "$2b$12$BLtwrR2COyd5W/UWfxp7BOc7OHua9jazo6zZdWOwRD9PV.pyaqKZC"
|
||||
|
||||
|
||||
async def authenticate_user(
|
||||
db: AsyncSession, username: str, password: str
|
||||
) -> User | None:
|
||||
@@ -35,7 +42,10 @@ async def authenticate_user(
|
||||
select(User).where(User.username == username, User.active == True)
|
||||
)
|
||||
user = result.scalar_one_or_none()
|
||||
if user is None or not verify_password(password, user.password_hash):
|
||||
if user is None:
|
||||
verify_password(password, _DUMMY_PASSWORD_HASH)
|
||||
return None
|
||||
if not verify_password(password, user.password_hash):
|
||||
return None
|
||||
return user
|
||||
|
||||
@@ -66,8 +76,14 @@ async def authorise_supervisor(db: AsyncSession, username: str, password: str) -
|
||||
|
||||
|
||||
async def login_user(db: AsyncSession, user: User) -> str:
|
||||
"""Generate API key and update last_login for user."""
|
||||
api_key = generate_api_key()
|
||||
"""Generate (or reuse) the API key and update last_login for user.
|
||||
|
||||
Reuses an existing key rather than rotating it on every login: the same
|
||||
account logged in from two stations at once (a common shift-account
|
||||
pattern here) must keep both sessions working. Rotating the key would
|
||||
silently log the other station out.
|
||||
"""
|
||||
api_key = user.api_key or generate_api_key()
|
||||
await db.execute(
|
||||
update(User)
|
||||
.where(User.id == user.id)
|
||||
@@ -77,8 +93,17 @@ async def login_user(db: AsyncSession, user: User) -> str:
|
||||
return api_key
|
||||
|
||||
|
||||
async def logout_user(db: AsyncSession, user: User) -> None:
|
||||
"""Invalidate user's API key."""
|
||||
async def logout_user(
|
||||
db: AsyncSession, user: User, all_sessions: bool = False
|
||||
) -> None:
|
||||
"""Log out. By default a no-op on the shared API key.
|
||||
|
||||
Clearing the key on every logout would kick out any other station still
|
||||
using the same shift account. `all_sessions=True` is the explicit
|
||||
override for a supervisor who wants every session logged out.
|
||||
"""
|
||||
if not all_sessions:
|
||||
return
|
||||
await db.execute(
|
||||
update(User).where(User.id == user.id).values(api_key=None)
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user