#Architectural advice for using Remix as a BFF against an external API

1 messages · Page 1 of 1 (latest)

cerulean rock
#

Hey folks, I'm new to the frontend (using Remix v2). I'd like to get some critique on how I'm setting up a new project:

I'm using Remix as an auth proxy of sorts:

  1. Remix sends credentials on behalf of user to an external API
  2. JWT is stored in a cookie using createCookieSessionStorage
  3. Before Remix makes an API call on behalf of the user, the expiration claim is checked and the token is refreshed if necessary
  4. Upon logout, the cookie is destroyed

Additional questions:

  • Are there libraries to automatically implement the refresh logic?
  • If a token is forcefully revoked on the backend, how do you "bubble up" an arbitrary 401 response to redirect the user back to login?
  • How do people implementing refresh tokens (OAuth/JWT) typically handle race conditions: e.g. multiple requests/responses on a refresh can lead to the sessionCookie being set in competing requests... (this seems like an issue common to any frontend deployment).

Appreciate the help in advance ❤️

dapper thunder
velvet charm
#

Are there libraries to automatically implement the refresh logic?
For web-oidc (https://github.com/sergiodxa/web-oidc) I added a refresh method, that package comes with a OIDCStrategy for Remix Auth so if you're using Remix Auth with a OIDC compatible (not just OAuth) provider you can use that strategy, it will only hide how to do the refresh, you still need to update your session, btw doing this is just a fetch so it's not that hard anyway.

If a token is forcefully revoked on the backend, how do you "bubble up" an arbitrary 401 response to redirect the user back to login?
Usually you can know that by doing a fetch, the response should be a 401 and you can then destroy the user session and redirect it to the login, if you handle 401s by refreshing the token the backend would need to also invalidate the refresh token, this way the refreshing will also fail with a 401 and then you can do the destroy session + redirect to login.

How do people implementing refresh tokens (OAuth/JWT) typically handle race conditions: e.g. multiple requests/responses on a refresh can lead to the sessionCookie being set in competing requests... (this seems like an issue common to any frontend deployment).
This is a hard one, to solve the specific issue in Remix where multiple loaders run together and you want to refresh only once the simplest way is to do it on the HTTP server (e.g. Express) before Remix's request handler runs, this way it will always happen once.

The problem is that this solves the issue only on SSR where multiple loaders handle the same request, but if your app fetches more than one loader client-side then each loader will have a unique request meaning they won't run in the same request context, so each one will run the refresh "middleware".

I'm not 100% sure how to handle this, I would probably check how it's solved in Rails/Laravel/etc since they should have the same issue, one way could be store the tokens in a central place and batch the refreshes there.

cerulean rock
#

Thanks for the in-depth responses guys.

WRT to the refresh tokens. I think the only solution is to use a mutex lock on a user instance in a distributed cache like Redis. My thinking is that most deployments use lambas, so parallel requests going to different server instances must be handled too.

Most server side frameworks like Django will lock a user instance in the DB while an update happens, or they'll construct a one-time use token based on a hash of some info that changes like user_id-last_updated_at.

I think the way forward here is to recognize that refresh tokens, specifically short-lived ones like in JWTs are probably unnecessary for Remix applications when authing with an external backend. We ended up implementing https://github.com/jazzband/django-rest-knox which has long time tokens who's TTL is extended upon each use:

Client <-- Cookie Auth --> Remix <-- Bearer Token with extensible TTL --> Django API

So TLDR: Only use JWTs/OAuth if you really need the benefits of short-lived TTLs (and you probably don't need them with proxying auth).

GitHub

Authentication Module for django rest auth. Contribute to jazzband/django-rest-knox development by creating an account on GitHub.

velvet charm
#

My thinking is that most deployments use lambas, so parallel requests going to different server instances must be handled too
Yes, this makes this things even harder, if you use a normal Node process and have one instance you could at least count with all requests being handled by the same process, with serverless or edge, this doesn't happen anymore since each request can be a completely separate instance in a completely different hardware, in some case even in different data centers, you have no way to know or rely on that

#

I think the way forward here is to recognize that refresh tokens, specifically short-lived ones like in JWTs are probably unnecessary for Remix applications when authing with an external backend

I also agree on this one, most but it's hard to convince most people to don't use them, years of working on SPAs make both frontend and backend devs expect to use refresh tokens, but honestly on a Remix app unless you filter the token somehow there's no need to refresh it, you can just trust app to keep it safe server-side

#

also this can work for your own backend, but if you need to keep refresh tokens for external APIs (like Google) you will need to use refresh tokens, but in that case it's better to handle it outside the request->response cylce and refresh them in background

cerulean rock
#

Spot on.

#

but if you need to keep refresh tokens for external APIs (like Google) you will need to use refresh tokens, but in that case it's better to handle it outside the request->response cylce and refresh them in background

I would argue, that in this case it's better to store these on the client than in Remix, since the client will be able to perform a mutex on the refresh itself. Or even better: let the client do the refreshing and push the access_token to Remix (if it needs to be used in loaders).

velvet charm
#

that may work only depending on how you need to use the token

#

what if you want to keep your DB up to date with the contacts of a Google user? you don't want to wait for the browser to use the app to refresh them, you can keep the token server-side along the refresh token and use a background job to consume Google's API

cerulean rock
#

That approach generally works. But the only problem here is that your refresh jobs become large as you scale: the more users you have, the more refreshes you'll be doing. I've worked on backends that do this at scale and it becomes challenging to deal with. A nice compromise is to initiate the background job only on a user becoming active, but this has the assumption that refresh_tokens don't have expirations.

The OAuth 2.0 spec for example doesn't define an expiration for refresh tokens and some providers (like Discord) don't have expirations... making my compromise above possible. But others, like JWTs have a short expiration (24 hours) so doing what you suggested is the only real solution.