AUTH_FLOW // React · React Native · Next.js

One wristband, a bouncer who never remembers you, and a quiet stamp at the back door.

How a logged-in user stays logged in — JWT, where to store it, attaching it to every request, and the silent refresh when it expires. One nightclub story that covers it all.

The one story to remember

You walk up to a nightclub. The doorman checks your ID once (login) and snaps a glow-in-the-dark wristband on you (the JWT). From then on, every bar inside just glances at your wristband — nobody re-checks your ID, because the wristband is self-proving. The catch: it glows for only an hour (token expiry). When it stops glowing, a bar turns you away (401). But you have a VIP card in your wallet (refresh token) — show it at the back door, get a fresh wristband, and walk right back to the bar you were heading to (retry the request). The whole job of "auth" is just: get a wristband, flash it everywhere, and quietly swap it when it stops glowing.

1Login send email + password to the server
2API returns JWT (usually an access token + a refresh token)
3Store the token web → cookie / memory · mobile → secure storage
4Attach token on every API call Authorization: Bearer <token>
5401 received the access token has expired
6Refresh token POST the refresh token → get a new access token
7Retry the original request now with the fresh token — user never noticed
01

Login & what a JWT actually is

Technical definition A JWT is a signed token (header.payload.signature) the server issues at login; the client sends it on each request and the server verifies the signature.

The user types email + password once. The server checks them, and if they're right it doesn't hand back "yes, you're logged in" — it hands back a token. The most common kind is a JWT (JSON Web Token): a long string the client keeps and shows on every future request.

Nightclub The doorman checks your ID exactly once. Instead of writing your name in a guest book he can't carry around, he snaps a wristband on you. The wristband is the proof.

A JWT has three parts joined by dots: header.payload.signature. The payload holds claims like your user id and an expiry time. The signature is the server's secret seal — anyone can read a JWT, but only the server can forge a valid one. That's why it's tamper-proof but not secret.

// a decoded JWT payload — readable by ANYONE, do not put secrets here
{
  "sub": "user_8412",   // who
  "role": "admin",
  "iat": 1717800000,    // issued at
  "exp": 1717803600     // expires (1 hour later)
}
A JWT is a self-proving wristband, not a database lookup. The server doesn't have to remember you — it just verifies the signature on each request. That's what makes JWT stateless.
02

Two tokens: access vs refresh

Technical definition The access token is a short-lived token sent on every API call; the refresh token is a long-lived token used only to obtain new access tokens.

Real apps hand back two tokens, not one. They solve opposite problems, so they have opposite lifespans.

Access Token

Short-lived (≈15 min – 1 hr). Sent on every API call. If stolen, it's only dangerous for a few minutes. The glowing wristband.

Refresh Token

Long-lived (days – weeks). Used only to get a new access token. Stored more carefully, sent rarely. The VIP card in your wallet.

Nightclub The wristband (access token) gets you into bars but glows for only an hour. The VIP card (refresh token) lives safely in your wallet and its only power is "get me a fresh wristband at the back door."

Why two? If you used one long-lived token everywhere, a single leak would give an attacker weeks of access. Splitting them means the token that travels constantly is the one that expires fast.

Access token = used often, dies fast. Refresh token = used rarely, lives long, guarded harder. Short access-token life is the entire security trade-off of JWT auth.
03

The whole token family

Technical definition A self-contained (JWT) token carries its own claims and is verified by signature; a reference/opaque token is a random string the server looks up in a session store.

"Token" is a category, not one thing. Beyond access + refresh, there are a few you'll meet — and the first real fork is self-contained vs reference tokens.

Self-contained (JWT)

Carries its own claims; the server verifies the signature and trusts it. Fast, stateless — but hard to revoke before it expires.

Reference / opaque

A random string that means nothing on its own. The server looks it up in a session store every time. Easy to revoke instantly — but needs a DB hit.

Nightclub A JWT is a wristband with your details printed right on it — any bar reads it directly. An opaque token is a numbered cloakroom ticket: meaningless until someone phones the front desk to check what it maps to.

