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.
Type any email and password, and then click Sign up. The following screen will appear, stating the success of the previous action.
Welcome page.
Next, click the “Register your application” link and, once redirected, fill in the following application data:
- Name: hb-oauth-app
- Main App URL: http://localhost:3000
- Callback URL: http://localhost:4567/auth/test (the one our Sinatra app will handle, where the server must redirect after successful login)
- Support URL: leave it empty.
Click the Register button, and you’ll be redirected to the 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.