Chapter 21

Clients Authentication

We are approaching the end of the implementation of the Alexandria API. Before we proceed with payments however, we need to lock down our API to prevent unwanted usage.

In Alexandria, we want to be able to identify, authenticate and authorize users - all using only one authentication scheme. With the custom scheme that we are going to implement, we will be able to authenticate not only users, but also clients.

21.1. Alexandria Custom Auth Scheme

Since the best practice for authentication is to use the Authorization header coming with HTTP, that’s exactly what we are going to do. Our authentication won’t be stateless however.

Indeed, to have a truly stateless authentication scheme, the server cannot keep any state on the server which means no login and logout. Instead, the client is supposed to handle that part which allows any HTTP request to be sent at any time without following a specific order like login -> do_something -> logout.

Note that with a real stateless authentication, the server cannot know who the user is and the entire state has to be sent by the client in every request.

We do want to know our users, since we are a Machiavellian e-commerce website keeping track of their every move. There will be two front-end applications for Alexandria, one iOS application and one JavaScript SPA, so we need to identify users and be able to synchronize data between them.

Keeping the state of our users on the server has a cost, though. It makes it harder to scale and manage the servers handling the requests. All servers must have access to shared databases which is luckily made easy by companies like Amazon Web Services.

To use the Authorization header, we need to define which authentication scheme we are going to use. How about making a custom version and call it Alexandria-Token? It’s similar to the token-based authentication but with one more little detail.

This authentication scheme should let us authentication both clients and users.

All resources will be protected from unwanted use by API keys sent by the clients. A subset of those resources will also have different level of access for the users. For example, any user from our clients can access GET /api/books but not everyone can create books with POST /api/books. To represent those two levels of authentication, we are going to use two realms: Client Realm and User Realm.

Authentication through the Client Realm requires the api_key parameter in the Authorization header as seen below:

Authorization: Alexandria-Token api_key=AAA

API keys are generated by admins on the server and embedded in the clients. They can be disabled if they have been compromised.

Once a user has logged in and received an access token (as we will implement in chapter 23), this access token should be added under the access_token parameter.

Authorization: Alexandria-Token api_key=AAA, access_token=AAA

That’s it! This is how the Alexandria-Token authentication scheme is going to work. Let’s get started!

21.2. Identify and Authenticate Clients with API Keys

The first thing we are going to do is implement the code to authenticate clients. We don’t care much about their identity as long as they have a valid API key.

If we wanted to go further, we could link the keys to specific clients to identify them.

API Keys will be generated by admins through an admin panel that will probably never exist. The actual key will be auto-generated using:

SecureRandom.hex

The ApiKey model will have the following fields:

  • id
  • key
  • active
  • created_at
  • updated_at

Use Rails generator to create a new model.

rails g model ApiKey key:string active:boolean

Output

Running via Spring preloader in process 8922
      invoke  active_record
      create    db/migrate/TIMESTAMP_create_api_keys.rb
      create    app/models/api_key.rb
      invoke    rspec
      create      spec/models/api_key_spec.rb
      invoke      factory_bot
      create        spec/factories/api_keys.rb

We need to add an index and a unique constraint to the key column in the migration file that was generated. Since we will be looking for ApiKey records using the key, we need to be able to query them as fast as possible. We also want to add a default value (true) to the active column.

# db/migrate/TIMESTAMP_create_api_keys.rb
class CreateApiKeys < ActiveRecord::Migration[5.2]
  def change
    create_table :api_keys do |t|
      t.string :key, index: true, unique: true
      t.boolean :active, default: true

      t.timestamps
    end
  end
end

Migrate the database for the development and test environments.

rails db:migrate && rails db:migrate RAILS_ENV=test

The tests for the ApiKey model are super simple. We don’t even need a factory for them, since an API key should be valid on creation without any parameters.

# spec/models/api_key_spec.rb
require 'rails_helper'

RSpec.describe ApiKey, :type => :model do

  let(:key) { ApiKey.create }

  it 'is valid on creation' do
    expect(key).to be_valid
  end

  describe '#disable' do
    it 'disables the key' do
      key.disable
      expect(key.reload.active).to eq false
    end
  end
end

However, we will need the factory in the future - so let’s update it.

