API User authentication with devise_token_auth

Linked Projects:

Calreact
 
Lesson code: 

Backend Rails API - https://github.com/learnetto/calreact/tree/m9l3-devise-token-auth

Frontend React app - https://github.com/learnetto/calreact-frontend/tree/m9l3-login

In this lesson, we’ll add user accounts to our API using the devise_token_auth gem.

This is a gem built on top of Devise which makes it easy to use Devise user authentication in a Rails API.

Let’s start by adding the gem to our Gemfile:

gem 'haml'
  
  gem 'rack-cors', :require => 'rack/cors'
  
  gem 'devise_token_auth'

and then run Bundle in the command line.

When it's done, run the generator:

rails generate devise_token_auth:install User auth

This will create a user model and add authentication to it. 

It has created an initialiser config file for Devise token auth. 

DeviseTokenAuth.setup do |config|
   # By default the authorization headers will change after each request. The
   # client is responsible for keeping track of the changing tokens. Change
   # this to false to prevent the Authorization header from changing after
   # each request.
   # config.change_headers_on_each_request = true
  
   # By default, users will need to re-authenticate after 2 weeks. This setting
   # determines how long tokens will remain valid after they are issued.
   # config.token_lifespan = 2.weeks
  
   # Sets the max number of concurrent devices per user, which is 10 by default.
   # After this limit is reached, the oldest tokens will be removed.
   # config.max_number_of_devices = 10
  
   # Sometimes it's necessary to make several requests to the API at the same
   # time. In this case, each request in the batch will need to share the same
   # auth token. This setting determines how far apart the requests can be while
   # still using the same auth token.
   # config.batch_request_buffer_throttle = 5.seconds
  
   # This route will be the prefix for all oauth2 redirect callbacks. For
   # example, using the default '/omniauth', the github oauth2 provider will
   # redirect successful authentications to '/omniauth/github/callback'
   # config.omniauth_prefix = "/omniauth"
  
  # By default sending current password is not needed for the password update.
   # Uncomment to enforce current_password param to be checked before all
   # attribute updates. Set it to :password if you want it to be checked only if
   # password is updated.
   # config.check_current_password_before_update = :attributes
  
   # By default we will use callbacks for single omniauth.
   # It depends on fields like email, provider and uid.
   # config.default_callbacks = true
  
   # Makes it possible to change the headers names
   # config.headers_names = {:'access-token' => 'access-token',
   #                        :'client' => 'client',
   #                        :'expiry' => 'expiry',
   #                        :'uid' => 'uid',
   #                        :'token-type' => 'token-type' }
  
   # By default, only Bearer Token authentication is implemented out of the box.
   # If, however, you wish to integrate with legacy Devise authentication, you can
   # do so by enabling this flag. NOTE: This feature is highly experimental!
   # config.enable_standard_devise_support = false
  end

And in the user model, it has added this line:

include DeviseTokenAuth::Concerns::User

And in the applicationcontroller, it has added:

include DeviseTokenAuth::Concerns::SetUserByToken

So as the name suggests, the user for the session is set by a token.

Now, in the routes file we have:

Rails.application.routes.draw do
    mount_devise_token_auth_for 'User', at: 'auth'
    get 'hello_world', to: 'hello_world#index'
    root 'appointments#index'
    resources :appointments
    end

So that means that the authentication URLs will be at /auth which we’ll use in our front-end app.

Now, let’s run

rake db:migrate

in Rails.

[screenshot 1:25-6]

That’s failed, looks like it requires omniauth, so let’s add that.

We are not really going to use omniauth, but it looks like it’s a dependency, so let’s just add it to get this working.

So in our Gemfile, let’s add:

gem 'haml'
  
  gem 'rack-cors', :require => 'rack/cors'
  
  gem 'devise_token_auth'
  gem omniauth

Then run Bundle and then run

rake db:migrate

again. It is set up.

One default setting we want to change is change_headers_on_each_request. It’s set to true by default and it means that with each request, the access token we need to send in API calls needs to change. This is for extra security but is too much of an overhead for our app. So we’ll set it to false in the Devise token auth config:

Let’s uncomment this line and set it to false.

