Skip to main content

Access Token vs Refresh Token: What I Finally Understood

Updated 5 min readMarkdown

I use tokens in almost every app I build as a Flutter developer. Login works, API calls work, so I never stopped to ask why we need two tokens. Why not just one?

Recently I watched a lecture on this topic on the Chai aur Code channel by Hitesh Choudhary. After that, things became much clearer, so I’m writing it here in my own words. It will also help me remember it.

The problem first

When you log in, the server needs a way to know it’s you on every next request. You can’t send your email and password every time, that would be unsafe.

So the server gives you a token. Now the question is how long this token should live.

  • Long life: if someone steals it, they can use your account for a long time.
  • Short life: it’s safer, but the user has to log in again every 15 minutes. That’s a terrible experience.

Two tokens solve this problem.

Access token

This is the token you send with every API request.

  • It lives for a short time, like 15 minutes to 1 hour.
  • It goes in the header: Authorization: Bearer <token>
  • It tells the server who you are and what you are allowed to do.

Because it travels with every request, there is more chance of it getting leaked. That’s why we keep its life short. Even if someone gets it, it will expire soon.

Refresh token

This one has a different job.

  • It lives for a long time, like 7 days, 30 days or more.
  • It is not sent with normal API calls.
  • It is only sent to one endpoint, the refresh endpoint, to get a new access token.

Think of it as a backup key that stays safely with you and only comes out when needed.

A simple way to think about it

Imagine you go to a hotel.

  • The access token is the room key card. You use it again and again, but it works only for a day.
  • The refresh token is your booking proof at the reception. You don’t show it every time. You show it only when your key card stops working, and they give you a new one.

How the flow works

  1. User logs in with email and password.
  2. Server sends back an access token and a refresh token.
  3. App uses the access token for all API calls.
  4. After some time, the access token expires and the server returns 401 Unauthorized.
  5. App quietly sends the refresh token to the server.
  6. Server checks it and sends a new access token (many apps also send a new refresh token).
  7. App retries the failed request, and the user doesn’t notice anything.
  8. If the refresh token is also expired or invalid, the user has to log in again.

Quick comparison

Access Token Refresh Token
Life Short Long
Sent where Every API request Only the refresh endpoint
Purpose Access protected data Get a new access token
If leaked Limited damage, it expires soon Serious, so store it carefully

Where to store them

This part is easy to ignore, but it matters.

  • Mobile (Flutter): use flutter_secure_storage, not plain SharedPreferences.
  • Web: keep the refresh token in an HttpOnly cookie so JavaScript can’t read it.

What I do in Flutter (and a mistake I made).

I use Dio and handle the refresh logic in an interceptor. When a request fails with 401, the interceptor calls the refresh endpoint, saves the new token, and retries the original request.

When I rebuilt the Unjob.ai app, users kept getting logged out while using it. At that time I didn’t know much about how refresh tokens really work, so I had no idea where to even start looking.

After digging into it, I found one of the reasons. When the access token expired, a few screens called APIs at the same time, and each call tried to refresh the token on its own. The refresh token got used more than once, and the user was suddenly sent back to the login screen. I fixed it by adding a lock, so only one refresh call runs and the other requests wait for it. After that, the random logouts stopped.

The old version of the app also kept the refresh token in SharedPreferences, which is plain, unencrypted storage. I moved it to sqflite. Later I learned that the better option is flutter_secure_storage, because it uses the Keychain on iOS and the Keystore on Android, so that’s what I recommend now.

Final thoughts

The idea is simple. The token that is used often should be short-lived, and the token that is powerful should be used rarely. That gives you good security without making the user log in again and again.

If you are learning this, try building it once yourself with a small Node.js API and see the whole flow. It makes the concept stick much better than just reading about it.

Thanks for reading. If I missed something or you do it differently in your projects, tell me in the comments. I’m still learning too.

No comments yet. Be the first to comment.