Understanding and Implementing OAuth2 in Ruby

Let me know if this feels familiar. Your users want to "login with GitHub," so you install a gem, follow the setup instructions, then pray it never needs maintenance because you have no real idea how OAuth2 works. Let's fix that. In this article, Diogo Souza shows us the fundamental concepts behind OAuth2 and how to implement them using Devise and Doorkeeper.

Adding authentication to your Ruby apps can be overwhelming, especially with OAuth 2.

I wrote this article to help orient you. It will introduce you to the fundamental concepts behind OAuth 2 and show how they're implemented in two different libraries: oauth-plugin and Doorkeeper. It will show you how these OAuth libraries integrate with Devise to provide a complete authentication solution for Rails.

If this article piques your curiosity, and you'd like to explore the OAuth 2 architecture in greater depth, we recommend taking a look at the OAuth 2.0 specification and IETF RFC.

Environment Setup

Rails needs Ruby. For this article, we’ll be using version 2.7.0. It’s better if you also use the same to avoid unnecessary complications.

To determine whether you already have it installed, run the following:

ruby -v

If your version is older, you’ll need to upgrade it before proceeding.

Now run the following:

gem install rails

If the command runs successfully, then you’re ready to proceed.

The oauth-plugin was built a while ago, originally targeting version 2.5 or earlier of Ruby. That’s why a couple of changes will be necessary in this article.

The Server Provider

The project is divided into two parts. The provider is the project that will serve OAuth 2 operations, including token generation and the main authorization flow.

The client, in turn, will simply issue HTTP requests containing the auth data.

So, let’s create the folders of each part accordingly:

mkdir hb-oauth2-diy && cd hb-oauth2-diy
rails new hb-oauth2-provider

If you're a beginner, I’d strongly advise you to review the structure of a Rails app.

Next, we need to add and update the dependencies our provider project will need. First, add the following to your Gemfile:

gem 'devise'
gem "oauth-plugin", ">= 0.5.1"
group :test do
    gem 'rspec-rails'
end

gem 'dynamic_form'

Devise and oauth-plugin dependencies come first. Regarding this last one, make sure to check the latest version and include it here appropriately.

We’re also including RSpec to see some tests from the plugin repository running at the end of the configuration. More details will be provided on this later.

Since we’re dealing with Rails3 models in the project (differently from the older versions of Rails, for which the plugin was originally designed), we’ll need to include the dynamic_form gem to easily access its model-helper methods.

Finally, change the line of sqlite3 to the following:

gem 'sqlite3', '~> 1.3.13'

This change is made to avoid compilation errors for oauth-plugins that also use SQLite to handle user data.

Rails Configurations

Now, we’re ready to run the bundle install command and have our dependencies updated.

With all the gems installed, we can now run the command to properly install rspec in our project:

rails generate rspec:install

This will update spec_helper.rb and rails_helper.rb in the spec folder. If you’re not familiar with rspec, please spend some time reading the comments on them, as well as the official docs.

Devise Installation

Next comes the Devise installation. Since OAuth deals with user data, we’d need to create the proper model for the database that Devise will create and manage, by running the generator:

rails generate devise:install

It will install an initializer that describes the possible configs. Just like we did with rspec, go ahead and read them.

Now, we need to set up our user model, pointing it to Devise. To do so, run the following:

rails generate devise User

You can switch the name to Admin or any other name of your choosing. Note the logs after the command ends. It shows which files were created (tests, the model, a db migration, and one route inserted).

Next, we need to create the OAuth 2 provider and its default generated controllers, models, and routes. Begin by running the command into the hb-oauth2-provider folder:

rails g oauth_provider --test-framework=rspec

This time, the test framework must be provided as well, rspec. Again, note the logs and the created files/routes. These logs are very instructive; make sure to always refer to any changes they list.

One interesting change was placed at routes.rb in the config folder. Please, don’t change the routes, since they already map perfectly the endpoints to the respective controller methods.

HTTP Routing

Note that each mapping also uses an old-fashioned declaration style in which each request can be sent via any HTTP method. However, this no longer applies, so let’s change the mappings to the following:

