Authentication in React Applications, Part 2: JSON Web Token (JWT)
In the previous part, we have built the initial application with presentational and container components for the sign-up form, the login form, and the home component. In this part, we continue to work on the app and implement authentication using an email address.
The source code for the project is available on Github.
Before proceeding further, we need to add new dependencies.
Let's review packages:
bcrypt is a package with the bcrypt algorithm implementation for hashing passwords
jsonwebtoken is an implementation of JSON Web Token standard
mongoose is a MongoDB ORM library
passport is a flexible authentication library
passport-local is a Passport strategy for authenticating with an email and a password
We can install these packages by typing:
After the installation of new packages my package.json looked like this:
Project Directory Structure
At the end of the tutorial, the application will have the next directory structure:
You can see a lot of the changes in the file since the previous part. We have initialized the Passport library, added new Express routes and a database connection.
For the storage of users’ data, we will use MongoDB, which I assume you have already installed. In the config file, we will save a database connection string:
As it’s on my local machine I don’t use a password to access the database. If you have to provide an username and a password use this format:
The property jwtSecret of the config object contains a secret phrase our application will use to sign tokens. More explanation about you’ll see later in the tutorial. And for the security reasons you should replace the value with something really complex.
Now let’s create a module with the database connection:
Before saving any data, we have to describe a collection structure (so-called schema). Yet MongoDB has dynamic schemas and it’s automatic fit almost any data to the collections, Mongoose, a library that will help us to connect our application to the database, required to have fixed schemas. But it’s very easy and sometimes painless to change them. Let’s describe a user collection:
For describing the user collection, we have only used three fields (an email, a name, and a password). It’s important to notice that for an email field we have set the restriction: it should be a unique in the entire collection.
You also can notice a hook method UserSchema.pre('save') that will be executed before saving. In this method, the bcrypt module will generate a hash from a generated earlier salt string and a user’s password. This hash instead of a user’s password will be saved in the collection.
This generation will be executed only if it’s a new document or the password field has been changed: user.isModified('password').
The schema also contains a method UserSchema.methods.comparePassword that we will call if we want to check if a user has provided a correct password.
Let’s create modules for Passport strategies. The first one will a strategy for the sign-up process. For the task, we will use the passport-local strategy that is highly customizable:
By default, LocalStrategy attempts to find data in username and password POST body properties. Instead of a username I want to my users use an email address to authenticate themselves. To change default settings we need to pass an object with the needed parameters:
The options usernameField and passwordField are responsible for the custom names of parameters in the POST body message. We have set the session option to false due to we will use the token approach to authentication. And the option passReqToCallback is required to be true if we want to be able to read other parameters in the POST body message.
The logic in the piece of code above is the creation of a new user document. If there were any errors like a user with a given email address already exists, we should get an error message that we will process further.
Now we will create a new Passport local strategy, This time for the login process:
The basic logic here is to check if a user with a given email exists. If so, we will compare the given password’s hash value with a value saved in the database. If the user exists and the password is correct, we will create a JSON Web Token (JWT).
The token looks similar to this:
And consists of three encoded parts divided by dots:
header (algorithm and token type)
The signature part contains an encoded header, a payload, and a secret key phrase.
In the example above we haven’t specified an algorithm and only have provided a payload part. If the algorithm option is omitted, the default algorithm will be HS256.
Notice the sub key in the payload part. It’s a reserved key for a subject item, which in our example will be a user’s id.
The next step is the creation of the authenticaion checker:
In the file, we’re checking if the authorization header exists in an HTTP request. Then we decode the token to get the user’s id. With this id, we’re trying to find out if the user really exists. If at any step something unacceptable happens we will send back a response with 401 status code (unauthorized).
Remember that In the entry file of the application we applied the authenticaion checker middleware before declaring ``/api routes.
This way we can be sure that the middleware function will be executed before proceeding to any /api route.
Now we need to add calls of the Passport strategy functions in /auth routes handler.
For the /auth/signup path in the Passport strategy callback function, we have provided three possible cases of execution. Each one has a different HTTP response status code. If a user with the same email address already exists, he should get a concrete error message, but if something unpredictable happens he shouldn’t get a very descriptive message containing error codes, as he or she may use this information for bad purposes.
For the /auth/login path we’re checking if there are no errors have appeared (for example, a user has provided a wrong password). In the successful case, we send a response with a token.
In the route handler, we do nothing but send a message available only to authorized users. Since our application doesn’t contain any logic of user roles, we authorize to see this messages all successfully authenticated users. Remember, we should get an access to this route only after a successful execution of the authentication checker middleware.
It’s time to proceed to the client-side of our application. Our entry React application file remains the same since the previous part of the tutorial:
For the root path, we added the getComponent method where we will render a component based on whether a user is authenticated or not.
Also, notice a new path for the /logout. In its handler, we call the Auth.deauthenticateUser method and after it, we will redirect a user to the index page.
And the auth module:
In the module, we have created several methods for saving, retrieving and deleting a token value in the LocalStorage of a user’s browser.
The updated Base component:
The only change here is a new the condition statement for deciding which menu should be shown.
Now we need to add a redirect to the login form page after successful registration:
The presentational form for the sign-up form is exactly in the same state as in the previous part:
Let’s update the login page container component:
In the container component after receiving a successful HTTP response, which means that a user has entered a correct email address and password combination, we will save a token and redirect him to the index page. On the index page the user should see the dashboard.
On the presentational login form component, we need to show the success message that will be received after completed registration:
We still need to update two last components. A container component for DashboardPage:
After the initial rendering in the method componentDidMount we will make an AJAX-request to the server to get a message available only to authorized users. For this, we include an HTTP header with a JSON Web Token value.
The presentational component for Dashboard:
Finally, we can see the application in action. Don’t forget to execute npm run bundle and npm start in a terminal in the project directory.
Let’s try to perform a sign-up action. Go to http://localhost:3000 in a browser and go the sign-up form. Fill the fields:
After the form submission and if everything is correct, you’ll be redirected to the login form and you’ll see a message:
We also can check if we can’t register using an email address that already has saved in the database. Go back to the sign-up form and try to proceed with the same email address you have used before:
Try to log in and on the dashboard page, you’ll see the message that available only to users authorized to see it.
In the tutorial, we have learned about JSON Web Token and how to provide authentication using it. For the next steps, I suggest you find out about OAuth and implement authentication using Google, Twitter or any other popular social network site.