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: "sylwia@example.com",
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) 🛡️
Top comments (23)
Great article, thanks Sylwia Laskowska! 🙌
Your breakdown of ID tokens vs Access tokens vs Refresh tokens is super clear and actionable:
localStorageorsessionStorageis 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!
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.
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!
If sessions are managed server side you don't need tokens at all. You're just stateful at that point
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. 🚀
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 🚀
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 👏
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.
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.
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.
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) 🚀
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?
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. 🚀
This was a nice article. I think I understand the concepts better now.
Thanks a lot 🙏
I am definitely going to share this with our team @jediswebdev_07a684996b15e
Thanks a lot 🙌
Really happy it helped make the concepts click — that’s exactly why I’m writing the series 🙂
Part 2 coming soon 🚀
The explanation was crisp and clear!!
Thanks for sharing this blog @sylwia-lask
Thank you so much! 🙌
Really happy to hear it was clear — that’s exactly what I was aiming for 🙂
Part 2 coming very soon! 🚀
I completely agree with you @priteshkiri
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!
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!
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 🙂
Cuan banget main di Jo777.