Really good catch, thanks for this. You're right, matching the error string
handles the content but not the timing. Since bcrypt is slow on purpose, the
"email exists" path takes longer, and that gap alone is enough to enumerate
valid emails even with identical error text. Fix is exactly what you said,
always run a bcrypt compare, even when the email isn't found, against a fixed
dummy hash, so both paths cost about the same. Folding this into the post with
a note, appreciate it.
Good rundown, especially the RBAC role-explosion section. One thing worth adding to the "return one identical message" habit: matching the error text is necessary but not sufficient if the two code paths take different amounts of time. Looking up a user and then running bcrypt costs real milliseconds, so a login endpoint that only calls bcrypt when the email exists leaks that difference through response timing even with identical error strings. The usual fix is to always run a bcrypt compare against a fixed dummy hash when the email isn't found, so both paths cost roughly the same and timing can't be used to enumerate valid emails.