Sessions, Cookies & Filters

Cookies

18 min Lesson 4 of 13

Cookies

A cookie is a small piece of data that the server sends to the browser, and the browser automatically attaches to every subsequent request to the same origin. Despite their simplicity, cookies are the bedrock of web identity: they carry session tokens, remember user preferences, and power analytics. This lesson covers how to create and read cookies in a Jakarta Servlet, what every attribute controls, and the security decisions a working developer must make deliberately.

How Cookies Flow

The mechanics follow HTTP precisely. When a server wants to set a cookie it adds a Set-Cookie response header. The browser stores the cookie and includes it in a Cookie request header on every matching future request. The Servlet API wraps this header dance in the Cookie class so you work with objects rather than raw header strings.

Cookies live in the browser, not the server. The server has no way to enumerate or delete a cookie without the browser's cooperation — "deleting" a cookie means telling the browser to expire it immediately by setting maxAge to 0.

Creating a Cookie

Construct a jakarta.servlet.http.Cookie, configure its attributes, and add it to the response before getWriter() or getOutputStream() is committed:

import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServlet; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; @jakarta.servlet.annotation.WebServlet("/set-pref") public class SetPreferenceServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { // 1. Create — name and value only; attributes come next Cookie theme = new Cookie("theme", "dark"); // 2. Scope: which paths send this cookie? theme.setPath("/"); // send on every request to this host // 3. Persistence: how long should the browser keep it? theme.setMaxAge(60 * 60 * 24 * 30); // 30 days in seconds // 4. Security flags theme.setHttpOnly(true); // JavaScript cannot read it theme.setSecure(true); // HTTPS only // 5. Add to response — MUST happen before body is committed resp.addCookie(theme); resp.getWriter().println("Preference saved."); } }

Reading Cookies

request.getCookies() returns all cookies the browser sent, or null if none were included. There is no "get by name" method — you iterate the array yourself. A utility method is worth writing once and reusing across servlets:

import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import java.util.Arrays; import java.util.Optional; public final class CookieUtil { private CookieUtil() {} public static Optional<String> getValue(HttpServletRequest req, String name) { Cookie[] cookies = req.getCookies(); if (cookies == null) return Optional.empty(); return Arrays.stream(cookies) .filter(c -> name.equals(c.getName())) .map(Cookie::getValue) .findFirst(); } }

At the call site:

String theme = CookieUtil.getValue(req, "theme").orElse("light");

Cookie Attributes in Detail

Each attribute on Cookie maps directly to a Set-Cookie directive. Getting them wrong costs you either functionality or security.

Max-Age and Expires — Persistence vs. Session

If you never call setMaxAge(), the cookie is a session cookie: it exists only in memory and disappears when the browser closes. Calling setMaxAge(seconds) with a positive integer makes it a persistent cookie — the browser writes it to disk and sends it again even after a restart.

  • setMaxAge(0) — instructs the browser to delete the cookie immediately (the delete pattern).
  • setMaxAge(-1) — equivalent to never calling it; the cookie is session-scoped.
  • setMaxAge(n) where n > 0 — persist for n seconds from the moment the response is received.
Prefer Max-Age over Expires. The older Expires attribute is an absolute date that is fragile if the client clock is wrong. Max-Age is relative and therefore reliable. Modern browsers honour both, but Max-Age takes precedence when both are present.

Domain and Path — Scoping Which Requests Include the Cookie

setDomain() controls which hostnames receive the cookie. Leaving it unset is the safe default: the cookie is sent only to the exact host that set it. Setting domain=example.com (with a leading dot implied) causes the cookie to be sent to api.example.com, app.example.com, etc. — useful for SSO across subdomains, but it widens the attack surface.

setPath() restricts cookie sending to URLs under that path. Setting /admin means only requests to /admin/* carry the cookie. Setting / means all requests to the host carry it — appropriate for a user identity cookie, but wasteful (and slightly risky) for a cookie that is only needed by one section of your site.

HttpOnly — Blocking JavaScript Access

An HttpOnly cookie is invisible to document.cookie in JavaScript. This single flag eliminates the most common consequence of XSS attacks: a script injected into your page cannot steal the session cookie. Always set this flag for authentication cookies.

Secure — HTTPS Enforcement

A Secure cookie is transmitted only over TLS. Without this flag, the cookie rides along with HTTP requests in plaintext — readable by anyone on the same network segment. In production, all sensitive cookies must be Secure. During local development you may omit it (since localhost is typically HTTP), but it must be on before you deploy.

SameSite — Cross-Site Request Forgery Mitigation

The Servlet API did not add native SameSite support until very recently. In Jakarta EE 10 / Servlet 6.0, Cookie gained setAttribute("SameSite", "Strict"). On older containers you must set it by writing the raw header:

// Jakarta EE 10 / Servlet 6.0+ (preferred) Cookie token = new Cookie("session_token", value); token.setAttribute("SameSite", "Strict"); // Strict | Lax | None // Older containers — write the Set-Cookie header manually resp.setHeader("Set-Cookie", "session_token=" + value + "; Path=/; HttpOnly; Secure; SameSite=Strict");
  • Strict — cookie is never sent with cross-site requests at all. Strongest CSRF protection; breaks flows where a link from another site should land the user in their existing session.
  • Lax — cookie is sent with top-level navigations (clicking a link) but not with embedded sub-requests (image, iframe, fetch). The browser default in most modern browsers.
  • None — cookie is always sent cross-site. Requires Secure. Use only for third-party integrations (payment widgets, embedded maps).

Deleting a Cookie

There is no "delete" API. Send a replacement cookie with the same name and path, but with maxAge set to 0:

Cookie expire = new Cookie("theme", ""); expire.setPath("/"); expire.setMaxAge(0); resp.addCookie(expire);
Name and Path must match exactly. A cookie named theme set on path / is a different cookie from one named theme set on path /app. Sending a zero-age cookie on the wrong path leaves the original untouched and creates a new expired one — which the browser ignores. This is a common, hard-to-debug bug.

Practical Security Checklist

Before shipping any cookie to production, verify:

  1. HttpOnly is set on all authentication and session cookies.
  2. Secure is set — and your load balancer or reverse proxy enforces HTTPS.
  3. SameSite=Lax or Strict is set on session cookies.
  4. Cookie values that carry security-sensitive data (session IDs, CSRF tokens) are randomly generated and not user-controlled — never store the username itself as a cookie value.
  5. Path is as narrow as functionally possible.
  6. Persistent cookies expire in a reasonable time window — not 10 years.

Summary

Creating a cookie takes three lines: construct a Cookie, configure its attributes, call resp.addCookie(). Reading one requires iterating req.getCookies() — a utility method saves repetition. The real craft is in the attributes: maxAge decides persistence, path and domain scope delivery, and httpOnly + secure + sameSite collectively determine whether your cookies are safe to carry sensitive identity data. In the next lesson you will see how sessions use a single HttpOnly cookie as their transport mechanism — and why that design decision matters.

ES
Edrees Salih
1 hour ago

We are still cooking the magic in the way!