PLAINFINCH learn by building

Sessions and logging in

You will be able to log people in safely by keeping their details on the server and giving the browser only a random id.

Why isn't a name in a cookie enough to log someone in?

The starting code logs a member in by setting user=wren, and /me believes whatever that cookie says. Press **Fake cookie**. It sends user=kestrel without any login, and the server cheerfully answers Logged in as kestrel. The httpOnly and sameSite options don't help here. They stop other websites and page scripts from misusing a cookie, but they do nothing to stop the visitor from editing their own.

The fix is to stop putting the answer in the cookie. The server keeps a private list of who is logged in, and the browser holds only a ticket that points into that list. That ticket is the session id, and the list is the session store.

What is a session?

A session is the server's memory of one visitor between requests. When wren logs in, the server creates an entry, say { name: 'wren' }, files it under a new random id, and sends that id to the browser in a cookie. On every later request the browser sends the id back, the server looks it up, and finds wren.

Think of a coat check at the bird club's annual dinner. You hand over your coat and get a numbered ticket. The ticket doesn't describe the coat. It only works because the person behind the counter has the matching hook. If the ticket numbers were 1, 2, 3, anyone could ask for coat 4. Real coat checks get away with it because staff can see faces. A server can't, which is why the id must be impossible to guess.

  • The visitor logs in, and the server checks who they are.
  • The server makes a random id and stores the session under it.
  • The response sets the id in an httpOnly cookie.
  • Every later request carries the id, and the server looks it up.
  • Logging out deletes the session and clears the cookie.
FigureThe browser cookie holds only sid, a random UUID. The server sessions Map looks it up and finds name wren, so /me answers Logged in as wren. A faked user=kestrel cookie finds nothing and gets 401.The browser cookie holds only sid, a random UUID. The server sessions Map looks it up and finds name wren, so /me answers Logged in as wren. A faked user=kestrel cookie finds nothing and gets 401.
The cookie carries only a random id; who you are lives in the server memory. Typing a name into a cookie no longer works, because a name is not a key in the sessions Map.

How do you make a session id nobody can guess?

Use the operating system's secure random number generator, which Node.js exposes through the built-in crypto module. crypto.randomUUID() returns an id like 3b241101-e2bb-4255-8caf-4136c566a962 with 122 random bits. crypto.randomBytes(32).toString('hex') gives 256 random bits as 64 hex characters. Either is fine for a session id.

Never build ids from Math.random(), the current time, a counter or the user's name. Math.random() isn't designed to be unpredictable, timestamps and counters can be guessed from your own login, and a name is already public. In the practice server, randomness is seeded so checks give the same results each run. On a real server, crypto gives a new unpredictable value every time.

const crypto = require('crypto');
const sessions = new Map();

const sid = crypto.randomUUID();
sessions.set(sid, { name: 'wren' });
res.cookie('sid', sid, { httpOnly: true, sameSite: 'lax' });

Where do sessions live on the server?

The simplest store is a JavaScript Map in memory, keyed by session id. sessions.set(sid, data) adds one, sessions.get(sid) finds it, and sessions.delete(sid) removes it. It is fast and needs no setup, which makes it a good way to learn the idea.

A memory store has two limits worth knowing before you ship. It empties every time the server restarts, which logs everyone out. The practice server restarts on every edit, so you will see this: change a line and your session is gone. It also can't be shared when a site runs on two or more machines. Live sites usually keep sessions in a database table or a dedicated store, with an expiry time on each row. The express-session package wraps this pattern, but writing it by hand once shows you exactly what it does.

Give sessions an end. Store a createdAt or expiresAt time with each one and treat old ones as missing. Set a matching maxAge on the cookie, such as a week, so the browser and the server agree.

What should the login and logout routes do?