DeviseTokenAuth.setup do |config|
   # By default the authorization headers will change after each request. The
   # client is responsible for keeping track of the changing tokens. Change
   # this to false to prevent the Authorization header from changing after
   # each request.
    config.change_headers_on_each_request = false
  
   # By default, users will need to re-authenticate after 2 weeks. This setting
   # determines how long tokens will remain valid after they are issued.
   # config.token_lifespan = 2.weeks
  
   # Sets the max number of concurrent devices per user, which is 10 by default.
   # After this limit is reached, the oldest tokens will be removed.
   # config.max_number_of_devices = 10
  
   # Sometimes it's necessary to make several requests to the API at the same
   # time. In this case, each request in the batch will need to share the same
   # auth token. This setting determines how far apart the requests can be while
   # still using the same auth token.
   # config.batch_request_buffer_throttle = 5.seconds
 
   # This route will be the prefix for all oauth2 redirect callbacks. For
   # example, using the default '/omniauth', the github oauth2 provider will
   # redirect successful authentications to '/omniauth/github/callback'
   # config.omniauth_prefix = "/omniauth"
  
   # By default sending current password is not needed for the password update.
   # Uncomment to enforce current_password param to be checked before all
   # attribute updates. Set it to :password if you want it to be checked only if
   # password is updated.
   # config.check_current_password_before_update = :attributes
 
   # By default we will use callbacks for single omniauth.
   # It depends on fields like email, provider and uid.
   # config.default_callbacks = true
  
   # Makes it possible to change the headers names
   # config.headers_names = {:'access-token' => 'access-token',
   #                        :'client' => 'client',
   #                        :'expiry' => 'expiry',
   #                        :'uid' => 'uid',
   #                        :'token-type' => 'token-type' }
  
   # By default, only Bearer Token authentication is implemented out of the box.
   # If, however, you wish to integrate with legacy Devise authentication, you can
   # do so by enabling this flag. NOTE: This feature is highly experimental!
   # config.enable_standard_devise_support = false
   end

 Have a look at the app in the browser.

The homepage loads with just the form. What we want to happen here is to get redirected to the login page.

[screenshot: 2:25]

Before we do that, one other thing we need to do is to configure the access control expose headers in our rack cors config.

We need to add this line:

:expose => ['access-token', 'expiry', 'token-type', 'uid', 'client'],

So let’s copy this and paste it into our config/application.rb file:

require_relative 'boot'
  
  require 'rails/all'
  
  # Require the gems listed in Gemfile, including any gems
  # you've limited to :test, :development, or :production.
  Bundler.require(*Rails.groups)
  
  module Calreact
   class Application < Rails::Application
    	config.api_only = true
      config.middleware.insert_before 0, Rack::Cors do
        allow do
          origins 'http://localhost:3000'
          resource '*', 
           :headers => :any, 
           :expose => ['access-token', 'expiry', 'token-type', 'uid', 'client'],
           :methods => [:get, :post, :patch, :delete, :options]
   end
   end
  
   end
  end

So the responses to our API requests will contain these headers.

Then, let’s test our authentication API from the command line using curl.

I have the Rails API server running here on port 3001. Now let’s run an example API call:

$ curl http://localhost:3001/auth --data "[email protected]&password=password"

This is an API call for creating a new user account i.e. for signing up.

When we run this, we get this JSON in response:

status: success

and some data in response, which includes a UID, a unique ID, which in this case is just an e-mail address.

[screenshot: 3:24]

Now let’s make an API call for logging in with this account.

Let’s just modify the previous call and add /sign_in to the URL and run it:

$ curl http://localhost:3001/auth/sign_in --data "[email protected]&password=password"

When I run it, that works fine, too.

[screenshot: 3:48]

Alright, now let’s once again add the links between the appointment and user models, just like we did in the Devise module.

We need to run the migration for linking the two models, then add the belongs_to and has_many statements to the models and modify the appointments controller.

I’m going to skip showing that again here, so please refer to the Devise module for details on how to do it. 

How we’ve got authentication working and have linked the two models.

Let’s go to our client app.

When we load the homepage, we want to redirect the user to a login page.

So let’s first create a login route and a component with the login form.

In AppRouter, let’s add a route

