Main page background image

Ruby on Rails token-based authentication system with React.js frontend, PART II


Ihor T.
RoR Developer

In the second part of the article, we will create a straightforward React application with an authentication system. Again, I won’t get hung up on styles, so we don’t get distracted by less important things, and I will focus entirely on the authentication functionality.

Also, I assume you are familiar with React.js and Redux libraries. Otherwise, I recommend reviewing the react and redux documentation before reading further.

It’s worth noting that the approach below is just an example, and there is always a way to do it differently. However, I tried to choose the simplest way to implement authentication.

The code from the article can be found here.

Step 0. Initiate a new project

Before we start, make sure you have create-react-app globally installed and in case you don’t, run the line below.

npm install -g create-react-app

We are now ready to create a new react application.

create-react-app react_jwt_auth
cd ./react_jwt_auth

Next, we need to install the libraries we need for this project.

Step 1. Libraries installation

npm i --save react-router-dom \
             redux \
             react-redux \
             axios \
             redux-thunk \
             redux-form

Step 2. The App Component

Open the file and replace the code inside with the following:

./src/App.js

const App = () => {
  return (
    <div>
      Hi, I'm the app!
    </div>
  )
}

export default App

Start the server

npm run start

Open the localhost:3000 and you should see:

Hi, I'm the app!

Step 3. Creating the header

Create a folder components and there a file Header.js with the following code:

./src/components/Header.js

import { NavLink } from "react-router-dom"

const Header = () => {
  return (
    <div>
      <NavLink to="/">Main</NavLink>
      <NavLink to="/signin">Signin</NavLink>
      <NavLink to="/heart">Heart</NavLink>
      <NavLink>Sign Out</NavLink>
    </div>
  )
}

export default Header;

Next, we want to add the header we just created to all pages, so let’s add it to the file:

./src/App.js

import Header from './components/Header'

const App = () => {
  return (
    <div>
      <Header></Header>
    </div>
  )
}

export default App

Step 4. Connect react-router

Our routing won’t work unless we add routing to the application. Open index.js and wrap the app component with the BrowserRouter.

./src/index.js

import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom'; // don't forget to import BrowserRouter

import App from './App';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </React.StrictMode>
);

We check that everything works, start the server, and go to localhost: 3000

run npm run

You should now see our ugly header.

Step 5. Scaffolding

Now let’s create some blank pages that we will fill in in the following chapters. Create the following files with components inside:

src/components/Welcome.js - the public page will be the application’s root:

const Welcome = () => {
  return (
    <div>
      Welcome page! You need to sign in first!
    </div>
  )
}

export default Welcome

src/components/Signin.js - the login page:

const Signin = () => {
  return (
    <div>
      Signin page!
    </div>
  )
}

export default Signin

src/components/Heart.js - the private page, access to which only authorized users have:

const Heart = () => {
  return (
    <div>
      Heart page!
    </div>
  )
}

export default Heart

Next, we want to include these pages in our routing. All of them will be displayed inside the App component, so the Header is available on all pages.

Open App.js and update it with the code snippet below:

./src/App.js

import Header from './components/Header'

const App = ({ children }) => {
  return (
    <div>
      <Header></Header>
      {children}
    </div>
  )
}

export default App

Let’s also add routing functionality to

./src/index.js

// Add the following import to the top of the file.
import { BrowserRouter, Route, Routes } from 'react-router-dom';
import Welcome from './components/Welcome';
import Signin from './components/Signin';
import Heart from './components/Heart';

// Replace <App/> component with the following code snippet
// it works with "react-router-dom": "^6.6.1" if you have an older version, you may need to change the code below
<App>
  <Routes>
    <Route path ="/" element={<Welcome />} />
    <Route path ="signin" element={<Signin />} />
    <Route path ="heart" element={<Heart />} />
  </Routes>
</App>

Finally, start the server and check localhost:3000 so we can navigate over pages.

npm run start

Step 6. Including Redux

It’s time to add redux to the app. Create folder reducers and another folder inside reducers/auth with file index.js and paste:

./src/reducers/auth/index.js