# config/routes.rb
Rails.application.routes.draw do
  resources :oauth_clients
  match '/oauth/test_request',  via: [:get], :to => 'oauth#test_request',     :as => :test_request
  match '/oauth/token',         via: [:post], :to => 'oauth#token',           :as => :token
  match '/oauth/access_token',  via: [:get], :to => 'oauth#access_token',     :as => :access_token
  match '/oauth/request_token', via: [:get], :to => 'oauth#request_token',    :as => :request_token
  match '/oauth/authorize',     via: [:get, :post], :to => 'oauth#authorize', :as => :authorize
  match '/oauth',               via: [:get], :to => 'oauth#index',            :as => :oauth
  devise_for :users

  root :to => "oauth_clients#index"

  # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
end

We provided the routes with the specific HTTP method they’ll navigate through and a root route for the API (index method).

Database Migration

Now, it’s time to migrate the database. Take a look at the db/migrate folder. There, you can see the database migration files that each of the previous commands we’ve issued has auto-generated.

There is one minor change here. For some reason, the oauth-plugin is not generating the files with the correct extension (.rb). This will interfere with our Rake migration task working properly, so go ahead and add the extension (ex: 2020xxxx_create_oauth_tables > 2020xxxx_create_oauth_tables.rb).

Have a look at these files. They’ll basically create the tables and indexes with the patterns that the oauth-plugin uses within its models.

Now, run the command to migrate the database and create our SQLite db.

rake db:migrate

The logs state the successful creation of the tables. Note that the development.sqlite3 file was auto-generated in the migrate folder. It is our database. You can also open this file and select the tables and data within them, but they're currently empty.

Getting The Tests Ready

Since we’re using rspec, and as I mentioned, we’ll run some of the oauth-plugin ready tests, let’s also migrate the database for tests:

rake db:test:prepare

test.sqlite3 was created. As the plugin was built to deal with OAuth 1 and 2, we need to remove some files (more on this by the author). They are:

  • spec/controllers/oauth_clients_controller_spec.rb
  • spec/models/oauth_token_spec.rb

Adapting Our Provider

Now’s the time to adapt the provider to deal with OAuth 2 exclusively, as well as understand how it will work with Rails 5+.

Let’s start with the controllers. Open the application_controller.rb in the app/controllers folder and add the following code snippet to inject the user as a current_user (required by oauth-plugin).

def current_user=(user)
  current_user = user
end

For the other two controllers in the folder, we need to add the following alias:

alias :login_required :authenticate_user!

This is necessary because the oauth-plugin only understands this alias structure that, in turn, will trigger the :login_required when called.

The oauth_clients_controller.rb still needs to be changed. The create method needs to state the required params before using them. Change its first line of code to the following:

@client_application = current_user.client_applications.build(user_params)

This is the user_params method:

def user_params
  params.require(:client_application).permit(:name, :email, :url)
end

Creating The API Controllers

We’ll also need two API controllers: Base (default root) and Data (to serve some test data). To create them, run the following:

rails generate controller API::V1::Base
rails generate controller API::V1::Data

They will be created in the app/controllers/api/v1 folder. It’s important to define the version so that the API will be ready for major changes in the future.

The first change here is related to the format types our Base controller will accept. Add the following:

respond_to :json, :xml

Feel free to add any other type you prefer for testing purposes.

Then, add the following to ensure only the OAuth flow will be processed (rather than the default login one) by setting false on interactive flag.

oauthenticate :interactive=>false

For the data_controller.rb, let’s set a default response to the show method by adding the following:

def show
   respond_with ({:super_secret => "oauth_data"})
end

For this response to work, we need to map the respective route at routes.rb:

namespace :api do
  namespace :v1 do
    match "data" => "data#show", via [:get]
  end
end

Adapting The Models

We’re done with the controllers. Let’s move on to the models.

First, let’s modify the relationships of the user table with the client_applications and tokens tables. A user must have many client applications and tokens. So, add this code to the class body:

has_many :client_applications
has_many :tokens, -> { includes(:client_application) },
  :class_name => "Oauth2Token"

Second, to match the current oauth-plugin models to the tables, the app/models/oauth_token.rb needs to have an expiration date. Add the following accessor:

attr_accessor :expires_at

We also need to adapt the main application config file, application.rb. Open it, and then add the Rack’s oauth-filter to help ensure filtering, sanitizing, and other features default to OAuth 2 protocol. It will be added to the application.rb class’ body:

config.middleware.use OAuth::Rack::OAuthFilter

Don’t forget to import it properly at the beginning of the file:

require 'oauth/rack/oauth_filter'