Login checks who the person is, creates the session and sets the cookie. Here the check is only a name from the members list, so you can see the session parts clearly. The next lesson adds proper passwords. Once a password check exists, also create a fresh id on every successful login and never reuse one the browser sent in. Reusing an id from before login lets an attacker plant a known id and wait.

Logout has two jobs. sessions.delete(req.cookies.sid) ends the session on the server, and res.clearCookie('sid') tells the browser to forget the id. Clearing only the cookie is a common slip: if the id was copied anywhere, it keeps working until the server forgets it. Make logout a POST, so a link or an image on another site can't log people out.

app.post('/logout', (req, res) => {
  sessions.delete(req.cookies.sid);
  res.clearCookie('sid');
  res.redirect('/');
});

Which cookie settings does a session id need?

A session id is a password with an expiry date, so its cookie needs every protection. httpOnly: true keeps page scripts from reading it. sameSite: 'lax' stops other sites from sending it along with requests they trigger in the background. secure: true keeps it off plain HTTP, and belongs on any live site, usually as secure: process.env.NODE_ENV === 'production'.

Keep the cookie's contents to the id alone. Name, role and anything else stay in the session on the server, where the visitor can't change them. If the id leaks, deleting the session from the store cuts it off at once, which a cookie full of data can't offer.

How can you see sessions working here?

Press **Log in as wren** with the finished code. The Requests view shows the POST /login answering 302 with Set-Cookie: sid=...; HttpOnly; SameSite=Lax, followed by the GET /me that carries the id back. The Data view's cookie jar now lists sid with a long random value, and no name anywhere.

Then press **Fake cookie** and see the 401. Press **Log out**, send GET /me again, and see the session end. If you want to see the store itself, add console.log(sessions.size) inside a route and read the Log. Remove it afterwards, and never log the ids themselves: anyone who can read a log full of live session ids can log in as those people.

What do AI tools get wrong about sessions?

Watch for ids made with Math.random(), Date.now() or an incrementing number. They look random in testing and fall over the moment someone tries. Check that the cookie holds only the id, and that the code never reads a role or user id straight from a cookie or a request body and trusts it.

Generated logout routes often clear the cookie and forget to delete the session, or use GET. Generated session code also tends to skip expiry entirely, which leaves sessions alive forever. When an AI tool adds express-session, read its options: it needs a secret from the environment (see the lesson on secrets), cookie: { httpOnly: true, sameSite: 'lax', secure: ... }, and a real store for production instead of its in-memory default.

Field notes

  • A cookie that holds a name can be edited, so anyone could pretend to be someone else.
  • A session keeps the visitor's details on the server, filed under a long random id.
  • Make session ids with crypto.randomUUID(), never with Math.random() or a counter.
  • Send the id in an httpOnly, sameSite: 'lax' cookie so page scripts can't read it.

Easy to get wrong

A timestamp can be guessed to within a few seconds of any login. Session ids must come from a secure random source like crypto.

Anything stored in a cookie can be edited by the visitor. Keep roles and names inside the session on the server.

Clearing the cookie only affects this browser. Deleting the session makes the id worthless everywhere, including any copy someone else made.

Questions people ask

Why does my session disappear when I edit the code?

The practice server restarts on every edit, and the sessions live in a Map in memory, so they are wiped. A real server with an in-memory store behaves the same way after a restart, which is one reason live sites keep sessions in a database.

Should I use express-session instead of writing this myself?

For a real project, a well-maintained session package or your hosting platform's tools save work and handle expiry and storage for you. Writing it once by hand, as here, makes the package's options make sense when you read them.

What about tokens such as JWTs?

A signed token puts the session data in the cookie and signs it so the server can detect changes. It avoids a store, but you can't easily end one early, for example on logout. For a website with its own server, a random session id and a server-side store is the simpler and safer default.

How long should a session last?

Match it to the risk. A bird club site can keep members logged in for a week or two. Anything involving money or private data usually ends sessions after a short idle time and asks for the password again before sensitive changes.

Keep going