Hi guys, as this is my first post, let me first say thank you all a lot for all the work done on RedwoodJS! Of the little time I’ve been working with it, I love it more and more and I really can’t wait for a release candidate to be available
I’ve recently been meddling with Authentication and I really didn’t want to integrate any third-party service as I felt like a local JWT Auth service would serve me better for my projects. Although there seem to be some custom Auth integrations on the forums, nothing really seemed to address what I wanted to do (Please correct me if I’m wrong tho) so I decided to place my implementation here.
The solution uses 2 JWT (an accessToken and a refresh token) stored on the browser cookies generated at the login and set automatically by the response headers of the request. The refreshToken is also stored on the DB for the specific user and is used to refresh the accessToken. The idea is to have a short-lived accessToken for authentication and a long-lived refreshToken, that if valid and matching the one stored on the DB, is used as a premise to refresh the expired accessToken.
Every time the accessToken is refreshed(newly generated), a new refreshToken is also generated and stored on the DB, in order to invalidate the previous one. The refreshToken is also set as HttpOnly in order to avoid being hijacked with scripting.
If some of this logic fails at some point, an error is thrown logging out the user. Logging out also removes the refreshToken stored on the DB, invalidating any further tokens until the user logs in again with the valid credentials.
Because the tokens are set automatically by the response headers, everything is transparent to the client, so even if the accessToken expires, its refreshed on the server and sent back to the client to be able to use on the next request, w/t having to call any another endpoint to do it.
Code - API
I had to meddle with a lot of internal code in order to understand how the authorization module works, but I was extremely happy to be able to find a way that allowed me to do it w/t having to change any core functionality
As I’m reading and changing headers, I had to have access to those on the global context scope, so I could modify them on the gql resolvers (or anywhere else on the API). The way I found to do it is to append them to the context using some ApolloServer plugins (Please let me know if there’s any better way to do it, tho I find this solution quite easy and non-intrusive)
With access to headers anywhere in the API, I proceeded to create my own auth methods for the JWT Auth
There’s a whole range of functionality from validating, invalidating and generating tokens, everything commented so I hope its enough to understand what each of the functions do, but the main method is the one that is responsible for refreshing the tokens
This method is the core of our solution and goes about doing what I have described on the Solution above, it checks if the accessToken is valid or expired, and if the former, validates the refreshToken and generates a new pair of tokens if its true, sending both to the client on the response headers and proceeding with the query as normal.
The methods for generation and invalidation of tokens are used on the login/logout services respectively
And we use the main validation on the auth lib. Note that because we use async methods on our custom validation, such as user fetching in order to get the refreshToken, we need to set the requireAuth method as an async function and await for the conclusion of the validation method, in order for the error handler to behave correctly
We can then use the requireAuth on the services the same way its on the docs, with the exception that the service function also needs to be async and we need to await for the conclusion of the method for the same reason above
Code - Web
There isn’t much we need to do on the Web side, other than to use a custom auth client. I’ve made it very simple, basically just fetching, removing and decoding the accessToken set on the cookies. As I think all of the methods are pretty self-explanatory, I didn’t comment them, but let me know if you need some guidance
To use the custom client, we need to change both the client and the type on the AuthContext
And that’s it, the rest is standard and you can follow the official docs to proceed from here, as an example, here’s how you can use it
I’ve tried to cover the most essential parts in order to prevent this from becoming a 5000 lines topic, but there’s other stuff like cookie expiration times and signatures. You can check the whole thing in here https://github.com/3nvy/rw-jwt-auth-example
Feel free to ask me anything regarding the implementation and please let me know if there’re any issues I’ve missed or Improvements I can make