Juicy Shop

← All lessons

Authentication & Session Security

Identification & AuthenticationA07:2021 – Identification and Authentication Failuresintermediate11 min

Authentication proves who a user is. Learn how injection and weak credential handling break it, why passwords must be salted and hashed, and how to harden the login flow against takeover.

Authentication vs authorisation

Authentication answers 'who are you?'; authorisation answers 'what are you allowed to do?'. They are distinct. A login bypass defeats authentication; an IDOR (covered in the access-control lesson) defeats authorisation even when authentication works. This lesson focuses on proving identity safely.

How logins get bypassed

The classic failure is building the credential query by concatenation: select * from users where email = '<input>'. An attacker submitting ' OR 1=1-- turns the query true and logs in as the first user — usually an admin. Even without injection, weak handling hurts: comparing plaintext passwords, leaking whether an account exists via different error messages, or allowing unlimited guesses all widen the attack surface.

Store passwords correctly

Never store or compare plaintext passwords. Use a slow, salted password hash designed for the purpose — bcrypt, scrypt, or Argon2 — so that a stolen database cannot be reversed quickly. Each password gets a unique salt to defeat rainbow tables, and the deliberate slowness (work factor) makes large-scale cracking expensive. Compare using the algorithm's verify function, which is constant-time.

Harden the whole flow

Beyond hashing: parameterise the lookup query, return a single generic 'invalid credentials' message regardless of which field was wrong, rate-limit and lock out after repeated failures, and prefer multi-factor authentication for sensitive accounts. Issue session tokens that are random, httpOnly, and rotated on privilege change.

Insecure vs secure

✗ insecure
const { email, password } = await request.json();
const rows = await sql.raw(
  `select * from users where email = '${email}'`
);
const user = rows[0];
if (user && user.password === password) login(user);

The email is concatenated (injectable), and the password is compared in plaintext — a database leak exposes every credential instantly.

✓ secure
const { email, password } = await request.json();
const rows = await sql`
  select * from users where email = ${email} limit 1
`;
const user = rows[0];
if (user && (await argon2.verify(user.hash, password))) login(user);

The lookup is parameterised and the password is verified against a salted Argon2 hash, so neither injection nor a database leak yields credentials.

Key takeaways

  • Authentication (who you are) is distinct from authorisation (what you may do).
  • Parameterise credential queries — login forms are a prime injection target.
  • Hash passwords with a salted, slow algorithm (bcrypt/scrypt/Argon2).
  • Use generic error messages, rate limiting, lockout, and ideally MFA.

Knowledge check

Practice

Apply this lesson hands-on in the matching find-it / fix-it challenge.

Open challenge →

Further reading