Whether it’s password or passwordless authentication, multi-factor authentication, or any of the other identity verification shenanigans, in the end, our identity is deduced to a single session cookie! We can’t deny the security importance of session cookies in web application access control.
HTTP is a stateless protocol. So for tracking usage or maintaining authenticated sessions, web applications need to maintain some form of state. A session state can be a simple random identifier value of a session or a set of claims identifying the client and server participating in an active session. Since a session state is required throughout an active session, we need a storage system for persistence. This is where HTTP cookies come in, as a storage medium to persist session state on the client side, usually in a web browser.
Session management is not unique to the HTTP protocol. Encrypted protocols such as TLS and SSH also utilize session ID to maintain a pool of client connections.
But in the context of HTTP, the user session is also attached to identity and used for access control. There is no standard directive on securing and managing the session. If the session ID generation or storage process is insecure, an attacker can guess/steal the session ID nulling the whole point of secure authentication.
There are certain factors that affect the security of session ID, including protection from guessability (cryptographically secure random), protection from replay (unique per session), and protection from theft (transport only using TLS and preventing client-side access). In this blog post, I will explain some of the best practices to address these security issues.
Session ID, usually a token or key that binds user session, should be of high entropy so that it is not easily guessable, unique so that it is not readily assigned to different users simultaneously, and of a good length to make brute force impractical.
It’s a common technique to create a session ID by hashing strings, including time of authentication, remote user IP, etc. It’s okay to attach these values to the session ID, but without additional high-entropy random value, it may be possible to predict the input strings of the hash and crack the session ID.
So using cryptographic random number generators (CRNG) with at least 128 bits of entropy should be an ideal base for creating session ID. Most modern web application frameworks assign high-entropy session IDs for authenticated sessions. But developers who are self-implementing session management understand that CRNG should be used instead of pseudo-random number generators (PRNG). Because PRNG iterates on a deterministic sequence using the initial seed value (at worst, it can be hard coded), they do not produce true random values; i.e., there’s a possibility for someone to predict the “random” number. Here’s a link to Go’s crypto random package, Python’s secret random library.
Bonus reading: This classic example and this (excellent video) of breaking PHP’s built-in session ID by Samy Kamkar demonstrates the importance of entropy in session ID.
When session data is used for access control, it is important that the data remains tamperproof. Otherwise, users may be able to modify the content of session data to bypass security control or claim as another user. There are two possible ways to make the session value tamperproof. Either maintain a server-side state and check the integrity of session values in each request, or use cryptographic signatures to ensure the data integrity. The two solutions I would recommend here are Nacl/secret box and JWT.
Nacl/secretbox supports authenticated encryption, making it suitable for concealing and tamperproofing session data. The cool thing about secretbox is that the nonce used during encryption can also be used as a CSRF token, binding both the session value and CSRF token into one single authenticated session without the hassle of maintaining a different state for CSRF. Here’s a reference to Go’s Nacl/secretbox package and Python’s Nacl library.
Another alternative for tamperproof session data is using JWT. JWT has a built-in mechanism to sign the claims (key-value pair with user or session data) using public-key cryptography. So any changes can be cryptographically detected. However, beware not to put any sensitive data in JWT claims as the JWT are only encoded with base64. JWT’s strength is basically that the claims can be signed by a trusted party attesting to the session data but it is not designed for concealing secrets. JWT tokens also have an expiry time, making them useful for access control, similar to using X.509 certificates.
Sample JWT token:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwicm9sZSI6ImFkbWluIiwicHJpdmlsZWdlIjoicm9vdCIsImlhdCI6MTUxNjIzOTAyMn0.Ae1m_2VRlu1biCWoDYv_XYb2DV30ax9v4arZVhdEzfs
When decoded, it’s just a simple text value:
{"alg":"HS256","typ":"JWT"}.{"sub":"1234567890","name":"John Doe","role":"admin","privilege":"root","iat":151623902}.HMAC
This depends on the use case. Maintaining a server-side session state is a common pattern in web applications. It also gives HTTP servers full control over managing the secure session lifecycle. But as the user base grows, so does the session database size, which can demand additional resources and maintenance overhead. On the other hand, can we really trust the session state completely maintained on the client-side HTTP cookie?
JWT tokens are a perfect solution for maintaining a client-side session state. With JWT, there may be no need to maintain a server-side state to track user roles and permission attributes as these values will be signed in the JWT claims itself. And for the application that defers identity and authentication to an external identity provider, maintaining client-side only state with JWT tokens seems like a perfect solution.
Maintaining a complete client-side session state may be an advantage in terms of operation, but there are still some tradeoffs that teams have to make. For example, if a user signs out from the web application before the expiry of JWT tokens, how can we invalidate the token without maintaining a server-side state? Keeping a short time-to-live is possible but may not be a good solution considering user experience.
Since HTTP cookies are attached to every HTTP request by default, it opens a myriad of attack vectors such as CSRF, clickjacking, etc. Web browsers support many cookie security attributes that help enhance the security of client-side session storage. These cookie security attributes include:
The sad part is that the security-enhancing attributes are not enforced as a default, and developers need to configure them for each HTTP response. Developers should also generate new session tokens for each new session to defend from session fixation attacks, implement an idle timeout feature and properly destroy the session token on the server side when the user signs out.
Do note that although these cookie attributes enhance security to a great extent, an XSS exploit can pretty much bypass these security protections. For example, although HTTPOnly would not allow the XSS payload to access the cookie, the XSS payload can simply ride an active session to perform a CSRF attack.
Should session data be stored in browser local storage? Data stored on local storage is not sent to a server on every request by default, preventing session-riding attacks. But unlike HTTPonly cookies, they are also easily accessible to on-page scripts. I prefer the idea of storing session tokens in HTTP-only cookies and CSRF tokens in local storage, taking advantage of both secure cookie attributes and a local storage sandbox.
In general, most CLI-based API clients do not maintain a session state. It’s a common practice (which I think is insecure) to attach API tokens with every single API request.
Here’s one example of an API request from DigitalOcean’s doctl
client.
Notice the “Authorization” header. The API token is passed in every request. Following the REST API pattern, there is an argument that the API server should be stateless. Maybe the idea of sending credentials in every request stems from this concept. But I believe credentials should not be directly exchanged with every request and that some sort of session tokens should be negotiated so that the client credential never leaves the client machine.
Besides, there may be some feature requirements, like API client tracking, sharing context between two different CLI clients, etc. In such cases, most of the HTTP libraries support formal ways of dealing with HTTP cookies (Go’s cookiejar, Python’s cookiejar) that can be utilized to store session tokens (although nothing stops us from maintaining in-memory data structure to maintain session state). And if CLI clients need to share sessions with another client program or need a feature to resume a session, the session tokens can also be written to a safe place on disk with proper file permission or written to a designated file like .netrc
(Heroku does this).
I’ve listed a few best practices to deal with HTTP sessions. One final thought is that session security should be designed in such a way that even if the session token gets stolen, it should render ineffective after a certain time. Sessions with short-lived TTL can be a very effective solution, but user experience should be of concern as it can be annoying for users to refresh the browser window multiple times in a single session. But if we consider non-human HTTP connections, i.e., machine to machine connections, we can automate the session refresh mechanism.
And finally, developers should also think about implementing WAF or server middleware hooks to identify and block session attacks. Ideally, there should be some way for user fingerprinting so that subtle changes in the client’s property should alert for a compromised session.
Question: API tokens are just a password in disguise. Why don’t we transmit passwords in every HTTP request but are okay with sending API tokens? Perhaps, this is a discussion that warrants another blog post. If you have an answer, let’s discuss!