// This is a simple state object where we will track whether the user is authenticated, and `errorMessage` is for errors we want to display to the user.
const INITIAL_STATE = {
  authenticated: '',
  errorMessage: ''
}

const auth = (state = INITIAL_STATE, action) => {
  return state
}

export default auth

Then add another file where we will include all the reducers and paste:

./src/reducers/index.js

import { combineReducers } from "redux";
import auth from './auth'

export default combineReducers({
  auth
})

After we want to add a redux store to the application, to do this, open the file and update it with the following code snippet:

./src/index.js

// Add the following imports to the top of the file
import { Provider } from 'react-redux';
import reducers from './reducers';
import { applyMiddleware, createStore } from 'redux';
import reduxThunk from 'redux-thunk';
// rest imports ...

const store = createStore(
  reducers,
  {}, // initial state
  applyMiddleware(reduxThunk)
)

const store = createStore(
  reducers,
  {}, // initial state
  applyMiddleware(reduxThunk)
)

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <Provider store={store}>
      <BrowserRouter>
        <App>
          <Routes>
            <Route path ="/" element={<Welcome />} />
            <Route path ="signin" element={<Signin />} />
            <Route path ="heart" element={<Heart />} />
          </Routes>
        </App>
      </BrowserRouter>
    </Provider>
  </React.StrictMode>
);

Start the server as usual, and make sure you don’t have any errors.

npm run start

Step 7. ReduxForm and User Sign in

Let’s start creating an action that will make an API call to the server API to get the JWT token needed for authentication and store it in local storage.

We want to have two types, one for successful login and one for failed authentication.

Create the following file and paste the code snippet below:

src/actions/types.js

export const AUTH_USER = 'AUTH_USER'
export const AUTH_ERROR = 'AUTH_ERROR'

Then create the following folder and file for the signin action and paste the code snippet below:

src/actions/index.js

import axios from 'axios'
import { AUTH_ERROR, AUTH_USER } from './types'

const signin = (formProps, callback) => async dispatch => {
  try {
    const response = await axios.post('http://localhost:3001/user_token', {
      auth: formProps
    })
    dispatch({ type: AUTH_USER, payload: response.data.jwt })
    localStorage.setItem('token', response.data.jwt)
    callback() // after a successful login, we may want to redirect the user to a specific page, so we need a callback function
  } catch {
    dispatch({ type: AUTH_ERROR, payload: 'Email or password is incorrect!' })
  }
}

export {
  signin
}

Next, update the auth reducer to update the state of both failed and successful logins:

src/reducers/auth/index.js

import { AUTH_ERROR, AUTH_USER } from "../../actions/types";

const INITIAL_STATE = {
  authenticated: '',
  errorMessage: ''
}

const auth = (state = INITIAL_STATE, action) => {
  switch (action.type) {
    case AUTH_USER:
      return { ...state, authenticated: action.payload }
      case AUTH_ERROR:
        return { ...state, errorMessage: action.payload }
    default:
      return state
  }
}

export default auth

Also, since we want to use a redux-form in our application, let’s add it to the reducers:

src/reducers/index.js

import { combineReducers } from "redux";
import { reducer as reducerForm } from "redux-form";
import auth from './auth'

export default combineReducers({
  auth,
  form: reducerForm
})

Finally, we need to update the Signin component to sign in and display errors.

src/components/Signin.js

import { connect, useSelector } from 'react-redux'
import { useNavigate } from 'react-router-dom'
import { compose } from 'redux'
import { Field, reduxForm } from 'redux-form'

import { signin } from '../actions'

const Signin = ({ handleSubmit, pristine, submitting, signin }) => {
  const navigate = useNavigate()
  const errorMessage = useSelector(({ auth: { errorMessage } }) => errorMessage)

  const onSubmit = (formProps) => {
    signin(formProps, () => { navigate('/heart') })
  }

  return (
    <div>
      <form onSubmit={handleSubmit(onSubmit)}>
        <fieldset>
          <label>Email</label>
          <Field
            name="email"
            type="text"
            component="input" />
        </fieldset>
        <fieldset>
          <label>Password</label>
          <Field
            name="password"
            type="password"
            component="input" />
        </fieldset>
        <div>{errorMessage}</div>
        <div>
          <button type="submit" disabled={pristine || submitting}>
            Submit
          </button>
        </div>
      </form>
    </div>
  )
}

