[Awesome Ruby Gem] Use doorkeeper gem to make your Rails project as an OAuth 2 provider

doorkeeper

Doorkeeper is a gem (Rails engine) that makes it easy to introduce OAuth 2 provider functionality to your Ruby on Rails or Grape application.

Supported features:

  • The OAuth 2.0 Authorization Framework

    • Authorization Code Flow

    • Access Token Scopes

    • Refresh token

    • Implicit grant

    • Resource Owner Password Credentials

    • Client Credentials

  • OAuth 2.0 Token Revocation

  • OAuth 2.0 Token Introspection

  • OAuth 2.0 Threat Model and Security Considerations

  • OAuth 2.0 for Native Apps

  • Proof Key for Code Exchange by OAuth Public Clients

Installation

You can install it as a gem:

1
$ gem install doorkeeper

or add it into a Gemfile (Bundler):

1
2
3
4
5
# Gemfile

# GitHub - doorkeeper-gem/doorkeeper: Doorkeeper is an OAuth 2 provider for Ruby on Rails / Grape.
# https://github.com/doorkeeper-gem/doorkeeper
gem 'doorkeeper', `5.5.0'

Then, run bundle install.

1
$ bundle install

Usage

Generator

After that, you need to generate relevant files with:

1
$ bundle exec rails generate doorkeeper:install

This will introduce three changes:

  • A new initializer in config/initializers/doorkeeper.rb

  • Add doorkeeper’s routes to config/routes.rb

  • Locale files in config/locales/doorkeeper.en.yml

Migrations

To generate appropriate tables, run:

1
2
$ bundle exec rails generate doorkeeper:migration
create db/migrate/20190324080634_create_doorkeeper_tables.rb

This migration will create all necessary tables for oAuth2 Applications - https://doorkeeper.gitbook.io/guides/concepts/application, Access Grants - https://doorkeeper.gitbook.io/guides/concepts/access-grant, and Access Tokens - https://doorkeeper.gitbook.io/guides/concepts/access-token. See the database design - https://doorkeeper.gitbook.io/guides/internals/database-design for more details.


NOTE: If using UUIDs instead of integer IDs, see Using PostgreSQL UUIDs as primary keys with Doorkeeper - https://github.com/doorkeeper-gem/doorkeeper/wiki/Using-PostgreSQL-UUIDs-as-primary-keys-with-Doorkeeper for changes you will need to make to your migration.


Integrating with existing User Model

Before executing the migration, you may want to add foreign keys to doorkeeper’s tables to ensure data integrity. Go to the migration file and uncomment the lines below:

1
2
3
4
5
# db/migrate/20190324080634_create_doorkeeper_tables.rb

# Uncomment below to ensure a valid reference to the resource owner's table
add_foreign_key :oauth_access_grants, :users, column: :resource_owner_id
add_foreign_key :oauth_access_tokens, :users, column: :resource_owner_id

Now you’re ready to run the migrations:

1
bundle exec rake db:migrate

As the next step, you may want to add associations to your model. If you skip this step, you’ll encounter ActiveRecord::InvalidForeignKeyerror when you try to destroy the User that has associated access grants or access tokens.

1
2
3
4
5
6
7
8
9
10
11
12
13
# app/models/user.rb

class User < ApplicationRecord
has_many :access_grants,
class_name: 'Doorkeeper::AccessGrant',
foreign_key: :resource_owner_id,
dependent: :delete_all # or :destroy if you need callbacks

has_many :access_tokens,
class_name: 'Doorkeeper::AccessToken',
foreign_key: :resource_owner_id,
dependent: :delete_all # or :destroy if you need callbacks
end

Routes

The installation script will also automatically add the Doorkeeper routes into your app:

1
2
3
4
5
config/routes.rb
Rails.application.routes.draw do
use_doorkeeper
# your routes below
end

This will mount following routes:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ bundle exec rails routes
native_oauth_authorization GET /oauth/authorize/native(.:format) doorkeeper/authorizations#show
oauth_authorization GET /oauth/authorize(.:format) doorkeeper/authorizations#new
DELETE /oauth/authorize(.:format) doorkeeper/authorizations#destroy
POST /oauth/authorize(.:format) doorkeeper/authorizations#create
oauth_token POST /oauth/token(.:format) doorkeeper/tokens#create
oauth_revoke POST /oauth/revoke(.:format) doorkeeper/tokens#revoke
oauth_introspect POST /oauth/introspect(.:format) doorkeeper/tokens#introspect
oauth_applications GET /oauth/applications(.:format) doorkeeper/applications#index
POST /oauth/applications(.:format) doorkeeper/applications#create
new_oauth_application GET /oauth/applications/new(.:format) doorkeeper/applications#new
edit_oauth_application GET /oauth/applications/:id/edit(.:format) doorkeeper/applications#edit
oauth_application GET /oauth/applications/:id(.:format) doorkeeper/applications#show
PATCH /oauth/applications/:id(.:format) doorkeeper/applications#update
PUT /oauth/applications/:id(.:format) doorkeeper/applications#update
DELETE /oauth/applications/:id(.:format) doorkeeper/applications#destroy
oauth_authorized_applications GET /oauth/authorized_applications(.:format) doorkeeper/authorized_applications#index
oauth_authorized_application DELETE /oauth/authorized_applications/:id(.:format) doorkeeper/authorized_applications#destroy
oauth_token_info GET /oauth/token/info(.:format) doorkeeper/token_info#show

At this point on the guide, we’re not going to change the routes. For more information on how to customize them, check out this page on the wiki - https://github.com/doorkeeper-gem/doorkeeper/wiki/Customizing-routes.

Configuration

Before you’re able to use Doorkeeper, you need to configure how resource owners (users) can be authenticated and who can manage such applications.

Resource Owner Authentication

This configuration should do two things:

  • Return the user is currently authenticated

  • Redirect the user to the authentication page

If you’re using devise, one option is to write the following:

1
2
3
4
5
6
7
# config/initializers/doorkeeper.rb

Doorkeeper.configure do
resource_owner_authenticator do
current_user || warden.authenticate!(scope: :user)
end
end

The block above runs in the context of your application so you have access to your models, session and routes helpers. However, it is not run in the context of the ApplicationController which means that it doesn’t have access to the methods defined over there.

You may want to check other ways of authentication - https://github.com/doorkeeper-gem/doorkeeper/wiki/Authenticating-using-Clearance-or-DIY here.

Application Management Authentication

By default, the applications list in /oauth/applications is unavailable. To let users see and manage all applications, you should configure admin_authenticator block:

1
2
3
4
5
6
config/initializers/doorkeeper.rb
Doorkeeper.configure do
admin_authenticator do |_routes|
current_user || warden.authenticate!(scope: :user)
end
end

The block follows the same rules as resource_owner_authenticator block.


Note: the application list is just a scaffold. It’s highly recommended to either customize the controller used by the list or skip the controller all together. For more information see the page in the wiki - https://github.com/doorkeeper-gem/doorkeeper/wiki/Customizing-routes.


Scopes

For this guide let’s create two scopes: read and write. Applications authorized with the read scope will be able to read public data from our API. The write scope will let applications change data in our API.

Go to doorkeeper’s initializer and add:

1
2
3
4
5
6
7
8
# config/initializers/doorkeeper.rb

Doorkeeper.configure do
default_scopes :read
optional_scopes :write

enforce_configured_scopes
end

The last line with enforce_configured_scopes ensures applications to be able to ask only for configured scopes defined in default_scopes and optional_scopes.

Doorkeeper::Application

In order to add Applications for authorizing, you will need to create them via console:

1
Doorkeeper::Application.create(name: "MyApp", redirect_uri: "urn:ietf:wg:oauth:2.0:oob", scopes: ["read", "write"])

Securing the API

To protect your controllers (usual one or ActionController::API) with OAuth, you just need to setup before_actions specifying the actions you want to protect. For example:

1
2
3
4
5
6
7
class Api::V1::ProductsController < Api::V1::ApiController
before_action :doorkeeper_authorize! # Requires access token for all actions

# before_action -> { doorkeeper_authorize! :read, :write }

# your actions
end

You can pass any option before_action accepts, such as if, only, except, and others.

Authenticated resource owner

If you want to return data based on the current resource owner, in other words, the access token owner, you may want to define a method in your controller that returns the resource owner instance:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Api::V1::CredentialsController < Api::V1::ApiController
before_action :doorkeeper_authorize!
respond_to :json

# GET /me.json
def me
respond_with current_resource_owner
end

private

# Find the user that owns the access token
def current_resource_owner
User.find(doorkeeper_token.resource_owner_id) if doorkeeper_token
end
end

In this example, we’re returning the credentials (me.json) of the access token owner.

API Mode

By default Doorkeeper uses full Rails stack to provide all the OAuth 2 functionality with additional features like administration area for managing applications. By the way, starting from Doorkeeper 5 you can use API mode for your API only Rails 5 applications. All you need is just to configure the gem to work in desired mode:

1
2
3
4
5
config/initializers/doorkeeper.rb
Doorkeeper.configure do
api_only
base_controller 'ActionController::API'
end

Keep in mind, that in this mode you will not be able to access Applications or Authorized Applications controllers because they will be skipped. CSRF protections (which are otherwise enabled) will be skipped, and all the redirects will be returned as JSON response with corresponding locations.

Authorization Code Flow

Authorization code is probably the most used flow. It basically consists of an exchange of an authorization token for an access token. For more detailed info, check out the RFC spec here

The first step is to register your client app.

Registering the client

Once you have doorkeeper up and running, set up a new client in /oauth/applications/new. For testing purposes, you should fill in the redirect URI field with urn:ietf:wg:oauth:2.0:oob. This will tell doorkeeper to display the authorization code instead of redirecting to a client application (that you don’t have now).

You can change this behaviour by changing the native_redirect_uri config in the doorkeeper initializer.

Requesting authorization

To request the authorization token, you should visit the /oauth/authorize endpoint. You can do that either by clicking in the link to the authorization page in the app details or by visiting manually the URL:

http://localhost:3000/oauth/authorize?client_id=YOUR_CLIENT_ID&redirect_uri=urn:ietf:wg:oauth:2.0:oob&response_type=code

Once you are there, you should sign in and click on Authorize and Access grant.

Requesting the access token

To request the access token, you should use the returned code and exchange it for an access token. To do that you can use any HTTP client. In this case, I used rest-client:

1
2
3
4
5
6
7
8
9
10
parameters = 'client_id=THE_ID&client_secret=THE_SECRET&code=RETURNED_CODE&grant_type=authorization_code&redirect_uri=urn:ietf:wg:oauth:2.0:oob'
RestClient.post 'http://localhost:3000/oauth/token', parameters

# The response will be
{
"access_token": "de6780bc506a0446309bd9362820ba8aed28aa506c71eedbe1c5c4f9dd350e54",
"token_type": "Bearer",
"expires_in": 7200,
"refresh_token": "8257e65c97202ed1726cf9571600918f3bffb2544b26e00a61df9897668c33a1"
}

You can now make requests to the API with the access token returned.

Client Credentials flow

The Client Credentials flow is probably the most simple flow of OAuth 2 flows. The main difference from the others is that this flow is not associated with a resource owner.

One usage of this flow would be retrieving client statistics for example. Since the access token would be connected to the client only, the access token won’t have access to private user data for example.

Enabling the grant flow

In your Doorkeeper configuration initializer, have a line like this:

1
2
# add other flows to this array if you want more to be enabled, e.g., %w{authorization_code implicit password}
grant_flows ['client_credentials']

Usage

To get an access token from client credentials flow, you have to do a post to /oauth/token endpoint:

1
2
3
4
POST /oauth/token
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
Content-Type: application/x-www-form-urlencoded;charset=UTF-8
grant_type=client_credentials

The Authorization header includes the encoded credentials for the client. For more information and options on how authenticate clients, check this page in the wiki.

In ruby, it would be something like this:

1
2
3
4
5
6
7
8
9
10
11
require 'rest-client'
require 'json'

client_id = '4ea1b...'
client_secret = 'a2982...'

response = RestClient.post 'http://localhost:3000/oauth/token', {
grant_type: 'client_credentials',
client_id: client_id,
client_secret: client_secret
}

Notice that in this case we used client_id/secret on parameters instead of using the encoded header.

After that you’ll have the access token in the response:

1
2
token = JSON.parse(response)["access_token"]
# => 'a2982...'

And then, you can request access to protected resources that do not require a resource owner:

1
2
RestClient.get 'http://localhost:3000/api/v1/profiles.json', { 'Authorization' => "Bearer #{token}" }
# => "[{"email":"tara_kertzmann@yundt.name","id":25,"name":"Jorge Ward","username":"leonor"}, ...]"

That’s everything.

References

[1] GitHub - doorkeeper-gem/doorkeeper: Doorkeeper is an OAuth 2 provider for Ruby on Rails / Grape. - https://github.com/doorkeeper-gem/doorkeeper

[2] Doorkeeper Guides - doorkeeper - https://doorkeeper.gitbook.io/guides/

[3] doorkeeper | RubyGems.org | your community gem host - https://rubygems.org/gems/doorkeeper/

[4] Authorization Code Flow · doorkeeper-gem/doorkeeper Wiki · GitHub - https://github.com/doorkeeper-gem/doorkeeper/wiki/Authorization-Code-Flow

[5] Client Credentials flow · doorkeeper-gem/doorkeeper Wiki · GitHub - https://github.com/doorkeeper-gem/doorkeeper/wiki/Client-Credentials-flow

[6] OAuth 2.0 — OAuth - https://oauth.net/2/