Key Takeaways
- Doorkeeper, a Rails engine, can be customized to secure OAuth 2 applications by restricting access to the list of registered OAuth 2 applications. This is done by setting the admin_authenticator in the Doorkeeper’s initializer file.
- Refresh tokens can be introduced in Doorkeeper to prevent users from having to repeat the authentication process every time their access tokens expire. This is done by uncommenting and modifying the access_token_expires_in line in the Doorkeeper’s initializer file and enabling the use_refresh_token.
- A custom OmniAuth strategy can be created and distributed as a gem if the application becomes popular. This is done by creating a new strategy inside the lib folder of the client’s app and modifying the client app’s initializer file.
- Doorkeeper allows for further customization, such as altering views by running the command “rails generate doorkeeper:views”, and tweaking default routes by using the use_doorkeeper method.
In the previous part of this series we met Doorkeeper, a Rails engine to build custom OAuth 2 providers. I showed how to integrate this solution into your app and how to use it to secure API requests.
Currently, users are able to register their OAuth 2 apps, receive access tokens, perform API requests, and work with scopes. However, there are a few things left to take care of:
- The list of OAuth 2 applications can be accessed by anyone
- Users cannot obtain refresh tokens and use them to fetch new access tokens
- It would be nice to craft a custom OmniAuth strategy that can be later packed as a gem
- We need a way to tweak views and routes
Therefore, in this article we will attend to these issues.
Source code can be found on GitHub.
If you didn’t follow the instructions in the previous part, you may clone this branch.
Securing OAuth 2 Applications
Just to remind you, at this point we have two apps: a so-called “server” (the actual authentication provider) and a “client” (an app for testing purposes). Currently, even if a user is not authenticated, they can still visit localhost:3000/oauth/applications and browse the list of registered OAuth 2 applications. Obviously, that’s not good, so we need a way to restrict access to that page.
Luckily, Doorkeeper provides a handy way to solve this problem. Tweak Doorkeeper’s initializer file:
config/initializers/doorkeeper.rb
[...]
admin_authenticator do
User.find_by_id(session[:user_id]) || redirect_to(new_session_url)
end
[...]
This is very similar to what we did in the previous part, however now we are setting admin_authenticator
rather than resource_owner_authenticator
. For the real app you’d probably want to use a more complex solution (like requiring the user to have an “admin” role, etc.), but this is fine for demonstration purposes.
Reload the server and navigate to localhost:3000/oauth/applications. If you are not authenticated, you’ll be redirected to the log in page. This means that everything is working just as planned.
Introducing Refresh Tokens
By default access tokens expire in 2 hours, after which users have to repeat the authentication process again. If you need to tweak this behavior, uncomment and modify the access_token_expires_in 2.hours
line inside the Doorkeeper’s initializer file. By the way, here you may also tweak the authorization_code_expires_in 10.minutes
setting. This code is used during the authentication phase to receive the access token and its lifespan should be short.
However, for user convenience we may want to introduce refresh tokens. By default, Doorkeeper does not issue them, but that can be changed easily:
config/initializers/doorkeeper.rb
[...]
use_refresh_token
[...]
Don’t forget to reboot your server after changing this file.
Now open your client app and modify SessionsController
to store refresh tokens and access token expiration time:
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']
session[:refresh_token] = response['refresh_token']
session[:expires_at] = response['expires_in'].seconds.from_now
redirect_to root_path
end
end
response['expires_in']
contains number of seconds saying how soon the token will expires but, for our convenience, I am turning this to a DateTime
object.
Now introduce two new routes:
config/routes.rb
[...]
get '/user', to: 'users#show', as: :user
get '/user/update', to: 'users#update', as: :update_user
[...]
Currently our main page has the following contents:
views/pages/index.html.erb
<h1>Welcome!</h1>
<% 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 %>
I want to extract these “Get User” and “Update User” URLs into the controller actions and that’s why we created new routes:
views/pages/index.html.erb
[...]
<% if session[:access_token] %>
<%= link_to 'Get User', user_path %>
<%= link_to 'Update User', update_user_path %>
<% else %>
<%= link_to 'Authorize via Keepa', '/auth/keepa' %>
<% end %>
Here is the new controller:
users_controller.rb
class UsersController < ApplicationController
def show
redirect_to "http://localhost:3000/api/user?access_token=#{session[:access_token]}"
end
def update
redirect_to "http://localhost:3000/api/user/update?access_token=#{session[:access_token]}"
end
end
What we need to do now is to check whether an access token has expired before performing an API request. The easiest way would be to employ a before_action
:
users_controller.rb
[...]
before_action :check_access_token
[...]
private
def check_access_token
redirect_to root_path unless session[:access_token]
if session[:expires_at] <= Time.current
req_params = "client_id=#{ENV['oauth_token']}&client_secret=#{ENV['oauth_secret']}&refresh_token=#{session[:refresh_token]}&grant_type=refresh_token&redirect_uri=#{ENV['oauth_redirect_uri']}"
response = JSON.parse RestClient.post("#{ENV['server_base_url']}/oauth/token", req_params)
set_oauth_info_from response
end
end
If no access token is set, we simply redirect the user back. If the token has expired, we generate a new string with request params client_id
, client_secret
, refresh_token
, redirect_uri
, and grant_type
(that has to be set to refresh_token
). You may ask, what about the scope? By default, a new access token will be granted to perform the same actions that were initially approved by the user. You may narrow the scope, but you cannot add scopes that were not provided previously.
Next we take the response and pass it to the set_oauth_info_from
method that will be introduced now:
application_controller.rb
[...]
private
def set_oauth_info_from(response)
session[:access_token] = response['access_token']
session[:refresh_token] = response['refresh_token']
session[:expires_at] = response['expires_in'].seconds.from_now
end
Use this method inside the SessionsController
to avoid code duplication:
sessions_controller.rb
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)
set_oauth_info_from response
redirect_to root_path
end
That’s it! Now our users are able to receive refresh tokens without any problem!
Building a Custom OmniAuth Provider
If your app becomes popular enough one day, you may want to craft a custom OmniAuth strategy and distribute it as a gem. It appears that this can be done pretty easily. Of course, we are not going to create a separate gem, rather, let’s store our new strategy inside the lib folder of the client’s app.
lib/omniauth/strategies/keepa.rb
require 'omniauth-oauth2'
module OmniAuth
module Strategies
class Keepa < OmniAuth::Strategies::OAuth2
option :name, :keepa
option :client_options, {
:site => "http://localhost:3000",
:authorize_url => "/oauth/authorize"
}
uid { raw_info["id"] }
info do
{
:email => raw_info["email"]
}
end
def raw_info
@raw_info ||= access_token.get('/api/user').parsed
end
end
end
end
I’ve named my strategy Keepa, so the class name and the option :name
should be set accordingly.
The raw_info
method is used to obtain information about a user that authenticates via our strategy. /api/user
is the route that we’ve introduced in the previous article. Let me remind you how the corresponding action looks like:
api/users_controller.rb
[...]
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
[...]
We basically fetch the user who has initiated the authentication phase and render his information in JSON format.
uid { raw_info["id"] }
explains what field to use as a unique identifier. We are using the identifier from the table, but you may introduce a special field instead.
info do
{
:email => raw_info["email"]
}
end
This indicates what fields will be available inside the callback action after a successful authentication. You may add other fields as necessary.
This code depends on omniauth-oauth2 abstract strategy so hook it up:
Gemfile
[...]
gem 'omniauth-oauth2', '1.3.1'
[...]
and run
$ bundle install
I am using version 1.3.1 here because of this issue that affects Doorkeeper.
Now create a new initializer file for the client app:
config/initializers/omniauth.rb
require File.expand_path('lib/omniauth/strategies/keepa', Rails.root)
Rails.application.config.middleware.use OmniAuth::Builder do
provider :keepa, ENV['oauth_token'], ENV['oauth_secret'], scope: 'public write'
end
If you have ever used OmniAuth in your projects, this should be very familiar. The only thing to note is that we need to require the strategy file explicitly as it is not being loaded automatically.
public
and write
are the scopes that were introduced in the previous article. A space should be used as a delimiter.
OmniAuth instructs us to add a special callback route, so replace the old one:
config/routes.rb
[...]
get '/oauth/callback', to: 'sessions#create'
[...]
with
config/routes.rb
[...]
get '/auth/:provider/callback', to: 'sessions#create'
[...]
Modify the config file:
config/local_env.yml
[...]
oauth_redirect_uri: 'http%3A%2F%2Flocalhost%3A3001%2Fauth%2Fkeepa%2Fcallback'
Also don’t forget to open your server app, navigate to localhost:3000/oauth/applications, and edit the redirect URI for your application as well.
Alter the main page of the client app:
views/pages/index.html.erb
[...]
<% if session[:access_token] %>
<%= link_to 'Get User', user_path %>
<%= link_to 'Update User', update_user_path %>
<% else %>
<%= link_to 'Authorize via Keepa', '/auth/keepa' %>
<% end %>
The next step is to alter the callback action:
sessions_controller.rb
[...]
def create
set_oauth_info_from request.env['omniauth.auth']['credentials']
redirect_to root_path
end
[...]
request.env['omniauth.auth']
should be familiar to you. It contains the authentication hash in the following format:
{"provider"=>:keepa,
"uid"=>1,
"info"=>{"email"=>"test@example.org"},
"credentials"=>
{"token"=>"8d8...",
"refresh_token"=>"321...",
"expires_at"=>1453301961,
"expires"=>true},
"extra"=>{}}
Note that the field is called token
(not access_token
) and instead of expires_in
we have expires_at
.
Therefore the set_oauth_info_from
method should be changed a bit:
application_controller.rb
[...]
def set_oauth_info_from(response)
session[:access_token] = response['access_token'] || response['token']
session[:refresh_token] = response['refresh_token']
if response['expires_in']
session[:expires_at] = response['expires_in'].seconds.from_now
else
session[:expires_at] = Time.at response['expires_at']
end
end
[...]
The new_oauth_token_path
helper method can be removed completely and you are ready to authenticate via your new shiny strategy. Great!
Further Customization
You are probably wondering whether it is possible to modify Doorkeeper’s views and routes. And yes – this is totally possible.
To alter the views, run this simple command:
$ rails generate doorkeeper:views
It will generate all the views and layouts used by Doorkeeper.
You are free to employ your own layouts by tweaking these settings inside the application.rb file:
config.to_prepare do
Doorkeeper::ApplicationsController.layout "my_layout"
Doorkeeper::AuthorizationsController.layout "my_layout"
Doorkeeper::AuthorizedApplicationsController.layout "my_layout"
end
Routes and helper methods from your app can be utilized inside the Doorkeeper’s views, but they had to be prefixed with the main_app
(for example, main_app.login_path
) because this solution is an isolated engine.
Tweaking default routes is pretty simple as well. The use_doorkeeper
method accepts a block with all the necessary configurations. For example, you can use own controller to manage OAuth 2 applications:
use_doorkeeper do
controllers :applications => 'custom_applications'
end
controllers
accepts :authorizations
, :tokens
, :applications
, and :authorized_applications
values.
You may even skip some controllers by saying:
use_doorkeeper do
skip_controllers :applications
end
skip_controllers
accepts the same values.
Refer to this wiki page for more information.
Conclusion
In this article we finalized our custom OAuth 2 provider by securing it a bit, introducing refresh tokens, and crafting a custom OmniAuth strategy. Now you are ready to further customize and enhance you app as needed! I really encourage you to browse the Doorkeeper wiki as it provides some nice tutorials on how to solve common problems.
I hope that this series of articles was useful and interesting to you. If you want me cover a specific topic don’t hesitate to ask – I am always looking forward to your feedback. Happy coding and stay authenticated!
Frequently Asked Questions (FAQs) about Tailoring Doorkeeper with Refresh Tokens, Views, and Strategies
What is the purpose of using refresh tokens in Doorkeeper?
Refresh tokens in Doorkeeper are used to obtain new access tokens. This is particularly useful when the existing access token has expired or has been revoked. The primary advantage of using refresh tokens is that the client application doesn’t need to involve the user to grant access again. This ensures a seamless user experience while maintaining the security of the application.
How can I customize views in Doorkeeper?
Doorkeeper allows you to customize views to suit your application’s needs. You can generate views using the command “rails generate doorkeeper:views”. This will create a copy of the Doorkeeper views in your application which you can modify as per your requirements. Remember to restart your server after generating the views.
What are the different strategies available in Doorkeeper?
Doorkeeper provides three different strategies for managing access tokens – ‘authorization code’, ‘implicit’, and ‘password’. The ‘authorization code’ strategy is used by server-side applications, the ‘implicit’ strategy is used by mobile or web applications, and the ‘password’ strategy is used by trusted applications.
How can I handle token and application secrets in Doorkeeper?
Doorkeeper provides a secure way to handle token and application secrets. It is recommended to use environment variables to store these secrets. You can use the ‘dotenv-rails’ gem to manage your environment variables. Never commit your secrets to the source code repository.
How can I troubleshoot issues related to refresh tokens in Doorkeeper?
If you are facing issues with refresh tokens in Doorkeeper, you can check the server logs for any error messages. Also, ensure that you have correctly configured the refresh token settings in the Doorkeeper initializer file. If the issue persists, you can raise an issue on the Doorkeeper GitHub page.
How can I contribute to the Doorkeeper project?
Doorkeeper is an open-source project and welcomes contributions from the community. You can contribute by reporting bugs, suggesting new features, or submitting pull requests. Before contributing, make sure to read the contribution guidelines on the Doorkeeper GitHub page.
How can I upgrade Doorkeeper to a newer version?
To upgrade Doorkeeper to a newer version, you need to update the Doorkeeper gem in your Gemfile and run the ‘bundle update’ command. After updating the gem, make sure to run the Doorkeeper migrations to update your database schema.
How can I revoke an access token in Doorkeeper?
To revoke an access token in Doorkeeper, you can use the ‘revoke’ method provided by the Doorkeeper::AccessToken model. This will mark the access token as revoked and it will no longer be valid for authentication.
How can I customize the token response in Doorkeeper?
Doorkeeper allows you to customize the token response by overriding the ‘Doorkeeper::OAuth::TokenResponse’ class. You can add additional fields to the response or modify the existing fields as per your requirements.
How can I secure my Doorkeeper application?
To secure your Doorkeeper application, you should always use HTTPS for all communication. Also, make sure to store your application secrets securely and never expose them in the source code. Regularly update the Doorkeeper gem to get the latest security updates.
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.