export default (props) => {
  	return (
  		<Router>
  			<div>
  				<Route path="/" component={AppHeader} />
  				<Route exact path="/" component={Appointments} />
  				<Route path="/login" component={Login} />
  				<Route exact path="/appointments/:id" component={Appointment} />
  				<Route path="/appointments/:id/edit" component={AppointmentForm} />
  			</div>
  		</Router>

We'll need to import Login from Login 

import AppointmentForm from './AppointmentForm';
  import Login from './Login';
  
  export default (props) => {
  	return (
  		<Router>
  			<div>

and then create that component in a new file called Login.js.

To get started, I’m going to paste in some code in the file:

import React from 'react';

  export default class Login extends React.Component {
 
render () {
   return (
   <div>
   <h2>Sign in</h2>
   <form onSubmit={this.handleLogin} >
   <input name="email" />
   <input name="password" type="password" />
  <input type="submit"/>
   </form>
   </div>
      )
   }
  }

We have a class component which renders a simple two-field login form with input fields for email and password and a submit button.

Ok, now let’s see this in the browser and make sure our form loads. Go to

localhost:3000/login

So our form loads. Now let’s make it functional.

Let’s first add an onsubmit attribute:

render () {
   return (
   <div>
        	<h2>Sign in</h2>
   <form onSubmit={this.handleLogin} >
   <input name="password" type="password" />
   <input type="submit"/>
   </form>
   </div>
      )
    }
  }

and let’s define handleLogin as a method here:

 handleLogin = (e) => {
   e.preventDefault();
}
render () {
   return (
   <div>
        	<h2>Sign in</h2>
   <form onSubmit={this.handleLogin} >
   <input name="password" type="password" />
   <input type="submit"/>
   </form>
   </div>
      )
    }
  }

Then we need to get the values of the input fields and make an AJAX call with them to our login endpoint.

Let’s use refs here to get the field values. So let’s add

ref = {(input) => this.email = input}

and let’s do the same for password

ref = {(input) => this.password = input}

Now we can use these in handleLogin. Let’s state

 handleLogin = (e) => {
   e.preventDefault();
   $.ajax({
        type: 'POST',
        url: 'http://localhost:3000/auth/sign_in',

Notice that it should be 3001 actually. And to add the data we want to sign, we type in

data: {
	email: this.email.value,
	password: this.password.value
}

.done

The e-mail we'll be getting from the refs that we defined earlier, we give space for password and use .done for when the signup is successful, we'll just redirect to the homepage.

So we’ll write:

 handleLogin = (e) => {
   e.preventDefault();
   $.ajax({
        type: 'POST',
        url: 'http://localhost:3001/auth/sign_in',
        data: {
          email: this.email.value,
          password: this.password.value
        }
      })
      .done(this.props.history.push('/');
      )
    }
  
   render () {
   return (
   <div>
        	<h2>Sign in</h2>
   <form onSubmit={this.handleLogin} >
   <input name="email" ref={(input) => this.email = input } />
   <input name="password" type="password" ref={(input) => this.password = input } />
   <input type="submit"/>
   </form>
   </div>
      )
    }
  }this.props.history.push(‘/‘)

Actually, this needs to be a function. So let’s update it:
  
 handleLogin = (e) => {
   e.preventDefault();
   $.ajax({
        type: 'POST',
        url: 'http://localhost:3001/auth/sign_in',
        data: {
          email: this.email.value,
          password: this.password.value
        }
      })
      .done(response) => {
   this.props.history.push('/');
      )
    }
 
   render () {
   return (
   <div>
        	<h2>Sign in</h2>
   <form onSubmit={this.handleLogin} >
   <input name="email" ref={(input) => this.email = input } />
   <input name="password" type="password" ref={(input) => this.password = input } />
   <input type="submit"/>
   </form>
   </div>
      )
    }
  }

We forgot to import jQuery, so let’s do that.

import React from 'react';
  import $ from 'jquery';
  
  export default class Login extends React.Component {
  
   handleLogin = (e) => {

Now, what we want to do is after the sign-in request has been successful, we want to store the returned access token in the session.

So in .done, let’s do

.done((response, status, jqXHR) => {

These three arguments mean: response is the response to that request, status is whether it's successful or not, and jqXHR is the request. We will use the browser session storage to store the tokens and headers which we’ll need for subsequent requests.

We need the access token, client and UID, which are returned by our API on successfully signing in. So let’s add

 .done((response, status, jqXHR) => {
   sessionStorage.setItem('user',

The item will be called 'user', so we need to create a new item in our session and we will create a JSON. We need to convert this JSON to a string because we can’t save it as is in the session.

 .done((response, status, jqXHR) => {
   sessionStorage.setItem('user',
   JSON.stringify({

We could save each value as a separate item but instead, we’re going to convert this object into a string and save it with the key ‘user’ and then, when we want to use it in an API call, we'll parse it as JSON.

So let’s do

 .done((response, status, jqXHR) => {
   sessionStorage.setItem('user',
   JSON.stringify({
   'access-token': jqXHR.getResponseHeader('access-token'),
            client: jqXHR.getResponseHeader('client'),
            uid: response.data.uid
          }));
   this.props.history.push('/');
      })
    }

as both of these are in the response header. 

And finally, UID is in the response itself, so we can say

uid: response.data.uid

Now let’s use this session data in the Appointments component. In our AJAX call in componentDidMount, in addition to the type and URL, we need to pass in headers:

 componentDidMount () {
   if(this.props.match) {
   $.ajax({
          type: "GET",
          url: 'http://localhost:3001/appointments',
          dataType: "JSON",
          headers: JSON.parse(sessionStorage.user)
        }).done((data) => {
   this.setState({appointments: data});
        });
      }
    }

Let’s check if a user exists in the session here by adding

componentDidMount () {
   if(this.props.match && sessionStorage.user) {

Let’s try this in the browser to see if it works. Let’s go to the login route.

Enter an account email and password.  We’ll use the one we created earlier on the command line.

Submit the form... and we get redirected to the homepage.

We don’t have any appointments for this account so the list is empty.

Let’s add an appointment now.

But before we do that, we need to add the headers to the API calls in AppointmentForm.js.

In the addAppointment function, let’s add the

 $.ajax 

call, just to be consistent. 

Then add

 $.ajax({
        type: 'POST',
        url: 'http://localhost:3001/appointments',
        data: {appointment: appointment},

And then we'll add the headers, copied from the appointments component:

 $.ajax({
        type: 'POST',
        url: 'http://localhost:3001/appointments',
        data: {appointment: appointment},
        headers: JSON.parse(sessionStorage.user)
      })
      .done((data) => {
   this.props.handleNewAppointment(data);
   this.resetFormErrors();
      })
      .fail((response) => {
   this.setState({formErrors: response.responseJSON,
                        formValid: false});
      });
    }

and run the app in the browser.

[screenshot 9:56-57]

Let me just fix this linting warning by removing extra spaces:

 <FormErrors formErrors={this.state.formErrors} />

Ok, now let’s test the app: add a title, choose a date, and submit.

It works!

Now, if I click through to view the appointment, the data won’t load because we haven’t added headers to the AJAX call on this route. 

[screenshot: 10:13-14]

So let’s do that now in the Appointment component.

In componentDidMount, copy and paste the headers.

componentDidMount () {
   if(this.props.match && sessionStorage.user) {
   $.ajax({
          type: "GET",
          url: 'http://localhost:3001/appointments',
          dataType: "JSON",
          headers: JSON.parse(sessionStorage.user)
        }).done((data) => {
   this.setState({appointments: data});
        });
      }
    }
 
Then reload and it works.

Finally, let’s look at the Edit route.

And we need to add the headers here too, in AppointmentForm, in componentDidMount

The form loads pre-populated with the data for that appointment. Let’s add the headers to the patch API call:

 $.ajax({
        type: "PATCH",
        url: `http://localhost:3001/appointments/${this.props.match.params.id}`,
        data: {appointment: appointment},
        headers: JSON.parse(sessionStorage.user)
      })

Then let’s try the form by changing the title.

That works too.

Actually, here, instead of using .user, it’s best to use the getItem method. To do that, instead of sessionStorage.user, write: 

getItem(‘user’)

Let’s change that everywhere else we’re using this session value (sessionStorage.user).

That’s our React app working with an API with user authentication.

Liked this tutorial? Get more like this in your inbox