The ones you'll actually name

Access tokenshort-lived proof you may call the API; sent on every request as a Bearer header.
Refresh tokenlong-lived, guarded; its only job is minting fresh access tokens.
ID token (OIDC)a JWT that describes who the user is (name, email) for the client to read — never sent to APIs as auth.
Opaque / session tokena random reference looked up server-side; the classic cookie-session model.
PAT / API keya long-lived token for machines or scripts, not interactive users.
Don't confuse them The ID token is for your frontend to learn who logged in. The access token is for your backend to authorize calls. Sending an ID token to an API as authorization is a classic mistake.
Access = call the API. Refresh = renew access. ID token = who the user is. JWT vs opaque = stateless-but-hard-to-revoke vs revocable-but-needs-lookup.
04

Where to store the token

Technical definition Token storage is choosing where the client keeps tokens so they survive page loads without being readable by attackers — driven by the platform's threat model.

This is the question interviewers actually care about, because the wrong storage opens you up to attacks. The answer is different on web vs mobile.

Web (React / Next.js)

OptionXSS-safe?Verdict
httpOnly cookie ✔ JS can't read it Best for refresh tokens. Set by the server, auto-sent on requests. Use Secure + SameSite to blunt CSRF.
In-memory (React state) ✔ gone on refresh Good for the access token. Never touches disk; cleared on tab close. Downside: lost on page reload (re-fetch via refresh token).
sessionStorage ✘ readable by any script Like localStorage but cleared when the tab closes. Same XSS exposure — shorter window, not actually safe.
localStorage ✘ readable by any script Common but risky: any XSS bug = token stolen, and it persists across sessions. Convenient, not safe. Avoid for sensitive apps.

Rule of thumb: anything JavaScript can read, an XSS payload can read too. httpOnly cookies are the only web storage JS genuinely cannot touch — which is exactly why the refresh token belongs there.

Mobile (React Native)

OptionSecure?Verdict
Keychain (iOS) / Keystore (Android) ✔ OS-encrypted The right answer. Use expo-secure-store or react-native-keychain. Hardware-backed, app-sandboxed.
EncryptedSharedPreferences / MMKV (encrypted) ✔ encrypted at rest Good middle ground for larger encrypted blobs. Still prefer the Keychain/Keystore for the actual tokens.
AsyncStorage ✘ plain text on disk Fine for non-sensitive prefs. Never store tokens here — it's unencrypted and readable on a rooted/jailbroken device.
Common trap "Just put the JWT in localStorage" works in a tutorial and fails a security review. If a single dependency ships malicious JS, localStorage tokens walk out the door. Prefer httpOnly cookies (web) and the Keychain/Keystore (mobile).
Web: refresh token in an httpOnly cookie, access token in memory. Mobile: both in secure storage (Keychain / Keystore). Never localStorage or AsyncStorage for tokens.
05

Attaching the token to every request

Technical definition Tokens are sent in the Authorization header using the Bearer <token> scheme, usually added centrally via an HTTP interceptor.

A protected endpoint won't trust an empty request. You attach the access token in the Authorization header on every call, using the Bearer scheme. Doing this by hand on each fetch is error-prone, so you centralize it in one interceptor.

Nightclub You don't re-explain yourself at every bar — you just flash the wristband. The interceptor is the reflex that flashes it automatically, so you never forget.
// axios — one place, every request gets the token
api.interceptors.request.use((config) => {
  const token = getAccessToken();
  if (token) config.headers.Authorization = `Bearer ${token}`;
  return config;
});
Set the header in one interceptor, not in every call. The format is exact: Authorization: Bearer <token> — the word "Bearer", a space, then the JWT.
06

The 401 & the silent refresh

Technical definition A 401 means the access token is invalid/expired; the silent refresh uses the refresh token to get a new access token and retries the original request automatically.

Access tokens expire on purpose. When one does, the next protected call comes back 401 Unauthorized. A naive app logs the user out here — but a good one fixes it invisibly using the refresh token.