Fixtures and Helpers

That’s it. Let’s test it. But first, we need to have fixtures and spec helpers to define what and how to run them.

To avoid verbosity, please download the api/v1, fixtures(data for the tests), support (rspec configurer), and spec_helper.rb (the same as rails_helper.rb; you can choose either one or another) files from the spec folder, from my version of the source code. They were copied from the oauth-plugin repository to enable us to test the OAuth setup.

The data_controller_spec.rb file basically defines two tests: one with a valid and another with an invalid token. They test the input, processing, and output of our OAuth endpoints for both JSON and XML formats.

Now, let’s run it. For this, run the command bundle exec rspec spec and check the results. You should see the following message:

Finished in 0.27095 seconds (files took 2.61 seconds to load)
28 examples, 0 failures

The Consumer

With the oauth-plugin’s OAuth provider API fully functional, we can focus on the consumer.

However, first, we need to change the application name and URL shown in oauth2_authorize.html.erb, the view of the authorization step. Open it and replace the @token with @.

<%= link_to @client_application.name,
  @client_application.url %> (<%= link_to
  @client_application.url, @client_application.url %>)

Next, let’s create the consumer folder, named hb-oauth2-consumer, at the same level as the provider.

For this part of the project, we’ll make use of Sinatra (a DSL for quickly creating web applications in Ruby) to simplify our lives, along with oauth2 (a Ruby wrapper for the OAuth 2.0 specification). For this, create a Gemfile file at the root of the consumer folder and add the following:

gem 'sinatra'
gem 'oauth2'

Then, run bundle install to download the dependencies.

Setting Up User Credentials

Before proceeding to the client code, we need to create, in the provider, the client application that will host the user credentials for OAuth 2. For this, inside of the provider folder, run the rails server command.

It will start the server application and enable access via http://localhost:3000. Click the Sign up link, and a screen like the following should appear:

Default sign up page Default sign up page.

Type any email and password, and then click Sign up. The following screen will appear, stating the success of the previous action.

Welcome page Welcome page.

Next, click the “Register your application” link and, once redirected, fill in the following application data:

Click the Register button, and you’ll be redirected to the OAuth details page.

OAuth details page. OAuth details page.

Creating Our OAuth Wrapper Client

The process used to create an OAuth wrapper client is very simple. Create a new file called app.rb in the root folder and add the imports and the OAuth client definition:

require 'sinatra'
require 'oauth2'
require 'json'
enable :sessions

def client
  OAuth2::Client.new("client_id", "client_secret",
  :site => "http://localhost:3000")
end

The client_id and client_secret are exactly the same as the ones displayed in the step from the details page. Replace them accordingly. Immediately below it, let’s add the endpoints:

get "/auth/test" do
  redirect client.auth_code.authorize_url(
    :redirect_uri => redirect_uri)
end

get '/auth/test/callback' do
  access_token = client.auth_code.get_token(
    params[:code], :redirect_uri => redirect_uri)
  session[:access_token] = access_token.token
  @message = "Successfully authenticated with the server"
  erb :success
end

get '/page_2' do
  @message = get_response('data.json')
  erb :success
end
get '/page_1' do
  @message = get_response('data.json')
  erb :page1
end

Once the client is defined and okay, you can access OAuth 2 operations under the auth_code object, such as retrieving a token or authorizing an URL.

The GETs for page_1 and page_2 are just for you to check whether the session is working. They make sure the authentication worked and is keeping the session up.

The Auxiliary Methods

Last, but not least, it follows the definition of the two auxiliary methods to extract the access token from the response and for the redirect URL, respectively:

def get_response(url)
  access_token = OAuth2::AccessToken.new(
    client, session[:access_token])
  p access_token
  JSON.parse(access_token.get("/api/v1/#{url}").body)
end

def redirect_uri
  uri = URI.parse(request.url)
  uri.path = '/auth/test/callback'
  uri.query = nil
  uri.to_s
end

The Views

Now, we only need the two .erb files in the views folder (please, make sure to create it) to serve the HTML responses.

Create two files:

  • page1.erb: to display the message from the API and a link to check the session persistence.
  • success.erb: to display the success message and a link to page1.

views/page1.erb

<h1>Page #1</h1>

<%=@message.inspect%>

<a href="/page_2">Verify if session persists</a>

views/success.erb

<h1>Success page</h1>

<%=@message.inspect%>

