Refresh mechanism

Assuming a setup where a client SPA has a JWT and the API server stores refresh tokens.

If a user has a JWT that has expired and they make a request it appears the the norm would be to return a 400 or 401 and have them use some other end-point to refresh the token (e.g. /token/refresh), this would then return them a new JWT with a new expiry. They would then have to make the original request again with the new JWT that should now work. This results in 3 requests (original, renew/refresh, original with new JWT).

Would a suitable mechanism also be to automatically refresh the token on their behalf on any request and return the new JWT in a header if the JWT had expired? API usage instructions would document that if the header is present it should replace the existing JWT they have. This would result in a single request.

If this is not suitable, why not?

That’s a procedure I would not recommend (being able to get a new bearer access token by performing a request with just another bearer access token). You just made the access token the equivalent of a refresh token and if it’s not recommended to keep refresh tokens in the browser, the same applies to any other token with the same capabilities (even you call it by a different name).

The recommended approach to refresh tokens in a pure SPA is to rely on an authenticated session at the identity provider and performing prompt=none requests (aka silent authentication) to obtain new tokens. The current limitation to this flow is that a user that actively uses your application will have to re-authenticate at least every thirty days as that is the current maximum lifetime you can configure for the session at the identity provider (your Auth0 account).

If you need to keep users logged in even beyond that you can consider treating your application as a traditional web application and implement your own session management and depending on the situation this may or may not require the usage of refresh tokens.

In this setup there is also a plan to use a session id. The session id would be an HTTP only cookie and the JWT in localStorage.
The refresh token stored server side will be scoped to the session id. In order to refresh you would need to pass both the expired JWT and the sessions id.
Does this make the process more secure?

Having the requirement of a local session identifier in a cookie can of course be an improvement, but then having cookie-based checks also implies considering CSRF. I’m not saying that an approach like that can’t be made safe, it’s just it requires more work and additional considerations on the client application implementation and it’s almost impossible to provide a certificate of “secureness”; it will always depend on the full implementation details.