Chapter 23

User Authentication & Authorization

Now that we have a functional user system, it’s time to allow them to log in and do stuff! Since we are not making a stateless API, we could use cookies to store the session information. However, not all clients might have a way to deal with cookies, so let’s use access tokens instead.

Access tokens will be used to identify and authenticate users that went through the login process.

23.1. Implementing Access Tokens

The AccessToken model needs the following fields.

  • id
  • token_digest
  • user_id
  • api_key_id
  • accessed_at
  • created_at
  • updated_at

The token_digest column will contain the hash of the access token that will be exchanged between the server and the client to authenticate the user. Just like a password, we hash it before storing it in the database for security purposes. Each access token will be linked to a user and an API key. A user can therefore have multiple access tokens: one for the iOS application and one for the SPA application.

The accessed_at timestamp is just a nice-to-have feature to keep track of the last time the token was used. We could use this to expire tokens when they haven’t been used for a week, for example. In this case, we will actually expire all access tokens 15 days after their creation.

Let’s generate this model.

rails g model AccessToken token_digest:string user:references \
              api_key:references accessed_at:timestamp

Output

Running via Spring preloader in process 30890
      invoke  active_record
      create    db/migrate/TIMESTAMP_create_access_tokens.rb
      create    app/models/access_token.rb
      invoke    rspec
      create      spec/models/access_token_spec.rb
      invoke      factory_bot
      create        spec/factories/access_tokens.rb

Before we run the migrations, we need to add indexes on the token column in order to retrieve the access tokens faster when authenticating. We also want a composite index on the relationship columns (user_id and api_key_id).

# db/migrate/TIMESTAMP_create_access_tokens.rb
class CreateAccessTokens < ActiveRecord::Migration[5.2]
  def change
    create_table :access_tokens do |t|
      t.string :token_digest
      t.references :user, foreign_key: true
      t.references :api_key, foreign_key: true
      t.timestamp :accessed_at

      t.timestamps
    end

    add_index :access_tokens, [:user_id, :api_key_id], unique: true
  end
end

Run the migrations.

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

First, update the generated factory.

# spec/factories/access_tokens.rb
FactoryBot.define do
  factory :access_token do
    token_digest { nil }
    accessed_at { '2016-06-09 18:14:41' }
    user
    api_key
  end
end

Then, let’s write a few tests ensuring that a token is correctly generated and that the user and api_key are mandatory.

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

RSpec.describe AccessToken, :type => :model do
  let(:access_token) { create(:access_token) }

  it 'has a valid factory' do
   expect(build(:access_token)).to be_valid
  end

  it { should validate_presence_of(:user) }
  it { should validate_presence_of(:api_key) }

  describe '#authenticate' do
    context 'when valid' do
      it 'authenticates' do
        token = access_token.generate_token
        expect(access_token.authenticate(token)).to be true
      end
    end

    context 'when invalid' do
      it 'fails to authenticate' do
        access_token.generate_token
        expect(access_token.authenticate('fake')).to be false
      end
    end
  end

  describe '#expired?' do
    context 'when expired' do
      it 'returns true' do
        access_token.update_column(:created_at, 15.days.ago)
        expect(access_token.expired?).to be true
      end
    end

    context 'when not expired' do
      it 'returns false' do
        access_token.update_column(:created_at, 10.days.ago)
        expect(access_token.expired?).to be false
      end
    end
  end

  describe '#generate_token' do
    it 'generates an access token digest' do
      access_token.generate_token
      expect(access_token.token_digest).to_not be nil
    end

    it 'returns an access token' do
      token = access_token.generate_token
      expect(token).to_not be nil
    end
  end

end

Here is the AccessToken model with the relationships and validation rules defined. We also implemented a method to generate the token before validation.

# app/models/access_token.rb
class AccessToken < ApplicationRecord
  belongs_to :user
  belongs_to :api_key

  validates :user, presence: true
  validates :api_key, presence: true

  def authenticate(unencrypted_token)
    BCrypt::Password.new(token_digest).is_password?(unencrypted_token)
  end

  def expired?
    created_at + 14.days < Time.now
  end

  # We generate the token before hashing it and saving it.
  # We also return it so we can send it to the client.
  def generate_token
    token = SecureRandom.hex
    digest = BCrypt::Password.create(token, cost: BCrypt::Engine.cost)
    update_column(:token_digest, digest)
    token
  end
end

Run the tests.

rspec spec/models/access_token_spec.rb

Success (GREEN)

...

AccessToken
  has a valid factory
  should validate that :user cannot be empty/falsy
  should validate that :api_key cannot be empty/falsy
  #authenticate
    when valid
      authenticates
    when invalid
      fails to authenticate
  #expired?
    when expired
      returns true
    when not expired
      returns false
  #generate_token
    generates an access token digest
    returns an access token

Finished in 1.28 seconds (files took 4.11 seconds to load)
9 examples, 0 failures

Let’s add a presenter for access tokens as well.

