Managing user sessions: localStorage vs sessionStorage vs cookies

Latest

Auth & identity

March 5, 2024

Author: Isaac Ejeh

To deliver consistent and frictionless user experiences on the browser, web applications leverage several storage options to persist user preferences and authentication data across multiple sessions.

While HTTPS adds a layer of encryption to the communication between your browser and web servers, it doesn’t inherently maintain state. This means that each HTTP request is handled independently, and the server doesn’t track or store data from previous interactions with your browser.

This statelessness is important for scalability and performance reasons, as it allows the protocol to handle large numbers of concurrent requests without the burden of retaining information about each request or user. However, from a UX perspective, it presents a challenge, as users would have to re-enter their credentials and preferences every time they visit a site.

To overcome this limitation, web developers rely on sessions or tokens to retain auth information across multiple requests and sessions. Both session IDs and access and/or refresh tokens can potentially be stored as a cookie or in web storage. The major difference lies in the level of security and storage space they offer, depending on your use case.

These session identifiers and tokens (either JWT or opaque)  are typically generated by the backend and issued to the frontend where they are stored for the duration of the user’s login session. As such, if these tokens are compromised, an attacker could gain unauthorized access to the breached account and its associated privileges or data.

In this article, we explore the differences, benefits, and drawbacks of using web storage and cookies to store auth data like sessions and tokens. We cover best practices and security considerations for handling user sessions on the client side, with particular emphasis on end-user experience.

Handling authentication data on the browser

So far, we’ve established sessions and tokens as the most popular means of persisting authentication data on the browser.

In the case of sessions, whenever a user logs in to a website using their credentials, the server generates a unique session ID. This ID is stored in the server’s database and a copy is sent back to the user’s browser for storage. With each subsequent request, the browser includes the stored session ID, enabling the server to identify the user and access their associated session data. By using session IDs, a server can determine the state of a user, such as whether they are logged in or out, and maintain a stateful connection between the client and server.

While session IDs are a popular way of managing authentication state on the web, they do come with several drawbacks. One significant concern is their vulnerability to cross-site request forgery (CSRF). In a CSRF attack, an attacker can trick a logged-in user into performing unintended actions on a website by manipulating their browser to send requests with valid session IDs.

Another disadvantage of session IDs is the need to store them in a database or retain them in memory on the server. This can create a significant bottleneck in production environments due to the high volume of browser-server requests required to deliver a consistent user experience across sessions.

This is why many web applications now leverage token-based authentication as an augment or alternative to session IDs. These tokens are cryptographically signed and contain information about a user and their permissions. The server generates these tokens using a private key and sends them to the client for storage in the browser.

During subsequent requests, the client only needs to include the access token in the authorization header, prefixed with the bearer, and the server can validate the signature of the token without needing to perform a database lookup.

It’s very common for microservices architectures to leverage JWTs over traditional session identifiers because each microservice can independently verify JWTs without relying on a central database or authorization logic.

Session management using local storage

Local storage is a type of browser storage that enables web applications to store and retrieve data in key-value pairs within a user’s browser. It can be accessed via the Web Storage API using the global localStorage object in Javascript.

Most modern web browsers provide a local storage capacity of around 5–10MB per domain or origin accessed by the user. This dedicated storage space can be utilized to store user-specific data, such as preferences, temporary files, or even settings.

One key advantage of local storage is that data is persistent across browser tabs and sessions. This eliminates the need for constant communication with the server to retrieve the same data, resulting in reduced network latency and faster page load times.

Additionally, data stored in local storage remains accessible even when the user is offline or has a poor internet connection. This enables web applications to provide limited yet valuable functionality in offline mode, enhancing the user’s experience and ensuring uninterrupted usage.

In this way, storing authentication data in local storage might appear to be a logical option for newbie web developers. However, this practice will expose your application to potential XSS attacks.

In cross-site scripting (XSS) attacks, hackers inject malicious code, typically Javascript, into an app or website. The attacker’s goal is to trick users into executing this harmful code when they load the malicious content in their browser. If an attacker successfully runs this malicious script within your application, they could steal your tokens from local storage and initiate authentication requests directly.

A successful XSS attack can give the attacker access to a user’s authentication token or credentials, allowing them to hijack the user’s account, remotely control their browser, or spread malware on the host system. To mitigate the risk of XSS attacks, it’s not advisable to store plain authorization tokens in local storage.

Storing authentication data in session storage

Session storage is another storage mechanism provided by the Web Storage API. Unlike local storage, session storage is tied to a specific browser session and is deleted once the session ends. This makes it ideal for storing temporary data that needs to be accessible within a single session but doesn’t need to persist across multiple sessions or browser restarts.