# spec/factories/api_keys.rb
FactoryBot.define do
  factory :api_key do
    key { 'RandomKey' }
    active { true }
  end
end

Here is the actual model. Notice how we generated the key before validation automatically. We also implemented an easy way to disable an API key with the disable method.

# app/models/api_key.rb
class ApiKey < ApplicationRecord
  before_validation :generate_key, on: :create
  validates :key, presence: true
  validates :active, presence: true
  scope :activated, -> { where(active: true) }

  def disable
    update_column :active, false
  end

  private

  def generate_key
    self.key = SecureRandom.hex
  end
end

The tests should be passing without any issue.

rspec spec/models/api_key_spec.rb

Success (GREEN)

...

ApiKey
  is valid on creation
  #disable
    disables the key

Finished in 0.0866 seconds (files took 1.75 seconds to load)
2 examples, 0 failures

Great! Now that we have API keys implemented, let’s use them to lock down Alexandria.

21.3. Lockdown

To handle authentication, we are going to need a bunch of methods that should be available at the controller level. To avoid polluting the ApplicationController class, let’s create a concern, Authentication, to hold all the authentication logic.

touch app/controllers/concerns/authentication.rb \
  spec/requests/authentication_spec.rb

We already wrote a lot of tests for our controllers. The best way to ensure that everything is working well here would be to update all of them to include an authenticated context. However, I don’t want to spend time doing that instead of showing you more interesting stuff, so I’ll leave it up to you. At the end of this chapter, I will go over a few ways to handle authentication in your tests.

For now, we are just going to test it in a new test file, using GET /api/books as an example. Read through the tests below, as they should document pretty well how we are going to implement the client authentication.

# spec/requests/authentication_spec.rb
require 'rails_helper'

RSpec.describe 'Authentication', type: :request do

  describe 'Client Authentication' do
    before { get '/api/books', headers: headers }

    context 'with invalid authentication scheme' do
      let(:headers) { { 'HTTP_AUTHORIZATION' => '' } }

      it 'gets HTTP status 401 Unauthorized' do
        expect(response.status).to eq 401
      end
    end

  end
end

Run this test to see it fail.

rspec spec/requests/authentication_spec.rb

Failure (RED)

...

Finished in 0.14827 seconds (files took 2.52 seconds to load)
1 example, 1 failure

...

And here we are, the Authentication module that will be included in ApplicationController. We are going to build in two steps. First, ensuring that the client provided the right authentication scheme and then checking the API key. Let’s start with the former.

First, let’s define the basic skeleton of our concern and a constant that will hold the name of our authentication scheme.

# app/controllers/concerns/authentication.rb
module Authentication
  extend ActiveSupport::Concern

  AUTH_SCHEME = 'Alexandria-Token'

  included do

  end

end

Next, we want an easy way to get the content of the Authorization header, so let’s add a short method for that.

# app/controllers/concerns/authentication.rb
module Authentication
  extend ActiveSupport::Concern

  AUTH_SCHEME = 'Alexandria-Token'

  included do

  end

  private

  def authorization_request
    @authorization_request ||= request.authorization.to_s
  end

end

When a request is unauthorized, the server needs to return a 401Unauthorized status code with the WWW-Authenticate header indicating the authentication scheme and the realm. To handle this logic, let’s add a new method, named unauthorized! that will halt the execution of the request and return 401. Since we will be reusing this method for user authentication, we want the realm to be passed as an argument.