touch app/presenters/access_token_presenter.rb
# app/presenters/access_token_presenter.rb
class AccessTokenPresenter < BasePresenter
  build_with    :id, :token, :user_id, :api_key_id,
                :accessed_at, :created_at, :updated_at
  related_to    :user
  sort_by       :id, :user_id, :api_key_id, :accessed_at,
                :created_at, :updated_at
  filter_by     :id, :user_id, :api_key_id, :accessed_at,
                :created_at, :updated_at

  def token
    @options[:token]
  end
end

We will hash the token as soon as we save the access token record, so we need a way to pass the token in clear to the AccessTokenPresenter instance. That’s why we are using @options in the token method above. The Serializer class and the presenters can already receive an options hash, now we just need to make the link between them.

Update the build_data method of the Serializer class by passing the @options variable to the current presenter.

# app/serializers/alexandria/serializer.rb
module Alexandria
  class Serializer

    def initialize # Hidden Code
    def to_json # Hidden Code

    private

    def build_data
      if @data.respond_to?(:count)
        @data.map do |entity|
          # Here
          presenter(entity).new(entity, @params, @options).build(@actions)
        end
      else
        # And here
        presenter(@data).new(@data, @params, @options).build(@actions)
      end
    end

    def presenter # Hidden Code
  end
end

Access tokens belong to users and API keys. Let’s add the relationships in those models and their presenters.

User model

# app/models/user.rb
class User < ApplicationRecord
  has_secure_password
  has_many :access_tokens

  # ...
end

UserPresenter

# app/presenters/user_presenter.rb
class UserPresenter < BasePresenter
  FIELDS = [:id, :email, :given_name, :family_name, :role, :last_logged_in_at,
           :confirmed_at, :confirmation_sent_at, :reset_password_sent_at,
           :created_at, :updated_at]

  related_to  :access_tokens
  sort_by     *FIELDS
  filter_by   *FIELDS
  build_with  *[FIELDS.push([:confirmation_token, :reset_password_token,
                             :confirmation_redirect_url,
                             :reset_password_redirect_url])].flatten
end

ApiKey model

We don’t have a presenter for the ApiKey model, so we only need to add the has_many relationship in the model.

# app/models/api_key.rb
class ApiKey < ApplicationRecord
  has_many :access_tokens

  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

Run all the tests to ensure that we didn’t break anything.

rspec

Success (GREEN)

...

Finished in 5.21 seconds (files took 1.19 seconds to load)
257 examples, 0 failures

It’s time to complete the lockdown!

23.2. Full Lockdown

We started it in Chapter 21, and now it’s time to complete it. Currently, we only authenticate clients. Let’s also identify and authenticate users!

Thanks to our preparation, it’s actually pretty easy. We only need to add three methods to the Authentication module.

The first method is authenticate_user and is needed, well, to authenticate the user. This method will return 401 Unauthorized unless the access token return a valid value.

def authenticate_user
  unauthorized!('User Realm') unless access_token
end

The access_token method will use the token sent in the Authorization header in order to try to find a matching access token in the database. We will use the user_id (sent with the access token) and the api_key to find the access token record before using the authenticate method to check the validity of the token. The current_user will tell us who the user making the request is. We will need this information to ensure they have access to the resource once we add authorization to the API.

def access_token
  @access_token ||= compute_access_token
end

def compute_access_token
  return nil if credentials['access_token'].blank?

  id, token = credentials['access_token'].split(':')
  user = id && token && User.find_by(id: id)
  access_token = user && api_key && AccessToken.find_by(user: user,
                                                        api_key: api_key)
  return nil unless access_token

  if access_token.expired?
    access_token.destroy
    return nil
  end

  return access_token if access_token.authenticate(token)
end

def current_user
  @current_user ||= access_token.try(:user)
end

Here is the updated Authentication module.

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

  AUTH_SCHEME = 'Alexandria-Token'

  included do
    before_action :validate_auth_scheme
    before_action :authenticate_client
  end

  private

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

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

  def authenticate_user
    unauthorized!('User Realm') unless access_token
  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

  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 access_token
    @access_token ||= compute_access_token
  end

  def compute_access_token
    return nil if credentials['access_token'].blank?

    id, token = credentials['access_token'].split(':')
    user = id && token && User.find_by(id: id)
    access_token = user && api_key && AccessToken.find_by(user: user,
                                                          api_key: api_key)
    return nil unless access_token

    if access_token.expired?
      access_token.destroy
      return nil
    end

    return access_token if access_token.authenticate(token)
  end

  def current_user
    @current_user ||= access_token.try(:user)
  end

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

end

Did you notice that we didn’t add a before_action calling authenticate_user? That’s because we don’t want it to be called for every resource. Listing books or publishers, for example, doesn’t require a logged-in user. Calling the authenticate_user method will be done in each controller depending on their needs.

Before we do that, how about a bit of refactoring? I find the Authentication module too big. To extract some of its logic, let’s create a new class: an Authenticator.

mkdir app/services && touch app/services/authenticator.rb

Here is the code for this new class. We just extracted the compute_api_key and compute_access_token methods.

