Local JWT Auth Implementation

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 :smiley:

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.

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

QoL
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 :slight_smile:

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

Conclusion
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 :slight_smile:

Peace :call_me_hand:

6 Likes

I didn’t have a look at the code yet, but thank you for doing this! This is precisely what I was missing too.

I never understand the reliance on third-party services, who like to nickel and dime their customers with their artificial limits. IMHO a local implementation should be the default.

1 Like

Amazing work and welcome to the community! We’ll definitely make sure to get this into the awesome-redwood repo so others can see it and check it out. If you’re interested in contributing to the redwoodjs.com repo as well I think this would be a great addition to the official docs.

We had a post about using JWT’s with Github back in May and we’ve had a ton of different work done with 3rd party Auth providers but this looks like the most general solution I’ve seen yet.

1 Like

Thanks, Happy to contribute to the community. I’ll definitely put some docs together and put them on the repo README at least, for easier integration. I also want to tidy up some bits so hopefully ill get it done by today or tomorrow :slight_smile:

2 Likes

Hi @3nvy and thanks for working through this solution.

I know I have made my position on rolling one’s own Authentication before – and it’s not something I advocate or do … but I do understand why there is a use case.

I just have to ask is that use case is worth the risk and the responsibilities one assumes as a an app developer and custodian of personal information and credentials.

And for many it might be. I just think anyone doing so needs to be aware of this and also do their earnest effort to safeguard that information.

To this end I have a few points:

1 - I like that you implemented the jwt-identify as a function – but what you have posted seems more of a lib abd not a Lambda function. If you see the RW function generator, it’s not really following that pattern.

That said, I really do think that the JWT auth “service” should be implemented as functions – but those should be secure and verified by some secret key.

The functions can verify, issue, revoke, refresh tokens etc. In fact it is not so different that what GoTrue sets out to do:

This also means that the token and user creation is done outside the GraphQL api and services.

2 - Please be aware that by having a model like:

model User {
  id            Int     @id @default(autoincrement())
  email         String  @unique
  username      String  @unique
  password      String
  refreshToken  String?
  createdAt     DateTime @default(now())
}

where the password and refreshToken is available (albeit the password is bcrypted) that because:

    users: [User!]!
    user(id: Int!): User
  }

and given:

export const users = () => {
  return db.user.findMany()
}

export const user = ({ id }) => {
  return db.user.findOne({
    where: { id },
  })
}

export const createUser = async ({ input }) => {
  const password = await bcrypt.hash(input.password, 12)
  const user = db.user.create({
    data: { ...input, password },
  })
  return user
}

export const updateUser = ({ id, input }) => {
  return db.user.update({
    data: input,
    where: { id },
  })
}

export const deleteUser = ({ id }) => {
  return db.user.delete({
    where: { id },
  })
}

… from what I can tell, none of these require authentication.

This means I can fetch your entire user list and their encrypted passwords.

In fact because UpdateUserInput and updateUser is open, I can change anyone’s password – especially because

 id            Int     @id @default(autoincrement())

you are using incremental integer ids.

So, update user 1, 2, 3, 4,5 etc.

I won’t go on because there’s some great pieces here and I think if this solution:

  • is re-arranged into a function-based API that follows the GoTrue interface service (auth, token management, profile management) that checks for and validates a secret token
  • don’t expose any user model data openly on the GraphQL api
  • perhaps have a separate Credentials model that uses the a uuid generated on user create but without a relation that keeps the User and the password/profile data related but not connected
  • Have a look at what Blitz has to do provide auth securely and here. It’s a lot of work.

This could be a great option for people who cannot use a third-party auth service.

Next:

My question to the redwood team is - does RW want to be in the Auth business or the Framework business. Building an auth service is hard and have to assess if efforts are well spent there.

Last:

I don’t meant to be the bearer (haha pun intended!) of bad news, but web apps cost money. Infrastructure costs money.

And I don’t work for Auth0 or anyone else, but I have been in the position to do a cost benefit and risk analysis of choosing a service or rolling one’s own and going with a service always wins. (And I know in certain countries some of these services are not an option – I get that. There is always a contrary case and that happens.)

