Stage 7 · Master
Authentication & Authorization
Refresh Token Rotation
Keep access tokens disposable and let refresh tokens renew sessions without becoming permanent bearer secrets.
Why Short-Lived Access Tokens Need Help
Once we decided that Fieldwork's access JWTs should be short-lived, the next question was predictable: how short is short enough without making the product annoying to use? Ten minutes is a reasonable security posture for a bearer token that unlocks projects and tasks across a tenant. It is also short enough that a human working through the UI would hit silent re-auth constantly if we did not pair it with a second credential designed for renewal rather than request authorization.
That is the refresh token's job. It is not presented to normal API routes. It is only sent to the auth endpoint to mint a new access token and, in Fieldwork, a replacement refresh token. The important phrase there is replacement. I rejected the simpler pattern where a refresh token lives for thirty days and can be replayed until it expires, because the concrete failure mode is ugly: steal it once and you have a month-long shadow session.
| Pattern | Operational upside | Fieldwork objection |
|---|---|---|
| Long-lived access tokens | Fewer refresh calls | A leaked bearer token stays useful far too long |
| Static refresh token | Easy to implement | Replay remains valid until expiration |
| Rotating refresh token | Reuse is detectable and scope is narrower | Requires server-side token family state |
The Problem with Static Refresh Tokens
Static refresh tokens look attractive because they preserve the dream of stateless authentication. Sign a long-lived token, store nothing, validate it later, done. The problem is that the first stolen copy is still good after the legitimate user refreshes, logs in elsewhere, or rotates their password unless you bolt on a revocation table. At that point you are stateful anyway, just with worse visibility into token lineage.
For Fieldwork, that trade-off was not worth it. Our auth gateway already owns JWT issuance, so it already has a persistence boundary. Using that same service to store refresh-session state is cheaper than teaching every downstream service how to treat long-lived bearer credentials carefully. I wanted one place that could answer three questions: which device session is this, what refresh token is current, and has a previously spent token been replayed?
A refresh token is closer to a password than to an access token. If the database leaks, the attacker should not immediately gain live session credentials. Fieldwork stores a hash of the refresh token and compares submitted values against that hash during rotation.
Rotating Refresh Tokens with Family State
Fieldwork models each login session as a refresh token family. The first login creates a family ID and a current token record. Every successful refresh invalidates the old token, writes a new hash, bumps the current token identifier, and returns a new access token plus a new refresh token to the client. That means a stolen earlier token is no longer a harmless duplicate; it becomes evidence of reuse.
I used a database transaction for the rotation step because refresh requests are exactly the kind of path where concurrent calls create weird edge cases. A user with two browser tabs can race their own session. Without transactional replacement, both requests might observe the same old token as valid and each mint a successor. The database is the only honest referee there.
Handling Reuse Without Breaking Every Session
Rotation only matters if reuse has consequences. Fieldwork's rule is straightforward: if a refresh request presents the wrong secret for the current token slot in a family, that family is revoked and the user must log in again on that device session. I did not escalate that to a global user logout by default, because that response is too blunt for ordinary client races and can punish the victim more than the attacker.
The family scope gives us a better unit of containment. A compromised browser session should not instantly kill the user's valid session on another laptop unless we have stronger evidence. The auth gateway logs the reuse event with user ID, tenant ID, family ID, IP address, and user agent. That gives observability and later incident response without turning every suspicious refresh into a site-wide panic button.
Refresh rotation is an infrequent, stateful workflow, so a transaction is worth it. Normal authorized API calls stay fast because the access token is still validated locally at the gateway.
What Fieldwork Keeps and What It Throws Away
Fieldwork keeps access tokens aggressively short-lived and treats refresh tokens as rotating session credentials with server-side family state. It stores refresh hashes, not raw tokens, and rotates them transactionally so concurrent refreshes do not accidentally fork the session chain. On detected reuse, it revokes the affected family and forces a fresh login for that device session.
The simpler alternative was a static, signed refresh token that lived for weeks. That would have been easier to explain in a toy app, but it was the wrong fit for a real gateway that already owns session policy. Rotation adds a table and a transactional path, but in exchange it gives us a sharply better answer to theft: the stolen token does not stay valid just because its expiration date has not arrived yet.
Mark this lesson complete to store local progress and unlock a cleaner resume path the next time you visit.