Session management best practices

Latest

Auth & identity

March 14, 2024

Author: Isaac Ejeh

Sessions are used to uniquely identify users, determine access privileges, and maintain their login state within an application. This relieves authenticated users from having to repeatedly enter their login credentials, ensuring a seamless and personalized user experience.

In monolithic applications, these user sessions are stored in server memory or a centralized session store/database. As such, whenever a user successfully authenticates their identity, the server generates a unique session on the server-side and sends the associated session ID to the client. This session ID must then be included in all subsequent client requests made to the server, allowing the server to associate each request with the corresponding user session.

However, in stateless microservices architectures, where multiple services are running concurrently on separate servers or containers, session management becomes more complicated than in simple monolithic web apps running on a single server.

This is why microservices leverage token-based authorization mechanisms like JSON Web Tokens (JWTs) to manage session data. These self-contained tokens can be used to manage user privileges and session information across multiple servers and services, without having to make frequent requests to a central server or session store.

In this article, we’ll explore how web developers can effectively create and manage user sessions on the client and server-side, as well as in typical monolithic and microservices environments, without compromising user experience, security, performance, and scalability of the system. We’ll cover five key session management best practices, and common session vulnerabilities, and show you how Stytch makes it easy for web developers to manage both traditional sessions and session JWTs.

Stateful (sessions-based) session management

In stateful (sessions-based) session management, the server maintains user information and state throughout multiple requests within a single session. As we mentioned before now, user data and session information are typically stored on the server, often in a database or memory. This unique session ID is exchanged between the client (browser) and the server with each request, either via cookies or by embedding it in the request URL (though the latter is considered less secure).

Throughout a user’s interaction with an application, session data remains on the server, persisting even if the user closes the browser and returns later, as long as the session is valid. However, once the session expires, the associated session information is invalidated by the server. Active sessions can be terminated due to user inactivity, security-related timeouts, or user actions, such as logging out.

Stateful session management is generally considered more secure against client-side attacks like cross-site scripting (XSS), since sensitive session information resides on the server, making it less susceptible to manipulation on the frontend. However, session data can also be stored on the client-side, typically using cookies.

Cookies are small text files sent by the server and stored in the user’s browser. They are then included in subsequent requests, allowing the server to identify the user and their session. However, cookies can also be vulnerable to attacks like session hijacking, XSS, and cross-site request forgery (CSRF) if not attributed properly or stored in insecure web storage locations.

Stateless (token-based) session management

In stateless (token-based) session management, the server doesn’t store session information between requests. Each client request is independent, containing all the necessary session and auth information, usually in a compact JSON Web Token (JWT).

For example, when a user logs into a web application that leverages tokens, the server authenticates their credentials and creates a signed JWT containing the user’s session information, known as claims. This signed JWT (also referred to as access token) is then sent back to the user’s browser, where it’s usually stored in a secure cookie for subsequent client requests.

However, there are more secure ways to handle JWTs. A safer method is to store access tokens in memory rather than in local storage or cookies, but this comes with a caveat.  If a user refreshes the page or closes the browser tab/window, all data stored in memory, including the access token, will be erased.

This is why web applications use a combination of access tokens and refresh tokens. To maintain seamless user experiences without compromising security, refresh tokens are securely stored in client-side cookies, while access tokens are retained on the server. Before an access token expires, the refresh token is used to silently refresh it, preventing any noticeable disruptions to the user.

Managing sessions in monoliths vs microservices

In monoliths, session data is typically stored in a centralized session store, such as a database or an in-memory cache, which is accessible by all components of the application. However, in a microservice architecture, each service is designed to be independent and self-contained, making it challenging to share session data across multiple services.

In a monolithic app, authentication and authorization are handled by a single module or middleware, which can grant or deny access to various parts of the application based on the user’s session data. However, in a microservice architecture, where each service is running on separate servers or containers and may have its own auth framework, this approach isn’t practical, as services can’t communicate or exchange data locally.

To address this challenge, microservices architectures often rely heavily on token-based authentication mechanisms, such as JSON Web Tokens (JWTs). JWTs are self-contained tokens that can encapsulate the necessary user information and can be propagated across multiple services, eliminating the need for back-and-forth requests to a centralized session store.

