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.
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!
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.
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?
We have three options to make our tests pass.
unauthenticated
and authenticated
contexts for each resource.
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.
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.
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.
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.
Let’s use the second option and update all our request 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
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
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
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
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.
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.
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
...
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
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.