Open Auroratide opened 2 years ago
This is surprisingly nontrivial in the face of parallel requests. That is, if two requests are sent with expired access simultaneously (which does happen on some pages), then both will want to refresh the token. One will win, and the other will lose; the problem is with Gotrue, when an old refresh token is attempted, the current token becomes invalidated, ruining future refresh attempts.
Gotrue reference - A revoked token is handled differently from an incorrect one; Gotrue perceives it as an abuse attempt and therefore clears stuff.
One strat I've seen is essentially forcing parallel on the client to wait for a refresh to finish. That is, when a request fails, it issues a refresh and then retries. Importantly, that request is stored somewhere as a promise. Parallel requests fail too, but we wait for the stored refresh request to resolve before processing these requests.
A natural question to ask is how to handle multi-tab workflows. Since tabs do not share their in-memory values, if they (somehow) issue requests simultaneously, the same sort of conflict can arise.
The most transparent way to handle refresh is to do so in SvelteKit's handle
function, but unless there some shared state regarding what request is currently refreshing, the parallel problem is unresolved.
A centralized store such as Redis could be used for this, or since we're using Supabase, a simple postgres table.
A table might have an entry for a hashed refresh token with a creation date. When a request is made with an expired token, handle
looks for an entry in the Table. If there is an entry, then that means a refresh has already occurred, and a 401 will be issued. The client should then utilize some retry strategy, effectively waiting for cookies to get set.
If there is not an entry, then it gets inserted into the DB, and the refresh is commenced. If the refresh fails, then the cookies are reset and the person will need to log in again.
Cron or something can be used to clean the table every once in a while to minimize collisions and free up data.
Note: this approach requires the superkey be used, as we do not want the table to be publicly accessible, nor is it possible to tie it to user permissions in any way (as, the whole point is the user doesn't have a token to begin with).
Decision: A refresh token endpoint will be created for refreshing the token. This endpoint is useful in two ways:
Although the Centralized Refresh idea probably offers the best immunity to parallelism problems, it comes with enormous complexity. The problem is not that a new table is needed, but that the table has a bunch of security implications: that refresh tokens being as powerful as passwords means they would need to be at least as securely stored, and that the supabase superkey would have to be exposed on the server (which is fine technically, but it's most secure if not used at all). On top of that, incredibly robust handling of 401 by the client must be implemented.
The endpoint creates an opportunity for transition into this architecture if, in practice, parallelism problems are common.
At the moment, when the token expires, it expires and the person has to log in again.
For as long as the person is active, the login should persist. Two strategies for this: