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.
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.
Let’s start with adding doorkeeper gem to Gemfile and run bundle install command.
# ...
# 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.
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
activerecord as an ORM.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.)api_only flag because it will skip all views management and change how the doorkeeper responds to requests.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.rbBefore 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:
# 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.
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.
# ...
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.
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.)
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.
# 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
# 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
# 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.
# 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.
And as the last step, we need to write a test for the authenticate method on the User model.
# 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 failuresUPDATE: 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.
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.