React with Rails User Authentication

A comprehensive full-stack User authorization guide

Alejandro Sabogal
How I Get It…
21 min readNov 11, 2019

--

Image by Gerd Altmann from Pixabay

Architecting a User authentication service on the Rails backend is very straightforward, but it can be a bit challenging to implement this system on the React client/front-end side of the application.

In this article, I show you how to build an authentication system for a React with Rails application using HTTP Cookies and the Rails Session Data. Now, you may be wondering about JWT or JSON Web Tokens, and although that approach seems to be the most popular, I think using session data is more practical and secure. For example, If a User password is hacked, with JWT we would have to wait for the token to expire before we could 100% remove access to the User account. With session data, we can simply delete all browser cookies and all the server’s session data associated with that User and immediately block access to the account with the hacked credentials. However, there are some benefits with using JWT over Session data. You should do some research and decide which method works best for you.

Ok, let’s start building!

Pt. 1 — Rails Setup

We’ll be working with Rails 5, but these techniques should also work on Rails 6.

First, let’s create a new rails project. On your terminal, navigate to your working directory and run the rails new command:

Notice we are using postgresql for the database. This will make your life much easier when it comes time to deploy your app. I wrote another article on deploying a React + Rails application here: https://medium.com/how-i-get-it/rails-react-js-heroku-deployment-43d7469e122e

Also notice we didn’t use the --api flag. We don’t want to initiate an API only application. This would prevent us from using the Rails Session without installing more dependencies. Below we’ll go over how to configure the app to behave like a real API server.

Once the rails new command is finished building the file structure, we need to install two essential gems: bcryt and rack-cors. Add these to your gemfile and run bundle in your terminal to install them:

CORS (Cross Origin Resource Sharing) will allow you to whitelist the origin of requests that can communicate with our Rails backend. Since we’ll be fetching server data originating from the front-end client, we need to configure CORS to allow the front-end to access and post data on the backend, and since we are dealing with authentication, we also want to prevent any other origins from communicating with the server.

In the config/initializers folder, create a new file called cors.rb and write the following:

By default, your React client will run on http://localhost:3000. This code allows requests originating from that address to have access to all server resources ('*') and utilize all HTTP methods: :get, :post, :put, :patch, :delete, :options, :head. This configuration is ideal while you are working on the development environment. Once your application is ready for production, you will need to change the origin to your deployed front-end client’s domain address.

We now need to configure the way our server will handle the HTTP cookies. To do so, in the same config/initializers folder, create a new file called session_store.rb. The server needs to know two things about the cookie; its key and domain. Note, the domain is only needed when the app is in production or deployed:

By convention, the key name is your application’s name and should start with an underscore.

Another thing we should change, is the default port for our Rails server. Rails 5 uses Puma to run the server, so in the config/puma.rb file, change the default port to 3001: port ENV.fetch(“PORT”) { 3001 }

This will prevent our back-end and front-end applications from attempting to run on the same port.

With this, and the two files above, we’ve now configured the Rails backend server to behave like a secure API.

Bcrypt and the User Model

Now that the server is configured, we can build a User model and migrate it to the database. In the terminal:

Note we used password_digest instead of password. By doing this we are instructing the bcrypt gem to encrypt the User’s password. bcrypt uses a process called ‘salt’ to hash and encrypt passwords. You can learn more about it here: https://auth0.com/blog/hashing-in-action-understanding-bcrypt/. For now, all you need to know is that bcrypt will secure our passwords.

We can now create and migrate our database with the User model. Make sure your postgres db client is running, otherwise you’ll run onto errors. In the terminal:

Check that your schema and database migrations were done correctly. If so, there is one more bcrypt configuration to do. bcrypt uses an attribute called has_secure_password We need to include this in our User model in order for bcrypt to work. While we are at it, let’s also add some validations. In app/model/user.rb :

Our validations prevent Users from entering invalid or empty data, and return errors that we can present to Users. With this setup, our User model is complete. Let’s add its corresponding routes. I think we should only allow Users to create a new User, see that User and all other Users. They shouldn’t be allowed to edit or delete the user information, so we will allow the following routes in config/routes.rb

Let’s now connect the Users Controller. Inside app/controllers create the users_controller.rb file. Please note I’m assuming you are familiar with these methods as they are basic Rails knowledge…

A couple of things to note here, one, I’m only rendering JSON objects because we won’t be using our Rails views. We want to treat our server as an API only app, so we pass the JSON objects to our React client for it to handle displaying them. Two, I’m handling errors in a very basic and semantic way. How you chose to handle errors is up to you. And three, I’m using a helper method login! to create a User. This method inherits from the Application Controller, where I have additional helper methods that will be useful later on. Let’s add them to application_controller.rb:

