Duetcode

Integrate Doorkeeper gem

06 September 2020

In the previous section, we talked about how to adjust CORS settings for API-only rails applications. This week, we will introduce doorkeeper gem that makes it easy to get the OAuth 2 provider functionality.

What is OAuth?

Oauth is an authorization specification that is widely used in web applications. Programming languages have different libraries that implement the OAuth 2.0 specification and provide an abstraction to use without knowing all the implementation details. We will use the doorkeeper gem for the bookmarker application.

To understand how OAuth works and how we’re going to use it, you need to read the blog post from Alex Bilbie. Keep in mind that we will use the resource owner credentials grant in the bookmarker application and make sure you understand this part before continue to read the chapter. As a difference from the blog post explanation, we will not have a specific trusted client application since we will not serve to any other external client.

Install Doorkeeper gem

Let’s start with adding doorkeeper gem to Gemfile and run bundle install command.

Gemfile
# ...

# Rails engine to introduce OAuth 2 provider functionality
gem 'doorkeeper', '~> 5.4'

# ...

Then we need to execute the following commands to install the doorkeeper gem and generate corresponding migration files. (Do not forget to run rubocop --auto-correct for generated files.)

~/code/bookmarker (master) rails g doorkeeper:install

  create  config/initializers/doorkeeper.rb
  create  config/locales/doorkeeper.en.yml
  route   use_doorkeeper

  ===============================================================================

  There is a setup that you need to do before you can use the doorkeeper.

  Step 1.
  Go to config/initializers/doorkeeper.rb and configure
  resource_owner_authenticator block.

  Step 2.
  Choose the ORM:

  If you want to use ActiveRecord run:

    rails generate doorkeeper:migration

  And run

    rake db:migrate

  Step 3.
  That's it, that's all. Enjoy!

  ===============================================================================

Let’s do step by step what the doorkeeper gem suggests to complete integration. First, we need to modify the doorkeeper initializer file to have resource_owner_from_credentials (also called password) grant.

config/initializers/doorkeeper.rb
Doorkeeper.configure do
  orm :active_record

  resource_owner_from_credentials do |_routes|
    User.authenticate(params[:email], params[:password])
  end

  api_only

  grant_flows %w[password]

  # ...
end
  • No need to change the ORM part since we’re going to use activerecord as an ORM.
  • We commented resource_owner_authenticator code block since we will use resource_owner_from_credentials grant. Inside the block, we called the authenticate class method on the User model to authenticate our users. (We will implement the authenticate method later in the chapter.)
  • We also specified the api_only flag because it will skip all views management and change how the doorkeeper responds to requests.
  • Lastly, we set grant_flows as the password.

I would also like to introduce oauth scopes when we build premium accounts for the bookmarker app, but we don’t need it until then since there will be only one type of user.

As a second step, we will run the migration generator for the doorkeeper gem.

~/code/bookmarker (master) rails generate doorkeeper:migration

  create  db/migrate/20200831144546_create_doorkeeper_tables.rb

Before running the migration that is created by the doorkeeper, I would like to remove ouath_applications and oauth_access_grants tables from the migration and also their corresponding foreign keys and indexes. We delete them because we will not expose our API to the external clients as I mentioned above, and therefore we don’t need to differentiate which application we’re using. (If you would like to publish your API to external developers and ask them to generate their oauth applications to use your API, then you need to keep those tables.)

Apart from removing those tables, we need to remove null: false constraint from application reference because of the reason we just mentioned. We also don’t need to create foreign key for application_id column in oauth_access_tokens and oauth_applications tables. So applying those changes, the migration should look like this:

db/migrate/20200904092357_create_doorkeeper_tables.rb
# frozen_string_literal: true

class CreateDoorkeeperTables < ActiveRecord::Migration[6.0]
  def change
    create_table :oauth_access_tokens do |t|
      t.references :resource_owner, index: true
      t.references :application

      t.string :token, null: false
      t.string   :refresh_token
      t.integer  :expires_in
      t.datetime :revoked_at
      t.datetime :created_at, null: false
      t.string   :scopes

      t.string   :previous_refresh_token, null: false, default: ''
    end

    add_index :oauth_access_tokens, :token, unique: true
    add_index :oauth_access_tokens, :refresh_token, unique: true
  end
end

Then we can run the rails db:migrate command to create oauth_access_tokens table in our database.

You need to run rubocop --auto-correct again to fix rubocop offenses that are caused by the doorkeeper migration file. It’s also better to exclude db/migrations folder and db/schema.rb file from rubocop rules since they are not going to be the places that we heavily write ruby code. Besides that, it’s better to be pragmatic here not to solve all the offenses from external party migrations and generated schema code.

.rubocop.yml
inherit_from: .rubocop_todo.yml

# ...

AllCops:
  Exclude:
    - 'db/migrate/**/*'
    - 'db/schema.rb'

Apart from the previous configurations, since we will not use applications and authorization flows from the doorkeeper, it’s better to disable them on the routes.

config/routes.rb
# ...

  scope 'api/v1' do
    use_doorkeeper do
      skip_controllers :authorizations, :applications, :authorized_applications
    end
  end

  # ...

