I was reading up on token expiration when a question hit me that I didn’t have a quick answer for. If the access token only lasts fifteen minutes, how is anyone still logged in from last week? No password prompt. No interruption. It just works. And most people have no idea why.
The answer is something most people have never seen and probably never heard of. A refresh token.
The badge behind the badge
Access tokens are supposed to be short-lived. Five minutes, fifteen minutes, maybe an hour. The idea is that if someone steals your access token, the window of damage is small. The token expires quickly and the attacker is locked out.
But short-lived tokens create a terrible user experience. Nobody wants to reenter their password every fifteen minutes. There’s got to be a way to get a new access token without making the user log in again.
That’s what a refresh token does. It sits quietly in the background. When your access token expires, the app uses the refresh token to request a new one. The user never notices.
Think of the access token as a visitor badge that expires at the end of the hour. The refresh token is what lets you walk back to the front desk and get a new one without going through the full security screening again. You never leave the building. You just swap badges.
What the badge is actually worth
The access token might have a fifteen-minute lifespan. The refresh token? It might live for days. Weeks. Sometimes months.
If an attacker steals your access token, they have fifteen minutes. If they steal your refresh token, they can generate new access tokens for as long as it’s valid. They keep coming back, day after day, and the front desk hands them a fresh badge every time.
The refresh token is more valuable than the access token. It’s not the badge itself. It’s the ability to walk up to the front desk and get a new one whenever you want, for as long as you want. And yet it gets a fraction of the security attention.
So where does this thing actually live?
Where the badge ends up
Someone has to store the refresh token on the user’s device. And where it ends up matters more than most teams realize.
In a browser, your options are limited. localStorage (a small storage space built into the browser) is wide open to any JavaScript running on the page. That means a single XSSCross-Site Scriptinga vulnerability where an attacker injects malicious code that runs in other users' browsers vulnerability can steal it. XSSCross-Site Scriptinga vulnerability where an attacker injects malicious code that runs in other users' browsers is when an attacker injects malicious code into a web page that then runs in other users’ browsers. One vulnerability, and every refresh token in localStorage is exposed.
Cookies are better if they’re locked down properly. Mark them HttpOnly so JavaScript can’t read them, Secure so they only travel over encrypted connections, and SameSite so they don’t get sent on cross-site requests. Think of it as putting the badge in a locked drawer instead of leaving it on the desk.
In a mobile app, you should be using the platform’s secure storage: the iOS Keychain, or Android’s equivalent encrypted storage. But “should” and “do” are different words.
In practice, refresh tokens end up in all sorts of places. Logged to the console during debugging. Stored in plain text in a database. Passed in URL query parameters (the part of a web address after the question mark) where they end up in browser history and server logs. Handed to AI agents that need persistent access to act on your behalf. Those tokens might not raise the same red flags a human session would.
One defense is supposed to catch this. It’s clever. And it’s not enough.
Rotation helps, but it isn’t a cure
One common defense is refresh token rotation. Every time the app uses a refresh token to get a new access token, the server also issues a new refresh token and invalidates the old one. Think of the front desk keeping a logbook. Every time someone swaps a badge, the old one gets shredded. If someone shows up with a badge that’s already been shredded, the front desk knows something is wrong and can lock down the entire chain.
Say an attacker steals a refresh token and uses it. The next time the real user tries to refresh, it fails. The token’s already been used. The server sees the same badge was used twice and revokes every badge that traces back to that original login, forcing the user to prove who they are all over again.
Smart idea. But it only works if the server detects the reuse, and if the user attempts to use the old token before the attacker’s new one expires. If the attacker uses the stolen token on a Friday evening and the user doesn’t open the app until Monday morning, the attacker has had the entire weekend to work with fresh badges.
Rotation raises the bar. But it says nothing about what happens when the badge never expires.
The badge with no expiration date
Users expect mobile apps to keep them logged in forever. Nobody wants to log in again to their banking app, their email, or their social media every few days.
So refresh tokens on mobile tend to have very long lifetimes. Some are set to never expire at all. Imagine a visitor badge with no expiration date. The thinking is that the device itself is a trust factor. If you have the phone, you’re the user.
That logic holds exactly until the phone itself becomes the problem. Malware on a phone can extract refresh tokens just like malware on a desktop can steal cookies. The difference is that the mobile refresh token might grant access for months, while the browser cookie might have expired days ago.
The long-lived mobile refresh token is a permanent credential. A permanent visitor badge. And permanent credentials are exactly the kind of thing the short-lived token model was designed to avoid.
The front desk nobody builds
I’ve seen teams get the hard parts right. The difference comes down to three questions. Which badges are active right now, who holds them, and what can they do?
Most teams I’ve worked with can’t answer that.
The ones that can have a few things in common. The refresh token lands in the most secure storage available: a locked-down cookie in the browser, the platform’s encrypted vault on mobile. Not a log file. Not a URL. Not plain text anywhere.
Every old badge gets shredded during the swap, and every swap gets logged. The front desk keeps a ledger of every badge that traces back to a single login. Someone shows up with a shredded badge? The entire chain gets revoked. And lifetimes have a ceiling. Even on mobile. Even when users complain. Because a refresh token that lives forever is a permanent credential wearing a temporary credential’s name.
Sounds thorough. It is. The rest built the front desk, locked the drawers, set the expiration dates. And then never looked at the ledger again.
The quiet one is the dangerous one
Access tokens get all the attention. They’re the ones flying around in headers, getting validated on every request, triggering rate limits and filling up logs.
The refresh token sits in the background, doing nothing most of the time. But when it moves, it moves with more authority than anything else in the system.
Most teams can tell you exactly how many active user sessions they have. Almost none can tell you how many active badges are still out there. Who’s wearing them. Whether anyone at the front desk is even checking anymore.
I wanted to know how someone stays logged in from last week. The better question is who else still is.
Previously on Off White Paper: The Flimsy Wristband explored session cookies, The Self-Signed Permission Slip followed the token once it leaves the server, and The Dialog Box You Never Read looked at the moment you hand that access to someone else. This post picks up the credential that was working behind the scenes the whole time.