# app/services/authenticator.rb
class Authenticator
  include ActiveSupport::SecurityUtils

  def initialize(authorization)
    @authorization = authorization
  end

  def 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 access_token
    return nil if credentials['access_token'].blank?

    id, token = credentials['access_token'].split(':')
    user = id && token && User.find_by(id: id)
    access_token = user && api_key && AccessToken.find_by(user: user,
                                                          api_key: api_key)
    return nil unless access_token

    if access_token.expired?
      access_token.destroy
      return nil
    end

    return access_token if access_token.authenticate(token)
  end

  private

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

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

end

Finally, we can update the Authentication module with our new class.

# 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

  private

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

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

  def authenticate_user
    unauthorized!('User Realm') unless access_token
  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

  def authenticator
    @authenticator ||= Authenticator.new(authorization_request)
  end

  def api_key
    @api_key ||= authenticator.api_key
  end

  def access_token
    @access_token ||= authenticator.access_token
  end

  def current_user
    @current_user ||= access_token.try(:user)
  end

end

Tadaa! Much cleaner.

23.3. AccessTokensController

The access_tokens controller will be responsible for logging users in and out. Creating a new access token will effectively log in a user while destroying it will end the user’s “session.”

Let’s generate files for the controller and its tests.

touch app/controllers/access_tokens_controller.rb \
      spec/requests/access_tokens_spec.rb

23.3.1. The create Action

When a client calls POST /api/access_tokens, it needs to send a user’s email and password. It then expects to receive an access token that can be used to access more resources.

Read through the tests below to understand the different contexts we will be testing.

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

RSpec.describe 'Access Tokens', type: :request do

  let(:john) { create(:user) }

  describe 'POST /api/access_tokens' do

    context 'with valid API key' do
      let(:key) { ApiKey.create }
      let(:headers) do
         { 'HTTP_AUTHORIZATION' =>
             "Alexandria-Token api_key=#{key.id}:#{key.key}" }
      end
      before { post '/api/access_tokens', params: params, headers: headers }

      context 'with existing user' do

        context 'with valid password' do
          let(:params) { { data: { email: john.email, password: 'password'  } } }

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

          it 'receives an access token' do
            expect(json_body['data']['token']).to_not be nil
          end

          it 'receives the user embedded' do
            expect(json_body['data']['user']['id']).to eq john.id
          end
        end

        context 'with invalid password' do
          let(:params) { { data: { email: john.email, password: 'fake'  } } }

          it 'returns 422 Unprocessable Entity' do
            expect(response.status).to eq 422
          end
        end
      end

      context 'with nonexistent user' do
        let(:params) { { data: { email: 'unknown', password: 'fake'  } } }

        it 'gets HTTP status 404 Not Found' do
          expect(response.status).to eq 404
        end
      end
    end

    context 'with invalid API key' do
      it 'returns HTTP status 401 Forbidden' do
        post '/api/access_tokens', params: {}
        expect(response.status).to eq 401
      end
    end
  end

end

Now, let’s add more routes for the access_tokens controller. To avoid duplication of information, we want to change the default route for the destroy action from…

DELETE /api/access_tokens/:id

to…

DELETE /api/access_tokens

Indeed, in order to access the destroy action, a valid access token will be needed; this means we already have it and there’s no need to include it in the URL.

# config/routes.rb
Rails.application.routes.draw do
  scope :api do
    resources :books, except: :put
    resources :authors, except: :put
    resources :publishers, except: :put
    resources :users, except: :put

    resources :user_confirmations, only: :show, param: :confirmation_token
    resources :password_resets, only: [:show, :create, :update], param: :reset_token

    resources :access_tokens, only: :create do
      delete '/', action: :destroy, on: :collection
    end

    get '/search/:text', to: 'search#index'
  end

  root to: 'books#index'
end

After creating a new access token, we will need to be able to pass it to the serializer that will give it to the appropriate presenter. To do this, we need to update the serialize method in the ApplicationController class.

# app/controllers/application_controller.rb
class ApplicationController < ActionController::API
  # Hidden Code

  protected

  def builder_error # Hidden Code
  def unprocessable_entity! # Hidden Code
  def orchestrate_query # Hidden Code

  def serialize(data, options = {})
    {
      json: Alexandria::Serializer.new(data: data,
                                       params: params,
                                       actions: [:fields, :embeds],
                                       options: options).to_json
    }
  end

  def resource_not_found # Hidden Code

end

Here’s the AccessTokensController class with the create action. The first thing we have to do here is try to get a user based on the email sent by the client. We use find_by! to raise a RecordNotFound error that will get caught by the rescue_from at the top of the file.

After that, it’s a pretty generic authentication, using the authenticate method that came with has_secure_password. Once the user has been authenticated, we check if a token already exists and delete it if that’s the case. We then create a new token, embed the user in it with the EmbedPicker, and finally return it as JSON.

If the given credentials are invalid, we return 422 Unprocessable Entity with a generic error message. This message is to avoid giving hints to people who are trying to hack into the system.

