Pebellslearn by building
0%
RoadmapsMatrixCoursesSandboxSign in
Auth and the API Contract  ›  Lesson

The Login Round Trip

JWT Auth 15 minLogin checks a password against a stored hash once, then the server issues a signed token in an HttpOnly cookie that the browser sends with every request and middleware verifies to identify the user.

The Login Round Trip

Logging in to Sprout feels like a single click, but it crosses all three layers: a form in the browser, a password check on the server, a user row in the database, and then a signed pass that rides along with every later request. Each piece sits where it does for a reason, and moving any of them is how login systems get broken.

The rule

Prove it once with a password; carry a signed pass after that.

app.post('/api/login', async (req, res) => {
  const { rows } = await pool.query(
    'SELECT id, password_hash FROM users WHERE email = $1', [req.body.email]);
  const user = rows[0];
  const ok = user && await bcrypt.compare(req.body.password, user.password_hash);
  if (!ok) return res.status(401).json({ error: 'Email or password is wrong' });

  const token = jwt.sign({ sub: String(user.id) }, process.env.JWT_SECRET, { expiresIn: '1h' });
  res.cookie('token', token, { httpOnly: true, secure: true, sameSite: 'lax', maxAge: 3600000 });
  res.json({ id: user.id });
});

function requireUser(req, res, next) {
  try {
    const payload = jwt.verify(req.cookies.token, process.env.JWT_SECRET);
    req.user = { id: Number(payload.sub) };
    next();
  } catch {
    res.status(401).json({ error: 'Please log in' });
  }
}

Proving it. The database never holds passwords, only a slow one-way hash of each (bcrypt or Argon2). The server hashes the attempt the same way and compares. Unknown email and wrong password get the same reply, so the login form cannot be used to discover who has an account.

The pass. The server signs a JSON Web Token whose payload says "this is user 5, valid for one hour". Anyone can read a JWT's payload, but only the holder of JWT_SECRET can produce a valid signature, so an edited token fails the check.

Carrying it. The token travels in a cookie with three flags. HttpOnly hides it from page JavaScript, so an injected script cannot steal it. Secure sends it only over HTTPS. SameSite=Lax keeps the browser from attaching it to form posts that other websites send to Sprout. The browser then attaches the cookie to every request on its own.

Checking it. requireUser is middleware (reading req.cookies needs the small cookie-parser package). A missing, edited or expired token makes jwt.verify throw, and the answer is 401. Otherwise req.user is set and the handler runs.

Notice what the client does with the token: nothing. It cannot even read it. To show "Hi, Asha" it calls GET /api/me, a protected route that returns the user's public details.

The shortcut

For each piece of login data, ask "Who needs to read this?" The password: only the server, once, to compare with a hash. The token: only the server, so an HttpOnly cookie. The user's display name: the client, so an ordinary JSON response.

Asha logs inStep 1 of 4
  1. The form

    The browser sends POST /api/login with her email and password, over HTTPS.

Drills

Answer fast — trust the rule.

quick drill

What does Sprout keep in the users table for a password?

quick drill

Why does Sprout put the token in an HttpOnly cookie?

quick drill

Someone changes their token's payload from {"sub": "5"} to {"sub": "6"}. What happens on the next request?

quick drill

A login attempt fails. Which reply is best?

Remember

  • Prove it once with a password; carry a signed pass after that.
  • Shortcut: "Who needs to read this?" The server alone reads passwords and tokens; the client asks /api/me.
  • Trap: storing passwords instead of hashes, or tokens where page scripts can read them.

Practice — level up

0/1 solved

Write your answer, then compare it with a rubric and a model answer.

ExplainLogin, layer by layer

Explain Sprout's login across the three layers: what the browser sends, what the server checks against the database, what it sends back, and how the server knows who is asking on the next request. Say why the client never reads the token.