Auth Explained (Part 1): ID vs Access vs Refresh tokens — 🤔what they ACTUALLY do (and why localStorage is a trap)
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
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")
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}`
}
});
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"
}
⚠️ decode
≠ verify
✅ 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
});
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?"
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) 🛡️