Ruby
Article

Getting Started with Doorkeeper and OAuth 2.0

By Ilya Bodrov-Krukowski

Authentication in Rails

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!

  • http://www.zulhilmizainudin.com/ Zulhilmi Zainudin

    Hi Ilya, can you share tutorial on how to do user auth using Rails and AngularJS? I’m planning to use Rails to create the API and store user credentials and use AngularJS for the front-end (client) side.

    I’ve no idea how to do the user auth so that I can protect my API resources and only allow registered users to use the protected resources.

    I try to understand oAuth2 and JWT but still couldn’t see how to move forward. Really appreciate if you can make a in-depth tutorial about it. Thanks!

    • Ilya Bodrov

      Well, I might do that but for now I’d recommend googling for it, as it takes some time…

      • http://www.zulhilmizainudin.com/ Zulhilmi Zainudin

        Thanks. Please ping me when it’s done. Thanks!

  • Allan Sachs

    This is the best tutorial on this subject I have seen anywhere on the web. I got this up and running and had basically no problems. Still trying to figure out the details though.

  • SeuRaul

    Ilya, thanks for the post, is helping me so much.
    But I have one issue here: my update method is not working, trying exactly as yours. I believe is in the scope of the doorkeeper, as I authorize does not show the update access, only the public one. The configurations are the same as yours.
    Can you help me?
    Thanks

  • Cuneyt Senturk

    Hi thank you for this great tutorial. But i stuck at Creating the Client App config/application.rb where exactly i have to put if Rails.env.development? … codes ?

Recommended

Learn Coding Online
Learn Web Development

Start learning web development and design for free with SitePoint Premium!

Get the latest in Ruby, once a week, for free.