Ruby On Rails: Token authentication
Guillaume Briday
3 minutes
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 :)