# app/controllers/access_tokens_controller.rb
class AccessTokensController < ApplicationController

  def create
    user = User.find_by!(email: login_params[:email])

    if user.authenticate(login_params[:password])
      AccessToken.find_by(user: user, api_key: api_key).try(:destroy)

      access_token = AccessToken.create(user: user, api_key: api_key)
      token = access_token.generate_token

      params[:embed] = if params[:embed].present?       
        params[:embed].prepend('user,')
      else
        'user'
      end
      render serialize(access_token, { token: token }).merge(status: :created)
    else
      render status: :unprocessable_entity,
             json: { error: { message: 'Invalid credentials.' } }
    end
  end

  private

  def login_params
    params.require(:data).permit(:email, :password)
  end

end

Let’s run the tests.

rspec spec/requests/access_tokens_spec.rb

Success (GREEN)

...

Access Tokens
  POST /api/access_tokens
    with valid API key
      with existing user
        with valid password
          gets HTTP status 201 Created
          receives an access token
          receives the user embedded
        with invalid password
          returns 422 Unprocessable Entity
      with nonexistent user
        gets HTTP status 404 Not Found
    with invalid API key
      returns HTTP status 401 Forbidden

Finished in 0.33674 seconds (files took 2.31 seconds to load)
6 examples, 0 failures

Awesome, they are passing. Let’s talk about the destroy action now.

23.3.2. The destroy Action

This action will be called when a user wants to log out of the application. All this action will do is delete the access token, meaning it cannot be used anymore.

Read through the tests below to understand all our expectations.

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

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

  let(:john) { create(:user) }

  describe 'POST /api/access_tokens' # Hidden Code

  describe 'DELETE /api/access_tokens' do

    context 'with valid API key' do
      let(:api_key) { ApiKey.create }
      let(:api_key_str) { "#{api_key.id}:#{api_key.key}" }

      before { delete '/api/access_tokens', headers: headers }

      context 'with valid access token' do
        let(:access_token) { create(:access_token, api_key: api_key, user: john) }
        let(:token) { access_token.generate_token }
        let(:token_str) { "#{john.id}:#{token}" }

        let(:headers) do
          token =
          { 'HTTP_AUTHORIZATION' =>
            "Alexandria-Token api_key=#{api_key_str}, access_token=#{token_str}" }
        end

        it 'returns 204 No Content' do
          expect(response.status).to eq 204
        end

        it 'destroys the access token' do
          expect(john.reload.access_tokens.size).to eq 0
        end
      end

      context 'with invalid access token' do
        let(:headers) do
           { 'HTTP_AUTHORIZATION' =>
              "Alexandria-Token api_key=#{api_key_str}, access_token=1:fake" }
        end

        it 'returns 401' do
          expect(response.status).to eq 401
        end
      end
    end

    context 'with invalid API key' do
      it 'returns HTTP status 401' do
        delete '/api/access_tokens', params: {}
        expect(response.status).to eq 401
      end
    end
  end # describe 'DELETE /api/access_tokens' end

end

Here is the implementation of the destroy action. It’s super simple, right? We are just calling access_token.destroy and returning 204 No Content. If you’re wondering about the access_token method, we implemented it in the Authentication module earlier and it’s just going to extract the token from the Authorization header.

# app/controllers/access_tokens_controller.rb
class AccessTokensController < ApplicationController
  before_action :authenticate_user, only: :destroy

  def create
    user = User.find_by!(email: login_params[:email])

    if user.authenticate(login_params[:password])
      AccessToken.find_by(user: user, api_key: api_key).try(:destroy)

      access_token = AccessToken.create(user: user, api_key: api_key)
      token = access_token.generate_token

      params[:embed] = if params[:embed].present?
        params[:embed].prepend('user,')
      else
        'user'
      end

      render serialize(access_token, { token: token }).merge(status: :created)
    else
      render status: :unprocessable_entity,
             json: { error: { message: 'Invalid credentials.' } }
    end
  end

  def destroy
    access_token.destroy
    render status: :no_content
  end

  private

  def login_params
    params.require(:data).permit(:email, :password)
  end

end

Run the tests.

rspec spec/requests/access_tokens_spec.rb

Success (GREEN)

...

Finished in 0.86329 seconds (files took 3.55 seconds to load)
10 examples, 0 failures

How about we try running all the tests? With our new implementation, some of them should be failing, right?

rspec

Success (GREEN)

...

Finished in 9.2 seconds (files took 4.83 seconds to load)
267 examples, 0 failures

Oops! They are still working. We need to use the before_action filter authenticate_user in each controller first!

23.3.3. Updating Our Controllers

Let’s update all the controllers and specify which actions require the user to be authenticated.

AuthorsController

Accessing the lists of authors or a specific author is available for non-logged in users. The rest of the actions requires a user, and more specifically an admin, as we will see in the next section.

# app/controllers/authors_controller.rb
class AuthorsController < ApplicationController
  before_action :authenticate_user, only: [:create, :update, :destroy]

  # Hidden Code

BooksController

This is the same as authors.

