Why refresh tokens are bound to User-Agent, not IP
When a user signs in, NovaVMS issues them a refresh token that is pinned to the browser’s User-Agent string but not to their IP address. If their IP changes — because they walked out of the office, switched to mobile data, or joined a VPN — they stay signed in. If their browser changes, they have to sign in again (per R2).
Why this works this way
The classic refresh-token binding hardens against session theft by refusing to re-issue an access token if the refresh request arrives from a different IP than the one the session was created on. It sounds safe on paper. In practice it generates constant forced logouts for the people who actually use the product.
Decision R2 records the change from the original design.
- A security operator reviewing an incident from a phone on LTE gets logged out the moment their phone hands off between towers in a different carrier segment.
- An admin on a corporate VPN gets logged out whenever the VPN renegotiates a tunnel and re-NATs them to a different public IP.
- A contractor working from multiple sites gets logged out driving between sites, even though the same laptop is in their hands the entire time.
Every one of those cases fires a password-prompt in the middle of someone trying to do their job, and the dominant response is “turn off whatever is causing this” rather than “sign in again.” Since there is no way for us to distinguish a roaming laptop from a stolen session by IP alone, IP binding achieves theatre more than security.
We picked a different rule: the refresh token is bound to the User-Agent header. A stolen refresh token used in a different browser (or a non-browser tool like curl) fails validation. A roaming laptop that keeps the same browser keeps the same session.
This is the same choice Verkada made — their sessions survive IP change — and is roughly the industry default for modern SaaS. EEN’s developer docs are quieter on the point, but their observed behaviour is also IP-insensitive.
What we accepted in exchange: a refresh token stolen out of the exact same browser process (malware running locally as the same user) would pass validation. We do not defend against that case at the refresh-token layer; we defend against it with short access-token TTLs (30 minutes), HttpOnly cookies for web clients so JavaScript cannot read the refresh token, HTTPS-only transport, and the Revoke Sessions action in the admin console. If a machine is compromised, the compromise is local; the defence is local.
A future enhancement would bind to a richer device fingerprint (User-Agent plus Accept-Language plus TLS JA3 plus a persistent client-generated ID), but that is a device-trust feature, not a session-binding one. It is on the roadmap under the same umbrella as SSO and MFA.
What this means for you
- Your users stay signed in across networks as long as they stay in the same browser.
- Switching browsers (Chrome to Firefox, desktop to mobile) requires a fresh sign-in. Private or incognito windows are separate from the main browser process for this purpose.
- Changing the User-Agent string manually (developer-tools device emulation) invalidates the session. This is a feature, not a bug.
- If a user’s laptop is lost or stolen, revoking their sessions from Admin → Active sessions kills both the access token and the refresh token immediately, no matter what IP the thief is on.
- Changing a user’s password revokes every session of that user across every device — this is how an admin force-logs-out a compromised account.
- Mobile app sessions use a Bearer-token variant of the same rule (D48). The same User-Agent-binding principle applies on a per-
client_typebasis.
Where to go next
- Revoke active sessions — the how-to when a laptop goes missing
- Why NovaVMS uses five roles — why only admins can revoke sessions
- Audit actions — the log entries session revocation writes