In this article we will discuss why we made the change, how the old system worked, the changes we made to incorporate the new authentication system, the challenges we faced, and how it benefits you as a user.
We used to use JWTs with a stateless API for making requests in the frontend. Initially, when we built Devfolio we used to store these JWTs in
localstorage when the user logged in or signed up and sent them with every subsequent request in the request headers.
localstorage is very versatile and provides very simple APIs for getting and storing the data, it comes with its own limitations like being accessible only on the domain it was created on. This worked fine initially with the hacker dashboard on devfolio.co as a standalone application, but when we added the organizer dashboard on org.devfolio.co, because of the nature of
localstorage we couldn't access any authentication info of the user saved on the hacker dashboard from the organizer dashboard, so we ended up maintaining its authentication in isolation as a separate app. This was not ideal because you could log in to the hacker dashboard with one account and then had to log in again to access the organizer dashboard, this was bad UX.
The problem was more prevalent when it came to the hackathon microsite in which every hackathon has its own individual subdomain(E.g. - https://inout.devfolio.co ). We couldn't implement the isolated authentication method as we had for the organizer dashboard here, because then every hackathon microsite would end up having its own authentication, which wouldn't be ideal. But since the information on this page was mostly public so we ended up showing the logged-out state regardless of whether a user was logged in or not -
The only downside was the user needed to login every time they wanted to apply to a hackathon from this page even when they were logged in. Again this was not good UX.
The new authentication system
Looking at the problems mentioned above we decided to rewrite the whole authentication architecture for Devfolio using HTTPOnly cookies. So now instead of storing the tokens in
localstorage and manually sending the tokens in the header of every request, we are storing them in cookies, and the browser sends them with every request.
With cookies we solved a major issue that was limiting us with
localstorage, i.e., sharing authentication across sub-domains. Cookies have a domain attribute that allows them to be scoped to be available on certain domains or sub-domains. So we set the domain for our cookies as
devfolio.co making it accessible on all of our subdomains.
secure allowing them to be only transmitted over HTTPS connections.
And finally, we have added a
SameSite attribute with the value as
strict. This allows the browser to only send the cookies with the request when the user is on a
devfolio.co domain thus preventing any CSRF attacks.
Since they are HTTPOnly cookies, they are set by the server when a user logs in or signs up, are sent with every subsequent request automatically, and are destroyed when the user logs out or their token in the cookie isn't valid anymore, causing them to be redirected to the login page.
Hence apart from solving the UX issues, this new system is also slightly more robust and secure.
During local development, we faced quite some issues because of the way browser cookies work.
The way we did local development was, spin up the frontend application, which is connected to a development API server (it runs on a .devfolio.co domain and can be deployed via a slackbot).
localstorage, it worked seamlessly because we only had to attach the JWT to the request headers. But with cookies, this flow broke for us here, as the cookies were not being set during local development since -
- There was a domain mismatch (localhost for the frontend app / .devfolio.co for the API server)
- The cookies are marked as secure, thus not accessible over the localhost
So we reduced the strictness of our cookies for the local development environment by setting the
SameSite attribute to
None (allowing the cookies to be sent even when the API server and the URL in the browser are on different domains) and set the
Secure attribute to false. The only problem is this doesn't work in modern browsers because "Cookies with SameSite=None must also specify Secure, meaning they require a secure context".
Finally, we ended up spinning up our backend API server locally, which also runs on localhost, thus giving us the ability to use the same attributes for local development that we have for production except for the
Secure attribute. Although we see tremendous potential in using docker images as a means of running our backend server locally.
How do these changes benefit the end-user
The biggest change for the end-user is that they can now login once and then visit any Devfolio subdomain without needing to log in again. This also allowed us to add the same profile dropdown now accessible from all the Devfolio subdomains.
We also have only a single sign-in page now, which handles all the redirection to the appropriate page if the user is not logged in.
The hackathon microsite also has slightly improved UX by allowing one-click apply if you are already logged in and haven't applied to the hackathon, instead of asking you to log in every time you apply to a hackathon.
It also now shows the proper state on the button depending on whether you have applied to a hackathon or not. For instance, since I have already applied to ETHOdyssey, it shows me 'Go to Dashboard' whenever I visit the microsite of this hackathon again.
That's the piece of how we moved our authentication system to work with cookies for an overall better UX, if there's something you think we could have done better, then please write to us at email@example.com.
Have a wonderful day 👋