Nonetheless, managing token lifecycles, including issuance, revocation, and ensuring consistent validation across all services can be complex. This often requires a central authorization server that can be shared by multiple services or instances.

While there are some drawbacks to this kind of centralized authorization, such as the increased complexity and risk of a single point of failure, the performance and scalability benefits often outweigh these risks.

Many microservices architectures use a sidecar decoupling pattern in which supporting functions of the main service, such as session management and auth, are provided by sidecars attached to this main service.

Auth sidecars are co-located with their associated microservice, sharing the same server, pod, or container. This ensures that any failures or restarts are isolated to that service. This approach shifts authorization responsibilities from the main service to the auth sidecars, which communicate directly with the centralized auth service.

Aside: on Stytch, session tokens and session JWTs are completely interoperable, and both are returned on every API response so that developers can use whichever option is best for their application. Explore our docs to learn more.

Common session vulnerabilities

Session hijacking

An attacker can unlawfully gain control over a legitimate user’s active session by stealing their session identifier or token after the user has successfully logged in. 

In situations where the attacker and the victim share the same network, such as a public Wi-Fi connection, the attacker can eavesdrop on unencrypted network traffic. This makes it possible for the attacker to intercept communication between the victim’s device (client) and the targeted server, potentially compromising the integrity and confidentiality of the session ID or token. Armed with this stolen token, the attacker can then impersonate the legitimate user and perform actions on their behalf, effectively hijacking their session.

Furthermore, storing session IDs or tokens in local storage or insecure cookies on the client-side can also make applications vulnerable to attackers. In such a scenario, a cross-site scripting (XSS) attack can introduce malicious code that extracts the user’s session information from the frontend and transmits it to the attacker.

To prevent session hijacking, always employ secure communication protocols like HTTPS and TLS/SSL to encrypt data transmission between clients and servers. Furthermore, to mitigate XSS attacks, always implement secure cookie flags, enforce automated session timeouts and expiration, and mandate user re-authentication when users perform sensitive actions within the app.

Session fixation

Session fixation vulnerabilities occur when web applications fail to properly generate or validate session IDs before authorizing access. Attackers can exploit these vulnerabilities by tricking the victim into adopting a predetermined identifier, often through phishing or malicious links. Once a user interacts with a link or email containing this attacker-determined session ID, the attacker can impersonate the user, control their account, perform actions on their behalf, or gain access to sensitive information.

To protect against session fixation, web applications leverage strong cryptographic algorithms and entropy sources to generate random and unpredictable session IDs. This significantly reduces the chances of an attacker successfully guessing or predicting a valid session ID, even if they have access to an old one.

The most secure web applications always enforce strict server-side validation for each request. This ensures that session IDs are legitimate and linked to the correct user before granting access. Furthermore, implementing short session durations that automatically expire when the user is inactive effectively minimizes potential vulnerabilities and exposure, even in the event of a session compromise.

Man-in-the-Middle attacks

In man-in-the-middle attacks, attackers use an intermediate server to sniff and intercept HTTP requests between a user’s device (client) and a web server. This allows attackers to potentially steal sensitive data like passwords, credit card information, or even session IDs.

To ensure secure data transmission and prevent MitM attacks, it’s important to use HTTPS connections for the entire web session.  HTTPS encrypts all communication between a user’s device and the server, making it unreadable to attackers, even if they intercept it.

Cross-site request forgery (CSRF)

Cross-site request forgery (CSRF) exploits a user’s existing login session to trick their browser into performing unauthorized actions. Unlike session hijacking, CSRF attackers don’t steal session IDs or tokens. Instead, they create malicious links, forms, or scripts that a user may interact with on a seemingly legitimate website. The browser unknowingly leverages the user’s active session to run these malicious scripts and execute the attacker’s commands, which could include stealing funds, changing account settings, or even sending emails, all without the user’s knowledge or consent.

Nonetheless, web applications can implement several security measures to protect against CSRF attacks. One approach is to use anti-CSRF tokens. These are unique, unpredictable values that are embedded within forms and validated on the server-side. This ensures that the submitted form originates from a legitimate user and not an attacker.