# app/controllers/concerns/authentication.rb
module Authentication
  extend ActiveSupport::Concern

  AUTH_SCHEME = 'Alexandria-Token'

  included do

  end

  private

  def unauthorized!(realm)
    headers['WWW-Authenticate'] = %(#{AUTH_SCHEME} realm="#{realm}")
    render(status: 401)
  end

  def authorization_request
    @authorization_request ||= request.authorization.to_s
  end

end

Finally, in order to check the Authorization header before any request, we add a before_action that will call validate_auth_scheme. This method will check if the Authorization header matches a simple regex containing the authentication scheme and call unauthorized! if it does not.

# app/controllers/concerns/authentication.rb
module Authentication
  extend ActiveSupport::Concern

  AUTH_SCHEME = 'Alexandria-Token'

  included do
    before_action :validate_auth_scheme
  end

  protected

  def validate_auth_scheme
    unless authorization_request.match(/^#{AUTH_SCHEME} /)
      unauthorized!('Client Realm')
    end
  end

  def unauthorized!(realm)
    headers['WWW-Authenticate'] = %(#{AUTH_SCHEME} realm="#{realm}")
    render(status: 401)
  end

  def authorization_request
    @authorization_request ||= request.authorization.to_s
  end

end

That’s it! Let’s include this concern in ApplicationController.

# app/controllers/application_controller.rb
class ApplicationController < ActionController::API
  include Authentication

  # Hidden Code

Run the tests and everything should be alright, since we are not correctly validating the authentication scheme yet.

rspec spec/requests/authentication_spec.rb

Success (GREEN)

...

Authentication
  Client Authentication
    with invalid authentication scheme
      gets HTTP status 401 Unauthorized

Finished in 0.11579 seconds (files took 2.47 seconds to load)
1 example, 0 failures

Now it’s time to actually check if the API key the client provided is correct or not. Let’s add 3 new contexts (with invalid API key, with disabled API key and with a valid API key) to the authentication_spec.rb spec file.

# spec/requests/authentication_spec.rb
require 'rails_helper'

RSpec.describe 'Authentication', type: :request do

  describe 'Client Authentication' do
    before { get '/api/books', headers: headers }

    context 'with invalid authentication scheme' # Hidden Code

    context 'with valid authentication scheme' do
      let(:headers) do
         { 'HTTP_AUTHORIZATION' => "Alexandria-Token api_key=#{key}" }
      end

      context 'with invalid API Key' do
        let(:key) { 'fake' }
        it 'gets HTTP status 401 Unauthorized' do
          expect(response.status).to eq 401
        end
      end

      context 'with disabled API Key' do
        let(:key) { ApiKey.create.tap { |key| key.disable }.key }
        it 'gets HTTP status 401 Unauthorized' do
          expect(response.status).to eq 401
        end
      end

      context 'with valid API Key' do
        let(:key) { ApiKey.create.key }
        it 'gets HTTP status 200' do
          expect(response.status).to eq 200
        end
      end
    end # context 'with valid authentication scheme' end

  end
end

We need two new methods in the Authentication concern to extract and validate the received API key. First, the credentials method will scan the Authorization header and give us a hash of the parameters contained inside. If we have Alexandria-Token api_key=123, this method will return {'api_key' => '123'}.

The second method, api_key, will attempt to find the API key inside the database.

# app/controllers/concerns/authentication.rb
module Authentication
  extend ActiveSupport::Concern

  AUTH_SCHEME = 'Alexandria-Token'

  included do
    before_action :validate_auth_scheme
  end

  protected

  def validate_auth_scheme # Hidden Code
  def unauthorized! # Hidden Code
  def authorization_request # Hidden Code

  def credentials
    @credentials ||= Hash[authorization_request.scan(/(\w+)[:=] ?"?(\w+)"?/)]
  end

  def api_key
    return nil if credentials['api_key'].blank?
    @api_key ||= ApiKey.activated.where(key: credentials['api_key']).first
  end

end

The last step is to ensure that the two methods we created are used. This can easily be done by adding a method named authenticate_client that is just going to call unauthorized! unless the api_key method returns something. We also want to add a before_action filter calling this method to ensure the API key is checked during any request.

# app/controllers/concerns/authentication.rb
module Authentication
  extend ActiveSupport::Concern

  AUTH_SCHEME = 'Alexandria-Token'

  included do
    before_action :validate_auth_scheme
    before_action :authenticate_client
  end

  protected

  def validate_auth_scheme # Hidden Code

  def authenticate_client
    unauthorized!('Client Realm') unless api_key
  end

  def unauthorized! # Hidden Code
  def authorization_request # Hidden Code
  def credentials # Hidden Code
  def api_key # Hidden Code
end

Let’s see how our tests are performing now.

rspec spec/requests/authentication_spec.rb

Success (GREEN)

Finished in 0.13704 seconds (files took 2.13 seconds to load)
4 examples, 0 failures

Great, isn’t it? What if we run all our tests?

rspec

Failure (RED)

...

Finished in 3.13 seconds (files took 0.89447 seconds to load)
199 examples, 131 failures

...

Ouch. All the tests we wrote for our controllers are now failing since they cannot go through the authentication. How are we going to fix that?

21.4. Fixing The Tests

We have three options to make our tests pass.

  1. We manually update all the tests to have unauthenticated and authenticated contexts for each resource.
  2. We stub the authentication methods in our request tests.
  3. We refactor our test suite using shared examples and use that opportunity to add some unauthenticated/authenticated contexts.

The first one is time-consuming and would require 2 or 3 chapters; we don’t have time for that. The third one also takes a while and has been partially extracted into a screencast coming with the medium and complete packages.

That leaves us with the second option which is pretty simple to implement, but won’t test all our resources for authentication. That’s not ideal, but it’s fine since we have written some authentication tests already.

Let’s go through each option anyway, since you might learn something along the way.

Option 1: Updating All The Tests

As we said, the first thing we could do is update our tests and define expectations when the client is authorized or unauthorized. Below, you can see some code of the books_spec file where we added two contexts: context 'with invalid API Key' and context 'with valid API Key'.

# spec/requests/books_spec.rb
require 'rails_helper'

RSpec.describe 'Books', type: :request do

  let(:api_key) { ApiKey.create }
  let(:headers) do
     { 'HTTP_AUTHORIZATION' => "Alexandria-Token api_key=#{api_key.key}" }
  end

  let(:ruby_microscope) { create(:ruby_microscope) }
  let(:rails_tutorial) { create(:ruby_on_rails_tutorial) }
  let(:agile_web_dev) { create(:agile_web_development) }
  let(:books) { [ruby_microscope, rails_tutorial, agile_web_dev] }

  describe 'GET /api/books' do
    context 'with invalid API Key' do
      it 'gets 401 Unauthorized' do
        get '/api/books'
        expect(response.status).to eq 401
      end
    end

    context 'with valid API Key' do
      before { books }

      context 'default behavior' do
        before { get '/api/books', headers: headers }

        it 'gets HTTP status 200' do
          expect(response.status).to eq 200
        end

        it 'receives a json with the "data" root key' do
          expect(json_body['data']).to_not be nil
        end

        it 'receives all 3 books' do
          expect(json_body['data'].size).to eq 3
        end
      end

      describe 'field picking' # Hidden Code
      describe 'pagination' # Hidden Code
      describe 'sorting' # Hidden Code
      describe 'filtering' # Hidden Code
    end

    # Hidden Code

We would need to change all the tests in this file and our other tests as well, which is way too time consuming.

Option 2: Stubbing the Authentication Methods

We use methods to authenticate clients. Methods can be stubbed as we’ve seen earlier in this book. We can skip the authentication with four lines of code.

For the books tests, for example, we would just need to add this before block at the beginning of the file.

# spec/requests/books_spec.rb
require 'rails_helper'

RSpec.describe 'Books', type: :request do

  before do
    allow_any_instance_of(BooksController).to(
      receive(:validate_auth_scheme).and_return(true))
    allow_any_instance_of(BooksController).to(
      receive(:authenticate_client).and_return(true))
  end

  # Hidden Code

While this might not be the best option, it’s the fastest to implement. As long as you understand the pros and cons of this approach, it’s fine to use it. I would still recommend updating all the tests, but if you don’t have enough time, stubbing will be fine.

Option 3: Refactoring Tests with Shared Examples

The idea here is to use RSpec shared examples to create common sets of tests for our resources that can easily be included where we need them.

This option is explored in one of the screencast coming with the medium and complete package.

Implementing Option 2

Let’s use the second option and update all our request tests.

Books Tests

Update the books_spec.rb file.

# spec/requests/books_spec.rb
require 'rails_helper'

RSpec.describe 'Books', type: :request do

  before do
    allow_any_instance_of(BooksController).to(
      receive(:validate_auth_scheme).and_return(true))
    allow_any_instance_of(BooksController).to(
      receive(:authenticate_client).and_return(true))
  end

  # Hidden Code
Publisher Tests

Update the publishers_spec.rb file.

# spec/requests/publishers_spec.rb
require 'rails_helper'

RSpec.describe 'Publishers', type: :request do

  before do
    allow_any_instance_of(PublishersController).to(
      receive(:validate_auth_scheme).and_return(true))
    allow_any_instance_of(PublishersController).to(
      receive(:authenticate_client).and_return(true))
  end

  # Hidden Code
Authors Tests

Update the authors_spec.rb file.

# spec/requests/authors_spec.rb
require 'rails_helper'

RSpec.describe 'Authors', type: :request do

  before do
    allow_any_instance_of(AuthorsController).to(
      receive(:validate_auth_scheme).and_return(true))
    allow_any_instance_of(AuthorsController).to(
      receive(:authenticate_client).and_return(true))
  end

  # Hidden Code
Search Tests

Update the search_spec.rb file.

# spec/requests/search_spec.rb
require 'rails_helper'

RSpec.describe 'Search', type: :request do

  before do
    allow_any_instance_of(SearchController).to(
      receive(:validate_auth_scheme).and_return(true))
    allow_any_instance_of(SearchController).to(
      receive(:authenticate_client).and_return(true))
  end

  # Hidden Code

Run the tests to ensure that everything is working as expected now.

rspec

Success (GREEN)

Finished in 3.94 seconds (files took 1.01 seconds to load)
199 examples, 0 failures

21.5. Preventing Timing Attacks

Before we conclude this chapter, we need to talk about timing attacks.

A timing attack relies on analyzing the duration of specific computing actions to gain insights about the application. For example, an attacker could try to guess a user’s access token by checking how long the server takes to respond to each request.

This is possible because of the way regular comparison algorithms work in programming. Checking if "something" and "someone" are equals will take longer than checking "something" against "whatever", because the algorithm will compare the character one by one (s, o, m and e) before finding a different one (t vs. o) and returning false.

A regular comparison algorithm would return as soon as the first characters (s and w) are found to be different.

In a timing attack, the attacker first makes requests using different random access tokens and seeing if any of the requests takes longer. That would mean the server started to match the API key supplied by the attacker with one from the server.

With enough requests, the attacker could build a valid token from scratch, simply by analyzing the request duration. To avoid this, we need to use constant-time algorithms that will always take the same amount of time. Let’s implement it to understand exactly how it works.

21.5.1. Fixing the API Key Check

The way we implemented the API key is vulnerable to timing attacks. That’s why many web APIs come with an access key and a secret key. The access key can be used to find the record in the database before doing a secure comparison of the secret keys.

In order to check whether or not an API key is valid in a safe way, we need to retrieve the database record without using the actual token. Indeed, doing a SQL query like ApiKey.where(key: 'abc') is vulnerable to timing attacks.

To avoid this, we can ask the client to provide us with another way to find the record, for example by sending the key id. We would end up with receiving api_key=1:abc instead of api_key=abc, where 1 is a API key id.

For a real-world application, we should have created a new field to save an access key instead of using the id.

With this approach, the Authorization header will look like this.

Authorization: Alexandria-Token api_key=1:my_api_key

Here is the updated api_key method. Note that we also updated the regular expression matching the parameter of the Alexandria-Token authentication scheme to allow :.

# app/controllers/concerns/authentication.rb
module Authentication
  include ActiveSupport::SecurityUtils
  # Hidden Code

  included # Hidden Code

  private

  def validate_auth_scheme # Hidden Code
  def unauthorized! # Hidden Code
  def authorization_request # Hidden Code

  def credentials
    @credentials ||= Hash[authorization_request.scan(/(\w+)[:=] ?"?([\w|:]+)"?/)]
  end

  def api_key
    @api_key ||= compute_api_key
  end

  def compute_api_key
    return nil if credentials['api_key'].blank?

    id, key = credentials['api_key'].split(':')
    api_key = id && key && ApiKey.activated.find_by(id: id)

    return api_key if api_key && secure_compare_with_hashing(api_key.key, key)
  end

  def secure_compare_with_hashing(a, b)
    secure_compare(Digest::SHA1.hexdigest(a), Digest::SHA1.hexdigest(b))
  end

end

Let’s update the authentication tests.

# spec/requests/authentication_spec.rb
require 'rails_helper'

RSpec.describe 'Authentication', type: :request do

  describe 'Client Authentication' do
    before { get '/api/books', headers: headers }

    context 'with invalid authentication scheme' do
      let(:headers) { { 'HTTP_AUTHORIZATION' => '' } }

      it 'gets HTTP status 401 Unauthorized' do
        expect(response.status).to eq 401
      end
    end

    context 'with valid authentication scheme' do
      let(:headers) do
         { 'HTTP_AUTHORIZATION' =>
            "Alexandria-Token api_key=#{api_key.id}:#{api_key.key}" }
      end

      context 'with invalid API Key' do
        let(:api_key) { OpenStruct.new(id: 1, key: 'fake') }
        it 'gets HTTP status 401 Unauthorized' do
          expect(response.status).to eq 401
        end
      end

      context 'with disabled API Key' do
        let(:api_key) { ApiKey.create.tap { |key| key.disable } }
        it 'gets HTTP status 401 Unauthorized' do
          expect(response.status).to eq 401
        end
      end

      context 'with valid API Key' do
        let(:api_key) { ApiKey.create }
        it 'gets HTTP status 200' do
          expect(response.status).to eq 200
        end
      end
    end # context 'with valid authentication scheme' end
  end
end
rspec

Success (GREEN)

Finished in 3.84 seconds (files took 1.02 seconds to load)
199 examples, 0 failures

Our API is now protected against timing attacks.

21.6. Generating an API Key

If you want to run some manual tests, you will now need to use an API key. Since we don’t have an admin panel, let’s use the rails console to create one.

 rails c
2.5.0 :001 > ApiKey.create!
#<ApiKey id: 1, key: "906e64b7b8297c3801c4b7942f9c359f", active: true,
         created_at: "2016-06-08 08:49:39", updated_at: "2016-06-08 08:49:39">

The generated key is great for us in production, but it’s going to be annoying for this book. Let’s replace it with something easier - something that will be shared by this book and your application. Type exit once you’re done.

2.5.0 :002 > ApiKey.first.update_column(:key, 'my_api_key')
[HIDDEN LINES]
=> true

From now on, we will be using my_api_key as our API key. Let’s give it a try.

Start the server.

rails s

And run the curl command below.

curl -i http://localhost:3000/api/books

It will fail as expected with a 401 Unauthorized error.

Output

HTTP/1.1 401 Unauthorized
...

If we set the Authorization header properly, it should work.

curl -i -H "Authorization: Alexandria-Token api_key=1:my_api_key" \
     http://localhost:3000/api/books

And indeed, it does!

Output

HTTP/1.1 200 OK
...

21.7. Pushing our changes

First, check the changes.

git status

Output

On branch master
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

	modified:   app/controllers/application_controller.rb
	modified:   db/schema.rb
	modified:   spec/requests/authors_spec.rb
	modified:   spec/requests/books_spec.rb
	modified:   spec/requests/publishers_spec.rb
	modified:   spec/requests/search_spec.rb

Untracked files:
  (use "git add <file>..." to include in what will be committed)

	app/controllers/concerns/authentication.rb
	app/models/api_key.rb
	db/migrate/20160607111550_create_api_keys.rb
	spec/factories/api_keys.rb
	spec/models/api_key_spec.rb
	spec/requests/authentication_spec.rb

no changes added to commit (use "git add" and/or "git commit -a")

Stage them.

git add .

Commit the changes.

git commit -m "Set up client authentication"

Output

[master c50555f] Setup client authentication
 12 files changed, 177 insertions(+), 2 deletions(-)
 create mode 100644 app/controllers/concerns/authentication.rb
 create mode 100644 app/models/api_key.rb
 create mode 100644 db/migrate/20160607111550_create_api_keys.rb
 create mode 100644 spec/factories/api_keys.rb
 create mode 100644 spec/models/api_key_spec.rb
 create mode 100644 spec/requests/authentication_spec.rb

Push to GitHub.

git push origin master

21.8. Wrap Up

In this chapter, we implemented client authentication. With it, we will be able to ensure that no unauthorized client can use our code. Clients now need a valid API key to be able to communicate with Alexandria.