SecurityIntermediate

Authentication that survives a security review

Sign-up, sign-in, refresh, logout. Every one of them has a way to be wrong that still passes a happy-path test.

The business problem

Build the service every other service will trust. It issues tokens, it rotates them, and it has to behave correctly when someone is trying to make it behave incorrectly.

The bar is not 'a login endpoint works'. It is that a stolen refresh token stops being useful, and that a password is never recoverable from anything you store.

What you will have at the end

  • Passwords stored so that a database dump does not reveal them
  • Short-lived access tokens and rotating refresh tokens
  • Logout that actually invalidates something
  • Rate limiting on the endpoints that get attacked

Milestones

Each one ends in something you can observe. Without that a milestone is a heading, and you have no way to know you finished.

  1. Registration and hashing

    BCrypt with a deliberate cost, or Argon2id. Never MD5, never SHA-256, never a hand-rolled salt.

    done whenTwo users with the same password have different hashes, and hashing takes long enough to feel it.

  2. Access tokens

    Short expiry — minutes, not days. Sign with a key that is not in the repository.

    done whenAn expired token is rejected, and the rejection is 401 rather than 500.

  3. Refresh rotation

    Each refresh issues a new refresh token and invalidates the old one. Reuse of an invalidated token revokes the whole family.

    done whenReplaying an old refresh token logs the session out instead of extending it.

  4. Logout that means something

    A JWT cannot be un-issued. Decide: a short expiry you wait out, or a deny-list you check. Both are real answers; pretending logout works without either is not.

    done whenAfter logout, the access token stops working within the window you documented.

  5. Rate limiting and lockout

    Per-account and per-IP. Both, because either alone is trivially defeated.

    done whenTen wrong passwords in a row does not let an eleventh through immediately.

Data

users, refresh_tokens (with a family id and a revoked flag), and a login_attempts table or a Redis counter. The refresh_tokens table is where rotation lives or dies.

Trade-offs you will have to defend

Stateless JWT scales and cannot be revoked. A session store can be revoked and needs a lookup on every request. The honest answer is usually both: short JWTs plus a revocable refresh.
Storing tokens in localStorage exposes them to XSS; in a cookie it exposes you to CSRF. There is no option that is safe by default.
Argon2 is stronger and hungrier. On a small instance, BCrypt at a sensible cost may be the better real-world choice.