There are a lot of important things going on here. Let’s start from the top: the skip_before_action :verify_authenticity_token command prevents Rails from using its authenticity token. This is a security token that Rails generates from our session data and adds to the parameters sent from a Rails form to a controller action to prevent cross-site request forgery (CSRF) attacks. Since we are treating our back-end as an API, we should disable this so that we don’t receive ‘forbidden’ parameters that will prevent our controller actions from executing without errors.

Next, we have: helper_method :login!, :logged_in?, :current_user, :authorized_user?, :logout! All this is doing is making sure the methods we define below will be passed to all other controllers in the app.

The login!, logged_in?, current_user, authorized_user?, logout! methods are pretty self explanatory; we are creating a session or deleting it, or we are authorizing a User based on the session data. These methods will be helpful when dealing with the sessions controller. Let’s write that next.

The Sessions Controller

Create a sessions_controller.rb file under app/controllers and write the following:

This controller’s purpose is to deal with the User’s Log In and Log Out functionality. Again, I only render JSON objects and handle errors in a very basic way. The .authenticate method works hand in hand with the User model’s has_secure_password method to authenticate the password.

These controller actions need their routes, so let’s write those:

The /logged_in route and corresponding action will come to play later when we design the User registration system in the React front-end. For now, just know that it will render a boolean to confirm that the User is authenticated and logged in and if so, it responds with the user object too.

Pt. 2— React Setup

A few notes before we begin the React part of this article:

  • I’m assuming you are familiar with React basics like create-react-app, class vs functional components, props and state basics.
  • I’m assuming you have npm or yarn installed in your system. We’ll use yarn in this article.
  • We’ll use the ‘axios’ dependency for out HTTP requests to the server, and ‘react-router’ and ‘react-router-dom’ to manage our routes.
  • We are going to keep the app’s style to a minimum as it’s really not relevant to this topic.
  • We’ll use four components: App.js, Home.js, Login.js, Signup.js. Our App.js component will handle most of the apps logic and routing.

Let’s start by creating our React app. In your terminal:

npx-create-react-app your-app-name-client && cd your-app-name-client

I tend to clean up and remove all the unnecessary files this scaffold comes with, i.e. App.css, App.test.js, logo.svg, serviceWorker.js This is my preference, but you decide how to initiate your app. Also I’m removing the serviceWorker because we won’t need it at all here.

I then also clean the App.js file to initiate like this:

And in index.js, we still pass App.js to the root element and clean the file as such:

You may also want to make some changes to your public folder, like changing the app’s name on index.html and manifest.json, and remove the unused logos.

Now let’s add the dependencies we need for our app. In your terminal:

yarn add axios && yarn add react-router && yarn add react-router-dom

Alright, so now that the app is setup, let’s start developing the authorization system.

Initial App Logic

We are going to establish the application’s logic in the App.js component. We will need to change to a class component to manage state, and we will need to import our dependencies axios plus {BrowserRouter, Switch, Route} from react-router-dom. The initial setup for our App.js component should now look like this:

Our App.js component is not going to render itself to the DOM, instead, it will serve as our router to render all other components. It will also manage the application’s state and authentication status; we use the component’s state to maintain the logged in status of a User, and to store the User data when we request it from the server. You may have noticed that the Routes aren’t rendering any components yet. We’ll build those and include them here later on.

Let’s add more functionality to the component. Let’s write two methods to interact with the app’s state:

With these methods we now control the login status of the application and the User data. As you can see in the handleLogin() method, we are passing a data argument. This is the response data we receive from the server. So you can think of these methods as being dependent of other functions and other return values. So how do we get this server response data? Let’s add another method to our component:

This method is very important! It is the backbone of our front-end authorization system. You’ll notice it’s a very straightforward fetch ‘GET’ request using axios, but you can see that we are also passing another argument {withCredentials: true}; this is the key here! This allows our Rails server to set and read the cookie on the front-end’s browser. ALWAYS pass this argument! Also notice the http address we are requesting. Remember our Rails /logged_in route and its corresponding controller action?:

Our React App.js component communicates with Rails through this route and controller action. If the User is verified in the Rails server, then a logged_in boolean is returned, along with the user object. App.js uses this response data to maintain the logged in status in the front-end. We need to ‘automate’ this request though. App.js should keep track of this status and request this information every time it’s mounted. We can use the componentDidMount() lifecycle method to call on loginStatus() and achieve this:

With this, our App.js component should look like this:

We now have the main authentication logic for the app, however, all this is useless without a way for the user to Sign Up, Log In, and Log Out. For this, inside src/components, let’s make a Home.js file. Also inside, src/components, let’s create a new folder called registrations, and inside of it, let’s add Signup.js and Login.js files.