# app/controllers/books_controller.rb
class BooksController < ApplicationController
  before_action :authenticate_user, only: [:create, :update, :destroy]

  # Hidden Code

PublishersController

This is the same as authors, too.

# app/controllers/publishers_controller.rb
class PublishersController < ApplicationController
  before_action :authenticate_user, only: [:create, :update, :destroy]

  # Hidden Code

UsersController

For users, only the create action can be accessed without being a logged in user. Everything else requires a user.

# app/controllers/users_controller.rb
class UsersController < ApplicationController
  before_action :authenticate_user, only: [:index, :show, :update, :destroy]

  # Hidden Code

Now, we should have broken some stuff!

rspec

Failure (RED)

...

Finished in 5.28 seconds (files took 1.08 seconds to load)
267 examples, 55 failures

...

Awesome, looks like everything’s broken. Let’s fix it!

23.3.4. Fixing The Tests

Instead of copy/pasting some stubbing block again, we are going to create a shared context.

Create a new folder in spec named support. We are also going to need a file in this folder named skip_auth.rb.

touch spec/support/skip_auth.rb

This shared context will contain a block that will stub all our authentication methods. As we’ve discussed before, the proper way of doing this would be to re-write the tests to include more contexts. We’ll take the stubbing route to save some time.

# spec/support/skip_auth.rb
RSpec.shared_context 'Skip Auth' do
  before do
    allow_any_instance_of(ApplicationController).to(
      receive(:validate_auth_scheme).and_return(true))
    allow_any_instance_of(ApplicationController).to(
      receive(:authenticate_client).and_return(true))
    allow_any_instance_of(ApplicationController).to(
      receive(:authenticate_user).and_return(true))
  end
end

We then have to include this context in all the spec files that need it. You can remove the before block shown below from all the test files.

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

Books Tests

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

RSpec.describe 'Books', type: :request do
  include_context 'Skip Auth'

  # Hidden Code

Authors Tests

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

RSpec.describe 'Authors', type: :request do
  include_context 'Skip Auth'

  # Hidden Code

Publishers Tests

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

RSpec.describe 'Publishers', type: :request do
  include_context 'Skip Auth'

  # Hidden Code

Users Tests

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

RSpec.describe 'Users', type: :request do
  include_context 'Skip Auth'

  # Hidden Code

If we run the tests again, everything is working fine. Awesome!

rspec

Success (GREEN)

...

Finished in 5.41 seconds (files took 0.98325 seconds to load)
267 examples, 0 failures

23.4. Authorizing Users

We can now identify and authenticate users. The last step to protect our resources is to implement authorization. Authorization answers the question, “Can this particular user really perform this action?”

We are not going to do it from scratch, however. There is a gem out there that is simple, clean, and only uses PORO (Plain-Old Ruby Objects) to define rules: Pundit.

Let’s add the gem to our Gemfile.

# Gemfile
source 'https://rubygems.org'
git_source(:github) { |repo| "https://github.com/#{repo}.git" }

ruby '2.5.0'

gem 'rails', '5.2.0'
gem 'pg'
gem 'puma', '~> 3.11'
gem 'bootsnap', '>= 1.1.0', require: false
gem 'carrierwave'
gem 'carrierwave-base64'
gem 'pg_search'
gem 'kaminari'
gem 'bcrypt', '~> 3.1.7'
gem 'pundit'

# Hidden Code

Get it installed with bundle.

bundle install

And use the pundit generator to create the policies folder and the ApplicationPolicy.

rails g pundit:install

Output

Running via Spring preloader in process 26249
      create  app/policies/application_policy.rb

Let’s talk a bit about the way pundit works. For each model, we are going to create a policy that will define the requirements for the CRUD actions (index, show, create, update, destroy).

Below is the beginning of the ApplicationPolicy generated by pundit. The initialize method receives two arguments: the user and the record to check permissions against.

# app/policies/application_policy.rb
class ApplicationPolicy
  attr_reader :user, :record

  def initialize(user, record)
    @user = user
    @record = record
  end

  def index?
    false
  end

  # Hidden Code
end

Pundit will also provide a method to use in controllers known as authorize. This method only takes one argument - the record to check permissions against. To get the user, this method expects a current_user to be defined somewhere in your application.

The authorize method will then instantiate the appropriate policy class, inferring the class name from the model passed. It will then call the method matching the name of the action where authorize was called.

For example, if I were to check if a user can update a book, I would use:

def update
  @book = Book.find(params[:id])
  authorize @book
  # Hidden Code
end

Which will end up calling this:

BookPolicy.new(current_user, book).update?

Depending on the code we put in the update? method, the user might, or might not, be able to update the given book. Let’s define the policies for our models now.

To get that out of the way, generate all the policy files we will need.

touch app/policies/access_token_policy.rb \
      app/policies/author_policy.rb \
      app/policies/book_policy.rb \
      app/policies/publisher_policy.rb \
      app/policies/author_policy.rb \
      app/policies/user_policy.rb

