Getting Started with Doorkeeper and OAuth 2.0

Share this article

Bellboy thin line icon for web and mobile minimalistic flat design. Vector white icon inside the red circle

In my previous series I showed how to set up a custom OAuth 2 provider using oPRO, a Rails engine. Today we are going to solve the same problem, but this time using another, more popular tool – Doorkeeper gem, created by Applicake in 2011. Since then, the project has greatly evolved and now presents a full-fledged and convenient solution. Doorkeeper can be used with basic Rails applications as well as with Grape.

In this article I will show you how to build your own OAuth 2 provider and secure API with the help of Doorkeeper. We will do basic preparations, integrate Doorkeeper, customize it a bit, and introduce scopes. In the second part of this series we’ll discuss more advanced things like customizing views, using refresh tokens, crafting a custom OmniAuth provider and securing Doorkeeper default routes.

The source code for the client and server applications can be found on GitHub.

Creating the Apps

I am going to use Rails 4.2 for this demo. We’ll create two applications: the actual provider (let’s call it “server”) and an app for testing purposes (“client”). Start with the server:

$ rails new Keepa -T

We’ll need some kind of authentication for this app, but Doorkeeper does not dictate which one to use. I’ve covered plenty of options recently, but, today, let’s code our own simple solution using bcrypt.

Gemfile

[...]
gem 'bcrypt-ruby'
[...]

Install the gem, generate and apply a new migration:

$ bundle install
$ rails g model User email:string:index password_digest:string
$ rake db:migrate

Now equip the User model with bcrypt’s functionality and validation logic:

models/user.rb

[...]
has_secure_password
validates :email, presence: true
[...]

Create a new controller for registration:

users_controller.rb

class UsersController < ApplicationController
  def new
    @user = User.new
  end

  def create
    @user = User.new(user_params)
    if @user.save
      session[:user_id] = @user.id
      flash[:success] = "Welcome!"
      redirect_to root_path
    else
      render :new
    end
  end

  private

  def user_params
    params.require(:user).permit(:email, :password, :password_confirmation)
  end
end

Here is the corresponding view:

views/users/new.html.erb

<h1>Register</h1>

<%= form_for @user do |f| %>
  <%= render 'shared/errors', object: @user %>
  <div>
    <%= f.label :email %>
    <%= f.email_field :email %>
  </div>

  <div>
    <%= f.label :password %>
    <%= f.password_field :password %>
  </div>

  <div>
    <%= f.label :password_confirmation %>
    <%= f.password_field :password_confirmation %>
  </div>

  <%= f.submit %>
<% end %>

<%= link_to 'Log In', new_session_path %>

views/shared/_errors.html.erb

<% if object.errors.any? %>
  <div>
    <h5>Some errors were found:</h5>

    <ul>
      <% object.errors.full_messages.each do |msg| %>
        <li><%= msg %></li>
      <% end %>
    </ul>
  </div>
<% end %>

Add some routes:

config/routes.rb

[...]
resources :users, only: [:new, :create]
[...]

To check whether a user is logged in or not, we’ll use a good, old current_user method:

application_controller.rb

[...]
private

def current_user
  @current_user ||= User.find(session[:user_id]) if session[:user_id]
end

helper_method :current_user
[...]

A separate controller to manage user sessions is also required:

sessions_controller.rb

class SessionsController < ApplicationController
  def new
  end

  def create
    @user = User.find_by(email: params[:email])
    if @user && @user.authenticate(params[:password])
      session[:user_id] = @user.id
      flash[:success] = "Welcome back!"
      redirect_to root_path
    else
      flash[:warning] = "You have entered incorrect email and/or password."
      render :new
    end
  end

  def destroy
    session.delete(:user_id)
    redirect_to root_path
  end
end

authenticate is a method provided by bcrypt that checks whether a correct password was entered.

Here are the views and routes:

views/sessions/new.html.erb

<h1>Log In</h1>

<%= form_tag sessions_path, method: :post do %>
  <div>
    <%= label_tag :email %>
    <%= email_field_tag :email %>
  </div>

  <div>
    <%= label_tag :password %>
    <%= password_field_tag :password %>
  </div>

  <%= submit_tag 'Log In!' %>
<% end %>

<%= link_to 'Register', new_user_path %>

config/routes.rb

[...]
resources :sessions, only: [:new, :create]
delete '/logout', to: 'sessions#destroy', as: :logout
[...]

Lastly, add a static pages controller, root page, and the route:

pages_controller.rb

class PagesController < ApplicationController
  def index
  end
end

views/pages/index.html.erb

<% if current_user %>
  You are logged in as <%= current_user.email %><br>
  <%= link_to 'Log Out', logout_path, method: :delete %>
<% else %>
  <%= link_to 'Log In', new_session_path %><br>
  <%= link_to 'Register', new_user_path %>
<% end %>

config/routes.rb

[...]
root to: 'pages#index'
[...]

Everything should be familiar so far. The server app is now ready and we can start integrating Doorkeeper.

Integrating Doorkeeper

Add a new gem into the Gemfile:

Gemfile

[...]
gem 'doorkeeper'
[...]

Install it and run Doorkeeper’s generator:

$ bundle install
$ rails generate doorkeeper:install

This generator will create a new initializer file and add use_doorkeeper line into routes.rb. This line provides a handful of Doorkeeper’s routes (to register a new OAuth 2, request access token, etc.) that we’ll discuss later.

The next step is to generate migrations. By default Doorkeeper uses ActiveRecord, but you can use doorkeeper-mongodb for Mongo support.

$ rails generate doorkeeper:migration

You may add a foreign key as described here, but I’ll just go ahead and apply migrations:

$ rake db:migrate

Open Doorkeeper’s initializer file and find the line resource_owner_authenticator do. By default, it raises an exception, so replace the block’s contents with:

config/initializers/doorkeeper.rb

[...]
User.find_by_id(session[:user_id]) || redirect_to(new_session_url)
[...]

The User model is now bound to Doorkeeper. You may boot the server, register and navigate to localhost:3000/oauth/applications. This is the page to create your new OAuth 2 application. Create one while providing “http://localhost:3001/oauth/callback” as a redirect URL. Note that we are using port 3001 for the client app (non-existent yet), because port 3000 is already in use by the server application. After doing that, you’ll see your Application Id and Secret key. Leave this page open for now or write down the values.

The next step is to create the client application.

Creating the Client App

Run Rails generator to create an app:

$ rails new KeepaClient -T

Add a static pages controller, root page, and a route:

pages_controller.rb

class PagesController < ApplicationController
  def index
  end
end

views/pages/index.html.erb

<h1>Welcome!</h1>

config/routes.rb

[...]
root to: 'pages#index'
[...]

Now let’s also create a local_env.yml file to store some configuration, specifically the Client Id and Secret received from the server app in the previous step:

config/local_env.yml

server_base_url: 'http://localhost:3000'
oauth_token: <CLIENT ID>' 
oauth_secret: '<SECRET>'
oauth_redirect_uri: 'http%3A%2F%2Flocalhost%3A3001%2Foauth%2Fcallback'

Load it inside ENV:

config/application.rb

[...]
if Rails.env.development?
  config.before_configuration do
    env_file = File.join(Rails.root, 'config', 'local_env.yml')
    YAML.load(File.open(env_file)).each do |key, value|
      ENV[key.to_s] = value
    end if File.exists?(env_file)
  end
end
[...]

Exclude the .yml file from version control if you want:

.gitignore

[...]
config/local_env.yml
[...]

Obtaining an Access Token

Alright, we are ready to obtain access token that will be used to perform API requests. You may use the oauth2 gem for this purpose, as described here. Once again, however, I am going to stick with a bit more low-level solution so that you understand how the whole process happens.

We’ll utilize rest-client to send requests.

Add the new gem and bundle install it:

Gemfile

[...]
gem 'rest-client'
[...]

To obtain an access token, a user first needs to visit the “localhost:3000/oauth/authorize” URL while providing the Client Id, redirect URI, and response type. Let’s introduce a helper method to generate the appropriate URL:

application_helper.rb

[...]
def new_oauth_token_path
  "#{ENV['server_base_url']}/oauth/authorize?client_id=#{ENV['oauth_token']}&redirect_uri=#{ENV['oauth_redirect_uri']}&response_type=code"
end
[...]

Display it on the main page of the client’s app:

views/pages/index.html.erb

[...]
<%= link_to 'Authorize via Keepa', new_oauth_token_path %>
[...]

Now, introduce the callback route:

config/routes.rb

[...]
get '/oauth/callback', to: 'sessions#create'
[...]

sessions_controller.rb

class SessionsController < ApplicationController
  def create
    req_params = "client_id=#{ENV['oauth_token']}&client_secret=#{ENV['oauth_secret']}&code=#{params[:code]}&grant_type=authorization_code&redirect_uri=#{ENV['oauth_redirect_uri']}"
    response = JSON.parse RestClient.post("#{ENV['server_base_url']}/oauth/token", req_params)
    session[:access_token] = response['access_token']
    redirect_to root_path
  end
end

After visiting “localhost:3000/oauth/authorize” the user will be redirected to the callback URL with the code parameter set. Inside the create action, generate the proper params string (with client_id, client_secret, code, grant_type, and redirect_uri) and then perform a POST request to the “localhost:3000/oauth/token” URL. If everything was done correctly, the response will contain JSON with the access token and its lifespan (set to 2 hours by default). Otherwise, a 401 error will be returned.

We then parse the response and store the access token inside the user’s session. Of course, you’ll need to introduce some kind of authentication for a real app, but I am keeping things simple here.

If you read my articles about oPRO, this process should be very familiar. If not – my congratulations, you’ve just coded the client app and users can now obtain access tokens.

This all is great, however what can we use these tokens for? Obviously, to secure our API, so let’s introduce that now!

Introducing a Simple API

Return to the server app and create a new controller:

controllers/api/users_controller.rb

class Api::UsersController < ApplicationController
  before_action :doorkeeper_authorize!

  def show
    render json: current_resource_owner.as_json
  end

  private

  def current_resource_owner
    User.find(doorkeeper_token.resource_owner_id) if doorkeeper_token
  end
end

Let’s proceed step-by-step here.

before_action :doorkeeper_authorize! allows us to secure a controller’s actions by preventing non-authorized requests. This means that users have to provide access tokens in order to perform an action. If no token is provided, Doorkeeper will block their way, saying “Thou shall not pass!” just like Gandalf. Well, actually in our case an 401 error will be returned, but you got the idea.

current_resource_owner is a method that returns the owner of an access token that was sent. doorkeeper_token.resource_owner_id returns the id of a user that performed the request (because, as you remember, we tweaked resource_owner_authenticator accordingly inside the Doorkeeper initializer).

as_json turns the User object into JSON. You may provide an except option to exclude some fields, for example:

current_resource_owner.as_json(except: :password_digest)

Add a new route:

config/routes.rb

namespace :api do
  get 'user', to: 'users#show'
end

Now if you try to access “localhost:3000/api/user”, you’ll see an empty page. Open the console, and you’ll notice a 401 error displayed, meaning that this action is now protected.

If you want to customize this unauthorized page, you may redefine doorkeeper_unauthorized_render_options method inside the ApplicationController. For example, add an error message like this:

application_controller.rb

def doorkeeper_unauthorized_render_options(error: nil)
  { json: { error: "Not authorized" } }
end

Tweak the main page of the client’s app:

views/pages/index.html.erb

[...]
<% if session[:access_token] %>
  <%= link_to 'Get User', "http://localhost:3000/api/user?access_token=#{session[:access_token]}" %>
<% else %>
  <%= link_to 'Authorize via Keepa', new_oauth_token_path %>
<% end %>

Now authorize, click the “Get User” link and observe the result – you should see your user’s details!

Working with Scopes

Scopes are the way to determine which actions the client will be able to perform. Let’s introduce two new scopes:

  • public – allows the client to fetch information about a user
  • write – allows the client to make changes to user’s profile

First of all, change the Doorkeeper’s initializer file to include these new scopes:

config/initializers/doorkeeper.rb

[...]
default_scopes :public
optional_scopes :write
[...]

default_scopes says which scopes to request by default if nothing was specified in the authorize URL.

optional_scopes is the list of all other scopes that may be passed. If some unknown scope is passed, an error will be raised.

Update the API controller:

api/users_controller.rb

[...]
before_action -> { doorkeeper_authorize! :public }, only: :show
before_action -> { doorkeeper_authorize! :write }, only: :update

def show
  render json: current_resource_owner.as_json
end

def update
  render json: { result: current_resource_owner.touch(:updated_at) }
end
[...]

We are passing scope’s name to doorkeeper_authorize! saying which scope is required to perform an action. Please note that multiple scopes can be passed as well:

doorkeeper_authorize! :admin, :write

This means that an action can be performed if the client has the admin or write permission. If you want to require both permissions to be present, use the following construct:

doorkeeper_authorize! :admin
doorkeeper_authorize! :write

Introduce a new route:

config/routes.rb

[...]
namespace :api do
  get 'user', to: 'users#show'
  get 'user/update', to: 'users#update'
end
[...]

To keep things as simple as possible, I am using the GET verb, but for the real app it would be better to use PATCH. Now, in the client app, let’s modify the helper method to request both public and write permissions:

application_helper.rb

[...]
def new_oauth_token_path
  "#{ENV['server_base_url']}/oauth/authorize?client_id=#{ENV['oauth_token']}&redirect_uri=#{ENV['oauth_redirect_uri']}&response_type=code&scope=public+write"
end
[...]

We added the scope parameter here. Multiple scopes should be delimited with the + sign.

Lastly, visit “http://localhost:3000/oauth/applications”, open your app for editing and fill in the “Scopes” field with public write value (separated with space).

Reload the server and visit the authorize URL. You’ll notice that now a user is being requested permission to perform two actions, but their names are not very helpful. For example, what does “write” mean? It may be obvious to you, but not to our users. Therefore, it would be better to provide a bit more information on each scope. This can be done by editing doorkeeper’s I18n file that was generated for you:

config/locales/doorkeeper.en.yml

en:
  doorkeeper:
    scopes:
      public: 'Access your public data.'
      write: 'Make changes to your profile.'
[...]

Now users will see a detailed description for each scope.

Lastly, add a new link to the main page:

views/pages/index.html.erb

[...]
<% if session[:access_token] %>
  <%= link_to 'Get User', "http://localhost:3000/api/user?access_token=#{session[:access_token]}" %>
  <%= link_to 'Update User', "http://localhost:3000/api/user/update?access_token=#{session[:access_token]}" %>
<% else %>
  <%= link_to 'Authorize via Keepa', new_oauth_token_path %>
<% end %>

Go ahead and try this out!

Conclusion

In this article we laid the foundation for our apps and integrated Doorkeeper. Currently, users are able to register their apps, request access tokens, and work with scopes. The API is now secured and unauthorized access is prevented.

In the next post, we’ll introduce refresh tokens, secure Doorkeeper’s generic routes, customize views, and craft a custom provider for OmniAuth that can be packed as a gem.

As always, don’t hesitate to post your questions and see you soon!

Frequently Asked Questions (FAQs) about Getting Started with Doorkeeper and OAuth 2.0

What is Doorkeeper and why is it important in OAuth 2.0?

Doorkeeper is a gem that simplifies the process of creating OAuth 2.0 providers in Ruby on Rails applications. It provides a set of tools and configurations that allow developers to quickly and easily implement OAuth 2.0 standards in their applications. OAuth 2.0 is a protocol that allows third-party applications to gain limited access to an HTTP service. It is widely used for authorization in major web applications. Doorkeeper plays a crucial role in this process by providing a secure and standardized way to handle these authorizations.

How do I install and configure Doorkeeper in my Rails application?

To install Doorkeeper, you need to add the gem to your Gemfile and run the bundle install command. After that, you can generate an initializer file using the rails generate doorkeeper:install command. This file will contain all the necessary configurations for Doorkeeper. You can customize these settings according to your needs. Finally, you need to create necessary models and tables using the rails generate doorkeeper:migration command followed by rake db:migrate.

How do I create OAuth applications with Doorkeeper?

Doorkeeper provides a built-in interface for creating and managing OAuth applications. You can access this interface by navigating to the /oauth/applications path in your application. Here, you can create new applications, set their redirect URIs, and generate client IDs and secrets.

How do I secure my OAuth applications with Doorkeeper?

Doorkeeper provides several options to secure your OAuth applications. You can restrict access to the OAuth applications interface by using the admin_authenticator option in the initializer file. You can also customize the token generation process by using the custom_access_token_expires_in and custom_access_token_generator options.

How do I use scopes with Doorkeeper?

Scopes allow you to limit the access of an OAuth application. You can define scopes in the initializer file using the default_scopes and optional_scopes options. When creating an application, you can specify which scopes it can access.

How do I handle errors with Doorkeeper?

Doorkeeper provides a way to handle errors through the error_responses option in the initializer file. You can customize this option to return custom error messages or statuses.

How do I test my Doorkeeper implementation?

You can test your Doorkeeper implementation by using the built-in test helpers. These helpers allow you to simulate authorization requests and responses in your tests.

How do I upgrade Doorkeeper to a newer version?

To upgrade Doorkeeper, you need to update the gem in your Gemfile and run the bundle update command. After that, you need to check the changelog for any breaking changes and update your code accordingly.

How do I integrate Doorkeeper with other authentication libraries?

Doorkeeper can be integrated with other authentication libraries like Devise. You can do this by setting the resource_owner_authenticator option in the initializer file to a block that returns the current user.

How do I contribute to Doorkeeper?

Doorkeeper is an open-source project, and contributions are always welcome. You can contribute by reporting bugs, suggesting features, or submitting pull requests. Before contributing, make sure to read the contributing guide on the project’s GitHub page.

Ilya Bodrov-KrukowskiIlya Bodrov-Krukowski
View Author

Ilya Bodrov is personal IT teacher, a senior engineer working at Campaigner LLC, author and teaching assistant at Sitepoint and lecturer at Moscow Aviations Institute. His primary programming languages are Ruby (with Rails) and JavaScript. He enjoys coding, teaching people and learning new things. Ilya also has some Cisco and Microsoft certificates and was working as a tutor in an educational center for a couple of years. In his free time he tweets, writes posts for his website, participates in OpenSource projects, goes in for sports and plays music.

GlennGoauthoauth2Ruby on Rails
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week