DEV Community

Cover image for Auth Explained (Part 1): ID vs Access vs Refresh tokens — 🤔what they ACTUALLY do (and why localStorage is a trap)
Sylwia Laskowska
Sylwia Laskowska

Posted on

Auth Explained (Part 1): ID vs Access vs Refresh tokens — 🤔what they ACTUALLY do (and why localStorage is a trap)

A while ago in a technical interview I got asked:

“Can you walk me through how authentication and authorization actually work under the hood?”

And like many devs I knew just enough to plug in Auth0/NextAuth etc… but not enough to explain the “why” behind the flow.

This series is the version I wish I had back then — plain English, no magic ✨, just a mental model that sticks.


Why does the frontend need AuthN? 🤷‍♀️

Because the frontend knows nothing about you.

To your browser, you’re just:

  • a tab with JavaScript,
  • a user… or a hacker,
  • or possibly a fridge 🧊 with Chrome 😅

It needs someone trusted to say “yes, that’s really Sylwia.” → that “someone” is the IdP.


AuthN vs AuthZ 🆚

Name Fancy Human
AuthN Authentication “Who are you?” 👤
AuthZ Authorization “What are you allowed to do?” ✅

👉 The frontend does NOT authenticate you — it just starts the process and carries tokens.
👉 The API is the thing that actually says “yes/no” to an action.


Who does what? 🧩

Actor Job
IdP Knows the user + issues tokens
Frontend Asks for tokens & stores the right ones
API Validates access token & allows/blocks actions

Analogy 🛂

Real World In Auth
Passport office IdP
Border control API
Traveler sweating at customs 😅 Frontend

The three token types 🎟️

Token What it is Analogy
ID Token who you are passport / ID
Access Token permission visa / entry stamp
Refresh Token ability to renew VIP wristband 🎤

⚠️ The ID token is for the frontend only.
The API doesn’t care about your life story — it cares about the access token


Where do tokens live (and why not localStorage)? 🏠

❌ localStorage = “please rob me” 🏴‍☠️

Super convenient… also super easy to steal:

localStorage.getItem("access_token")
Enter fullscreen mode Exit fullscreen mode

One tiny XSS → 💸 token gone.


And what about sessionStorage? 🤔

A common question is: “If localStorage is risky, is sessionStorage safer?”

Answer: No — same problem.

Storage Can JS steal it? XSS resistance
localStorage ✅ yes ❌ weak
sessionStorage ✅ yes ❌ weak
HttpOnly cookie ❌ no ✅ strong

sessionStorage only dies when the tab closes — it doesn’t protect against theft during the session.
So it doesn’t make tokens safer, just shorter-lived loot.


✅ Access Token → in memory 🧠

What it is: “permission” – proof you can call the API
Where it lives: in memory (JS variable, not storage)
Lifetime: short: ~5–15 min (sometimes up to 30)
Why short?: if stolen → damage window stays tiny

What it actually does:

You attach it to every API request so the API knows who is calling.

fetch("https://api.example.com/profile", {
  headers: {
    "Authorization": `Bearer ${accessToken}`
  }
});
Enter fullscreen mode Exit fullscreen mode

The API verifies:
✔️ signature
✔️ issuer
✔️ audience
✔️ expiry
✔️ scopes (permissions)


✅ ID Token → in memory 🧠✨

What it is: identity snapshot — “who the user is”
Where: in memory
Lifetime: short (~5–15 min)
Used for: UI (display name, avatar, etc.), not for calling APIs

You decode it just to show profile data — not to grant access.


BONUS — Reading the ID Token 🔍

const idToken = "...your.jwt.here...";
const payload = JSON.parse(atob(idToken.split('.')[1]));
console.log(payload);

// object will look something like:
{
  name: "Sylwia",
  email: "sylwia@example.com",
  picture: "https://example.com/avatar.png",
  sub: "user-123"
}
Enter fullscreen mode Exit fullscreen mode

⚠️ decodeverify


✅ Refresh Token → HttpOnly cookie 🍪🔒

What it is: the thing that gives you new access tokens
Where: HttpOnly Secure SameSite cookie
Lifetime: long: days/weeks/months
Why: frontend should never read it

What it actually does:

Its only job is to refresh an expired access token.

await fetch("/token/refresh", {
  method: "POST",
  credentials: "include" // <-- RT cookie auto-sent
});
Enter fullscreen mode Exit fullscreen mode

