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

AI Summary6 min read

TL;DR

Authentication uses ID, access, and refresh tokens for identity, permissions, and renewal. Store access and ID tokens in memory, refresh tokens in HttpOnly cookies to prevent theft. Avoid localStorage due to XSS risks.

Key Takeaways

  • ID tokens verify user identity for the frontend UI, access tokens grant API permissions, and refresh tokens renew access tokens securely.
  • Store access and ID tokens in memory (not localStorage) to minimize theft risk from XSS attacks.
  • Use HttpOnly cookies for refresh tokens to prevent frontend access and enhance security.
  • APIs validate access tokens for authorization, while the frontend handles token storage and requests.

Tags

javascriptprogrammingwebdevfrontend

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: "[email protected]",
  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) 🛡️

Visit Website