How To Setup an OAuth Provider in Ruby on Rails With Doorkeeper and Devise
In a previous post on understanding OAuth I stated I had implemented an OAuth provider in Ruby on Rails 5 along with a test client. In this post, I describe how I implemented the provider.
The client will be described in an upcoming post.
Getting started with this tutorial
As a refresher, if you recall from the previous post on OAuth, the OAuth provider is responsible for giving your client application an access token so it can access the OAuth provider’s protected resource.
I present this tutorial as a series of steps. If any step is optional, I let you know in the subheading with a marking like (Optional).
Step 1 – Configure Doorkeeper In Your Existing Rails Application
In your Rails project Gemfile, add the following line and issue a bundle install command.
gem 'doorkeeper'
Once the gem is installed, it’s time run the gem’s install task as follows:
rails generate doorkeeper:install
This will install a configuration file in config/initializers/doorkeeper.rb.
Next, run a migration task to generate some database migrations for ActiveRecord. ActiveRecord is the default ORM doorkeeper is configured to run with.
rails generate doorkeeper:migration
Then issue a rake db:migrate command to make the changes to your database.
Step 2 – Routes
The following will be added automatically to your config/routes.rb file by doorkeeper’s installation tasks you ran in the previous step.
Rails.application.routes.draw do
use_doorkeeper
# your routes
end
Step 3 – Install Devise
I’m going to cover the bare minimum you need to get up and running with Devise but you can find more configuration options over at the main GitHub page.
Add the gem to your gemfile as follows:
gem 'devise'
Run the generator task:
rails generate devise:install
If you’d like, you can do the configuration that will appear in the console to setup more configuration options.
Step 4 – Install Devise User
# generate User model
rails g model User
rails g devise User
You should see something like the following in your terminal:
Running via Spring preloader in process 4931
invoke active_record
create db/migrate/20160920045132_add_devise_to_users.rb
insert app/models/user.rb
route devise_for :users
Next, I install some additional fields to help with this particular demonstration:
rails g migration AddFieldsToUsers first_name:string last_name:string company:string
And finally, I run the migrations:
bundle exec rake db:migrate
Step 3 – Authentication
In config/initializers/doorkeeper.rb I have:
Doorkeeper.configure do
resource_owner_authenticator do
# User.find_by_id(session[:current_user_id]) || redirect_to(login_url)
if user_signed_in?
current_user
else
redirect_to root_path
end
end
end
In this particular block of code, I’m using Devise’s built in helpers. I’m essentially returning the current_user if a user is signed in, otherwise, I redirect you to the root_path.
Step 4 – Protecting /oauth/applications
An easy way to do this is to use the HTTP basic authentication that Rails gives you out of the box.
class DoorkeeperClientAuthorizationController < ApplicationController
http_basic_authenticate_with name: ENV['CLIENT_USER'], password: ENV['CLIENT_PASSWORD']
def access_oauth_client_apps
redirect_to oauth_applications_path
session[:oauth_applications] = true
end
end
In config/routes.rb:
# added for minimal security around adding trusted client apps for oauth
get '/access_oauth_client_apps', to: 'doorkeeper_client_authorization_#access_oauth_client_apps'
In config/environments/development.rb
Rails.application.configure do
ENV['CLIENT_USER'] = 'oauth_client'
ENV['CLIENT_PASSWORD'] = 'password'
end
Step 5 (optional) - Send custom information along with the access token
For this, I setup a custom controller that inherits from Doorkeeper.
class CustomTokensController < Doorkeeper::ApplicationMetalController
def create
response = authorize_response
headers.merge! response.headers
body = response.body
# modify the typical doorkeeper response
if response.status == :ok
# User the resource_owner_id from token to identify the user
user = User.find(response.token.resource_owner_id) rescue nil
unless user.nil?
### If you want to render user with template
### create an ActionController to render out the user
# ac = ActionController::Base.new()
# user_json = ac.render_to_string( template: 'api/users/me', locals: { user: user})
# body[:user] = Oj.load(user_json)
### Or if you want to just append user using 'as_json'
body[:email] = user.email
body[:first_name] = user.first_name
body[:last_name] = user.last_name
body[:company] = "Company Example"
end
end
self.response_body = body.to_json
self.status = response.status
rescue Doorkeeper::Errors::DoorkeeperError => e
handle_token_exception e
end
# OAuth 2.0 Token Revocation - http://tools.ietf.org/html/rfc7009
def revoke
# The authorization server, if applicable, first authenticates the client
# and checks its ownership of the provided token.
#
# Doorkeeper does not use the token_type_hint logic described in the
# RFC 7009 due to the refresh token implementation that is a field in
# the access token model.
if authorized?
revoke_token
end
# The authorization server responds with HTTP status code 200 if the token
# has been revoked successfully or if the client submitted an invalid
# token
render json: {}, status: 200
end
private
# OAuth 2.0 Section 2.1 defines two client types, "public" & "confidential".
# Public clients (as per RFC 7009) do not require authentication whereas
# confidential clients must be authenticated for their token revocation.
#
# Once a confidential client is authenticated, it must be authorized to
# revoke the provided access or refresh token. This ensures one client
# cannot revoke another's tokens.
#
# Doorkeeper determines the client type implicitly via the presence of the
# OAuth client associated with a given access or refresh token. Since public
# clients authenticate the resource owner via "password" or "implicit" grant
# types, they set the application_id as null (since the claim cannot be
# verified).
#
# https://tools.ietf.org/html/rfc6749#section-2.1
# https://tools.ietf.org/html/rfc7009
def authorized?
if token.present?
# Client is confidential, therefore client authentication & authorization
# is required
if token.application_id?
# We authorize client by checking token's application
server.client && server.client.application == token.application
else
# Client is public, authentication unnecessary
true
end
end
end
def revoke_token
if token.accessible?
token.revoke
end
end
def token
@token ||= Doorkeeper::AccessToken.by_token(request.POST['token']) ||
Doorkeeper::AccessToken.by_refresh_token(request.POST['token'])
end
def strategy
@strategy ||= server.token_request params[:grant_type]
end
def authorize_response
@authorize_response ||= strategy.authorize
end
end
In config/routes.rb:
use_doorkeeper do
controllers :tokens => 'custom_tokens'
end
Step 6 (optional) - Skip authorization for trusted applications
In config/initializers/doorkeeper.rb
Doorkeeper.configure do
skip_authorization do
# allow all client apps to be "trusted"
true
end
end
Step 7 - Setup a protected resource endpoint
module Api
module V1
class UsersController < ApplicationController
before_action :doorkeeper_authorize!
respond_to :json
def me
respond_with current_resource_owner
end
def user
user = User.find_by(id: params[:id])
respond_with user
end
private
def current_resource_owner
# find logged in user (via devise) if doorkeeper token
User.find(doorkeeper_token.resource_owner_id) if doorkeeper_token
end
end
end
end
In config/routes.rb:
Rails.application.routes.draw do
namespace :api do
namespace :v1 do
resources :users, only: [:user] do
collection do
get :me
end
end
end
end
end
Step 8 - Setup a root path
Next, we’ll setup a default root path in our config/routes.rb file. If you don’t, you’ll see an error like the below in your console.
Rendered /Users/bruce/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/doorkeeper-4.2.0/app/views/doorkeeper/applications/index.html.erb within layouts/doorkeeper/admin (17.1ms)
Completed 500 Internal Server Error in 528ms (ActiveRecord: 3.5ms)
ActionView::Template::Error (undefined local variable or method `root_path' for #<#<Class:0x007fedc207df98>:0x007fedc206dd00>
Did you mean? font_path):
19: <%= link_to t('doorkeeper.layouts.admin.nav.applications'), oauth_applications_path %>
20: <% end %>
21: <%= content_tag :li do %>
22: <%= link_to t('doorkeeper.layouts.admin.nav.home'), root_path %>
23: <% end %>
24: </ul>
25: </div>
```
In config/routes.rb, add:
```ruby
Rails.application.routes.draw do
root to: 'static#index'
end
Next, create a StaticController:
rails g controller Static --no-assets --no-helper
Finally, create a view in app/views/static/index.html.haml
%h1 Welcome to OAuth Provider
%p welcome placeholder
Summary
If you’ve made it this far, you have now setup an OAuth provider. In an upcoming post, I talk about how to implement a test client application so you can see everything in action.