The frontend doesn’t “see” it — it just benefits from it.


Mental model 🧠📍

Token Where Why
Access in memory short-lived, API calls
ID in memory UI only
Refresh HttpOnly cookie unreadable, long-lived

✅ Mental model diagram

[User] 
   |
   v
[Frontend]  -- "I don't know who this is" -->  
   |
   v
[IdP]  -- "Okay, here's who they are" --> (ID Token + Access Token + Refresh Token)
   |
   v
[Frontend]  -- stores ID+Access (memory), RT in cookie
   |
   v
[API]  -- "Do you have a valid Access Token?"
Enter fullscreen mode Exit fullscreen mode

Up next — Part 2 🚀

In Part 2 we’ll:
✅ walk the full redirect “dance” 💃
✅ explain PKCE (secure code exchange) 🔐
✅ show how the refresh cookie appears 🍪
✅ cover refresh token rotation ♻️
✅ mention BFF (extra-safe pattern) 🛡️

Top comments (23)

Collapse
 
cyber8080 profile image
Cyber Safety Zone

Great article, thanks Sylwia Laskowska! 🙌
Your breakdown of ID tokens vs Access tokens vs Refresh tokens is super clear and actionable:

  • ID token = who the user is, for the UI. (DEV Community)
  • Access token = permission to call APIs, short-lived, keep in memory. (DEV Community)
  • Refresh token = hidden, long-lived cookie, used to renew access. (DEV Community) Also totally agree that storing tokens in localStorage or sessionStorage is a risky move—XSS wins too easily. (DEV Community)

One question though: when you say the Access token lives just in memory, how do you manage safe page refreshes or tab closes in single-page apps without hurting UX? Would love a few mini patterns if you plan a part 2.

Looking forward to part 2 and digging into PKCE + BFF flows!

Collapse
 
hashbyt profile image
Hashbyt

Managing access tokens purely in memory definitely introduces UX challenges with page refreshes and tab closes in SPAs. As Sylwia hinted, common patterns to handle this include implementing silent refresh mechanisms where a hidden iframe or background request fetches a new access token using the refresh token stored securely in an HttpOnly cookie. Another solid approach is the Backend-for-Frontend (BFF) pattern, where tokens never hit the browser but sessions are managed server-side, effectively eliminating client-side token storage risks while maintaining seamless UX.

Collapse
 
sylwia-lask profile image
Sylwia Laskowska

That's an excellent and spot-on summary!

You've perfectly captured the two most robust and widely adopted strategies for solving the in-memory token storage dilemma in SPAs.

Silent Refresh (using an HttpOnly Refresh Token cookie) provides the balance of security and seamless UX.

The BFF Pattern offers the highest security baseline by eliminating client-side token exposure entirely.

Thanks for enriching the discussion!

Collapse
 
rcls profile image
OssiDev • Edited

If sessions are managed server side you don't need tokens at all. You're just stateful at that point

Thread Thread
 
sylwia-lask profile image
Sylwia Laskowska

Absolutely — 100% agreed 👍
Once you move to a BFF or any server-side session model, you’re no longer doing “token-based auth” in the browser — the session is the auth, and the browser just carries a session cookie.

The moment you go stateful, the whole “where do I store the access token” problem disappears entirely.

I’m planning to touch on this contrast in Part 2 as well — because a lot of SPA devs don’t realise “no tokens on the client” is actually a valid (and often superior) option. 🚀

Collapse
 
sylwia-lask profile image
Sylwia Laskowska

Thanks so much 🙌 really glad the breakdown clicked!

Re: access token + page refresh — that’s exactly the topic I start with in Part 2, because that’s where the “in-memory” strategy meets real UX 🙂
Different ways to survive a reload (silent refresh / BFF etc.) will be covered there.

Part 2 is coming next 🚀

Collapse
 
shemith_mohanan_6361bb8a2 profile image
shemith mohanan

This is such a clear and fun read, Sylwia! The passport/visa analogy nailed it 😂

Totally agree on avoiding localStorage — I’ve seen so many early-stage apps trip on that one.
Excited for Part 2 and the PKCE section 👏

Collapse
 
hashbyt profile image
Hashbyt

Authentication is like showing your passport to prove who you are, while authorization is like the visa that grants you permission to enter certain places. In tech terms, the ID token verifies your identity for the frontend, and the access token grants your app permission to call APIs. This mental model helps build secure and clear authentication flows that align perfectly with the blog’s explanation.