In terms of security, session storage is commonly deemed more secure than local storage because it automatically deletes data when the browser or tab is closed. However, similar to local storage, session storage is susceptible to XSS attacks. Attackers can steal your tokens from the storage and use them to impersonate users. As such, it’s also not advisable to store session tokens and other sensitive data in session storage.

Aside: using Stytch’s JSON Web Tokens (JWTs), developers can locally verify user sessions without having to contact Stytch’s servers on every request. You can set custom expiration times, revoke tokens as needed, and implement additional security measures to protect user accounts, both manually and programmatically. Explore our docs to learn more.

Using cookies to manage user sessions

Cookies are tiny pieces of data that are sent from a server to a user’s browser, where they are stored in a text file. Each time the browser requests content from the server, the cookie is usually transmitted, allowing the server to recognize the user and tailor their experience.

Cookies are relatively lightweight (4 KB per domain), making them ideal for storing short-lived data such as session IDs, JWTs, and other sensitive information that needs to be temporarily accessed by the web server. 

The lifespan of a cookie is determined by its time-to-live (TTL) or expiration time, which essentially dictates the duration of an active session. When a cookie expires, the associated session information in the database becomes outdated, prompting the user to re-authenticate to establish a new session.

By default, browsers automatically transmit all cookies, including authentication cookies, whenever they make requests to any website. This behavior can be exploited by malicious websites disguised as legitimate sites, leading to cross-site request forgery (CSRF) attacks.

This means that if a user is logged into a legitimate website (such as an online banking platform) and then visits a malicious website that is disguised as legitimate, the malicious website can exploit the browser’s default behavior. The malicious website can create a request that appears to come from the user, and this request can trigger an action on the legitimate website that the user is already logged in to. 

Nonetheless, cookies possess attributes that can mitigate this risk and improve their overall security. One example is the SameSite attribute, which reduces the risk of cross-site request forgery (CSRF) attacks. When the SameSite attribute is set to “Strict,” the browser includes the cookie only in requests that originate from and target the same site as the cookie’s origin.

Another security feature is the HTTP-only flag. When enabled, modern browsers prevent JavaScript and other malicious CDN scripts from accessing the cookie’s value, thereby mitigating data leakage through cross-site scripting (XSS) attacks. On the other hand, the Secure attribute ensures that cookies are only transmitted over HTTPS connections, safeguarding against cookie leakage through man-in-the-middle attacks and session hijacking.

Handling sessions and JWTs: local storage vs session storage vs cookie?

Choosing the right storage option for your web application depends on your unique security requirements and constraints. For instance, cookies are best suited for storing data that needs to be readable by the server, making them ideal for handling sessions and tokens. Session storage is designed for storing data within a single session, making it suitable for temporary and tab-specific data. localStorage is the recommended choice for data that needs to be retained across multiple sessions, as it provides persistent data storage.

When it comes to handling JWTs, most modern applications store their access tokens in memory, rather than local storage or cookies. This is because storing the access token in memory makes it inherently inaccessible to JavaScript and potential attackers. However, storing tokens in memory does come with some drawbacks—if a user refreshes their page or closes the tab, any stored data will be lost.

To address this issue, many modern applications leverage refresh tokens. By performing a silent or background refresh just before the access token expires, applications can seamlessly keep users logged in without any noticeable disruptions.

Refresh tokens are typically stored in cookies, allowing users to obtain new access tokens when the old ones expire. However, to ensure security, the server issuing tokens must have appropriate cross-origin resource sharing (CORS) configuration to restrict refresh token requests only from authorized origins. Additionally, cookies used for storing refresh tokens should have the httpOnly, Secure, and SameSite attributes to protect against theft.

The thing is, always avoid storing your access or refresh tokens in non-memory or non-cookie locations. When configured appropriately, browsers effectively prevent the leakage of both tokens via cookies.

Session management via Stytch

On Stytch, user sessions are identified using either a session token (session_token) or session JWT (session_jwt). These are typically stored on the client-side, often within a browser cookie, and are used for authentication on each request.

The session token is a unique 44-character string and the session JWT is a string that follows standard JWT protocols. Both are returned on every API response and can be used interchangeably. The session JWT is a signed snapshot of a session at the time it was issued and is valid even after the session is revoked in the Stytch API.

With Stytch, developers can create and manage custom claims within their JWTs, providing a flexible and secure way to store additional user-specific information.

Stytch signs all JWTs with project-specific signing keys. The verification keys for those signatures are available at a project-specific URL, which is publicly accessible. This allows other JWT consumers outside of your app to verify JWTs from your project.

To get started, check out our documentation and sign up for a developer account. If you have any questions, please don’t hesitate to contact us at support@stytch.com.

SHARE

Get started with Stytch