October 30, 2026
Stolen Access Tokens: Stopping Someone Else From Using a Token
Token theft cannot be fully stopped, so the goal is to make a stolen token short-lived, hard to reuse from another device, and quick to…

By Sribalajivelan
10 min read
Token theft cannot be fully stopped, so the goal is to make a stolen token short-lived, hard to reuse from another device, and quick to detect. IP binding helps but hurts usability; short expiry, refresh token rotation and sender-constrained tokens protect better.
The problem: a token is a key, and keys can be copied
A colleague recently asked a question that comes up on many teams. User 1 logs in and receives an access token. User 2 somehow gets a copy of that token. How can the system stop User 2 from using it to get in as User 1?
The uncomfortable answer is that a plain access token cannot tell the difference. It is a bearer token: whoever presents it is treated as the user it was issued to. The server sees a valid token and has no way to know whether the sender is User 1 or someone holding a copy.
The signature on a JWT proves the token is genuine and unmodified. It does not prove the sender is the person it was issued to, and opaque tokens have the same weakness, since possession is the only proof.
Encrypting the token does not fix this either. Encryption or a signature only protects the contents of a token from being read or altered. A thief who copies the whole token, encrypted or not, can replay it exactly as the real client would. The missing ingredient is proof that the sender owns the token, and that proof has to come from somewhere other than the token itself.
User 2 can get a copy in several ways:
- a token placed in a URL, which then leaks through logs, browser history and Referer headers
- cross-site scripting (XSS) that reads the token from local storage
- a man-in-the-middle on an unencrypted connection
- malware on User 1's device, or a shared computer
- a debug log, crash report or support ticket that captured request headers
So prevention alone is not enough. The realistic goal is to make theft harder, make a stolen token short-lived and hard to reuse, and detect misuse quickly. The sections below cover the options, starting with the idea that prompted the question: binding the token to an IP address.
Idea 1: bind the token to the user's IP address
Binding a token to an IP address stops a thief on a different network, but it logs out honest users whenever their network changes and does nothing against attacks from the user's own device.
How it works: when the token is issued, the server stores the client's IP address, either as a claim in the token or in a server-side session record. On every request it compares the caller's IP with the stored one and rejects a mismatch.
Why it helps: User 2 is on a different network, so the stolen token fails even though the signature is valid.
Where it hurts:
- Network switching. A user moves from home Wi-Fi to mobile data, or between Wi-Fi networks. The IP changes, the session is rejected, and the user is logged out although the session never ended.
- Shared IPs. Offices, campuses and mobile carriers put many users behind one public address (NAT). An attacker on the same network passes the check.
- Dynamic and rotating IPs. Some ISPs and mobile carriers change a user's address during a session, and IPv6 privacy addresses can rotate on their own schedule.
- VPNs and proxies. The visible IP can change at any moment.
Two more limits are easy to miss. First, XSS and malware send requests from the victim's own browser, so they come from the stored IP and pass the check. Second, behind a load balancer or CDN the server sees the proxy's address, and the client IP arrives in a header such as X-Forwarded-For that an attacker can forge unless the server trusts only its own proxy's value. Most web frameworks have a setting for how many proxy hops to trust. With that setting wrong, an IP check can be bypassed by sending a fake header.
The trade-off: strict IP binding gives stronger protection and a worse user experience. For a banking or admin system, forcing a re-login is an acceptable price. For a consumer app, it is often too painful.
A softer version is to treat an IP change as a risk signal rather than an automatic logout. The server compares coarse signals such as the country or network provider (ASN) rather than the exact address, and asks for re-authentication only when the change looks suspicious. A move within the same country passes quietly, while a jump to another continent within minutes triggers a challenge.
Short lifetimes and refresh token rotation
The first and cheapest layer limits how long a stolen token is useful. An access token that expires in 5 to 15 minutes gives a thief a short window. The user never notices the short lifetime, because a longer-lived refresh token quietly obtains a new access token in the background.
That moves the risk to the refresh token, which may live for days or weeks. Refresh token rotation protects it. Every time a refresh token is used, the server issues a new one and invalidates the old one:
- The client sends refresh token R1 to the token endpoint.
- The server looks up R1. If it is unknown or expired, the request is rejected.
- If R1 was already used once before, someone is replaying a copy. The server revokes every token in that login's family and requires a fresh sign-in.
- Otherwise the server marks R1 as used and returns a new access token plus a new refresh token, R2, in the same family.
Why this works: only one party can hold the latest refresh token. If a thief and the real user both try to use R1, whichever comes second triggers the revocation. The theft turns into a visible event instead of a silent one.
The costs are real. The server must store refresh tokens, ideally as hashes so a database leak does not hand out usable tokens. It must also handle the case where a client retries a refresh after a network failure, and many systems allow a short grace period for that. The IETF's OAuth 2.0 Security Best Current Practice (RFC 9700) requires that refresh tokens for public clients be sender-constrained or rotated.
Two lifetimes matter for refresh tokens. An idle timeout ends the session after a period of no use, for example a few days. An absolute timeout ends it after a fixed total age, for example 30 days, however active the user is. The absolute limit caps the damage of a theft that is never noticed, because a stolen refresh token cannot be rotated forward forever. Banking and admin systems usually choose short values and consumer apps choose longer ones, which is the same security versus usability trade-off in another form.
Sender-constrained tokens: DPoP and mTLS
Short lifetimes shrink the window. Sender-constrained tokens close it, because a copied token stops working at all. The token is bound to a cryptographic key that only the legitimate client holds, and the server checks possession of that key on every request. The same RFC 9700 recommends sender-constrained access tokens for exactly this reason.
Two standards cover this:
- DPoP (RFC 9449) works at the application layer. The client generates a key pair and sends a small signed proof with each request. The proof names the HTTP method and URL and carries a timestamp and a unique ID. The access token carries a fingerprint of the client's public key, so a thief holding only the token cannot produce a valid proof.
- Mutual TLS (RFC 8705) works at the connection layer. The access token is bound to the client's certificate, and the server accepts it only on a connection that presents that certificate. This suits server-to-server APIs, where certificates are easy to manage.
Why this is the strongest answer to the original question: it proves the caller owns the token without relying on a network address that users legitimately change. User 2 can steal the token but not the private key, so the token is useless.
The limits are practical. The client, the authorization server and the API must all support the mechanism, so adoption takes work. And a script running inside the page, through XSS, may still use a browser-held key to sign proofs while the page is open. Sender-constraining reduces the damage of a copied token, but it does not replace XSS prevention.
Cookies, XSS and browser storage
For browser apps, where the token is stored decides how easily it can be stolen. A token in local storage can be read by any script on the page, so a single XSS bug exposes it. A token in an HttpOnly cookie cannot be read by JavaScript at all. A hardened session cookie looks like this:
Set-Cookie: __Host-session=<token>; Path=/; Secure; HttpOnly; SameSite=LaxSet-Cookie: __Host-session=<token>; Path=/; Secure; HttpOnly; SameSite=LaxEach part has a reason:
- HttpOnly hides the cookie from scripts.
- Secure sends the cookie only over HTTPS.
- SameSite limits cross-site sending, which reduces CSRF risk. Sensitive actions still deserve an anti-CSRF token.
- The __Host- prefix makes the browser enforce Secure, Path=/ and no Domain attribute, so another subdomain cannot overwrite the cookie.
The limit is that HttpOnly stops XSS from reading the token, but XSS can still send requests as the user from inside the page. A Content Security Policy and prompt fixes for injection bugs are therefore part of token protection, not a separate topic.
For single-page apps, a backend-for-frontend (BFF) goes one step further and keeps tokens on the server entirely. The browser holds only a session cookie, and the BFF attaches the access token when it calls the APIs. Why this helps: a token that never reaches the browser cannot be stolen from it.
Other layers worth adding
Smaller controls round out the design:
- HTTPS everywhere, with HSTS. TLS prevents interception on the network, and HSTS stops downgrades to plain HTTP. Neither helps against malware or XSS.
- Narrow scopes and a single audience. A token limited to one API and the minimum permissions can do little damage if stolen.
- Device or fingerprint binding. Tying a session to a device identifier survives network changes better than an IP. It is easy to copy if the attacker controls the device or script, so it is a weak signal, and it raises privacy questions.
- Server-side revocation. A deny list or session store lets the server kill a token on demand, and a revocation endpoint (RFC 7009) gives clients a standard way to ask for it. The cost is an extra lookup, which weakens statelessness.
- Clean logs. Tokens stay out of URLs, logs, analytics and error reports. A token that is never written down cannot leak from there.
Detect and respond when a token is misused
Prevention will sometimes fail, so the server should notice a stolen token in use and react.
- Anomaly detection. The server flags a token used from two countries within minutes, a sudden change of browser or device, or an unusual request pattern. Why: a thief behaves differently from the owner, even with a valid token. The rules need tuning, because VPNs and travel cause false alarms.
- Step-up authentication. On a suspicious change, the server asks for the password, a one-time code or a passkey again instead of logging the user out outright. Why: the real user recovers in seconds, while the attacker is stopped.
- Revoke on reuse. If a rotated refresh token is presented twice, the whole session family is revoked. Why: only one party can legitimately hold the latest token, so the second use means a copy exists.
- User control. The app shows active sessions, offers a "log out of all devices" button, and notifies the user of a new sign-in. Why: the user often spots theft before any rule does.
When theft is confirmed, the response follows a fixed order: revoke the affected tokens, force re-authentication, review what the token accessed, and notify the user. Audit logs that record which token made each request make that review possible. If a signing key itself leaked, rotating the key invalidates every token it signed.
Matching each theft route to its defense
No single control covers every way a token can leak, so each route needs its own answer:
- A token in a URL or a log. Tokens travel only in the Authorization header or a cookie, and log scrubbing removes them from anything written to disk. Short lifetimes limit what a leaked log can still do.
- XSS in the browser. A Content Security Policy, output encoding and HttpOnly cookies, or a backend-for-frontend that keeps tokens off the page.
- Interception on the network. HTTPS with HSTS, and no plain HTTP endpoints at all.
- Malware on the device. Short lifetimes, keys held in operating-system storage for DPoP, and anomaly detection. Malware on the device is the hardest case, because the attacker acts from the real user's own machine.
- A leaked backup or database. Refresh tokens stored as hashes, so a copy of the database holds nothing usable.
One theft, three designs
The same theft plays out differently under three designs. In each one, User 2 copies User 1's access token at 10:00.
- A plain token that lives 24 hours. User 2 has access until 10:00 the next day, and nothing in the system notices.
- A 10-minute token with refresh rotation. The stolen access token dies by 10:10. To continue, User 2 also needs the refresh token. If User 2 has that too and uses it first, User 1's next refresh presents an already-used token. The server revokes the whole family and forces a fresh sign-in, which only User 1 can complete.
- A sender-constrained token (DPoP). User 2 cannot make even the first request succeed, because the proof requires a private key that never left User 1's device.
The difference between the first and last designs is not cleverness in detection. It is whether the stolen artifact is enough on its own.
Security versus usability: choose by what is protected
Stronger binding means more friction for honest users, so the strictness should match the risk of the system.
- High risk (banking, admin panels, healthcare): short token lifetimes, sender-constrained tokens, strict IP or device checks, and step-up authentication. Users will sometimes log in again, and that is an accepted price.
- Medium risk (typical web apps): short-lived access tokens, rotating refresh tokens in HttpOnly cookies, and anomaly detection that asks for re-authentication only on suspicious changes.
- Low risk (public content, low-value accounts): short expiry and HTTPS are usually enough.
The key point for the original question: IP binding is a valid idea, and the logout on a network switch is a real cost, not a flaw in the idea. That cost is worth accepting where security matters most. Elsewhere, layered defenses that make a stolen token short-lived, traceable and hard to reuse protect users without punishing them for moving between networks.
A practical checklist
- Access tokens expire in 15 minutes or less.
- Refresh tokens rotate, with reuse detection and hashed storage.
- Tokens never appear in URLs, logs or error reports.
- Browser sessions use HttpOnly, Secure, SameSite cookies, or a backend-for-frontend.
- A Content Security Policy is in place, and XSS findings are fixed quickly.
- Scopes are minimal, and each token targets one API.
- High-risk APIs use sender-constrained tokens.
- Anomalies trigger step-up authentication instead of silent acceptance.
- Users can see and end their own sessions.
Common questions
Is JWT the problem? No. A JWT and an opaque token are both bearer tokens, and both can be copied. The defenses in this article apply to either.
Does logging out cancel a stateless access token? Not by itself. A stateless JWT stays valid until it expires unless the server also checks a deny list, which is why short lifetimes matter.
Is IP binding ever the right choice? Yes, for high-risk systems where an occasional forced re-login is acceptable, ideally combined with the other layers described above.