CodingNic

Authentication and User Security

Sessions and cookies

Authentication and User Security 18 min read

Sessions and cookies

Sessions and cookies

In Lesson 0 we said authentication works in four stages, and the last one — session — is what makes the user “stay logged in” across pages. But what is a session, actually? And what’s a cookie, and how do the two work together?

This lesson answers those questions before we touch any real authentication code. By the end you’ll understand exactly what happens behind the scenes every time you click “Stay signed in” on a website — and you’ll have built a tiny throwaway app that lets you watch a session work with your own eyes.


The problem: HTTP forgets everything

Here’s something strange about the web. Every time your browser asks a server for a page, it’s a brand new conversation. The server has no memory of what came before.

You can prove it to yourself. Open a website, log in, and then imagine the server’s point of view as you click around:

  • Request 1: “GET /login” — Someone wants the login page. Send it.
  • Request 2: “POST /login” with email + password — Someone is submitting credentials. Check them.
  • Request 3: “GET /dashboard” — Someone wants the dashboard. But who?

By request 3, the server has already forgotten the login from request 2. To HTTP, every request is a stranger walking up to the counter for the first time.

This forgetfulness is called statelessness, and it’s a deliberate design choice — it makes the web scalable and simple. But it creates a problem: how does the server remember who you are between clicks?

The answer is sessions and cookies.


A cookie is a small piece of text that the server asks the browser to save and send back on every future request.

That’s the whole idea. When the server wants the browser to remember something, it includes a special header in the response:

http
Set-Cookie: session_id=abc123

The browser sees this header, saves the cookie, and from that moment on, every request to the same site includes the cookie in its headers:

http
Cookie: session_id=abc123

The server reads the cookie on each request and uses it to recognise the visitor. The browser does this automatically — you never have to write code to send a cookie. That’s how clicking from /dashboard to /profile still feels like the server knows you.

A cookie can hold any small text value. The server decides what to put in it. The browser just stores it and faithfully sends it back.


What a session is

A cookie alone is just a piece of text. The interesting part is what the server does with it.

A session is the server’s record of who a visitor is and what they’re doing. It usually contains things like:

  • The user’s ID (so we know which row in the database is theirs)
  • When they logged in
  • Maybe their preferences or temporary data

The session lives on the server. The cookie just holds a key — usually called a session ID — that points to the session record.

Think of it like a coat check at a restaurant. You hand over your coat, the attendant gives you a numbered ticket. The ticket itself is small and almost meaningless — but when you bring it back, the attendant uses the number to find your coat. The ticket is the cookie. The coat (and the rack it sits on) is the session.

This separation matters. The browser never sees the actual session data. It only holds the ticket. If someone steals the cookie they get the ticket, not the full session record — but they can still impersonate you while the ticket is valid. (That’s why cookies need careful security, which we’ll cover later in the module.)


The full round-trip

Now let’s put it together. Here’s exactly what happens from the very first visit through every later page load:

Session flow

Walk through the six steps with me:

  1. First request. A visitor opens the site. Their browser sends a normal request to the server. There’s no cookie yet, because they’ve never been here before.

  2. The server creates a session. Behind the scenes, the server creates a record — usually in memory or in a database. The record might say something like “session abc123 belongs to user 42, logged in at 14:05.”

  3. The server sends back the cookie. Along with the page, the server includes a Set-Cookie: session_id=abc123 header in the response.

  4. The browser stores the cookie. Quietly, on the user’s machine. They don’t see it. They never have to think about it.

  5. Every later request sends the cookie. When the user clicks any link or submits any form on the same site, the browser automatically attaches Cookie: session_id=abc123 to the request.

  6. The server recognises the user. It reads the cookie, looks up session abc123 in its records, sees “ah, this is user 42,” and serves the right page.

This loop repeats on every click. The user feels like the site “knows them” — but really, it’s just looking them up by the ticket they’re carrying.


How Flask handles sessions

Flask gives you all of this almost for free. There’s a built-in object called session that behaves like a Python dictionary. You put things in it; Flask handles the cookie machinery automatically.

A tiny example:

python
from flask import Flask, session, redirect, url_for

app = Flask(__name__)
app.secret_key = "change-this-to-a-long-random-string"

@app.route("/sign-in-as-alice")
def sign_in_as_alice():
    session["user_id"] = 1
    session["user_name"] = "Alice"
    return redirect(url_for("welcome"))

@app.route("/welcome")
def welcome():
    name = session.get("user_name", "guest")
    return f"Welcome, {name}!"

What’s happening here is quietly powerful:

  • When you set session["user_id"] = 1, Flask packages the session data, signs it with your secret_key, and sends it back as a cookie automatically.
  • When the user visits /welcome, Flask reads the cookie, unpacks it, and gives you back a familiar Python dictionary.
  • The browser never sees raw session data, and even if someone tampered with the cookie, the signature would no longer match — and Flask would reject it.

You don’t write any Set-Cookie headers yourself. You don’t generate session IDs by hand. You just put things in session and read them back, the way you would with any dictionary.

About secret_key: This is the value Flask uses to sign every session cookie. It must be long, random, and kept private. If your secret leaks, anyone can forge sessions. We’ll set this up properly in Lesson 4.


Server-side sessions vs signed cookies

There’s a subtle detail worth knowing now, because it’ll come up later.

By default, Flask uses signed cookies — the session data itself is packed into the cookie and sent to the browser. It’s protected from tampering by the signature, but the user can read it (it’s just base64-encoded). This works great for small amounts of data like a user ID.