Here is the policy for the access tokens. As you can see, creating access tokens is always allowed, even without a user. However, deleting an access token is only allowed for the user to whom it belongs.

# app/policies/access_token_policy.rb
class AccessTokenPolicy < ApplicationPolicy

  def create?
    true
  end

  def destroy?
    @user == @record.user
  end

end

For authors, publishers and books, anyone can access index and show. However, users need to be admin in order to create, update or delete books. We defined the role attribute on users in the previous chapter, so we can simply use this and call user.admin? to check if a user is an admin or not.

# app/policies/author_policy.rb
class AuthorPolicy < ApplicationPolicy

  def index?
    true
  end

  def show?
    true
  end

  def create?
    user.admin?
  end

  def update?
    user.admin?
  end

  def destroy?
    user.admin?
  end

end
# app/policies/book_policy.rb
class BookPolicy < ApplicationPolicy

  def index?
    true
  end

  def show?
    true
  end

  def create?
    user.admin?
  end

  def update?
    user.admin?
  end

  def destroy?
    user.admin?
  end

end
# app/policies/publisher_policy.rb
class PublisherPolicy < ApplicationPolicy

  def index?
    true
  end

  def show?
    true
  end

  def create?
    user.admin?
  end

  def update?
    user.admin?
  end

  def destroy?
    user.admin?
  end

end

Finally, the User policy is the most complex one. Most of the actions can either be performed by admin users, or if the record being accessed is the current user. Creating is the exception since anyone can do it.

# app/policies/user_policy.rb
class UserPolicy < ApplicationPolicy

  def index?
    user.admin?
  end

  def show?
    user.admin? || user == record
  end

  def create?
    true
  end

  def update?
    user.admin? || user == record
  end

  def destroy?
    user.admin? || user == record
  end

end

With our policies ready, it’s time to update the ApplicationController class to use them and enforce the rules. To avoid polluting the controller too much, let’s create a new module and call it Authorization.

touch app/controllers/concerns/authorization.rb

First, we need to make sure that the Pundit module is included. Then, we need to ensure that we rescue from Pundit::NotAuthorizedError by returning 403 to the client. Finally, we use after_action :verify_authorized, which is a filter method provided by Pundit. This will raise an exception if an action has not checked the permissions.

# app/controllers/concerns/authorization.rb
module Authorization
  extend ActiveSupport::Concern
  include Pundit

  included do
    rescue_from Pundit::NotAuthorizedError, with: :forbidden
    after_action :verify_authorized
  end

  def authorize_actions
    if action_name == 'index'
      return authorize(controller_name.classify.constantize)
    end

    authorize resource
  end

  def forbidden
    render(status: 403)
  end

end

Don’t forget to include this new module in the ApplicationController file.

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

  # Hidden Code

Now, we need to start doing some authorization in our controllers. Indeed, if you run the tests now, you will end up with many of them failing because Pundit is raising the Pundit::AuthorizationNotPerformedError exception.

rspec
...

Finished in 5.73 seconds (files took 1.14 seconds to load)
267 examples, 128 failures

23.4.1. Updating Controllers

First, let’s take care of the controllers that don’t need authorization: the password_resets controller and the search controller.

PasswordResetsController

# app/controllers/password_resets_controller.rb
class PasswordResetsController < ApplicationController
  skip_before_action :validate_auth_scheme, only: :show
  skip_before_action :authenticate_client, only: :show
  before_action :skip_authorization

  # Hidden Code

SearchController

# app/controllers/search_controller.rb
class SearchController < ApplicationController
  before_action :skip_authorization

  # Hidden Code

Next up, the access_tokens controller. For this one, we are going to add authorize(access_token) in the create and destroy actions. If we cannot authenticate the user, we also need to call skip_authorization to prevent Pundit from raising its exception.

AccessTokensController

# app/controllers/access_tokens_controller.rb
class AccessTokensController < ApplicationController
  before_action :authenticate_user, only: :destroy

  def create
    skip_authorization # We add this
    user = User.find_by!(email: login_params[:email])

    if user.authenticate(login_params[:password])
      AccessToken.find_by(user: user, api_key: api_key).try(:destroy)

      access_token = AccessToken.create(user: user, api_key: api_key)
      token = access_token.generate_token

      params[:embed] = if params[:embed].present?
        params[:embed].prepend('user,')
      else
        'user'
      end

      render serialize(access_token, { token: token }).merge(status: :created)
    else
      render status: :unprocessable_entity,
             json: { error: { message: 'Invalid credentials.' } }
    end
  end

  def destroy
    authorize(access_token) # And this
    access_token.destroy
    render status: :no_content
  end

  private

  def login_params
    params.require(:data).permit(:email, :password)
  end

end

For the rest of our controllers (authors, publishers, books and users), we can just use the authorize_actions method we created in the Authorization module.

AuthorsController

# app/controllers/authors_controller.rb
class AuthorsController < ApplicationController
  before_action :authenticate_user, only: [:create, :update, :destroy]
  before_action :authorize_actions

  # Hidden Code

PublishersController