export default compose(
  connect(null, { signin }),
  reduxForm({ form: 'signin' })
)(Signin)

Every time we reload the page, the state of our application is lost, which means we need to log in after every page reload. To fix this, let’s set the initial state from localStorage. Replace store with the code snippet below:

src/index.js

const store = createStore(
  reducers,
  {
    auth: {
      authenticated: localStorage.getItem('token') // every time we reload the page we get the token from localStorage
    }
  },
  applyMiddleware(reduxThunk)
)

Finally, let’s start the rails server, and the react server to test what we’ve done so far. Since both applications use port 3000, we can switch the rails server to 3001.

rails s -p 3001 # run it in the rails application directory
npm run start # run it in react app directory

Nothing special is currently happening. We update the state of the react app on successful or unsuccessful login and redirect the user to the /heart route.

Step 8. Add HOC to secure private routes

We want to show some components to an unauthorized user (Welcome or Signin), but we don’t show some components (Heart).

Let’s create a High Order Component (HOC) to render the components conditionally.

Create a file and paste the code snippet below:

src/components/withAuth.js

import React, { useEffect } from 'react';
import { useSelector } from 'react-redux';
import { useNavigate } from 'react-router-dom';

const withAuth = (Component) => ({ ...props }) => {
  const navigate = useNavigate()
  const authenticated = useSelector(({ auth: { authenticated } }) => authenticated);

  // every time the authenticated property changes, we check if the user is authenticated or not
  // if not authenticated, the user is redirected to the main page
  useEffect(() => {
    if (!authenticated) navigate('/')
  }, [authenticated, navigate])

  return authenticated && <Component {...props} />;
};

export default withAuth;

Use withAuth HOC for our private Heart component.

Open the file and paste: Use withAuth HOC for our private Heart component.

Open the file and paste:

src/components/Heart.js

import withAuth from "./withAuth"

const Heart = () => {
  return (
    <div>
      Heart page!
    </div>
  )
}

export default Heart
export default withAuth(Heart) // when we want to make a component protected, we need to wrap it - withAuth(someComponent)

Step 9. Logout and update the Header

To log out, we need to add another action called logout and export it. First, we remove the token from localStorage and then update the application state so that the “authenticated” property is set to an empty string.

Update the file below with one more action:

src/actions/index.js

const signout = () => {
  localStorage.removeItem('token') // remove the token from the localStorage
  return {
    type: AUTH_USER,
    payload: '' // the {authenticated} property is set to an empty string
  }
}

export {
  signin,
  signout // don't forget to export the new action
}

Finally, let’s update the Header component so that we don’t display the Heart and Sign Out links for unauthenticated users, and by clicking the Sign Out link, we trigger the signout action.

src/components/Header.js

import { connect, useSelector } from "react-redux";
import { NavLink } from "react-router-dom"
import { signout } from '../actions'

const Header = ({ signout }) => {
  const authenticated = useSelector(({ auth: { authenticated } }) => authenticated)

  if (authenticated) {
    return (
      <div>
        <NavLink to="/">Main</NavLink>
        <NavLink to="/heart">Heart</NavLink>
        <NavLink onClick="{signout}">Sign Out</NavLink>
      </div>
    )
  } else {
    return (
      <div>
        <NavLink to="/">Main</NavLink>
        <NavLink to="/signin">Signin</NavLink>
      </div>
    )
  }
}

// with connect we are injecting {signout} action to the Header component
export default connect(
  null,
  { signout } // injecting {signout} action
)(Header);

Start the rails and react servers to test the app’s authentication.

rails s -p 3001 # run it in the rails app directory
npm run start # run it in the react app directory

Summary

It’s all for today, my friends. Finally, we have both a backend and a frontend that can work together. I hope the authentication flow is much clearer now. See you in the following articles :)

The code from the article can be found here.