<a href="/page_1">Test Page 1</a>

Finished. Now, on to the tests. In the consumer folder, run the following:

ruby app.rb

Your client application will be served at http://localhost:4567. To test it, go to http://localhost:4567/auth/test. An authorization screen will appear, asking you to allow access to the given client application. Make sure to check the checkbox and click the Save changes button.

The result is going to be our success page. Now, you can flip from one page to another and verify the session remains up.

Devise/Doorkeeper Strategy

Until now, we had a lot of code. Although we were using a plugin to help with boilerplate code, ready endpoints, and webpages for sign in/sign up management, a lot of adaptations were necessary.

This is when Doorkeeper comes to the rescue. It is not only an OAuth 2 provider for Rails but also a full OAuth 2 suite for Ruby and related frameworks (Sinatra, Devise, MongoDB, support for JWT, and more).

Let’s dive into this fresher universe, so you can analyze the differences.

Regarding the setup, we don’t need anything other than what’s already installed. So, let’s create the new Rails app:

rails new hb-oauth2-devise-doorkeeper

Now, repeat the step from the previous strategy and add the dependencies to your Gemfile:

gem 'devise'
gem 'doorkeeper'
gem 'sqlite3', '~> 1.3.13'

The version of sqlite3 is important, as otherwise your app may show an error “Specified 'sqlite3' for the database adapter, but the gem is not loaded. Add gem 'sqlite3'”.

Run the bundle install command to install the gems.

Scaffolding Our Notes CRUD