Auth0 starts charging at 1,000+ monthly active users. And I think that is $23 US or $276 per year.

I’ve freelanced and hired developers and in the best case scenario that – for the year – is 2 hours of work or maybe 5 if you can pay someone $~50/hr. But even if you pay them $15/hr (which believe it or not is $3 more than most minimum wage jobs in the US). That’s 4 hours work for 5 days (or 1 week). And you might say, with this auth client I can get it up and running in an hour – and you’d be right. But - you don’t have: password change, email verification, rules pipeline, RBAC support a login/logout or any UI, etc.

So, for the cost of a handful or two hours of work – you have an Auth services than can service 1,000 active users per month. For. A. Year.

Yes, the cost goes up as the user base goes up – but … guess what … everything about web apps gets more expensive one you have to support more users.

So, no surprise here, but graphql in Redwood is a function – it’s a Lambda function running on AWS somewhere if you deploy to Netlify.

Netlify on it’s Free/Starter and Pro ($19/mo) plans give you 125,000 function invocations per month. At $99/mo Business (and Enterprise) you get unlimited.

So, let’s say you do have 1,000 active monthly users and we say that they use your app everyday except some weekends … so 25 days actively using the app (to make it simple).

1,000 users * 25 days = 25,000 user active days
125,000 pool of invocations for month / 25,000 = 5 function invocations

Or - 5 functions calls per day * 1,000 users * 25 days = 25,000 * 5 = 125,000

So (if I did my math right – and call me out if I did not) then each user can make 5 GraphQL api calls a day. 5.

So, if this is Twitter. Login, View timeline. Paginate 3 times. and… done. You hit your quota.

But, no problem. For $99 month, you get unlimited calls. Problem solved. Or there is a then $7 per 500 call charge. So, make 1,000 more calls, then that’s $14.

Even if you deploy yourself to AWS Serverless (which i have in RW for certain background jobs) you pay AWS per function call.

Things cost money. You just have to spend it well - -and find things that provide the most value.

And for my money, Netlify (and Vercel and others) as well as Auth0 or Magic or Netlify Identity is money well spent - -so I can focus on building my app, building a great UX, focus on my app and not the auth.

Please, I do not mean to discourage – in fact I encourage that a custom auth client be made – I just want to be transparent in the responsibilities and effort and costs to make it so.

Happy to discuss further.

2 Likes

Hi @dthyresson, let me first start by saying major thanks for taking the time in reviewing this and give ur feedback, definitely didn’t discourage, feedback good or bad is always welcome to build upon. I mostly did this because not only in my specific case a local auth system would suffice, but it also allowed me to go about lots of internal code for the framework which proved to be an extremely educational journey :smiley: , and since it was done, I may as well just share it with the community if someone wants to follow the same path or it’s on the same circumstances.

I also want to say I agree with you in whether or not RW should follow the path of having its own Auth client, security is always a hot trend everywhere, and delegating that responsibility to proved third-parties is always a smart move because, in case of data leak, the responsibility often lies on the provider, saving us a lot of headaches xD Plus as you said, it’s running on a lambda function, therefore you pay for each execution which can obviously increase costs.

For that same reason, I was extremely keen on having this done in a way that would not require changing any core files on the framework, and provide this implementation more in a way to show people that are willing to take the risks associated with a local auth or that like me, want something local for their small projects, how to do it and what’s possible.

That said, I took a lot of valuable information from your feedback. For starters, it was actually a mistake to allow the user data to be exposed. I was going to put it behind a role-based auth which I’m doing right now, and I mostly wanted to show how you could go about just implementing the auth client itself rather than explicitly protect sensitive info from the DB at the time of the post, tho I agree that I can at least protect User info on the example if people are going to use it as a basis, so rest assured that will be immediately rectified XD

I was actually thinking about striping sensitive info like password and refreshToken on the gql resolvers for the user, but having a different model for credentials may not be a bad idea, I’ll definitely look into it.

