Auth in MERN: Every Mistake I Made So You Don't Have To
I implemented authentication three times before getting it right. JWT confusion, cookie problems, and security holes I didn't know I had.
I thought authentication in a MERN stack application was going to be easy when I first started. The user types in their username and password. Then you send them a special token. After that the user is logged in to the MERN stack application. It sounds simple.. The thing is, authentication, in a MERN stack application is not that simple.
Then reality hit me. Where should I put this token? What happens when the token expires? Can someone take the token from me? Before I knew it I was in a lot of trouble, with security ideas that I did not know about.
Here is what I learned from making mistakes with the many times.
Version One: LocalStorage (Seemed Fine Until It Wasn't)
I tried to store the JWT in LocalStorage. This is what I saw in YouTube tutorials. I stored the JWT in LocalStorage. Included it in the Authorization header with every request. It was clean and easy to understand.
I use Node.js and React for most of my work. So I thought JWTs would be easy to use.
It worked. Users could log in. Make requests that needed authorization. Everything worked like it should.
Then a friend who is a developer looked at my code. We were going over my code together. He said "you are storing tokens in LocalStorage?" I said yes. I did not know what the problem was.
He told me that any JavaScript code that runs on my page can look at LocalStorage. If someone can add code to my page because of an XSS vulnerability they can take the token and send it to themselves. I did not know this was possible, with LocalStorage and JWT.
Version Two: Cookies (And the CORS Nightmare)
I decided to use HTTP- cookies. This meant that the browser would send them with every request and JavaScript could not do anything with them. It seemed like a solution. Nothing worked out.
My frontend was making requests, to the backend. The cookie was never sent with these requests.
I looked at the browser tools to see what was happening I looked at my backend logs. I even looked at my code again to make sure everything was correct. The cookie was being set,. It was not being sent with the requests.
This became a problem when I started to deploy my application to production environments and I had to handle requests that came from different places, which is part of my DevOps deployment process.
I had to deal with -origin requests and this was very important. The cookie issue was still a problem. I had to find a way to solve it.
Here's what finally worked:
// Backend (Express)
app.use(cors({
origin: 'http://localhost:3000',
credentials: true
}))
// Frontend (fetch calls)
fetch(url, {
credentials: 'include'
})
The server and the client have to be, on the page when it comes to sending credentials. If either the server or the client misses this step it will just fail without saying anything. The server and the client really need to agree on this or else the server and the client will not work together properly. This is really frustrating because the server and the client will not give you any warning if something goes wrong.
The Logout Problem
My access tokens would expire after one hour, which seemed like a good idea for security.. The thing is, users would get kicked out all the time. This would happen when they were in the middle of doing something like filling out a form or working on a project.
This was really frustrating for the users. It was not an experience at all.
Then I learned about refresh tokens. The idea behind them is that you have two kinds of tokens. You have a lived access token that lasts for a short time like 15 minutes and you use this token to make requests to the application programming interface.. Then you have a long-lived refresh token that lasts for a longer time, like 7 days and you use this token to get a new access token when the old one expires.
I store the refresh token in a kind of cookie that can only be accessed by the web server. When the access token expires I use the refresh token to get an access token. This way the user can stay logged in without having to log in.. If someone gets their hands on the access token it will only work for a short time like 15 minutes.
The hard part is making sure that refresh tokens cannot be used in a way. To solve this problem I store the refresh tokens in the database along with the user ID. When I issue a new refresh token I make sure the old one does not work anymore. This is not a solution but it is a lot better, than what I had before.
Logging Out with JWTs. A Head Scratcher
I had a time figuring this out.
When using JWTs the token stays valid until it expires even if a user clicks the "logout" button. You can't simply invalidate it because JWTs are stateless. They are data that is cryptographically signed not something that can be revoked on the server.
Some developers keep a list of tokens but that means checking the database for every request, which defeats the purpose of statelessness.
My approach is to store a version number in the users record. When someone logs out I increase their version number. Each token includes the version it was issued for. If the versions do not match during validation the token is rejected, even if it has not expired yet.
Is this the way to do it? I am not sure.. It works and does not feel too complicated.
Passwords and Timing Attacks
I knew to use bcrypt for password hashing. That's rule number one for authentication right?
I didn't know about timing attacks. When you compare passwords with a string comparison attackers can measure how long it takes. They can use this to guess the password character by character.
It sounds a bit paranoid. Timing attacks are real.
Thankfully bcrypts compare function does constant-time comparison automatically. I had written custom validation logic that didn't. That could have been vulnerable. I removed the code quickly.
Should I Have Just Used Sessions?
Here's the thing. After all this I realized sessions with server-side storage might have been simpler for my use case.
JSON Web Tokens or JWTs are great for apps. They're also great if you need to share authentication across services or if you really need stateless authentication.. For a standard web app talking to its own backend? Session cookies are fine and less complicated.
I'm glad I implemented JWTs. I understand the tradeoffs now. I know why someone would choose one, over the other. I didn't just follow a tutorial.
My Current Checklist
These days before I say that authentication is done I make sure that authentication is really done. I do a things to make sure of this.
- We store tokens in HTTP- cookies or we use sessions.
- In production we set the `flag so that cookies are only sent over HTTPS.
- The
SameSiteattribute is set up to stop CSRF attacks. - The login endpoint has rate limits to prevent people from trying lots of passwords.
- The JWT payload does not have data because it is base64 encoded not encrypted.
- Passwords are hashed using bcrypt, with least 10 rounds.
- If I use refresh tokens I make sure to rotate them.
I also make sure that these authentication changes go through automated tests before they are deployed. This is something that I wrote about in my guide on setting up CI/CD, for MERN with GitHub Actions.
Not Totally Confident
I still get a little nervous about authentication.
Every months some new vulnerability gets found and I wonder if my authentication implementation is affected.
I will read an article about session fixation or token sidejacking. I will go double-check my authentication code.
Maybe that is a thing.
Authentication is one of those areas where being a little paranoid probably keeps you from getting lazy with your authentication.
I am not an expert, in authentication.
I am someone who has messed up authentication enough times to know what not to do with authentication.
If you are building authentication for the time you will probably make mistakes with your authentication.
That is fine.
Just fix your authentication mistakes before you go to production with your authentication.