Learn how session-based authentication works and how to implement it in a React app with Express.
Authentication is a crucial aspect of any web application that requires users to sign in and manage their accounts.
In this article, we'll be exploring how to implement a basic authentication system using Express as well as a signup and login form in React.js. You'll learn the difference between the JWT- and session-based authentication and some associated best practices. You'll then learn how to implement session authentication step by step using a real-world demo that's before getting access to a ready-to-use React login page template based on the steps outlined in this guide.
By the end of this tutorial, you'll have a solid understanding of how to create a login page using React, handle user registration, and protect routes from unauthenticated users.
Session-based authentication is a method of tracking a user's login state by creating a unique session for each authenticated user. When a user logs in, the server generates a session ID and stores session information server-side, typically in memory or a database. This session ID is then sent to the client, usually as a cookie, which the client includes with subsequent requests to prove authentication.
Cookies are used since they are sent back to the server with each request. When the server receives the session ID, it can look up the session in the database to both confirm its validity, as well as reference the associated user.
This article covers how to implement session-based authentication into a React application.
JSON Web Token (JWT) authentication is an authentication method where a signed, encoded token is generated upon user login and returned to the client.
The JWT contains information such as the user ID, issue date, expiration date, etc. Once verified by the server using a private key, the server can trust that the information within the JWT correctly identifies the party that made the request.
Unlike session-based authentication, JWTs are self-contained and eliminate the need for server-side session storage.
A storage mechanism is required to hold information about the users and the sessions. Using a database would require two tables, one for each entity. The following table diagram outlines the minimum requirements to implement session-based authentication.
The users table will store general information about the user such as their name, email address, and a hashed version of the password. The sessions table will store details about each session.
Notice how the sessions table contains a userId field which is a foreign key of the users table. This is used to associate that session with a specific user.
Signing up the user will require a form to be created on the front end that is accessible if the user is not already authenticated. This typically includes, at minimum, inputs for username and password.
The server needs a route handler that can accept the user’s credentials when they click “Sign up”. The simplest implementation of this will create a user record and store the username and a hashed version of the password.
Warning
NEVER store the user credentials in plain text. If anyone obtains unauthorized access to your database, all of your users’ credentials will be exposed.
It’s also a best practice to implement validation on both the form the user interacts with and the route handler on the server. This ensures that any requirements for the inputs (ex: password complexity) are met before any records are created.
Using the login page in React, the user submits their username and password to the server.
The server creates a user record in the database.
The database returns the ID of the new record.
The server creates a session with the user ID.
The database returns the ID of the new record.
The server sets the cookie and responds back to the front end.
A login form is also required to allow the user to sign in. Again, the simplest implementation is at least a username and password.
As with handling sign-up, a route handler also needs to be created on the server to handle the credentials when they are submitted. Instead of creating a new user record, the credentials are verified with the existing user record.
If the React login page contains the proper credentials and they match when checked with the database, a session is created in the database. The token is added to a cookie that is sent back to the client.
Cookies are used for several reasons:
They are automatically sent with each request to the server.
Cookies can be configured to prevent client-side scripts from accessing them, making them more secure than local storage.
An expiration can be set directly on the cookie, preventing the browser from trying to access a protected route when the session is expired.
Using the login page in React, the user submits their credentials to the server.
The server queries the user record to check the credentials.
The database returns the user record if found.
The server compares the hashed passwords. If successful, create a new session in the database.
The database responds with the ID of the session
The server sets the session ID in the cookie and sends it back to the client.
Finally, routes on both the front end and back end need to be configured to be protected. In a typical React application using react-router-dom, Routes can be wrapped in a parent component which will check the authorization status with the server before rendering them. When the React login page is used to authenticate a user, the <ProtectedRoute /> component will automatically permit access to those views.
On the server, the session ID that is sent to the server will be checked with the sessions table in the database to make sure the sessions is valid before processing the request. In Express, middleware can be created that wraps routes or route collections to make sure this is done automatically on the proper requests.
To demonstrate how session-based authentication can be implemented, we’ll use a realistic project called Qulllmate.
Quillmate is an open-source web application where writers can create articles with the help of an AI assistant. The core entity of Quillmate is an article, and each article has its own AI assistant that understands what has already been written and helps when asked questions about the material.
The following video demonstrates the finished product, where the user can sign up, sign in, create articles, and ask AI for assistance:
Quillmate is built with React and uses Express to both serve the application, as well as handle backend requests. The project uses Open AI to ask questions about articles, and Neon to store the data.
Quillmate is built with the following tech stack:
React - The front end of the application is built with React.
Express - The application is hosted with Express. Requests to paths starting with /api will be handled by various API routes, whereas any other requests will serve up the React app.
Neon - Neon is a PostgreSQL database platform that is used for storing structured data.
Prisma - To simplify requests to the database, Prisma is used as an ORM.
Open AI - The AI Assistant functionality is backed by requests to the Open AI API.
Note
You may optionally clone the quillmate-react repository to follow along yourself with this article. Follow the directions in the readme to get the project running on your own system.
Let’s start by adding the users table and sessions table. This can be done by modifying the Prisma schema and running a script to apply the changes to the Neon database.
Modify the prisma/schema.prisma file and add the User and Session models:
Next, update the Article model to include a relationship with the user:
Finally, push the schema changes to Neon and update the local client by running the following commands in your terminal:
Next, create the Express middleware that will be used to protect routes. This will be used by protected routes to make sure that the request is authorized, returning a 401 status if it is not. It will also handle removing the session from the database if it’s expired.
Create a file at server/middleware/auth.ts and paste in the following:
If your editor is complaining about setting the req.user and req.session values, create a file at server/types/express.d.ts and paste in the following:
Next, we’ll need to update the /api/articles routes to ensure that when database records are created, the user information is saved with the record as well. This will allow queries to return only the articles created by that user.
The last thing to do on the server is update server/index.ts to set up cookie-parser, register the /api/auth routes, and protect the /api/articles and /api/ai routes.
Since the authentication state can affect the way the entire application behaves, we’ll create a React Context and Provider so we can check if the user is logged in from anywhere. We’ll also add the logic to sign up, sign in, and sign out from the context, making it easy to call those functions from various points of the app.
Create the src/hooks/use-auth.tsx file and add the following code:
Next, update the src/App.tsx file to wrap the entire application with the provider:
Next, let’s add the sign-up and sign-in views. These views will both be very similar, each containing form elements where the user can enter their credentials. Both also use zod for client-side validation, which prevents an unnecessary network trip to the server if the user does not populate the fields properly. If validation fails, errors will be shown on the respective fields.
The key difference between them is what happens when the user submits the form, specifically, the method called from the useAuth hook created earlier.
Create the src/views/sign-in.tsx page and populate it with the following code:
To protect routes, we’ll create a separate component that will use the context & provider and check the authentication state before allowing the user to proceed. If the authentication state is being checked by the provider, a “Loading…” message will be rendered for the user. If the user is not logged in, they will be redirected to the sign-in view.
Create the src/components/protected-route.tsx file and add the following:
Update src/App.tsx and wrap the element for the /app route in the <ProtectedRoute> component:
The last thing to add is a sign-out button that lets users log out of the app once they are finished. This will use the signOut function of the useAuth hook to remove the session from the database and clear the cookie set in the browser.
Update src/components/article-list.tsx with the following:
With all the changes in place, you may test the application! Run the application with the following command in your terminal:
Now open localhost:3000 in your browser and try creating a user to use the application, create an article, and experiment with the AI functionality! I encourage you to also log into Neon and check the contents of the database, specifically the users and sessions tables as they contain the records needed to support authentication.
Clerk is a user management and authentication platform, so it might surprise you that we’re publishing an article walking you through how to implement authentication yourself.
This article covers how to implement a single method of authentication, but in reality, user management is much more than just session-based authentication. For example, it’s commonplace in modern web applications to also support social login providers like Google or Apple. A password reset flow is also a critical requirement for supporting your own authentication setup.
These are just a few of the many features Clerk supports out of the box, oftentimes with a single line of code.
Using Clerk, you can easily create a sign-in page by just importing and rendering the <SignIn />component like so:
If you want to learn how easy it is to get Clerk working in a React application, check out the quickstart on our docs.
Note
If you want to download a template version of the React login page template to use in your own project, check out the react-session-auth-template repository on GitHub.
In conclusion, this article has walked you through how to implement a basic authentication system using React and Express. By setting up a session-based authentication flow, we've covered how to create a sign-in page, handle user registration, log users in and out, as well as protect routes with a simple ProtectedRoute component.
While implementing a custom authentication system can be a great learning experience, it's also important to consider the larger picture of what user management really entails. In reality, you'll often need to support features like social login providers, password reset flows, and more.
That's where Clerk comes in - a user management and authentication platform that simplifies the process of implementing these features with just a few lines of code. If you're interested in learning more about how easy it is to get Clerk working in a React application, check out the quickstart guide on our documentation site.