Real production apps often use server-side sessions — the cookie only holds an opaque session ID, and all the actual data lives on the server. This is more like the coat-check analogy. It’s safer for storing larger or more sensitive data.

For this module, Flask’s default signed cookies are perfect. You’ll know when you need to upgrade to server-side storage — and there’s a Flask extension called Flask-Session that handles it for you.


Hands-on: a tiny session counter

Theory’s good, but you’ll understand sessions ten times better after running a real one. Let’s build the smallest possible Flask app that uses the session — a counter that remembers how many times you’ve clicked.

No database, no users, no login form. Just a number that survives between page loads, scoped to your browser.

Project structure

Create a new folder anywhere outside your Books app. The whole demo is two files:

text
lesson-1-counter/
├── session_counter.py
└── templates/
    └── index.html

session_counter.py

python
# session_counter.py
from flask import Flask, session, render_template, redirect, url_for

app = Flask(__name__)
app.secret_key = "dev-only-change-me"


@app.route("/")
def index():
    count = session.get("count", 0)
    return render_template("index.html", count=count)


@app.route("/increment", methods=["POST"])
def increment():
    session["count"] = session.get("count", 0) + 1
    return redirect(url_for("index"))


@app.route("/reset", methods=["POST"])
def reset():
    session.pop("count", None)
    return redirect(url_for("index"))


if __name__ == "__main__":
    app.run(debug=True)

Three routes, about twenty lines. Watch what each one does:

  • / reads count from the session, defaulting to 0 if it isn’t there. Renders the page.
  • /increment reads the count, adds one, writes it back to the session, redirects home. Flask handles the cookie automatically.
  • /reset removes count from the session entirely.

templates/index.html

html
<!-- templates/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Session counter</title>
  <style>
    body {
      font-family: system-ui, -apple-system, sans-serif;
      max-width: 480px;
      margin: 80px auto;
      padding: 0 20px;
      color: #1f2a26;
      text-align: center;
    }
    h1 { font-size: 22px; margin-bottom: 0.5rem; }
    p.lead { color: #6b7a72; font-size: 14px; }
    .count {
      font-size: 64px;
      font-weight: 700;
      margin: 32px 0;
      color: #4a6b5c;
    }
    form { display: inline-block; margin: 0 4px; }
    button {
      padding: 10px 18px;
      border-radius: 8px;
      border: 1px solid #d8d3c7;
      font-size: 14px;
      cursor: pointer;
      background: #fff;
    }
    button.primary { background: #4a6b5c; color: #fff; border-color: #4a6b5c; }
    button.danger  { color: #a33a2a; border-color: #e5c5bc; }
  </style>
</head>
<body>
  <h1>Session counter</h1>
  <p class="lead">Each click increments a number stored in your session.</p>

  <div class="count">{{ count }}</div>

  <form action="{{ url_for('increment') }}" method="post">
    <button type="submit" class="primary">+ Increment</button>
  </form>
  <form action="{{ url_for('reset') }}" method="post">
    <button type="submit" class="danger">Reset</button>
  </form>
</body>
</html>

Run it

From the project folder:

bash
pip install flask
python session_counter.py

Then open http://127.0.0.1:5000 and click Increment a few times. The number goes up, and it stays up across page refreshes — because the value is in your session, attached to your browser via your cookie.

Open your browser’s dev tools (F12), go to Application → Cookies, and click 127.0.0.1. You’ll see one cookie called session. Its value is a long opaque string — that’s your signed session data, packed inside the cookie.

Try these three small experiments:

  1. Open the same URL in a different browser (or a private window). You’ll see count = 0. Your two browsers have separate cookies, so separate sessions.
  2. Delete the session cookie in dev tools, then refresh the page. Count is back to 0. The “ticket” is gone — the server has nothing to look up.
  3. Click Increment again. A new cookie appears immediately. The server just started a new session for you.

That’s the whole authentication mechanism, condensed into a counter. Sessions in real apps store user_id instead of count, but the plumbing is identical.


You’ve seen the mechanism in your own demo. Now look at a real one.

Steps:

  1. Open any website you’re logged into (Gmail, Twitter, anything).
  2. Open dev tools (F12), go to Application → Cookies.
  3. Find the entry that looks like authentication: session_id, auth_token, sid.

Look at the columns:

  • Value — a long random-looking string. That’s the ticket. Real apps use longer, more opaque tickets than our demo, but the idea is the same.
  • HttpOnly — when checked, JavaScript on the page can’t read this cookie. Only the server can. This protects against attackers who manage to inject scripts.
  • Secure — when checked, the cookie only travels over HTTPS, never plain HTTP.
  • Expires — how long the cookie lives.

If you’ve ever wondered how Gmail just knows you’re you when you open a new tab, this is the entire mystery. A cookie called something like __Secure-1PSID is being sent on every request, and the server is looking it up to recognise you.


Summary

  • HTTP is stateless — every request is a fresh stranger to the server.
  • Cookies are small pieces of text the browser saves and sends back automatically.
  • Sessions are server-side records of who a visitor is. The cookie just holds the key.
  • The flow is always: server creates session → sends cookie → browser stores it → browser sends it back on every request → server uses it to recognise the user.
  • Flask gives you a session object that handles all of this with a dictionary-like API.
  • You just built and ran a working session-based counter. It uses the same machinery a real login system does.

Outcome

You now understand how a server remembers a user between requests, and you’ve watched the mechanism (cookies + sessions) work in your own browser. Next, we’ll move from counters to people — building a tiny app where you can register actual user accounts.