Key Takeaways
- Doorkeeper Setup and Integration: Learn how to install and configure the Doorkeeper gem in a Rails application to create a secure OAuth 2.0 provider, including setting up the necessary routes and migrations.
- Building and Testing OAuth Applications: Discover the process of creating OAuth applications using Doorkeeper’s built-in interface, where you can manage applications, set redirect URIs, and handle client IDs and secrets.
- Securing APIs and Handling Scopes: Understand how to secure your API using Doorkeeper’s authentication system and manage access levels through scopes, allowing for fine-tuned control over what third-party applications can and cannot access.
- Customization and Error Handling: Explore how to customize Doorkeeper’s settings to fit your application’s needs, including token expiration and error handling to improve the security and user experience of your OAuth provider.
- Practical Implementation: Gain insights into practical OAuth 2.0 applications by setting up a client application that interacts with the secure API, including handling access tokens and using these tokens to make authorized API requests.
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 userwrite
– 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 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.