All articles

OfficeFighters — JWT, refresh-token rotation and multi-tab session management

In-memory access tokens, an HttpOnly refresh cookie, token families, concurrent refreshes and coordinated sessions across browser tabs.

t.Turgay
OfficeFighters — JWT, refresh-token rotation and multi-tab session management

Session handling in OfficeFighters must cover multiple tabs, reconnecting players and concurrent requests that discover an expired access token. SessionService and the Angular AuthService divide this work between server policy and browser coordination. Their implementation makes token storage, rotation and revocation separate decisions.

Use distinct storage policies

TokenStore keeps the access token in an Angular signal in memory. A new page does not restore it from persistent browser storage; refresh re-establishes the session. The refresh token travels in a cookie with the policy below.

// RefreshCookie.Write: policy from the application.
response.Cookies.Append("__Secure-of_rt", token, new CookieOptions {
    HttpOnly = true,
    Secure = true,
    SameSite = SameSiteMode.Strict,
    Path = "/api/auth",
    MaxAge = maxAge,
    IsEssential = true
});

HttpOnly prevents JavaScript from reading the cookie value. Secure requires HTTPS transport, while the path narrows where it is sent. Those properties do not prevent injected application scripts from making authorized requests, so XSS prevention and endpoint authorization remain relevant.

Cookie endpoints require a request contract

RequireCsrfHeader checks X-Requested-With: of on refresh and logout. The value is not a secret. The defense relies on the custom-header request boundary together with SameSite=Strict and the CORS policy. Allowing arbitrary origins to make credentialed requests with that header would undermine the intended boundary.

Find the stored token by hash

SessionService hashes the received refresh value and looks up its row. It checks the token expiry, absolute family expiry and revocation state. Before issuing access, it also checks whether the account is active and whether a ban applies. A previously issued token therefore does not bypass current account decisions.

Claim single use with a conditional write

UsedAt changes only when the row remains unused and unrevoked. Concurrent readers cannot both win the ordinary claim.

// SessionService.RefreshAsync: claim the single-use token.
var claimed = row.UsedAt is null && await db.RefreshTokens
    .Where(t => t.Id == row.Id && t.UsedAt == null && t.RevokedAt == null)
    .ExecuteUpdateAsync(s => s.SetProperty(t => t.UsedAt, now), token) == 1;

// The new token inherits the absolute family expiry.
var (refresh, next) = NewRefresh(
    user.Id, row.FamilyId, now, row.FamilyExpiresAt, row.DeviceLabel);

The replacement inherits FamilyId and FamilyExpiresAt. Refreshing does not restart the family's absolute lifetime. ReplacedById identifies the primary successor, while logout and detected reuse revoke the family.

Bound the concurrency tolerance

The source defines a ten-second ReuseTolerance. A repeated use in that short window can create a sibling token, subject to a count of successors issued after the original use. It is not an unlimited retry allowance. Outside the window or beyond the sibling bound, the family is revoked for ReuseDetected.

This is an explicit availability-versus-reuse-detection policy. Important boundary cases are a second request just inside the window, one outside it and another competing refresh after the allowed successors already exist.

Serialize refresh with revocation

Refresh and logout run through AtomicWrite's Serializable transaction. A conflicting operation can be retried against current family state. Security changes made on a rejected path must still commit when intended: returning a failure while silently rolling back the revocation would leave the token usable. Business success and persistence of a security decision are different questions.

Coordinate tabs and ignore obsolete results

AuthService shares in-flight refresh work inside a tab and uses Web Locks to serialize work across tabs where available. BroadcastChannel announces session changes. A generation counter rejects results that arrive after logout, preventing a late response from restoring an abandoned session.

The service also distinguishes temporary unavailability from a denied session. Useful end-to-end cases include a reload, two simultaneous tab refreshes, logout during refresh, an expired family and server revocation. Correctness means that the browser follows the current session decision, not merely that a new JWT can be minted.

Kaynaklar / References

Microsoft Learn — SignalR authentication

More in the OfficeFighters series

You have reached the end of this orbit.
Explore more articles
turgay.A little code, a few pixels, a lot of curiosity.© 2026 · Made on Earth