Security

How to manage a session after login

September 26, 2026 · Updated September 30, 2026

Issue a new session identifier the moment the login succeeds, and put it in a cookie the browser will not hand to JavaScript or to another site. Two clocks end the session after that: one for idle time, one for total age. Logging out has to destroy the server's copy, because expiring the cookie only hides it.

A Set-Cookie header annotated attribute by attribute. The __Host- prefix stops a sibling subdomain overwriting the cookie, Secure stops it travelling over plain HTTP, HttpOnly keeps it out of document.cookie so one scripting bug cannot ship every session, and SameSite=Strict stops the browser attaching it to another site's request. Below, an idle timer of 15 to 30 minutes and an absolute timer of 4 to 8 hours, both enforced on the server record instead of by the cookie expiry.

One header carries the whole decision:

Set-Cookie: __Host-id=9f2b7c1e4a8d6b03f5e1c9a2d7b4e806; Secure; HttpOnly; SameSite=Strict; Path=/
AttributeWhat it stops
SecureThe cookie travelling over plain HTTP, where anyone between the user and you can read it
HttpOnlydocument.cookie reaching it, so a cross-site scripting bug cannot ship the session off the page
SameSite=StrictThe browser attaching it to requests another site started, which is most of cross-site request forgery
__Host- prefixA page on a sibling subdomain overwriting it. The prefix is only honoured when the cookie is Secure, has no Domain, and has Path=/
Path=/Nothing on its own here, and the prefix requires it

The OWASP Session Management Cheat Sheet states that session cookies must set SameSite=Strict where possible and Lax otherwise, and that SameSite=None is never used without Secure. Add Cache-Control: no-store to any response that sets one, so a shared proxy does not keep a copy.

The identifier

The value in that cookie comes from a cryptographic random source and carries at least 64 bits of entropy. OWASP puts the floor there and notes that 16 hexadecimal characters reach it. Framework defaults sit well above it, and an identifier built from a counter or a timestamp sits well below.

It should mean nothing by itself. Encoding the user id or the role into the value hands your data model to anyone who decodes one, and it invites an attacker to edit the value into another account. The server keeps a table mapping the identifier to the account, and the client keeps a number that is useless without that table.

Rename it while you are there. A framework default announces your stack to anyone reading a response header, which is the first step in choosing an exploit: PHPSESSID for PHP, JSESSIONID for Java, ASP.NET_SessionId for .NET. OWASP suggests a generic name such as id, which tells a reader nothing about what is serving the page.

Regenerate it on every change of privilege

A session fixation attack works by planting a known identifier in the victim's browser before they log in, then reusing it afterwards. The defence is one line of code in the login handler: throw the pre-login identifier away and issue a new one once the password and any second factor have been checked.

The same attack twice. Without regeneration, the attacker plants a known identifier, the victim logs in while that identifier is kept, and the planted value is now a logged-in session the attacker can use. With regeneration, a new identifier is issued at login and the planted one points at nothing.

OWASP puts it as a requirement that the identifier is renewed after any privilege level change. In practice that means a fresh identifier at login, and another one whenever the account's rights change, which covers a password change and a step up to administrative access. Ordinary navigation keeps the same identifier, and rotating it on every request breaks the back button and multiple tabs for no gain.

The two timers

TimerEnds the sessionValues from OWASP
IdleAfter a period with no requests15 to 30 minutes for low risk applications, 2 to 5 minutes for high value ones
AbsoluteAfter a fixed period, whatever the user is doing4 to 8 hours for an application used through a working day

Both are enforced on the server, on the session record. A cookie Expires date is a hint the client is free to ignore, and a session that is still valid server-side after the cookie is gone is a session an attacker can use.

Long-lived sign-in is a separate mechanism. Raising the absolute timeout to a month gives you month-long sessions, which is the thing you were trying to avoid. Keep the remembered device as its own single-use token in its own cookie: each use exchanges it for a fresh session and replaces the token, so a stolen copy stops working the next time the real user returns.

Logging out

Three things happen, in this order:

  1. Delete the session record on the server. The framework has a call for it: HttpSession.invalidate() in Java, Session.Abandon() in .NET, session_destroy() in PHP. The record has to go whether or not the cookie does.

  2. Overwrite the cookie with an expired one, so the browser drops it.

  3. Send Clear-Site-Data: "cache", "cookies", "storage" on the logout response, which tells the browser to clear what the page left behind.

Logging out everywhere is the same operation against every session record for that account. A password change runs it for you, per How to build a password reset flow.

Not in local storage

OWASP is blunt about where a session identifier does not go: do not store authentication tokens, session identifiers, JSON web tokens, refresh tokens or any credential in localStorage or sessionStorage. Anything reachable by JavaScript in the origin is reachable by injected JavaScript in the origin, which turns one cross-site scripting bug into every account it can reach.

A single-page application that wants a token in the browser wants an HttpOnly cookie and a small server in front of its API, holding the token and attaching it to outgoing calls. OWASP names that the backend-for-frontend pattern.

What to record

Every login and every session invalidation is worth a log line, and the identifier itself never appears in one. How to log a login attempt covers the fields.

Sources