Nightclub Your wristband stopped glowing, so the bar waves you off (401). You don't go home — you slip to the back door, flash your VIP card (refresh token), get a fresh wristband, and walk straight back to the same bar. The night never paused.

You catch the 401 in a response interceptor: call the refresh endpoint, get a new access token, then replay the original request with it. The user sees their data load normally — they never knew a token expired.

// response interceptor — refresh once, then retry the original request
api.interceptors.response.use(
  (res) => res,
  async (error) => {
    const original = error.config;

    if (error.response?.status === 401 && !original._retried) {
      original._retried = true;
      const newToken = await refreshAccessToken();  // uses refresh token
      setAccessToken(newToken);
      original.headers.Authorization = `Bearer ${newToken}`;
      return api(original);   // replay it
    }

    return Promise.reject(error);  // refresh failed → log out
  }
);
The stampede trap If ten requests fire at once and all get 401, you'll trigger ten refreshes — and nine fail. Fix: while a refresh is in flight, queue the other failed requests and replay them all once the single refresh resolves.
A 401 means "expired," not "log out." Refresh once, retry the original request. Only when the refresh itself fails (refresh token expired/revoked) do you actually log the user out.
07

Single Sign-On (SSO) & "Log in with Google"

Technical definition SSO lets one login work across many apps by delegating authentication to a trusted identity provider (via OAuth 2.0 / OpenID Connect / SAML).

SSO means one login gets you into many apps. Instead of your app checking the password itself, it delegates to a trusted identity provider (IdP) — Google, Microsoft, Okta, Auth0, your company's login. The user proves who they are there, and the IdP vouches for them back to your app.

Nightclub Instead of every bar in town checking your ID, one trusted government office (the IdP) issues a verified pass. Show that pass anywhere and the door just trusts the office's stamp. You proved yourself once; every venue accepts the vouch.

The three names you'll hear

OAuth 2.0an authorization framework — "this app may access X on your behalf." It hands out access tokens.
OpenID Connecta thin authentication layer on top of OAuth 2.0 — adds the ID token that says who you are. This is what "Log in with Google" actually uses.
SAMLthe older XML-based enterprise SSO standard; common in big-company/B2B intranets.

The modern flow is OIDC Authorization Code + PKCE (PKCE is the variant safe for SPAs and mobile, where you can't keep a client secret):

The redirect dance

1) App sends user to the IdP's login page → 2) user authenticates there → 3) IdP redirects back with a one-time code → 4) app exchanges the code for ID + access tokens → 5) from here it's the same access/refresh story as before.

↻ your app never sees the user's password — only the IdP does
Why delegate at all? Your app never touches the user's Google/Microsoft password — so you can't leak it. You also inherit the IdP's MFA, device checks, and account recovery for free. The cost: you depend on the IdP being up and configured correctly.
OAuth = access (can this app do X?). OIDC = identity (who is this user?). SAML = the enterprise elder. "Log in with Google" = OIDC. After the redirect dance, you're back to plain access + refresh tokens.
★ THE FULL PICTURE

Everything working together

This is the whole cycle on one page. Follow the numbered arrows: log in once, attach the wristband on every call, and when it stops glowing, swap it at the back door and retry. Memorize this shape and you can rebuild every answer from it.

CLIENT (React / RN / Next) your app · "the guest with the wristband" TOKEN STORE where the wristband lives access (memory) refresh (cookie/secure) interceptor attaches it AXIOS interceptors: → add Bearer ← catch 401 SERVER / API issues & verifies wristbands · "the bouncer" POST /login → returns JWT 🔑 GET /data → verify signature 🛡 POST /refresh → new access token ♻ stateless: each request is verified on its own ↓ 401 UNAUTHORIZED ✦ token expired the access token's exp time has passed do NOT log out yet — try to refresh first REFRESH → RETRY swap the wristband, replay the original call if refresh ALSO fails → real logout AUTH CYCLE "expired? refresh + retry" ① login + every call (Bearer wristband) → token expired → 401 ③ refresh ④ new access token → store it ⑤ retry the original call — user never knew USER logs in once, then the cycle runs itself ↓
JWTa signed, self-proving token (header.payload.signature) the client shows on every request.
Access Tokenshort-lived JWT sent on every API call via the Authorization header.
Refresh Tokenlong-lived, guarded token whose only job is minting new access tokens.
Interceptorone central hook that attaches the token outbound and catches 401s inbound.
401 → refresh → retrythe silent recovery: on expiry, get a new token and replay the call.