Since we won’t have any webpages ready at this time (i.e., there's no longer an oauth-plugin), let’s create a simple CRUD API for you to see the data flowing through the authentication flows.

rails generate scaffold note name:string description:text quantity:integer due_date:datetime

Review the created folders and files. Next, repeat the steps of Devise installation and user creation:

rails generate devise:install
rails generate devise User

Now, let’s install Doorkeeper properly:

rails generate doorkeeper:install
rails generate doorkeeper:migration

The last command will print a message stating the Doorkeeper needs Rake db migration. However, first, we must adapt the database creation file. A lot of unnecessary tables for more fine-grained OAuth controls were included, and we must remove them.

To do so, change the code of your db/migrate/xxx_create_doorkeeper_tables.rb file to the one shown here.

Then, migrate the database:

rake db:migrate

Skipping Unwanted Configs

Now, let’s add the code to the routes.rb that skip some Doorkeeper controllers.

use_doorkeeper do
  skip_controllers :authorizations, :applications, :authorized_applications
end

Since we won’t be making use of applications this time, the respective controllers must be avoided.

Plus, add the root route for our API.

root to: 'notes#index'

ALso, add the namespace for our Notes API resource (to be created):

namespace :api do
  resources :notes
end

Base and Data API Controllers

Just like we did for the DIY solution, we need two controllers to manage the base routes and specific data:

rails generate controller API::Base
rails generate controller API::Notes

Remember that these commands create controller classes, not API ones. Open them up and change their declaration:

app/controllers/api/base_controller.rb

class Api::BaseController < ActionController::API
    respond_to :json
end

app/controllers/api/notes_controller.rb

class Api::NotesController < Api::BaseController
    before_action :doorkeeper_authorize!

    def index
      @notes = Note.all
      respond_with @notes
    end
end

The :doorkeeper_authorize! is equivalent to authenticate_user!.

You’ll also need to add the same before_action to the controllers/notes_controller.rb.

Regarding our user model, only one minor code snippet must be added to it:

class << self
 def authenticate(email, password)
   user = User.find_for_authentication(email: email)
   user.try(:valid_password?, password) ? user : nil
 end
end

Authenticating With Devise and Doorkeeper

Since the strategy to test the API is going to be a plain username/email and password, let’s integrate Devise with Doorkeeper by letting the User class (devise) perform the authentication.

This mechanism must be set at doorkeeper.rb. These are the changes:

  • Check whether the resource owner is authenticated:
resource_owner_from_credentials do |_routes|
  User.authenticate(params[:email], params[:password])
end

  • Allow the specified grant flow of the password. Here, you can change to authorization or client credentials as you wish. The file is self-documented, including links for more information:
grant_flows %w(password)

  • Uncomment use_refresh_token to allow tokens to be refreshed automatically.
  • Skip the authorization flow:
skip_authorization do
 true
end

Testing The Endpoints

That’s it. Now, on to the tests. Start the server via the rails server command.

Before running the commands to get an access token, you may go through the same steps of entering http://localhost:3000/ and signing up. This time, you’ll see that the CRUD form is available immediately. Go ahead a play a bit with creating, updating, and deleting some notes.

Then, to retrieve a valid access token, run the following:

curl -X POST -d
  "grant_type=password&email=email&password=password"
  localhost:3000/oauth/token

Don’t forget to replace the email and password with the ones you’ve created. The result may be similar to the following:

{
  "access_token": "XtL9QgAXS1UxRivPtqMbtDL0bBXn2gDcM7SdGnFRPsQ",
  "token_type": "Bearer",
  "expires_in": 7200,
  "refresh_token": "lqReFBjlt5FqPMFjN0kUIP5yymM6fIjkmBQBOcdWG5s",
  "created_at": 1586959874
}

If we were using the client credentials flow, like the previous example, you’d have a similar path to get there.

To get the list of notes, for example, which is the only endpoint we’ve created in the API so far, you’d go with

curl -v localhost:3000/api/notes?
  access_token=lqReFBjlt5FqPMFjN0kUIP5yymM6fIjkmBQBOcdWG5s

Change the access_token to yours, and the list of notes will be displayed in response.

It’s important to note that the session is already being managed by Devise, just like we’ve had before. That’s why you can close the browser, open it again, and still see the listing working.

Summary

In this article, we’ve explained two different approaches to achieve the same result. Prior to Doorkeeper, it was a painful process to set up an OAuth 2 provider in the simplest way possible, not to mention the lack of good documentation or tutorials for the more recent versions of Ruby on Rails.

Even with a plugin to save a lot of boilerplate code, many adaptations are still needed, and migrating from one version of Ruby/Rails to another is almost an impossible task.

With such a mature framework like Doorkeeper, we'll leave the hard work of adapting to new versions to the community and focus on what really matters: your app development.

For our second example, most of the changes were more related to the CRUD business logic (fine tuning the OAuth flow, auth steps, etc.) rather than trying to make the framework itself work properly.

You can find the source code repositories of both examples here and here.

What to do next:
  1. Try Honeybadger for FREE
    Honeybadger helps you find and fix errors before your users can even report them. Get set up in minutes and check monitoring off your to-do list.
    Start free trial
    Easy 5-minute setup — No credit card required
  2. Get the Honeybadger newsletter
    Each month we share news, best practices, and stories from the DevOps & monitoring community—exclusively for developers like you.
    author photo

    Diogo Souza

    Diogo is a more of an explorer than a programmer. Most of the best discoveries are made prior to the code itself. if free_time > 0 read() draw() eat() end

    More articles by Diogo Souza
    Stop wasting time manually checking logs for errors!

    Try the only application health monitoring tool that allows you to track application errors, uptime, and cron jobs in one simple platform.

    • Know when critical errors occur, and which customers are affected.
    • Respond instantly when your systems go down.
    • Improve the health of your systems over time.
    • Fix problems before your customers can report them!

    As developers ourselves, we hated wasting time tracking down errors—so we built the system we always wanted.

    Honeybadger tracks everything you need and nothing you don't, creating one simple solution to keep your application running and error free so you can do what you do best—release new code. Try it free and see for yourself.

    Start free trial
    Simple 5-minute setup — No credit card required

    Learn more

    "We've looked at a lot of error management systems. Honeybadger is head and shoulders above the rest and somehow gets better with every new release."
    — Michael Smith, Cofounder & CTO of YvesBlue

    Honeybadger is trusted by top companies like:

    “Everyone is in love with Honeybadger ... the UI is spot on.”
    Molly Struve, Sr. Site Reliability Engineer, Netflix
    Start free trial
    Are you using Sentry, Rollbar, Bugsnag, or Airbrake for your monitoring? Honeybadger includes error tracking with a whole suite of amazing monitoring tools — all for probably less than you're paying now. Discover why so many companies are switching to Honeybadger here.
    Start free trial
    Stop digging through chat logs to find the bug-fix someone mentioned last month. Honeybadger's built-in issue tracker keeps discussion central to each error, so that if it pops up again you'll be able to pick up right where you left off.
    Start free trial
    “Wow — Customers are blown away that I email them so quickly after an error.”
    Chris Patton, Founder of Punchpass.com
    Start free trial