Password authentication
Design safe signup
Normalize account identifiers, validate input, allow password managers, and create accounts without leaking whether another user exists.
Signup takes untrusted input and turns it into the most sensitive records in your database. Two decisions come first: which identifier is canonical, and which fields need verification later. For the Books API the identifier is the email address, and it needs verifying.
Normalize the email, carefully
Users type [email protected] one day and [email protected] the next. Both should reach the same account. So we store a normalized form for lookups, and keep the original for display and for sending mail.
Keep the normalization conservative: trim whitespace and lowercase. Avoid provider-specific rewriting, like stripping dots or +tags from Gmail addresses. That’s a policy with consequences, and you adopt it only if your product owns them.
Let the database settle races
Checking “does this email already exist?” in application code has a race. Two signup requests arrive together, both see no row, both insert. Now you have two accounts with one email. The fix is a unique index:
CREATE UNIQUE INDEX users_email_unique
ON users (normalized_email);
The database is the final judge. The second insert fails with a uniqueness error, and your handler translates that into whatever response the product wants to show.
Don’t leak who has an account
If signup says “this email is already registered”, anyone can test a list of addresses against your app. Where that matters, return a generic outcome and send an email instead. The existing owner gets “someone tried to sign up with your email”, the new user gets a verification link. The public response is identical.
Create the account unverified
Sending an email proves only that you tried to send one. The account starts unverified and becomes verified when a valid, unexpired, single-purpose token gets consumed. We build that flow in the recovery module.
Password rules that help instead of hurt
Password rules should work with password managers, not fight them. Allow paste, spaces, Unicode, and at least 64 characters. Drop the “one uppercase, one symbol” composition rules, they push people toward Password1!.
Never truncate a password silently. If you accept 64 characters and hash only the first 20, you’ve weakened every long password without telling anyone.
As a current baseline, treat passwords shorter than 15 characters as weak when there’s no MFA, and shorter than 8 as too short even with MFA. Check new passwords against known breach lists without sending the full password to a third party. The Have I Been Pwned range API does this with the first five characters of the SHA-1 hash.
Try this: write the signup request schema for the Books API, add the unique index, and decide the duplicate behavior. Then fire two signup requests for the same email at the same moment and look at the users table. One row, one verification token.
Lesson completed