5 min read · Mar 16, 2024
--
This article assumes you understand what access tokens and refresh tokens are and how they are meant to work.
For quite some time, I’ve wondered why we need a short-lived access token and a long-lived refresh token. In my mind, having a single long-lived access token made more sense. How does a refresh token add security? If someone can steal our short-lived access token, why wouldn’t they also be able to steal our long-lived refresh token? It seemed redundant to have two tokens instead of just one. What risks are present in a long-lived access token that aren’t also in a long-lived refresh token?
I will attempt to answer this question here but will also critique the most common answers and descriptions I’ve found online. I find that most resources are great at explaining the “what” and “how” of JWT and OAuth 2.0 but not the “why.”
A refresh token is used less frequently, therefore has less chance of being stolen.
This is technically true, but it feels like it addresses an edge-case rather than the larger issue at hand. This answer implies that a bad actor will give up before the access token expires, missing the chance to steal the refresh token. Perhaps this could be the case — the attacker could only steal a few of the victim’s packets — but it doesn’t seem to solve the general problem. An attacker can steal a refresh token and use it to generate new access tokens. The outcome is the same as if they stole a long-lived access token.
A refresh token is only sent to an authorization server and is therefore more secure.
This didn’t make much sense to me. It’s a frequent response, yet why it’s more secure to send it to an authorization server than to a resource server is never explained. Maybe there’s something I’m not understanding, but this seems like another case of solving an edge-case and not the overall problem. If a refresh token is stolen from the client, what benefit does sending it to an authorization server provide? And what if I don’t have a separate authorization server?
A refresh token can be added to a blacklist.
This is the answer I have the most issue with — not because it’s wrong; in fact, it’s the most correct. My issue is that it’s incomplete. It doesn’t fully explain the “why.” Sure, a blacklist adds security by allowing us to revoke refresh tokens, but why not just use session tokens if we’re going to query a database anyway?
This is how I made sense of it.
The question about refresh token blacklists comes down to frequency. Using sessions, each API call requires querying the database to ensure the session exists. With JWT, we only need to query once if our access token expires. Querying a database for every API is slower than validating an access token through code.
Auth using access tokens is popular because it’s a self-contained check that doesn’t require querying a database. However, access tokens on their own can’t be revoked once issued. This is where refresh tokens come in: they prevent future access tokens from being created if placed into the blacklist.
“Why not just use a long-lived access token and blacklist that?” I’ve wondered. The answer is that querying a database for our blacklist for every API request eliminates the benefit of a locally self-validating access token. The reason we need a short-lived access token and a long-lived refresh token, rather than a long-lived access token, is specifically because of the necessity of a blacklist.
We need a way to invalidate an access token that, by its nature, cannot be invalidated. The workaround is making this non-invalidatable access token short-lived. If we can’t invalidate the access token, we’ll ensure that the client can only use this seemingly invincible access token for a limited amount of time. To avoid forcing our users to log in every time their access token expires, we provide a refresh token to create new access tokens. If an access token is compromised, the thief will have only until the token expires to cause damage. If a refresh token is stolen, we can place this token on our blacklist to prevent it from generating any new access tokens, similar to how one might remove a session.
This method isn’t perfect — the attacker can still use the access token until it expires. Like with any strategy, there are trade-offs. This approach sacrifices a bit of security for the sake of performance. The key lies in minimizing how much security is compromised.
When learning about JWT and OAuth 2.0, I thought access tokens and refresh tokens were created equal. They are not. Access tokens offer a performance boost during auth. A refresh token, on the other hand, is a band-aid solution to the imperfections of access tokens. If there were no concerns about security, a long-lived access token would be preferable. Unfortunately, security is a concern, and the benefits of access tokens are overshadowed by the security issues they introduce. The introduction of a refresh token and a blacklist does slightly slow down the authentication process, but this combination is essential for addressing the security vulnerabilities posed by access tokens. This approach might be slightly less secure than using sessions, but it significantly enhances performance. The ‘slight security trade-off’ for added performance is acceptable, especially when considering that without the refresh token and blacklist, the security trade-off would be much larger. Essentially, the refresh token and blacklist reduce the security risks to a more manageable level, making the overall solution more secure than it would be with just long-lived access tokens.
Refresh tokens aren’t bad implementations; they’re necessary and fix the problem as best as they can. It’s just not perfect, and there likely is no perfect fix. The imperfection of refresh tokens probably the root of my misunderstanding. Like with many topics, it’s hard to see the negatives without diving deeper.
Short-lived access tokens, long-lived refresh tokens, and blacklists are a great approach for most services. Whether to use these over a session approach depends on how sensitive your data is and how much damage can be done in a short amount of time.
For instance, if you’re storing private personal health information, security is paramount over performance. For a social media application, the priorities might shift slightly. Here, while security remains important, the emphasis on performance and user experience becomes more pronounced. Users expect quick and responsive interactions, making the speed of the auth processes critical. However, this doesn’t mean security measures like refresh tokens and blacklists are any less important — even if they do slow down the flow slightly. They still play a crucial role in safeguarding users’ data against unauthorized access, but the balance leans more towards maintaining a fluid user experience. Essentially, the decision between prioritizing security or performance is influenced by the specific needs and risks associated with the type of data being handled.