Auroratide / sifetti

ISC License
0 stars 0 forks source link

Refresh Token Automatically #35

Open Auroratide opened 2 years ago

Auroratide commented 2 years ago

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:

Auroratide commented 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.

Queueing Requests on the Client

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.

Centralized Refresh

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).

Auroratide commented 2 years ago

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.

  1. Scheduled refresh is relatively transparent. Downsides include the potential for different tabs to refresh simultaneously, requiring JS (though the site already assumes JS), and over-generating access tokens.
  2. Refresh only when needed, aka when endpoints fail with 401, with good retry logic.
  3. Centralized refresh on the server relying on retry logic from step 2 (aka, draw the rest of the owl).