# app/controllers/publishers_controller.rb
class PublishersController < ApplicationController
  before_action :authenticate_user, only: [:create, :update, :destroy]
  before_action :authorize_actions

  # Hidden Code

BooksController

# app/controllers/books_controller.rb
class BooksController < ApplicationController
  before_action :authenticate_user, only: [:create, :update, :destroy]
  before_action :authorize_actions

  # Hidden Code

UsersController

# app/controllers/users_controller.rb
class UsersController < ApplicationController
  before_action :authenticate_user, only: [:index, :show, :update, :destroy]
  before_action :authorize_actions

  # Hidden Code

Try running the tests to see how things are now.

rspec

While some tests have been fixed by adding that, a lot of them are still failing.

Failure (RED)

...

Finished in 5.1 seconds (files took 0.97459 seconds to load)
267 examples, 57 failures

...

23.4.2. Fixing The Tests

We are now going to fix all the failing tests in a few steps. First, we’re going to stub the user authentication and make it return an admin user. That means we won’t be blocked by anything related to authentication or authorization.

Begin by adding a new factory to build admins.

# spec/factories/users.rb
FactoryBot.define do
  factory :user do
    # Hidden Code
  end

  factory :admin, class: User do
    email { 'admin@example.com' }
    password { 'password' }
    given_name { 'Super' }
    family_name { 'Admin' }
    confirmed_at { Time.now }
    role { :admin }
  end
end

The second step involves updating the skip_auth.rb file that we used to skip client authentication. Let’s update it to skip user authentication and authorization as well.

# spec/support/skip_auth.rb
RSpec.shared_context 'Skip Auth' do
  let(:admin_user) { create(:admin) }
  let(:api_key) { ApiKey.first || create(:api_key) }
  let(:access_token) { create(:access_token, user: admin_user, api_key: api_key) }

  before do
    allow_any_instance_of(ApplicationController).to(
      receive(:validate_auth_scheme).and_return(true))
    allow_any_instance_of(ApplicationController).to(
      receive(:authenticate_client).and_return(true))
    allow_any_instance_of(ApplicationController).to(
      receive(:authenticate_user).and_return(true))
    allow_any_instance_of(ApplicationController).to(
      receive(:access_token).and_return(access_token))
    allow_any_instance_of(ApplicationController).to(
      receive(:current_user).and_return(admin_user))
  end
end

With that in place, our tests should be passing without any issue.

rspec
...

Finished in 6.83 seconds (files took 1.12 seconds to load)
267 examples, 0 failures

Some of the tests that you wrote in the exercises of the previous chapters might be failing now. Try to fix them ;).

23.5. Testing Our Policies

We are currently not testing the authentication/authorization we implemented; instead, we skip it with some stubbed methods. We did write some tests checking the authentication in the tests for the access_tokens controller, but not for authorization. That’s because we will rely on testing the policies to ensure permissions are followed.

Create a new folder and the files for all our policies tests with the command below.

mkdir spec/policies && \
  touch spec/policies/book_policy_spec.rb \
        spec/policies/publisher_policy_spec.rb \
        spec/policies/author_policy_spec.rb \
        spec/policies/user_policy_spec.rb \
        spec/policies/access_token_policy_spec.rb

We need to require pundit/rspec to make it easier to write our tests. Add it to the spec/rails_helper.rb file.

# spec/rails_helper.rb
require 'spec_helper'
ENV['RAILS_ENV'] ||= 'test'
require File.expand_path('../../config/environment', __FILE__)
abort("The Rails environment is running in production mode!") if Rails.env.production?

# Require RSpec for Rails, webmock and database_cleaner
require 'rspec/rails'
require 'webmock/rspec'
require 'database_cleaner'
require "pundit/rspec"

# Hidden Code

Here are the tests for the book policy. Thanks to the helpers provided by Pundit, writing those tests was pretty straightforward.

# spec/policies/book_policy_spec.rb
require 'rails_helper'

describe BookPolicy do
  subject { described_class }

  permissions :index?, :show? do
    it 'grants access' do
      expect(subject).to permit(nil, Book.new)
    end
  end

  permissions :create?, :update?, :destroy? do
    it 'denies access if user is not admin' do
      expect(subject).not_to permit(build(:user), Book.new)
    end

    it 'grants access if user is admin' do
      expect(subject).to permit(build(:admin), Book.new)
    end
  end
end
rspec spec/policies/book_policy_spec.rb
...

BookPolicy
  index? and show?
    grants access
  create?, update?, and destroy?
    denies access if user is not admin
    grants access if user is admin

Finished in 0.13231 seconds (files took 2.95 seconds to load)
3 examples, 0 failures

Good, it seems our policy follows our expectations.

Want to hear the good news? Writing the tests for the remaining policies will be one of the exercises for this chapter. Good luck!

23.6. Adding Some Integration Tests

To complete everything we’ve done during this chapter, let’s write an integration test that will go through everything, from logging users in to logging them out after having requested some data.

Create a new file in the features folder.

touch spec/features/user_auth_flow_spec.rb