In the first step of doorkeeper suggestions, we mentioned that we would use the authenticate method from the User model to authenticate our users. Let’s create an authenticate method in the User model and use it from the doorkeeper to find authenticated resources. Add authenticate method to app/models/users.rb file as the following.

app/models/user.rb
class User < ApplicationRecord
  # ...

  class << self
    def authenticate(email, password)
      user = User.find_for_authentication(email: email)
      user.try(:valid_password?, password) ? user : nil
    end
  end
end

We’re basically using find_for_authentication method from devise to authenticate user. If we can’t authenticate the user, we’re returning nil.

We can now send a request to get an OAuth token for the user that we created with the user creation endpoint.

curl --location --request POST 'http://localhost:3000/api/v1/oauth/token' \
--header 'Content-Type: application/json' \
--data-raw '{
  "grant_type": "password",
  "email": "user@duetcode.io",
  "password": "samplepassword"
}'

And the response should be like this one:

{
  "access_token": "lZNpopiWx_gbRCSuaRIKfDo9XOQPjTJQAEgsZBPST6I",
  "token_type": "Bearer",
  "expires_in": 7200,
  "created_at": 1592051957
}

(I suggested to have the generic structure for API payloads in make API payloads generic chapter, but I would like to break this rule for doorkeeper related endpoints. We can have the same structure, but I don’t think it’s worth to do so many workarounds without getting a huge benefit.)

Swagger documentation

Now we completed the functionality of the token creation endpoint. We can continue by documenting it with the swagger. Let’s create OauthController, OauthTokenInput and OauthToken classes.

app/controllers/swagger/controllers/oauth_controller.rb
# frozen_string_literal: true

class Swagger::Controllers::OauthController
  include Swagger::Blocks

  swagger_path '/oauth/token' do
    operation :post do
      key :description, 'Creates a new token from user credentials'
      key :tags, [
        'oauth'
      ]

      parameter do
        key :name, :user_credentials
        key :in, :body
        key :description, 'Email and password information of the new user with grant type.'
        key :required, true
        schema do
          key :'$ref', :OauthTokenInput
        end
      end

      response 201 do
        key :description, 'Token created'
        schema do
          key :'$ref', :OauthToken
        end
      end

      response 400 do
        key :description, 'Bad Request'
        schema do
          key :type, :object

          property :error do
            key :type, :string
          end

          property :error_description do
            key :type, :string
          end
        end
      end
    end
  end
end
app/controllers/swagger/models/oauth_token_input.rb
# frozen_string_literal: true

module Swagger::Models::OauthTokenInput
  include Swagger::Blocks

  swagger_schema :OauthTokenInput do
    key :type, :object
    key :required, %i[email password grant_type]

    property :email do
      key :type, :string
    end

    property :password do
      key :type, :string
    end

    property :grant_type do
      key :type, :string
    end
  end
end
app/controllers/swagger/models/oauth_token.rb
# frozen_string_literal: true

module Swagger::Models::OauthToken
  include Swagger::Blocks

  swagger_schema :OauthToken do
    key :type, :object
    key :required, %i[access_token token_type expires_in created_at]

    property :access_token do
      key :type, :string
    end

    property :token_type do
      key :type, :string
    end

    property :expires_in do
      key :type, :integer
    end

    property :created_at do
      key :type, :string
      key :format, 'date-time'
    end
  end
end

Let’s add those three classes into the ApidocsController class since we generate swagger documentation from this file.

app/controllers/apidocs_controller.rb
# frozen_string_literal: true

class ApidocsController < ActionController::Base
  # ...

  # A list of all classes that have swagger_* declarations.
  SWAGGERED_CLASSES = [
    # ...
    # ...
    Swagger::Models::OauthTokenController,
    Swagger::Models::OauthTokenInput,
    Swagger::Models::OauthToken,
    self
  ].freeze

  # ...
end

If you enter to the http:/localhost:3000/swagger you should see the api/v1/oauth/token endpoint documentation.

Complete User model tests

And as the last step, we need to write a test for the authenticate method on the User model.

spec/models/user_spec.rb
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe User, type: :model do
  # ...

  describe '#authenticate' do
    let(:user) do
      create(:user, email: 'user@duetcode.io', password: 'sample')
    end

    it 'returns user when the credentials are correct' do
      expect(User.authenticate(user.email, user.password)).to eq(user)
    end

    it 'returns nil when the credentials are not correct' do
      expect(User.authenticate(user.email, 'wrong')).to be_nil
    end
  end
end

Let’s run all the tests again to see everything is still working as expected as well as new changes.

~/code/bookmarker (master) $ bundle exec rspec
............

Finished in 0.16506 seconds (files took 0.85801 seconds to load)
12 examples, 0 failures

UPDATE: Added one more commit after publishing the chapter to fix OauthTokenController class and file names for swagger documentation. Make sure you also get those changes here.

Summary

This chapter, we integrated the OAuth 2.0 authorization framework into the bookmarker application by using the doorkeeper gem. From now on, the clients can request a new access token whenever they want to log any user in. We also provided swagger documentation for api/v1/oauth/token endpoint. You can find the source code of bookmarker application on github. You can also find the previous chapter here.

Thank you for reading! If you want to be notified when I publish a new chapter, you can follow me on twitter.