Read it as a sentence: log in once → store the tokens → attach the access token to every call → on a 401, use the refresh token to get a new access token → retry the original request (and only log out if the refresh itself fails). That single sentence is the entire model.

If you can draw these five steps — login, store, attach, 401-refresh, retry — plus the rule "refresh once, log out only if refresh fails," you can answer almost any "how does auth work" question on the spot.
08

The classic "where do you store the JWT?" question

This is the auth question that separates tutorial-followers from people who've shipped. The trap answer is "localStorage." The strong answer names the threat and splits web from mobile.

// WEB (React / Next.js)
access token   → in memory (React state / closure)
refresh token  → httpOnly + Secure + SameSite cookie

// MOBILE (React Native)
both tokens    → Keychain (iOS) / Keystore (Android)
              via expo-secure-store or react-native-keychain

// NEVER
localStorage (web)  ·  AsyncStorage (mobile)  ← readable by attackers

Walk the interviewer through the why:

localStorage is readable by any JavaScript on the page → one XSS bug leaks the token.
httpOnly cookies can't be read by JS at all → XSS can't steal them (pair with SameSite to handle CSRF).
In-memory dies on tab close and never hits disk → smallest attack window for the access token.
Keychain / Keystore is OS-level encrypted and app-sandboxed → the mobile equivalent of "safe by default."

There is no "store it anywhere" answer. The right storage depends on the threat model (XSS on web, device compromise on mobile) and the token's lifespan (short access vs long refresh).

Rapid-fire one-liners

What is a JWT, in one line?
A signed token (header.payload.signature) the server issues at login and the client shows on every request. It's tamper-proof but readable — never put secrets in the payload.
Why two tokens instead of one?
The access token travels on every request, so it's short-lived to limit damage if leaked. The refresh token is used rarely and guarded harder, so it can live long without the same risk.
Where do you store tokens on web vs mobile?
Web: refresh token in an httpOnly cookie, access token in memory. Mobile: both in Keychain/Keystore via secure storage. Never localStorage or AsyncStorage.
What happens on a 401?
It means the access token expired. You call the refresh endpoint, get a new access token, and retry the original request — silently. You only log the user out if the refresh itself fails.
How do you attach the token to requests?
In one request interceptor that sets the header Authorization: Bearer <token> on every call, so you never forget it per-request.
What's the "refresh stampede" and how do you avoid it?
When many requests 401 at once and each triggers its own refresh. Fix it by running a single refresh and queuing the other failed requests to replay once it resolves.
Is JWT stateless? What does that mean?
Yes — the server verifies each token's signature without storing session state. The trade-off: you can't instantly revoke a JWT, so you keep access tokens short and revoke at the refresh-token level.
JWT vs opaque/session token — when would you pick each?
JWT when you want stateless, fast verification across services and can live with short lifetimes. Opaque/session tokens when you need instant revocation and don't mind a server-side lookup per request.
ID token vs access token?
The ID token (OIDC) tells your frontend who the user is. The access token authorizes API calls. Don't send an ID token to an API as authorization.
OAuth vs OpenID Connect vs SAML?
OAuth 2.0 is authorization (can this app access X?). OpenID Connect adds authentication (who is the user?) on top of OAuth. SAML is the older XML enterprise-SSO standard. "Log in with Google" is OIDC.
What is SSO and why use it?
One login that works across many apps by delegating to a trusted identity provider. Your app never sees the password and inherits the IdP's MFA and recovery — at the cost of depending on that provider.