Dev

Ruby On Rails: Token authentication

Profil Picture

Guillaume Briday

3 minutes

Rails authentication with token
Rails authentication with token

After articles concerning ways to install Rails on different platforms, today we're going to do something more concrete!

Authentication tokens

With Rails 5, it is very easy to configure Rails to be solely an API. I will let you read the documentation to understand the differences from the standard version.

When creating a new application, simply add the --api flag:

$ rails new my_super_api --api

If your application is not configured to be only an API, what I will present here still works!

Introduction

As the title of the article suggests, we want the ability to authenticate securely with a token.

For this example, I will add token authentication to an existing application, but it will be very simple to adapt if it is new.

Our application

I will create an application that manages books with only a title.

$ rails generate scaffold Book title:string

I want to access all of this resource only if I am authenticated with an access token. Let's start by adding a field to our User model, which will allow us to save our token for each member.

Generating tokens

$ rails generate migration AddAuthTokenToUser auth_token:string

When creating a member, we will automatically generate a private token so they can use it in the API.

In the User model, I will use the before_create callback to automatically generate a token, ensuring it is unique for each user.

# app/models/user.rb
class User < ApplicationRecord
  before_create :set_auth_token

  private

  def set_auth_token
    self.auth_token = generate_auth_token
  end

To generate the token, I use the method used by has_secure_token in Active Record.

If I create a User, I notice that the auth_token field has been automatically filled!

Using the tokens

Rails comes with the method authenticate_or_request_with_http_token, which will check the Authorization header for us and return the token directly in a block if it is valid.

# app/controllers/application_controller.rb
class ApplicationController < ActionController::API
  include ActionController::HttpAuthentication::Token::ControllerMethods

  # This before_action will verify the token for each request.
  # You can place it in child controllers if you want authentication
  # for only certain methods
  before_action :authenticate

  protected

  def authenticate
    authenticate_token || render_unauthorized
  end

  def authenticate_token
    authenticate_or_request_with_http_token do |token, options|
      @current_user = User.find_by(auth_token: token)
    end
  end

From now on, each request to the controllers must have a valid token in the Authorization header, and I can verify it directly with curl:

$ curl -I http://localhost:3000/books
HTTP/1.1 401 Unauthorized

And with a token generated when creating a user:

$ curl -IH "Authorization: Token token=coUra7m9yYGBR6SN66bzWefB" http://localhost:3000/books
HTTP/1.1 200 OK

If I want to access the list of books without a token, I can use the skip_before_action callback:

# app/controllers/books_controller.rb
class BooksController < ApplicationController
  skip_before_action :authenticate, only: [:index, :show]

And if we test with curl:

$ curl -I http://localhost:3000/books
HTTP/1.1 200 OK

Everything works as expected, but let's use tests to verify it.

Tests

I will use the tests that were generated for our Books, which we will need to modify a bit to pass the Authorization header. But first, I will add a method to the helpers to encode authentication tokens during my tests:

# test/test_helper.rb
class ActiveSupport::TestCase
  # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.
  fixtures :all

And finally, I can modify my tests:

# test/controllers/books_controller_test.rb
require 'test_helper'

class BooksControllerTest < ActionDispatch::IntegrationTest
  setup do
    @book = books(:one)
    # I create a User to obtain a token
    @demo = User.create!({email: '[email protected]', password: 'demodemo', password_confirmation: 'demodemo'})
  end

  test "should get index" do
    get books_url, as: :json
    assert_response :success
  end

  # I must add the 'Authorization' header...
  test "should create book" do
    assert_difference('Book.count') do
      post books_url,
           params: { book: { title: @book.title } },
           as: :json,
           headers: { 'Authorization' => token_header(@demo.auth_token) }
    end
    assert_response 201
  end

  test "should show book" do
    get book_url(@book), as: :json
    assert_response :success
  end

  test "should update book" do
    patch book_url(@book),
          params: { book: { title: @book.title } },
          as: :json,
          headers: { 'Authorization' => token_header(@demo.auth_token) }
    assert_response 200
  end

  # ... Otherwise it doesn't work
  test "should not update book" do
    patch book_url(@book),
          params: { book: { title: @book.title } },
          as: :json
    assert_response 401
  end

  test "should destroy book" do
    assert_difference('Book.count', -1) do
      delete book_url(@book),
             as: :json,
             headers: { 'Authorization' => token_header(@demo.auth_token) }
    end

Everything goes as we expected!

Conclusion

We have now completed this quick implementation of token authentication. The application is available on this GitHub repository if you wish.

Feel free to provide feedback or suggest improvements!

Thank you :)

Simplify your time tracking with Timecop

Timecop is a time tracking app that brings simplicity in your day to day life.

Timecop projects