User Auth? Sessions? Tokens? WTH?
Intro
If we want to write or work with almost any web application today, we will have to implement or deal with authentication and authorization. How do we authenticate users? How to make it secure and scalable?
There is a lot of confusing information online about these topics. This article is my attempt at clarifying the confusion.
Heads-Up
I assume some familiarity with topics related to authentication.
I am not explaining individual token structures in detail. For that you can refer to this (JWT), this (access/refresh tokens) and this (openid token).
When I refer to a "client" in this article, I mostly mean a web browser, not a mobile device. If you are interested in storing tokens securely on mobile devices, consider looking into KeyStore (android) or KeyChain (iOS).
What are the available solutions?
There are a few options for auth on the web but we'll take a look at a couple popular ones:
- Session based
- Token based
Security Measures
Some security measures should be in-place for any auth architecture:
What is session based authentication?
Session based authentication is a process where a user exchanges login credentials for a session ID with a server / backend. The process looks something like this:
How does it work?
- The user fills a form with login credentials e.g. email and password and clicks on a submit button.
- The backend receives the request and validates if a user with that email and password (hashed) exists in the database.
- If the user exists, an ID is generated and stored as a record in the database with additional details like expiry, the associated user, etc.
- This ID is then sent back to the client, usually as an HTTP set-cookie response header.
- The browser then stores that ID as a cookie in its cookies storage and uses it for subsequent requests to the backend.
- The backend then authenticates subsequent requests using the session ID and its associated record in the database instead of an email and password.
Security considerations
When using session based authentication you should be aware of potential security issues like:
For mitigation and prevention of these refer to the section above - "Security Measures"
Pros
- Simple and easy to implement
- Most backend languages and frameworks do most of the work for you
- Automatic expiration
- Easy way of blocking / revoking / restricting access for specific users
- Battle tested
Cons
- Requires some sort of persistence
- Cookies (sessions) bound to one domain
- Requires optimization and scaling out of storage at large scale, but what solution doesn't?! :D
Summary
Session based authentication has been used successfully for a long time and securely for those who had security on their minds. It's simple and also works great up to a certain scale (covers probably 99.9% of use cases). However, when you reach a certain scale, session persistence might become a bottleneck and you will have to start optimizing and scaling it out.
What is token based authentication?
What if I told you, you can authenticate users at massive scale without having a session storage? I'd probably be lying. But if you need an extremely complicated solution that will still need persistence then tokens might be a good fit.
How does basic JWT auth work?
Step 3 is where the path changes compared to session auth.
- The user fills a form with login credentials e.g. email and password and clicks on a submit button.
- The auth service receives the request and validates if a user with that email and password (hashed) exists in the database.
- The auth service generates a JWT (JSON web token) that contains basic user information of our choosing and optionally an expiration timestamp.
- The auth service signs the JWT with a private token / secret that is used across the distributed system to validate JWT authenticity.
- The auth service sends the token back to the client via an HTTP-only cookie (up to 4096 bytes in size).
- All subsequent requests will contain the cookie i.e. JWT token so the backend will be able to appropriately authenticate and authorize the request based on data contained in the JWT token.
Voila! We have distributed authentication and authorization that can be used across a distributed system without checking the database every time we do the auth.
This is the most basic auth flow using JWT tokens.
...
Colleague: "I see suspicious activity in the logs coming from a specific user... Can we block this user?"
Me who implemented basic JWT auth: "Sure we can! We delete the user in the user database and just change the secret we use in all the services to verify JWT tokens."
Colleague: "Oh, great! But won't that force all other users to re-login?"
Me: "Yes. Is that a problem?"
It might not be an issue in your use case but if it is ... Well, this is where the fun part begins :D
The Problem of Token Invalidation / Revocation
As you can see, in case of malicious user activity or compromised tokens, we are not able to block or limit a targeted user without invalidating tokens of all other users forcing them to re-login.
We cannot solve this issue without introducing additional state and checks.
The Problem of Stale Tokens
As mentioned previously, JWT tokens contain a small amount of data related to the user like email, age, role etc. But if the JWT token never expires or has long validity, how do we update the JWT token data?
We cannot update the data if we don't ask for the JWT token again with our username and password.
What are refresh and access tokens?
Now we are aware of a couple shortcomings of basic JWT token auth. Are there solutions?
If you thought, "Wait, I think we need more tokens!", you thought right.
To solve the issues of revocation, stale data and long lived (insecure) tokens we will split our basic JWT token mentioned above, into two tokens - the refresh token and the access token defined in the OAuth 2.0 spec.
The access token is short lived (e.g. 10 min) and will be used for protected resource requests (e.g. /orders)
The refresh token is used solely for obtaining new access tokens and cannot be used to access protected resources.
How do refresh and access tokens work?
- The user fills a form with login credentials e.g. email and password and clicks on a submit button.
- The auth service receives the request and validates if a user with that email and password (hashed) exists in the database.
- The auth service generates a refresh JWT token and a access JWT token and sends them back to the client.
- The client uses the short lived (e.g. 10 min) access token for subsequent protected resource requests.
- The client uses the refresh token to get another access token after the current one expires.
What are the benefits of a refresh / access token flow
- Forcing the client to ask for access tokens every e.g. 10 min, solves both the revocation and stale data issue.
- After we block or limit a specific user, the next access token request will fail stopping or limiting the users activity.
- Data contained in the tokens can also not really become stale since we are asking for it every 10 min.
- We still keep the stateless benefit of our tokens when using it with the other services in the distributed system since they
Note On Refresh Token Expiration
With a dedicated, full-featured auth service and clients are forced to ask for access tokens periodically, I don't see a reason to have long-lived refresh tokens.
Instead I recommend invalidating and issuing new refresh tokens every time the client asks for a new access token.
Voila! Problems solved.
...
Worried and puzzled colleague: "Multiple panicked customers contacted us saying that someone else gained access to their accounts and deleted all their data."
Me who suggested we use refresh and access tokens: "OMG! That's a nightmare for our business. How could they gain access? Wait ... Where are we storing the tokens on the client side?"
Colleague: "Yes, I asked the same question. We are storing the tokens in local browser storage."
Me: "Local storage!? That's a severe security risk!"
Where to store tokens securely?
There are a few options for storing tokens:
- Local storage
- Session storage
- Regular cookies
- Browser memory
- HTTP-only / secure / lax same-site cookies (spoiler: this is the best one)
So we have 2 or more types of tokens and 4 options for storage of the tokens. Let's go over the options ...
Storing Sensitive Tokens In Local Storage (Spoiler: DON'T)
Local storage is accessible by all JavaScript running in all browser tabs. This is horrific from a security perspective and disqualifying for sensitive data such as refresh and access tokens.
Storing Sensitive Tokens In Session Storage (Spoiler: DON'T)
Session storage is accessible by all JavaScript running in the same tab / page making it a little less horrific compared to local storage but still disqualifying as it is accessible by vendor JavaScript.
Storing Sensitive Tokens As Regular Cookies (Spoiler: DON'T)
Regular cookies (non-http-only) are accessible by all JavaScript running on the page. This is disqualifying for sensitive data such as refresh and access tokens.
Storing Sensitive Tokens In Memory
Storing the access tokens in memory might be acceptable if your website is a single page application i.e. users are not changing/refreshing the page every minute.
Refresh tokens on the other hand cannot be stored in memory since we have to provide our credentials to get a refresh token i.e. we would have to re-login every time we change/refresh the page.
Storing Tokens As HTTP-Only Cookies
Finally we arrive at the most secure option and my recommendation for optimal security.
Refresh and access tokens should be stored as HTTP-only / secure / strict (or lax) same-site cookies and not be accessible to JavaScript at all instead only allow us to call an endpoint for access token rotation.
Storing refresh and access tokens as HTTP-only cookies prohibits accessing data stored in the tokens e.g. username, email, role, expiration etc. But that is what we have ID tokens for.
What are ID tokens?
"An ID token contains information about what happened when a user authenticated, and is intended to be read by the OAuth client. The ID token may also contain information about the user such as their name or email address, although that is not a requirement of an ID token." - OAuth 2.0 Spec
The ID token is not used for auth against protected API resources.
Since this token is intended to be read by the client, contains information about the auth request and can contain basic user information, it is perfect for at least a couple use cases:
- Enhance user experience with e.g. a username displayed somewhere on the page or display a basic user profile / card on the page
- Store access token expiration so the client can request a new one on time
If your page is a SPA I'd recommend storing the ID token in memory and refreshing it every time you request a new access token from the auth service.
In case of a classical server rendered page, let the server deal with it and render the page with all relevant data on the server.
Pros
- Services, other than auth, don't need to make database calls to authenticate users
- Allows for auth decoupling
- Allows the auth service to pick the fastest and most scalable DB technology
Cons
- High level of complexity
What About Cookies And The Multi-Domain Problem?
Let's get the easy solution out of the way, for sub-domains you can use the cookie domain attribute.
The cookie domain attribute is, unfortunately, not a solution for different root domains e.g. sharing a cookies between xyz.com and zyx.com. This is not possible.
To solve this issue our auth service needs to be able to create very short-lived tokens (e.g. 10s) that can be consumed / authenticated by a different domain using a shared secret.
The Flow
- User authenticates with credentials on auth.xyz.com
- auth.xyz.com responds with refresh/access token as HTTP-only / strict (or lax) same site / secure cookies
- The user is now able to access protected resources under the domain xyz.com
- The user can now call an endpoint on the auth.xyz.com service e.g. /domain/zyx.com
- auth.xyz.com will generate a very short-lived (10s) JWT token containing user identification information
- auth.xyz.com will respond with a redirect pointing to e.g. auth.zyx.com/auth/token/[JWT-token]
- auth.zyx.com will receive the authentication request with the token attached
- auth.zyx.com will authenticate the token using a shared secret
- auth.zyx.com will use the user identification contained in the token to generate a refresh and access token for that user
- auth.zyx.com will return the refresh/access token as HTTP-only / strict (or lax) same site / secure cookies
- The user is now able to access protected resources under zyx.com
Note: The auth service doesn't have to be a completely different service. It can be one service that responds on multiple domains.
Note: The token that is used between two domains doesn't have to be a JWT token. However, the auth service receiving the token must be able to find the user based on the token.
Conclusion
Maybe you have more questions about auth now than you had at the beginning. This indicates the complexity of the topic. But I hope I was able to shed some light on at least a few points.
Regarding security, there is no compromise here. We need to understand our security and it has to be as air-tight as possible.
Regarding complexity, that depends, as always, on your context and point in the journey. If you are starting off from zero, keep it as simple as possible without compromising security and adapt as requirements change.
PS: Apologies for spelling mistakes.