Additionally, when using cookies, setting the SameSite attribute as either ‘Strict’ or ‘Lax’ can further mitigate CSRF risks by restricting the context in which cookies are sent during cross-origin requests. This makes it harder for attackers to intercept the user’s cookies (which might contain a session ID) to perform unauthorized actions on the targeted website.

5 session management best practices

Exclusively store authorization data on the server-side

Never store sensitive authorization data, such as session IDs, tokens (JWTs), user credentials, or roles and permissions, on the client-side (e.g., in browser storage or insecure cookies). This information should always be stored and managed on the server-side, where it can be properly secured and protected from potential client-side vulnerabilities and unauthorized access.

Implement secure cookie flags and attributes

When using cookies for session management, it’s important to set the appropriate flags and attributes to enhance their security. The following are some of the most important flags and attributes to consider:

  • HttpOnly: This flag instructs the browser to prevent client-side scripts, including malicious scripts from accessing the cookie. This can help protect against cross-site scripting (XSS) attacks.
  • Secure: This flag ensures that the cookie is only transmitted over secure (HTTPS) connections. This helps to protect against eavesdropping and man-in-the-middle attacks, that could potentially capture session IDs from insecure browser traffic.
  • SameSite: This attribute restricts cross-site cookie sharing, thereby mitigating the risk of CSRF attacks. To prevent unauthorized cross-site requests from accessing or modifying session cookies, you can set the SameSite cookie attribute to “Strict” or “Lax” based on your application’s requirements.
  • Expires/Max-Age: These attributes define the lifespan of a cookie. When set, the cookie will remain persistently stored on the browser until the specified expiration time or max age.
  • Domain: This attribute instructs the browser to only send the cookie to a specific domain and its subdomains. It prevents cookies from being sent to unintended destinations.
  • Path: This attribute instructs the browser to only send the cookie to a specific directory or subdirectory within a domain.

Avoid predictable session IDs

To mitigate the risk of unauthorized access and brute-force attacks, it’s important to use cryptographically secure random number generators (CSRNGs) or strong cryptographic algorithms with sufficient entropy to generate highly secure session IDs. These algorithms are designed to produce truly random and unpredictable output, reducing the likelihood of collisions or patterns that could be exploited by attackers.

Always generate session IDs with a length of at least 128 bits (or 32 characters for Base64-encoded IDs). Using longer session IDs significantly increases the complexity and computational effort required for brute-force attacks, making them practically infeasible for attackers to guess or deduce. Avoid using predictable patterns or sequential numbering to generate session IDs, as these weaknesses can be easily exploited by attackers.

Implement manual session expiration (logout functionality)

Always provide users with a clear and accessible way to manually terminate their sessions, such as a “Logout” button or link in a prominent location on the user interface. This ensures that users can securely end their sessions when they are finished, preventing unauthorized access to their accounts or data.

Upon logging out, the application must invalidate the user’s session, clear any session-related data stored on the server or client-side, and redirect the user to a secure location, such as the login page or a dedicated logged-out page.

Enforce automatic session expiration and timeouts

To protect user accounts and data from unauthorized access, it’s important to implement automatic session expiration and timeouts.

In addition to manual session expiration, setting reasonable session timeouts based on the sensitivity of the application and the expected usage patterns can help mitigate the risk of session hijacking. After a predetermined period of inactivity, the application should automatically invalidate the current session and prompt the user to re-authenticate, thereby preventing the extended and/or expired session from being exploited.

How session management works on Stytch

On Stytch, user sessions are identified by session tokens and JSON Web Tokens (JWTs) that are authenticated on each request. To initiate a session, developers can call the authenticate magic link or authenticate OTP endpoint, specifying the desired session_duration_minutes to set the session’s lifespan.

Stytch returns both a session_token, which is a static value that remains valid for the duration of a session, and a session_jwt that has a shorter expiration time of 5 minutes. However, when JWTs expire, you can pass them to our session API to retrieve newer JWTs. Our servers ensure that the underlying session is still active before issuing a new JWT.

To extend a session’s validity, you can call the authenticate session endpoint with the desired session_duration_minutes parameter to set a new expiration time without modifying the session_token or session_jwt. On the other hand, you can also revoke an active session by passing the corresponding session ID or JWT to the revoke session endpoint.

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