Collapse
 
sylwia-lask profile image
Sylwia Laskowska

Ahh thank you! 😄
So glad the border/passport analogy landed — it’s my favorite way to make sense of all the token chaos 😂
And yes, localStorage is like leaving your passport at the café — looks fine until it’s not.

Collapse
 
lico profile image
SeongKuk Han

Great article, thank you!
I have a question.

Where should I store access token and refresh token in react based server or any html, without backend?

If I store access token and refresh token in sessionStorage, it will be blown away, but I want to keep them, so when user reopens it, the login status is still remained.

Collapse
 
sylwia-lask profile image
Sylwia Laskowska

Thanks! 🙌

Access token should live in memory, and the refresh token should be stored as an HttpOnly cookie set by the IdP, not in local/sessionStorage.
On page load, the app just does a silent refresh using that cookie to get a new access token — that’s how login survives a reopen without storing tokens in JS storage.

I’ll cover this step-by-step in the next article (Part 2) 🚀

Collapse
 
lico profile image
SeongKuk Han

I look forward to it.

Just a couple of questions are wandering in my mind.

So in this case, http cookie should be set by the server. But if the backend server has a different domain, how can I set the cookie for client?

Client domain A.com, server domain B.com. And client is html files served by web server such as nginx.

Second and last,

If we store accessToken in http cookie, is there any reason we use refreshToken?

Thread Thread
 
sylwia-lask profile image
Sylwia Laskowska

Thanks! Great questions — super common in real apps.

Different domains (A.com app, B.com IdP/API):

A server on B.com can’t set cookies for A.com.

Use one of these:

Same-site setup: serve IdP on a subdomain (e.g. auth.a.com) so it can set a first-party HttpOnly cookie (SameSite=Lax/Strict).

Top-level redirect: briefly navigate the browser to b.com so it’s first-party there; B.com sets the cookie, then redirects back.

BFF pattern: tiny backend under a.com holds tokens; browser only has a session cookie.

Access token in an HttpOnly cookie — do we still need a refresh token?

For SPAs, don’t put the access token in a cookie (CSRF + it’s sent automatically everywhere). Keep access in memory and use Authorization: Bearer.

You still want a refresh token (in HttpOnly cookie) to renew short-lived access tokens without re-login (and to enable rotation/reuse-detection).

If you move to BFF + server session, then you typically don’t use access tokens in the browser at all (so no refresh token in the browser either).

I’ll cover these options (same-site vs redirect, PKCE, BFF, CSRF gotchas) in Part 2. 🚀

Collapse
 
jediswebdev_07a684996b15e profile image
JedisWebDev

This was a nice article. I think I understand the concepts better now.
Thanks a lot 🙏

Collapse
 
hashbyt profile image
Hashbyt

I am definitely going to share this with our team @jediswebdev_07a684996b15e

Collapse
 
sylwia-lask profile image
Sylwia Laskowska

Thanks a lot 🙌
Really happy it helped make the concepts click — that’s exactly why I’m writing the series 🙂
Part 2 coming soon 🚀

Collapse
 
priteshkiri profile image
Pritesh Kiri

The explanation was crisp and clear!!

Thanks for sharing this blog @sylwia-lask

Collapse
 
sylwia-lask profile image
Sylwia Laskowska

Thank you so much! 🙌
Really happy to hear it was clear — that’s exactly what I was aiming for 🙂
Part 2 coming very soon! 🚀

Collapse
 
hashbyt profile image
Hashbyt

I completely agree with you @priteshkiri

Collapse
 
hashbyt profile image
Hashbyt

Great breakdown! To put it simply: authentication is about verifying who you are like showing your ID at the border. Once verified, authorization is about what you’re allowed to do like having the right visa or permissions to enter certain areas. In the tech world, the ID token confirms your identity, while the access token determines what actions or resources you can access. Understanding this helps clarify how secure systems manage user access efficiently!

Collapse
 
n3nad profile image
Nenad Mitrovic

We can never have too many mental models for auth. Each one offers its own unique perspective and gives us a chance to learn something new.

Thanks for your effort on this one!

Collapse
 
sylwia-lask profile image
Sylwia Laskowska

Thanks so much 🙌
Totally agree — auth really clicks only once you’ve seen it from a few angles.
Glad this model added another lens to the toolkit 🙂

Collapse
 
syahla_nahda_0ca4b37b609b profile image
syahla nahda

Cuan banget main di Jo777.