Never store passwords in plaintext
Always store the password hash, not the password. And not just any hash: use a slow and salted one designed for this, like bcrypt, scrypt or argon2.
- Salt: a random value unique per user that's mixed in before hashing. It makes two people with the same password have different hashes and breaks attacks with precomputed tables (rainbow tables).
- Slow on purpose: bcrypt is expensive to compute, which slows down brute-force attacks.
password + salt ──bcrypt──> $2b$10$N9qo8uLOick... (this is what you store)
At login, you hash again what the user types and compare hashes; you never recover the original password (you can't, and that's good).
Other golden rules
- HTTPS always: without TLS, anyone on the network can read the token or the password in transit. The credential travels in plaintext without it.
- Don't put secrets in the token's payload: remember, it's readable by anyone. No passwords or sensitive data inside the JWT.
- Short expiry + refresh: short-lived access tokens and a refresh token to renew. Reduces the damage window if a token is stolen.
- Logout and revocation: a stateless JWT can't be "deleted" from the client, so to truly revoke it you keep a revocation list (or invalidate the refresh token) on the server. Sessions, in contrast, are revoked simply by deleting them from the store.
- Secure cookies: if you use cookies, mark them
httpOnly,SecureandSameSiteto mitigate XSS and CSRF at once.
Examples
Verify expiry: compare exp with the current moment
const now = Math.floor(Date.now() / 1000);
const payload = { sub: "ana", exp: now - 60 }; // expired 1 minute ago
console.log(payload.exp < now ? "expired" : "valid");