f069fccd96
Three related auth weaknesses: - Access and refresh tokens were structurally identical, so a 30-day refresh token was accepted as a bearer access token. Tokens now carry a "typ" claim; the access path rejects refresh tokens and /refresh rejects access tokens. - Login stored the hash of a throwaway refresh token (sid=0) but returned a re-issued one, so the stored hash never matched and /refresh always 401'd. Tokens are no longer re-issued: the refresh token is located by hash and carries no session id, while the access token embeds the real session id. A random jti keeps tokens unique within the same second. - Login skipped bcrypt for unknown users (a timing oracle) and returned 403 for blocked accounts before checking the password (leaking account existence). It now always runs a bcrypt comparison and verifies the password before disclosing blocked state. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>