I’ll also look into re-arrange the whole thing into a more function-based API like the GoTrue one u sent :+1:

And finally, thks for the Blitz link, they seem to use a similar implementation with cookies, so I oughta take a lot of good intel from there :smiley:

4 Likes

Also here is some info on using bcrypt: https://auth0.com/blog/hashing-in-action-understanding-bcrypt/

Note the use of multiple salt rounds.

const bcrypt = require("bcrypt");
const saltRounds = 10;
const plainTextPassword1 = "DFGh5546*%^__90";

bcrypt gives us access to a Node.js library that has utility methods to facilitate the hashing process. saltRounds represent the cost or work factor. We are going to use a random password, plainTextPassword1 , for the example.

I believe Argon2 using argon2id is the best option for hashing nowadays. Note, the salt is included in the hashed result. :sunglasses:

const hashedPassword = await argon2.hash(password, { type: argon2.argon2id })

Source:

We use bcrypt officially for the Node backend curriculum at Lambda School but one of the instructors (Jason Maur) recommends Argon2 as his personal choice.

Hi guys, thanks a lot for the feedback. Have been working on improvements throughout the weekend and I bring some news :smiley:

Major updates are:

  1. RBAC support added

  2. Auth logic has been moved away from graphql to its own λ functions

  3. Removed sensitive data from graphql queries and mutations

In-Depth Explanation:

Role-base authentication has been added, all new users will be given the default role of user. Ive added the ability to add a default admin user on the seed file so you can populate your DB with a root/amind user. I’ve given it some generic data that you can (and should) change to your like at /api/prisma/seeds.js

Auth logic on the API side has been moved away from graphql to its own λ functionst, allowing for a more robust solution that can be used for other services outside the graphql one. For now there’re are 4 distinct functions, login, logout, signup & refresh, each of them doing exactly what it looks like

functions

Before, as the auth was coupled with graphql, I was looking at the tokens expiration date on each call, and refresh it immediately if they had expired on the auth service to prevent the user from being logged out, but such isnt possible now that both services are detached. I’ve seen a solution around that handles token refresh on the client-side, but it does so on the error callback and I wasn’t too fond of that solution because it would end-up making unnecessary calls (given it would call the graphql function again upon token refresh) and so I used a similar logic but as a middleware for ApolloClient, using an ApolloLink, allowing me to check and refresh tokens (if needed) before sending any call to graphql

Removed sensitive data from graphql queries and mutations, meaning its not possible to fetch/change/delete information like passwords or roles through grapql. The schema has also been changed to separate concerns between Auth data and User data into 2 separate models:

The Auth data has been put into the User model:

And the User info has been moved into the Profile model:

Essentially, you should never need to change anything on the User model, and all changes required in there, like password change, refresh token change, …, should be handled by the auth service. All data you may want to add and export to the FE should be added to the Profile model, separating concerns and presenting a much less security risk

The logic behind how the Auth works is still the same, short-lived accessToken for authentication and a long-lived refreshToken to use in order to generate a new pair of tokens if valid (comparing to the one stored on the DB), so nothing has changed on that aspect. The refreshToken is set as an HttpOnly cookie to prevent it from being scrapped using scripting.

TODO:

  1. Have a look at the proposed Argon2 implementation. For now I’ve used bcrypt until I read more about that algorithm, but it does look promising :slight_smile: You are also free to change it yourself, theres not much going on with bcrypt other than encrypting passwords, so it should be an easy change to anyone :+1:

  2. Secure flag for cookies. Make sure that the refreshToken cookie is also sent as secure once in a production envoirment

  3. Forgot Password. Add ability to the user to change his own password

  4. You tell me :smiley:

Notes
Update: Ive merged the changes with the initial repo as I can see some people were already following it (https://github.com/3nvy/rw-jwt-auth-example). Old code can be founded at the deprecated branch

3 Likes

@3nvy amazing work with this. Are you using this system on any production sites?

Also a feature request from me:

  • Magic links :slight_smile:
1 Like