Here are the tests. The flow should be pretty clear with the comments.

# spec/features/user_auth_flow_spec.rb
require 'rails_helper'

RSpec.describe 'User Auth Flow', type: :request do

  def headers(user_id = nil, token = nil)
    api_key_str = "#{api_key.id}:#{api_key.key}"
    if user_id && token
      token_str = "#{user_id}:#{token}"
      { 'HTTP_AUTHORIZATION' =>
          "Alexandria-Token api_key=#{api_key_str}, access_token=#{token_str}" }
    else
      { 'HTTP_AUTHORIZATION' =>
          "Alexandria-Token api_key=#{api_key.id}:#{api_key.key}" }
    end
  end

  let(:api_key) { ApiKey.create }
  let(:email) { 'john@gmail.com' }
  let(:password) { 'password' }
  let(:params) { { email: email, password: password, given_name: 'Johnny'  } }

  it 'authenticate a new user' do
    # Step 1 - Create a user
    post '/api/users', params: { data: params }, headers: headers
    expect(response.status).to eq 201
    id = json_body['data']['id']

    # Step 2 - Try to update given_name
    patch "/api/users/#{id}",
          params: { data: { given_name: 'John' } },
          headers: headers
    expect(response.status).to eq 401

    # Step 3 - Login
    post '/api/access_tokens',
          params: { data: { email: email, password: 'password' } },
          headers: headers
    expect(response.status).to eq 201
    expect(json_body['data']['token']).to_not be nil
    expect(json_body['data']['user']['email']).to eq email
    token = json_body['data']['token']
    user_id = json_body['data']['user']['id']

    # Step 4 - Update given_name
    patch "/api/users/#{id}",
          params: { data: { given_name: 'John' } },
          headers: headers(user_id, token)
    expect(response.status).to eq 200
    expect(json_body['data']['given_name']).to eq 'John'

    # Step 5 - Try to list all users
    get '/api/users', headers: headers(user_id, token)
    expect(response.status).to eq 403

    # Step 6 - Logout
    delete '/api/access_tokens', headers: headers(user_id, token)
    expect(response.status).to eq 204

    # Step 7 - Try to access user info with invalid token
    get "/api/users/#{id}", headers: headers(user_id, token)
    expect(response.status).to eq 401
  end

end
rspec spec/features/user_auth_flow_spec.rb
...

User Auth Flow
  authenticate a new user

Finished in 0.58597 seconds (files took 2.46 seconds to load)
1 example, 0 failures

Great, it seems everything is working well together!

23.7. Exercises

Here are a few additional exercises from this chapter for you to complete on your own.

1. Fix the tests, if some are still failing

If any of your tests is still failing, it’s time to fix it.

2. Write tests for policies

To ensure that we set the correct permissions, write tests for all the policy classes.

  • AccessTokenPolicy
  • AuthorPolicy
  • PublisherPolicy
  • UserPolicy

23.8. Pushing Our Changes

Run all the tests to ensure that everything is working.

rspec

Success (GREEN)

...

Finished in 7.5 seconds (files took 1.03 seconds to load)
286 examples, 0 failures

Let’s push the changes.

git status

Output

On branch user-auth
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:   Gemfile
	modified:   Gemfile.lock
	modified:   app/controllers/application_controller.rb
	modified:   app/controllers/authors_controller.rb
	modified:   app/controllers/books_controller.rb
	modified:   app/controllers/concerns/authentication.rb
	modified:   app/controllers/password_resets_controller.rb
	modified:   app/controllers/publishers_controller.rb
	modified:   app/controllers/search_controller.rb
	modified:   app/controllers/users_controller.rb
	modified:   app/models/api_key.rb
	modified:   app/models/user.rb
	modified:   app/presenters/user_presenter.rb
	modified:   app/serializers/alexandria/serializer.rb
	modified:   config/routes.rb
	modified:   db/schema.rb
	modified:   spec/factories/users.rb
	modified:   spec/rails_helper.rb
	modified:   spec/requests/authors_spec.rb
	modified:   spec/requests/books_spec.rb
	modified:   spec/requests/publishers_spec.rb
	modified:   spec/requests/users_spec.rb

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

	app/controllers/access_tokens_controller.rb
	app/controllers/concerns/authorization.rb
	app/models/access_token.rb
	app/policies/
	app/presenters/access_token_presenter.rb
	app/services/
	db/migrate/20160709070329_create_access_tokens.rb
	spec/factories/access_tokens.rb
	spec/features/user_auth_flow_spec.rb
	spec/models/access_token_spec.rb
	spec/policies/
	spec/requests/access_tokens_spec.rb
	spec/support/skip_auth.rb

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

Stage them.

git add .

Commit the changes.

git commit -m "Implement user authentication and authorization"

Push to GitHub.

git push origin master

23.9. Wrap Up

In this chapter, we implemented authentication and authorization. Those are two complex subjects which explains the length of this chapter. However, now that it’s done, we are getting very close to the end of this module. In the next chapter, we will implement payments to make our e-commerce application complete.