1. A token is random, shown once, and never stored
store/tokens.py:56-98
def mint(conn, owner_id, name, scopes=("read",), ttl_days=None):
...
token = PREFIX + secrets.token_urlsafe(_TOKEN_BYTES) # 32 random bytes
row = conn.execute(
"INSERT INTO api_token (owner_id, name, token_hash, prefix, scopes, expires_at) "
"VALUES (%s, %s, %s, %s, %s, now() + %s) RETURNING id",
(owner_id, name, _hash(token), token[:8], list(scopes), timedelta(days=ttl_days))).fetchone()
conn.commit()
return token, row["id"] # the token goes to the person once; only its SHA-256 is kept
def verify(conn, token):
if not looks_like_token(token): # shape first: no database work for junk
return None
row = conn.execute("SELECT ... FROM api_token t JOIN app_user u ON u.id = t.owner_id "
"WHERE t.token_hash = %s AND t.revoked_at IS NULL AND t.expires_at > now()",
(_hash(token),)).fetchone()- Only a hash is stored, so a database leak does not leak working tokens.
- Every write commits explicitly: the app's connection is not autocommit, and tests fail without the commits.
- Scopes are checked by a database constraint as well as by the code.