Introduction

This is part of a series of tutorials on using React with Ruby on Rails. Each section is self-contained. In this section we will restrict access to our app by adding a User model to the Rails back end, a Log In page to the React front end, and use JSON Web Tokens for authentication.

We will duplicate the app we built in previous tutorials of this series which consists of:

  • A Ruby on Rails API. We will create an API-only app.
  • A separate React client generated with the create-react-app package for quick setup.
  • An "articles" resource and database table, with full CRUD capabilities.
  • To this we will add:
  • A User model with JWT authentication using the Knock gem. We are not using Devise which has it's own setup for working with JWTs.

  • JSON Web Tokens

    In a traditional Ruby on Rails web application you would have a User model with a signup page. Then you would manage authentication with a login page that checks the user's username or email and password against the database. When the user is successfully logged in, their user id is added to the secured session cookie that Rails passes back and forth with the user's browser for each request-response cycle. That way you can restrict access to certain pages and actions to only registered users.

    With an API this works the same except for the session cookie part. By definition the API server and the Client application are on different domains. In the app we are about to build, in development they will be on localhost:3001 and localhost:3000 respectively. Rails and other web frameworks have a same-origin policy where they won't send session cookies to another domain. It exposes the server to Cross-Site-Request-Forgery attacks. We need another solution. In this tutorial we will use JSON Web Tokens.

    A JSON Web Token or JWT (pronounced J-W-T or "jot") allows us to authenticate requests between the client and the server by encrypting authentication information into a compact JSON object called a token. Then that token can be included with HTTP requests that require authentication. Session cookies are automatically sent back and forth with every single request. JWTs on the other hand, must be specifically programmed to be included with a request, which you would do only when authentication is needed.

    A JWT consists of three strings separated by dots - the header, the payload, and the signature.

    1. The header is encoded using base64. Uncoded, it lists the type of token, which is JWT, and the hashing algorithm used.
    2. The payload, also called the JWT claims, contains the actual data concerning the user such as the User id. It also contains the expiration date. It is also encoded in Base64.
    3. The signature is what makes the token tamper proof. It runs the encoded header, the encoded payload, and the secret through the hashing algorithm. For a Rails app the secret is its secret_key_base. Since only the server knows the secret, no one can tamper with the payload and the server can detect any tampering using the signature.

    Since the first two strings are encoded using Base64, they can be decoded. Later in this tutorial when we create JWTs, there is an online tool that can decode them so you can see the header and payload: jwt.io.


    Create a Rails API-only Application

    We'll only give explanations for steps not covered in Rails APIs Tutorial of this series.

    Generate a new API-only application:

  • mkdir rails-react-jwt; cd rails-react-jwt
  • If you are using RVM for Ruby and Gems version control you can set your gemset at this point.
  • rails new . --api

    Generate an Articles scaffold which will give us a database migration file, a model, a JSON-api ready controller, and a route resource.

  • rails generate scaffold Article title:string content:text
  • rails db:migrate

  • Add some seed data:
  • # db/seed.rb
    articles = Article.create([
      {title: "Learn Ruby", content: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."},
      {title: "Learn Rails", content: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."},
      {title: "Learn React", content: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."}
    ])
    

    rails db:seed

    Add the CORS gem to allow access to our API.

    # Gemfile
    gem 'rack-cors'
    
  • bundle install
  • # config/initializers/cors.rb
    Rails.application.config.middleware.insert_before 0, Rack::Cors do
      allow do
        origins 'http://localhost:3000'
    
        resource '*',
          headers: :any,
          methods: [:get, :post, :put, :patch, :delete, :options, :head]
      end
    end
    

    This is the same API we build in part 1. Run the server and view the JSON data at http://localhost:3001/articles to make sure it's working so far.

    rails server -p 3001

    User Model and Knock gem

    Let's add two more gems. Bcrypt for password hashing and knock which is specifically for JWT authentication.

    # Gemfile
    gem 'bcrypt', '~> 3.1.7'
    gem 'knock'
    

    bundle install

    Generate the User scaffold:

    rails generate scaffold User name:string email:string password_digest:string admin:boolean

    Bcrypt will encrypt the password for us. You need to add "has_secure_password" to the User class. We'll add some validations too:

    # app/models/user.rb 
    class User < ApplicationRecord
      has_secure_password
      validates :name, presence: true
      validates :email, presence: true, uniqueness: { case_sensitive: false }
    end
    

    The users controller generated by the scaffold gave us a field called password_digest, just like we told it to. But Bcrypt, in addition to hashing the password, it also turns password_digest into two fields: password and password_confirmation. So we need to change the permitted params from :password_digest to those two fields:

    # users_controller.rb 
    def user_params
      params.require(:user).permit(:name, :email, :password, :password_confirmation, :admin)
    end
    

    In the migration file make the default value for admin false:

    # db/migrate/timestamp_create_users.rb 
    ...
    t.boolean :admin, default: false
    ...
    

    Now migrate the database:

    rails db:migrate

    Add some additional seed data with at least one admin and one non-admin, something like the below. Make sure to delete or comment out the articles seed data, otherwise it will add the same articles again!

    # db/seeds.rb 
    User.create!(name: "Joey Ramone", email: "joey@ramones.com", password: "ramones", password_confirmation: "ramones", admin: true)
    User.create!(name: "Johnny Ramone", email: "johnny@ramones.com", password: "ramones", password_confirmation: "ramones")
    

    rails db:seed

    Configure Knock

    We installed the Knock gem, now we need to configure it to work with our User model. You can read more about knock on their Github page.

    Run the install generator:

    rails generate knock:install

    This will create an initializer file at config/initializers/knock.rb that contains the default configuration. By default, Knock will expire the token in 24 hours. If you want to change that then uncomment the line and make the appropriate adjustments:

    # config/initializers/knock.rb 
    config.token_lifetime = 1.day
    

    Generate a controller for users to sign in through:

    rails generate knock:token_controller user

    This generates a controller called user_token_controller. It inherits from Knock::AuthTokenController which comes with a create action that will create a JWT when a user successfully logs in. The generator also inserts a route in the routes.rb file: post 'user_token' => 'user_token#create' as an API endpoint for the client to call when logging in.

    Include the Knock::Authenticable module in your ApplicationController so you can add authentication before filters to to any of your controllers:

    # app/controllers/application_controller.rb 
    class ApplicationController < ActionController::API
      include Knock::Authenticable
    end
    

    Add an authenticate_user before filter to the articles controller. And let's say we only want admin users to be able to delete articles. In that case you can wrap the @article.destroy statement in a conditional.

    # app/controllers/articles_controller.rb 
    before_action :authenticate_user
    ...
    def destroy
      if current_user.admin?
        @article.destroy
      end
    end
    

    Knock will add the User id to the JWT payload by default, assigned to a key called "sub." If you want to add any other information, you would add a method to the user model called to_token_payload and add it there. If you do that you need to also explicitly add sub: id or it won't be included. The below is just for reference and not needed in our app.

    # app/models/user.rb 
    ...
    def to_token_payload
      {
        sub: id,
        name: name
      }
    end
    

    If you are using Rails 5.2 or higher you need to take two more steps. First, protect_from_forgery is included in ActionController::Base by default now, so you need to skip that in the Knock controller we generated for logging in from our React client. API clients are from a different domain so they won't have the standard Rails authenticity token.

    # app/controllers/user_token_controller.rb
    class UserTokenController < Knock::AuthTokenController
      skip_before_action :verify_authenticity_token, raise: false
    end  
    

    The other change is Rails no longer uses config/secrets.yml to hold the secret_key_base that is used for various security features, including generating JWTs with the Knock gem. Rails now uses an encoded file called config/credentials.yml.enc. Add the below line to the Knock configuration file. If there is a line like this in the knock.rb file that is commented out, don't just uncomment it. Make sure it points to Rails.application.credentials and not Rails.application.secrets. The latter is for Rails 5.1 and earlier apps.

    # config/initializers/knock.rb 
    ...
    config.token_secret_signature_key = -> { Rails.application.credentials.fetch(:secret_key_base) }
    

    Add an api namespace using scopes

    This time let's add an "api" scope to all of our api routes. That will add "api" to the path but not to the controller or model.

    # config/routes.rb 
    scope '/api' do
      post 'user_token' => 'user_token#create'
      resources :users
      resources :articles
    end
    

    Now for example, once the user has been authenticated they would access the articles API with http://localhost:3001/api/articles.


    Test the User API

    This is where an API testing app like Postman is useful. The Curl command line tool will also work but is less user friendly. Especially when you get an error since Rails will send back an HTML error page which is unreadable from the Terminal screen. If you don't have Postman and don't feel like spending an hour or more loading it and getting a feel for it, you can skip this step and go to the next section.

    In the tool, create a new user as a JSON string:

      
    Method: POST
    URL: http://localhost:3001/api/users
    Headers: Content-type: application/json
    Body:
    {
      "user": {
        "name": "DeeDee Ramone",
        "email": "deedee@ramones.com",
        "password": "ramones"
      }
    }
    

    Make sure to restart the server since we made some configuration changes.

    rails server -p 3001

    If you send this request you should get a 201 successfully created response, and have a new User record in the database. You can see all the users in JSON format at http://localhost:3001/api/users. We won't make a sign up page in our app though. This is just to see if the API endpoint is working.

    We will, however, be requesting JSON Web Tokens from our React client as our log in process. We can test that the same way. We just change the URL and the body.

    Method: POST 
    URL: http://localhost:3001/api/user_token
    Headers: Content-type: application/json
    Body:
    {
      "auth": {
        "email": "deedee@ramones.com",
        "password": "ramones"
      }
    }
    

    Send this request. Knock will check the database for the email and password. If they match it will generate and return a JWT token which you should see in the response panel of the tool. If so, all is working as it should. On to React.


    Create the React Client

    This user interface will be similar to the one from part 2 of this Rails-React series. We'll only explain what's different. Start by generating a separate React application using the create-react-app Node package. While you can put this anywhere, we'll put it in our Rails API app in it's own folder called "client."

  • create-react-app client
  • cd client

    Install the react-router and axios packages:

    yarn add react-router-dom axios

    It's a good idea to run the React App just to make sure it's working. We could do this with the yarn start command, but let's make things a little easier with the Foreman gem.

    Foreman Gem

    Right now we need to use two separate commands to start the Rails API server, and the React client server. The Foreman gem will let us do that with one command. Let's set that up. Per the Foreman setup instructions, don't add the gem to the Gemfile. Rather just install it directly on your system with gem install foreman (if you haven't installed it already).

    By default, when you issue the foreman start command it will look for instructions in a file called Procfile. That same file is used in production as well (Heroku uses Procfile in production). Since our server start commands won't apply to the production environment, let's create a Procfile specifically for the dev environment:

    touch Procfile.dev

    Populate it with the following:

    # Procfile.dev 
    api: rails server -p 3001
    web: cd client && PORT=3000 npm start
    

    The first line will start our rails API server on port 3001. The second line will cd into the client folder then start our React server on port 3000.

    Make sure to stop the Rails server you had running earlier. Now you can start both servers with one command:

    foreman start -f Procfile.dev

    The -f flag is forcing the use of the Procfile.dev file instead of the default Procfile.

    In your browser if you go to localhost:3000 you should see the Welcome to React default page. React is working. And the Rails API should also be running on port 3001. If you go to localhost:3001/api/articles you should get a 401 error because we restricted access in the controller. But if you go to http://localhost:3001/api/users you should see a JSON rendering of your Users since we didn't restrict access in the users controller (which we would in a production app).

    Finish the React Setup

    Lets add some structure to your React folders. From the "client" directory add the components, stylesheets, and images directories, move some of the default files, and add the component files that we'll populate later.

    mkdir public/images
    mv public/favicon.ico public/images/favicon.ico
    mkdir src/stylesheets
    mkdir src/components
    rm src/logo.svg
    mv src/index.css src/stylesheets/index.css
    mv src/App.css src/stylesheets/App.css
    mv src/App.js src/components/App.jsx
    mv src/App.test.js src/components/App.test.js
    touch src/components/Home.jsx
    touch src/components/Login.jsx
    touch src/components/Logout.jsx
    touch src/components/ArticleList.jsx
    touch src/components/ArticleInfo.jsx
    touch src/components/ArticleAdd.jsx
    touch src/components/ArticleEdit.jsx
    

    Fix the links to reflect the new folders and moved files:

    # public/index.html 
    <link rel="shortcut icon" href="%PUBLIC_URL%/images/favicon.ico">
    
    # src/index.js
    import './stylesheets/index.css';
    import App from './components/App';
    

    Add the Bootstrap CDN to the index.css file for convenience.

    # client/src/stylesheets/index.css 
    @import url('https://stackpath.bootstrapcdn.com/bootstrap/4.1.2/css/bootstrap.min.css');
    

    In the package.json file add a proxy. This will allow us to use shortcuts in our api calls within our React code. So instead of calling 'http://localhost:3001/api/articles', you only need to call '/api/articles'. Make sure to add the comma before the preceding property name as shown below.

    # package.json
      ...
      },
      "proxy": {
        "/api": {
          "target": "http://localhost:3001"
        }
      }
    }
    

    # src/components/App.jsx

    Now let's set up the central component of our React client, the App.jsx file. This will hold our navigation bar and the custom element that will hold the content from each link. This is essentially the same as from our earlier app except we took the default header out and we're adding Login and Logout routes with corresponding components. Your code should look like this:

    import React, { Component } from 'react';
    import '../stylesheets/App.css';
    import Home from './Home';
    import Login from './Login';
    import Logout from './Logout';
    import ArticleList from './ArticleList';
    import ArticleInfo from './ArticleInfo';
    import ArticleAdd from './ArticleAdd';
    import ArticleEdit from './ArticleEdit';
    import {BrowserRouter as Router, Route, NavLink, Switch} from 'react-router-dom'
    
    class App extends Component {
      render() {
        return (
          <Router>
            <div className="container">
              <Navigation />
              <Main />
            </div>
          </Router>
        );
      }
    }
    
    const Navigation = () => (
      <nav className="navbar navbar-expand-lg navbar-dark bg-dark">
        <ul className="navbar-nav mr-auto">
          <li className="nav-item"><NavLink exact className="nav-link" activeClassName="active" to="/">Home</NavLink></li>
          <li className="nav-item"><NavLink exact className="nav-link" activeClassName="active" to="/articles">Articles</NavLink></li>
          {
            localStorage.getItem("jwt") ?
              <li className="nav-item"><NavLink exact className="nav-link" to="/logout">Log Out</NavLink></li>
            :
              <li className="nav-item"><NavLink exact className="nav-link" activeClassName="active" to="/login">Log In</NavLink></li>
          }
        </ul>
      </nav>
    );
    const Main = () => (
      <Switch>
        <Route exact path="/" component={Home} />
        <Route exact path="/login" component={Login} />
        <Route exact path="/logout" component={Logout} />
        <Route exact path="/articles" component={ArticleList} />
        <Route exact path="/articles/new" component={ArticleAdd} />
        <Route exact path="/articles/:id" component={ArticleInfo} />
        <Route exact path="/articles/:id/edit" component={ArticleEdit} />
      </Switch>
    );
    
    export default App;
    

    There is something funny going on in the Navigation component with the Log Out and Log In NavLinks. We'll get to that later.

    # src/components/Home.jsx

    Populate the Home component:

    # client/src/components/Home.jsx 
    import React from 'react';
    
    const Home = () => {
      return (
        <div className="jumbotron">
          <h1>Home Page</h1>
        </div>
      );
    }
    
    export default Home;
    

    You may need to restart the servers with foreman because of all the file changes: Ctrl+C to stop.

    foreman start -f Procfile.dev

    Now in http://localhost:3000 you should see the Home Page. None of the other links will work yet. Next we'll hook up the Log In page.

    # client/src/components/Login.jsx

    Populate the Login.jsx file with:

    import React, { Component } from 'react'
    import { post } from 'axios';
    
    class Login extends Component {
      constructor() {
        super()
        this.handleSubmit = this.handleSubmit.bind(this);
      }
    
      handleSubmit (event) {
        event.preventDefault();
        const email = document.getElementById('email').value;
        const password = document.getElementById('password').value;
        const request = {"auth": {"email": email, "password": password}};
        post('/api/user_token', request)
          .then(response => {
            localStorage.setItem("jwt", response.data.jwt);
            this.props.history.push("/");
          })
          .catch(error => console.log('error', error));
      }      
    
      render() {
        return (
          <div>
            <h1>Log In</h1>
            <form onSubmit={this.handleSubmit}>
              <div className="form-group">
                <label htmlFor="email">Email: </label>
                <input name="email" id="email" type="email" className="form-control" />
              </div>
              <div className="form-group">
                <label htmlFor="password">Password:</label>
                <input name="password" id="password" type="password" className="form-control" />
              </div>
              <button type="submit" className="btn btn-dark">Submit</button>
            </form>
          </div>
        );
      }
    }
    
    export default Login;
    

    Let's break down some of this code, starting with the render method:

    render() {
      return (
        <div>
          <h1>Log In</h1>
          <form onSubmit={this.handleSubmit}>                                                  #1a
            <div className="form-group">
              <label htmlFor="email">Email: </label>
              <input name="email" id="email" type="email" className="form-control" />          #2
            </div>
            <div className="form-group">
              <label htmlFor="password">Password:</label>
              <input name="password" id="password" type="password" className="form-control" /> #3
            </div>
            <button type="submit" className="btn btn-dark">Submit</button>                     #1b
          </form>
        </div>
      );
    }
    

    1) This is a React form. When the user clicks the submit button it triggers the onSubmit event which calls the handleSubmit handler function.
    2&3) There are two form fields. We are approaching this form a little differently than we will with the article new and edit forms. Those use what React calls controlled components where the state of each form field is updated with every character typed. It requires a separate onChange attribute in the form fields that calls the handleChange function to setState with every keystroke. Here we are skipping the onChange event and just using the onSubmit event.

    The handleSubmit handler function is where we send the login form data to the API.

    handleSubmit (event) {
      event.preventDefault();                                            #1
      const email = document.getElementById('email').value;              #2
      const password = document.getElementById('password').value;        #3
      const request = {"auth": {"email": email, "password": password}};  #4
      post('/api/user_token', request)                                   #5
        .then(response => {                                              #6
          localStorage.setItem("jwt", response.data.jwt);                #7
          this.props.history.push("/");                                  #8
        })
        .catch(error => console.log('error', error));                    #9
    }  
    

    1) Since we are submitting a form we have to prevent the HTML default action which is to go to a new URL with the form data. Instead we are sending the from data via AJAX.
    2&3) We added id attributes to the form fields. We use getElementById to get the form values.
    4) Our API is using the Knock gem which will look to receive a login request with a property called "auth" set to an object containing the the log in form field names and values. If you want to see the request variable in the console at this point you can insert a console.log(request);.
    5) We are using the Axios package for our AJAX calls. Earlier we added a proxy to the package.json file allowing us to use just the path instead of the full URL. You could still use the full URL if you wanted to. The path we are using is the specially created route for the Knock gem's log in process. It contains the form data in the "request" variable.
    6) If the email and password match a user in the database, Knock will send back a JWT as part of the response's data string. If you want to view the response in the console you can insert a console.log(response);.
    7) Once we get the JWT token we need to save it somewhere and send it with our requests. Two options are sessionStorage and localStorage. Whatever we put in sessionStorage will be stored in the browser until the user closes their browser. Then it is automatically erased. LocalStorage, on the other hand, will save it even after the user closes the browser. Which you use depends on how you want your app structured. By default, JWTs expire in 24 hours so it won't make much difference either way. But if you make your JWTs last much longer, then save the token in local storage, that will allow users to automatically be logged in whenever they visit your site.
    8) We'll send the user to the home path when they log in.
    9) In a real app you would want to give the user a useful message if there is an error when they log in. This is an introductory tutorial so we'll just log the error to the console.

    Axios is a React package that simplifies making AJAX calls. If you want to know how to make the request using the fetch API instead, you would replace the lines 5-9 with:

    fetch('/api/user_token', {
        method: 'POST',
        body: JSON.stringify(request),     
        headers: {'Content-Type': 'application/json' }
      })
      .then(response => response.json())
      .then(data => {
        localStorage.setItem("jwt", data.jwt);
        this.props.history.push("/");
      })
      .catch(error => console.log('error', error));
    

    # client/src/components/Logout.jsx

    Populate the logout component:

    import React from 'react';
    import { Redirect } from 'react-router-dom'
    
    const Logout = () => {
      localStorage.removeItem('jwt');
      return <Redirect to='/' />
    }
    
    export default Logout;
    

    It only does two things.
    1) It removes the jwt item from localStorage (or sessionStorage if you put it there).
    2) It uses React Router's Redirect component to redirect to the home path.

    Now let's revisit the Navigation component in the App.jsx file where something funny was going on.

    const Navigation = () => (
      <nav className="navbar navbar-expand-lg navbar-dark bg-dark">
        <ul className="navbar-nav mr-auto">
          <li className="nav-item"><NavLink exact className="nav-link" activeClassName="active" to="/">Home</NavLink></li>
          <li className="nav-item"><NavLink exact className="nav-link" activeClassName="active" to="/articles">Articles</NavLink></li>
          {                                                                                                     #1
            localStorage.getItem("jwt") ?                                                                       #2 
              <li className="nav-item"><NavLink exact className="nav-link" to="/logout">Log Out</NavLink></li>  #3
            :
              <li className="nav-item"><NavLink exact className="nav-link" activeClassName="active" to="/login">Log In</NavLink></li> #4
          }
        </ul>
      </nav>
    );
    

    This component is our navigation bar which contains the React Router package's NavLink elements. In addition to the Home and Articles links, we want to display either a Log In or Log Out link as determined through a conditional statement. This might seem like a good place for an if-else statement. And it is. Except you can't put if statements within JSX. Here's the details as to why in case you're curious: react tips. But you can insert an expression in JSX including a ternary statement. A ternary statement is an if-else statement with short syntax: condition ? if true : if false. So we'll use that.
    1) To include an expression in JSX, wrap it in curly braces.
    2) Our ternary statement will start with the condition of whether there is a jwt object in localStorage (or sessionStorage if you are using that).
    3) If there is, display the NavLink element to the Logout path.
    4) If not, display the NavLink element to the Login path.

    Let's try it out to see if it works. In your browser go to the Login page. Enter a user email and password (from the db/seeds.rb file). Log in and Log out a few times. Hopefully, this works! If you are curious to see your local storage, after you log in, open Chrome Developer Tools and click on the "Application" tab. On the left panel you'll see a section labeled Storage. There you can click on Local Storage (or Session Storage) and you should see the jwt key and it's value. Then if you are really curious you can decode it online at jwt.io.

    # client/src/components/ArticleList.jsx

    Populate the Articles resource components starting with ArticleList.

    import React, { Component } from 'react';
    import  axios  from 'axios';
    import { Link } from 'react-router-dom';
    
    class ArticleList extends Component {
      constructor() {
        super();
        this.state = { articles: [] };
      }
    
      componentDidMount() {
        let token = "Bearer " + localStorage.getItem("jwt");
        axios({method: 'get', url: '/api/articles', headers: {'Authorization': token }})
          .then(response => { 
            this.setState({ articles: response.data })
          })
          .catch(error => console.log('error', error));
      }
    
      render() {
        return (
          <div>
            {this.state.articles.map((article) => {
              return(
                <div key={article.id}>
                  <h2><Link to={`/articles/${article.id}`}>{article.title}</Link></h2>
                  {article.content}
                  <hr/>
                </div>
              )     
            })}
            <Link to="/articles/new" className="btn btn-outline-primary">Create Article</Link>  
          </div>
        )
      }
    }
    
    export default ArticleList;
    

    Now in your browser if you click on the articles menu you should get the list of articles if you are logged in, and get nothing if you are logged out.

    This component is mostly the same as we created in Part 2 of this series. But this time, on the back end we restricted access to the Articles resource to only logged in users. So now when we make the AJAX request we need to send the JWT token in the request headers. Remember that unlike session cookies, JWT tokens are not sent automatically. We have to add them to the headers.

    componentDidMount() {
      let token = "Bearer " + localStorage.getItem("jwt");                              #1
      axios({method: 'get', url: '/api/articles', headers: {'Authorization': token }})  #2
        .then(response => {                                                             #3
          this.setState({ articles: response.data })
        })
        .catch(error => console.log('error', error));
    }
    

    1) Set a variable that gets the jwt string from localStorage (or sessionStorage). It should be in the format of "Bearer" then the string.
    2) Use Axios to make the GET request. We must add the JWT to the request headers under the "Authorization" key.
    3) Use ES6 promises to change the state for articles from an empty array to the response data when it is received.

    If you want to see what this would look like using Fetch instead of Axios, here it is:

    componentDidMount() {
      let token = "Bearer " + localStorage.getItem("jwt")
      fetch('/api/articles', { method: 'GET', headers: {'Authorization': token }})
        .then(response => response.json())
        .then(data => {
          this.setState({articles: data});
        })
        .catch(error => console.log('error', error));
    }
    

    # client/src/components/ArticleInfo.jsx

    Here's the ArticleInfo code:

    import React, { Component } from 'react';
    import axios from 'axios';
    import { Link } from 'react-router-dom';
    
    class ArticleInfo extends Component {
      constructor() {
        super();
        this.state = { article: {} };
        this.handleDelete = this.handleDelete.bind(this);
      }
    
      componentDidMount() {
        let token = "Bearer " + localStorage.getItem("jwt");
        axios({method: 'get', url: `/api/articles/${this.props.match.params.id}`, headers: {'Authorization': token }})
          .then((response) => { 
            this.setState({
              article: response.data
            })
          })
          .catch(error => console.log('error', error));
      }
    
      handleDelete() {
        let token = "Bearer " + localStorage.getItem("jwt");
        axios({ method: 'delete', url: `/api/articles/${this.props.match.params.id}`, headers: {'Authorization': token}})
          .then(() => {
            this.props.history.push("/articles")
          })
          .catch(error => console.log('error', error));
      }
    
      render() {
        return (
          <div>
            <h2>{this.state.article.id}: {this.state.article.title}</h2>
            <p>{this.state.article.content}</p>
            <p>
              <Link to={`/articles/${this.state.article.id}/edit`} className="btn btn-outline-dark">Edit</Link> 
              <button onClick={this.handleDelete} className="btn btn-outline-dark">Delete</button> 
              <Link to="/articles" className="btn btn-outline-dark">Close</Link>
            </p>
            <hr/>
          </div>
        )
      }
    }
    
    export default ArticleInfo;
    

    Here we are also adding the JWT token to the request headers. In the Rails articles_controller we restricted article deletes to admin users. In a real application we would only display the delete button to admin users, but we'll keep it simple here and leave it as is.

    # client/src/components/ArticleAdd.jsx

    Populate the ArticleAdd component:

    import React, { Component } from 'react';
    import axios from 'axios';
    
    class ArticleAdd extends Component {
      constructor() {
        super();
        this.state = { title: '', content: ''};
        this.handleChange = this.handleChange.bind(this);
        this.handleSubmit = this.handleSubmit.bind(this);
        this.handleCancel = this.handleCancel.bind(this);
      }
    
      handleSubmit(event) {
        event.preventDefault();
        let token = "Bearer " + localStorage.getItem("jwt")
        axios({ method: 'post', url: '/api/articles', headers: {'Authorization': token }, data: this.state})
          .then((response) => {
            this.props.history.push(`/articles/${response.data.id}`);
          })
          .catch(error => console.log('error', error));
      }
    
      handleChange(event) {
        this.setState({ [event.target.name]: event.target.value });
      }
    
      handleCancel() {
        this.props.history.push("/articles");
      }
    
      render() {
        return (
          <div>
            <h1>Create Article Post</h1>
            <form onSubmit={this.handleSubmit}>
              <div className="form-group">
                <label>Title</label>
                <input type="text" name="title" value={this.state.title} onChange={this.handleChange} className="form-control" />
              </div>
              <div className="form-group">
                <label>Content</label>
                <textarea name="content" rows="5" value={this.state.content} onChange={this.handleChange} className="form-control" />
              </div>
              <div className="btn-group">
                <button type="submit" className="btn btn-dark">Create</button>
                <button type="button" onClick={this.handleCancel} className="btn btn-secondary">Cancel</button>
              </div>
            </form>
          </div>
        );
      }
    }
    
    export default ArticleAdd;
    

    # client/src/components/ArticleEdit.jsx

    And finally, populate the ArticleEdit component:

    import React from 'react';
    import axios from 'axios';
    
    class ArticleEdit extends React.Component {
      constructor() {
        super();
        this.state = { title: '', content: ''};
        this.handleChange = this.handleChange.bind(this);
        this.handleSubmit = this.handleSubmit.bind(this);
        this.handleCancel = this.handleCancel.bind(this);
      }
    
      componentDidMount() {
        let token = "Bearer " + localStorage.getItem("jwt");
        axios({method: 'get', url: `/api/articles/${this.props.match.params.id}`, headers: {'Authorization': token }})
          .then((response) => { 
            this.setState(response.data)
          })
          .catch(error => console.log('error', error));
      }
    
      handleSubmit(event) {
        event.preventDefault();
        let token = "Bearer " + localStorage.getItem("jwt")
        axios({ method: 'patch', url: `/api/articles/${this.state.id}`, headers: {'Authorization': token }, data: this.state})
          .then(() => {
            this.props.history.push(`/articles/${this.state.id}`);
          })
          .catch(error => console.log('error', error));
      }
    
      handleChange(event) {
        this.setState({ [event.target.name]: event.target.value });
      }
    
      handleCancel() {
        this.props.history.push(`/articles/${this.state.id}`);
      }
    
      render() {
        return (
          <div>
            <h1>Edit {this.state.title}</h1>
            <form onSubmit={this.handleSubmit}>
              <div className="form-group">
                <label>Title</label>
                <input type="text" name="title" value={this.state.title} onChange={this.handleChange} className="form-control" />
              </div>
              <div className="form-group">
                <label>Content</label>
                <textarea name="content" rows="5" value={this.state.content} onChange={this.handleChange} className="form-control" />
              </div>
              <div className="btn-group">
                <button type="submit" className="btn btn-dark">Update</button>
                <button type="button" onClick={this.handleCancel} className="btn btn-secondary">Cancel</button>
              </div>
            </form>
          </div>
        );
      }
    }
    
    export default ArticleEdit;
    

    Add, delete, and edit some articles. If you remove the headers: {'Authorization': token } from any of the requests you should see a 401 (Unauthorized) error in the console. If it all works (and it should), you now have basic authentication with JSON Web Tokens!