The Home Component

This will be a very basic functional component:

Very simple. We just render two links using the Link component from react-router-dom. These links point to the /signup and /login routes which will render their respective components. Let’s build those components:

Registration System

The Signup.js and Login.js components will handle the User signup and login functionality through the use of controlled forms. Users are presented with a form to enter their credentials, and then these are sent to the server for validation.

A quick note here. These two components are almost identical. To keep the code ‘DRY’, we could use a Higher Order Component, or use Styled Components to pass repeating elements, but this is beyond the scope of this article and as such, we’ll just use two separate components.

Login.js:

Signup.js:

Both containers are pretty much the same. They are a basic controlled form, where Signup.js has and additional input field, password_confirmation. This is common when using bcrypt on Rails and it’s good practice in general to have Users confirm their passwords.

Other things to note about the forms; we are adding an errors attribute to state so we can handle response errors from the server. We are also destructuring the component’s state and the form’s inputs. On handleChange, we use this technique to avoid hardcoding each of the state’s properties while setting state on the input’s change. Also, we are using event.preventDefault() on handleSubmit() to prevent the browser from submitting the form in the usual way. We are going to add some additional logic to this method — and to other parts of the components, so we need to prevent the form from submitting.

Before we can add this logic to our registrations components, we must expand the App.js component and the router functionality. This component is actively checking the logged in status of the User. What we really want to do, is pass this status to components that require User authentication. The idea is to only render or display authorized data when the User is logged in, so our components will need this information, and with it, we can conditionally display information to the Users. The content you decide to render to your Users based on their logged in status is up to you. Below we expand App.js to be able to pass this status — along with our other App.js methods to our components. We also fully expand the router logic:

We imported our Home.js, Login.js andSignup.js components. We are also now rendering these components using render=props. This allows us to pass props to the components to be rendered, and in that way, we can pass isLoggedIn state status, handleLogin(), and handleLogout(), to our components as props!

Note: you can also pass the User object from state down to the necessary components. Which of your components will need that data is up to you, I’m not passing user at this time.

Back to the Registration System

Now that we have access to App.js's state and methods, we can expand the functionality of Login.js and Signup.js:

Login.js:

Signup.js:

We expanded the same functionality to both Login.js and Signup.js. Let’s break down what we added:

On handleSubmit(), we are creating a user object based on the component’s state. This is the data argument that axios will POST to the Rails server for authentication. Again, note we are also passing {withCredentials: true}. ALWAYS pass this in the header as it’s what allows Rails to set the cookie! We then check the server response and if it’s valid, we can call App.js's handleLogin() method, which will change the app’s isLogged_in status. Else, the server responds with errors and we set the state’s errors attribute. Since this attribute evaluates to true, the handleErrors() method is called in the component’s render() method and displays these errors to the user:

We are also utilizing redirect():

We built this method so we can redirect the User if the server responds with a valid authentication. We have access to the history props passed to us from react-router-dom when we use render=props on App.js.

We now have a working authorization system on our React front-end. But how do we really implement it? This is really up to you. Now that we have access to App.js's isLoggedIn state, we can render or prevent rendering of content. For example, if a User is already logged in, allowing navigation to the /login route, wouldn’t make sense. Let’s prevent that:

With this, we prevent a logged in User from navigating to /login. You can see how the logged in status can be used in several ways; allowing or preventing displaying the User page, rendering items on a NavBar, loading User preferences, or rendering a Log Out link for example. Let’s create the Log Out functionality:

We are going to add a ‘Log Out’ link to the home page in Home.js. Normally though, I would add this link on a NavBar so that is available to Users at anytime.

Here, we are displaying the link only if the User is logged in, and we are adding an onClick event that triggers the handleClick function which will handle the logout functionality. This will have to do two things, 1) update the isLoggedIn state in our front-end to false and remove the User data, 2) logout the User on the back-end server.

App.js already has a handleLogout() method that we can use to update the isLoggedIn state and remove the User. We need to pass this method as a prop to Home.js. To do so, we update the route in App.js:

And now to logout the User on the back-end, we make a request to the server's /logout route, which triggers the sessions_controller destroy action. We also keep the User on the home page by redirecting to the root route. As ALWAYS, remember to pass {withCredentials: true}:

We put it all together and our Home.js component should now look like this:

And the other components should look like this:

App.js:

Login.js:

Signup.js

And that’s it! You now have a working React and Rails User authorization system. I know it’s a long one, but this is a good guide to keep.

Sources:

Jordan Hudgens CTO at devCamp

--

--

Alejandro Sabogal
How I Get It…

Full-Stack Developer. Audio Engineer. Recovering Banker.