Security-wise, as long as you've got strong anti-CSRF protection, cookies (with Secure
and HttpOnly
, of course) are the way to go. They're simpler (less room for error), scale to arbitrary numbers of documents (tabs, other windows, child windows, frames, etc.), and won't get leaked in the event of XSS (though an attacker can still remote-control the session so long as the victim has the page open).
However, the "strong anti-CSRF protection" part matters a lot. There's a lot of ways to do CSRF protection, with varying degrees of security, effort, and compatibility. Sometimes people also try things that don't actually reliably work, or rely on protections they think are more universal than in reality (e.g. I once saw a site that rejected requests with the Origin
header on the grounds that all cross-origin requests - even non-CORS ones like form submissions - have that, which might have been true of new versions of Chrome specifically but was by no means universally true back then, and might not even be universally true today). Additionally, while CSRF is the primary security risk for cookies, you should make sure they've got all the right flags (Secure
is in theory redundant with HSTS but use it anyhow), don't allow domains including subdomains you don't need, etc.
Side notes:
the back-end will invalidate the previous pair of tokens
This surprises me, since the whole reason to use refresh tokens is that access tokens are usually JWTs, and completely stateless, which makes revocation/invalidation extremely inconvenient. They thus must have extremely short lifetimes so that a compromised token becomes useless as soon as possible. If you can reliably invalidate access tokens prior to their expiry, why bother with refresh tokens at all?
Also, problems like this are one of the reasons that making refresh tokens single-use is questionable from a UX perspective. Another is anybody on a poor connection where a refresh might reach the server but not get back to the client. While it does add complexity, consider a change such as making the old token not expire until the new one has been used once, rather than immediately upon issue. This will also avoid problemsor when many requests are submittedmay be in flight at around the same time due to latency. There's no good way to fix that while preserving the ability to detect compromised tokens being reused, though asking clients to immediately ping the auth service after refreshing (and not invaliding the old token[s] until that ping shows up with the new one[s]) is an option.