Devise Part 11: Authentication Tokens With Doorkeeper
In part eleven of the Devise series, we continue on with the previous project to add the capability of authenticating to a public API using authentication tokens with Doorkeeper.
On this page
Doorkeeper is a gem that can be used to enable scoped provider authentication for your Rails (or Grape) applications.
In this post, we will be stripping back the full extend of the Doorkeeper capability to enable a basic authentication scheme to return an authentication token that can be used to access endpoints that we will create on our public API at the /api/v2 endpoints.
This post will continue on from the previous part.
Source code can be found here.
Prerequisites
Getting started
The expectation is that you have the code up the same level as we had on the previous section (I messed up and forgot to branch at the end of the previous section!).
Open up that code and we will get started with adding and installing the Doorkeeper gem.
# Add gem
$ bundler add doorkeeper
# Ruby
$ bin/rails g doorkeeper:install
$ bin/rails g doorkeeper:migration
A migration file will be generated from those previous commands. Open up that migration file and make some adjustments.
The final file should look like the following:
# frozen_string_literal: true
class CreateDoorkeeperTables < ActiveRecord::Migration[7.0]
def change
create_table :oauth_access_tokens do |t|
t.integer :resource_owner_id
t.integer :application_id
# If you use a custom token generator you may need to change this column
# from string to text, so that it accepts tokens larger than 255
# characters. More info on custom token generators in:
# https://github.com/doorkeeper-gem/doorkeeper/tree/v3.0.0.rc1#custom-access-token-generator
#
# t.text :token, null: false
t.string :token, null: false
t.string :refresh_token
t.integer :expires_in
t.datetime :revoked_at
t.datetime :created_at, null: false
t.string :scopes
end
add_index :oauth_access_tokens, :token, unique: true
add_index :oauth_access_tokens, :resource_owner
add_index :oauth_access_tokens, :refresh_token, unique: true
# Uncomment below to ensure a valid reference to the resource owner's table
add_foreign_key :oauth_access_tokens, :users, column: :resource_owner_id
end
end
Here we are effectively removing all capabilities other than oauth_access_tokens.
This will be a basic reference to a user that owns that access token.
Configuring the Doorkeeper initializer file
Let's update the Doorkeeper initializer config file at config/initializers/doorkeeper.rb.
# frozen_string_literal: true
Doorkeeper.configure do
skip_client_authentication_for_password_grant true
# Change the ORM that doorkeeper will use (requires ORM extensions installed).
# Check the list of supported ORMs here: https://github.com/doorkeeper-gem/doorkeeper#orms
orm :active_record
# This block will be called to check whether the resource owner is authenticated or not.
resource_owner_authenticator do
current_user || warden.authenticate!(scope: :user)
end
resource_owner_from_credentials do |_routes|
User.authenticate(params[:email], params[:password])
end
# Issue access tokens with refresh token (disabled by default), you may also
# pass a block which accepts `context` to customize when to give a refresh
# token or not. Similar to +custom_access_token_expires_in+, `context` has
# the following properties:
#
# `client` - the OAuth client application (see Doorkeeper::OAuth::Client)
# `grant_type` - the grant type of the request (see Doorkeeper::OAuth)
# `scopes` - the requested scopes (see Doorkeeper::OAuth::Scopes)
#
use_refresh_token
# Define access token scopes for your provider
# For more information go to
# https://doorkeeper.gitbook.io/guides/ruby-on-rails/scopes
#
default_scopes :read
optional_scopes :write
enforce_configured_scopes
# Specify what grant flows are enabled in array of Strings. The valid
# strings and the flows they enable are:
#
# "authorization_code" => Authorization Code Grant Flow
# "implicit" => Implicit Grant Flow
# "password" => Resource Owner Password Credentials Grant Flow
# "client_credentials" => Client Credentials Grant Flow
#
# If not specified, Doorkeeper enables authorization_code and
# client_credentials.
#
# implicit and password grant flows have risks that you should understand
# before enabling:
# https://datatracker.ietf.org/doc/html/rfc6819#section-4.4.2
# https://datatracker.ietf.org/doc/html/rfc6819#section-4.4.3
#
# grant_flows %w[authorization_code client_credentials]
grant_flows %w[password]
# Under some circumstances you might want to have applications auto-approved,
# so that the user skips the authorization step.
# For example if dealing with a trusted application.
#
# skip_authorization do |resource_owner, client|
# client.superapp? or resource_owner.admin?
# end
skip_authorization do
true
end
end
The important part is that for our setup, our grant flow will use basic authentication with a password.
Updating our User model
Our app/models/user.rb file needs to be adjusted to provide the authenticate method for authenticating, as well as adding the has_many relationships required.
class User < ApplicationRecord
include Devise::JWT::RevocationStrategies::JTIMatcher
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
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable
include DeviseTokenAuth::Concerns::User
class << self
def authenticate(email, password)
user = User.find_for_authentication(email: email)
user.try(:valid_password?, password) ? user : nil
end
end
end
Configure the Doorkeeper routes
Finally, we need to configure our routes to use Doorkeeper.
The final config/routes.rb file should look like the following:
Rails.application.routes.draw do
use_doorkeeper do
skip_controllers :authorizations, :applications,
:authorized_applications
end
mount_devise_token_auth_for 'User', at: 'auth'
# Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html
# Defines the root path route ("/")
# root "articles#index"
namespace :api do
namespace :v1 do
resources :home, only: [:index]
end
namespace :v2 do
resources :home, only: [:index]
end
end
end
We are setting up our use_doorkeeper method to add in the routes, but skipping the unnecessary controllers.
Also notably is the v2 namespace that we are adding in. We will effectively repeat what we have for v1 but change the required authorization.
Creating our v2 home controller
In the terminal, we need to now create the controller that the routes were set up for.
# Generate the controller
$ bin/rails g controller api/v2/home
This will output the file app/controllers/api/v2/home_controller.rb. Update that file to have the following:
class Api::V2::HomeController < ApplicationController
before_action :doorkeeper_authorize!
def index
render json: { message: 'Welcome to the Public DoorKeeper API' }
end
end
The most notable difference between this file and app/controllers/api/v1/home_controller.rb is the before_action. V1 makes use of the authenticate_user! function, while v2 enforces the doorkeeper_authorize! function.
Testing our route
At this point, we can start our Rails application bin/rails s and run some tests.
First of all, we want to get a token that we can use against the routes.
# Get token
$ curl -X POST -d "grant_type=password&email=hello@example.com&password=password" localhost:3000/oauth/token
An example of this in Postman:

Once you have a token, you can supply it as a query parameter to authorize a request:
# This won't work
$ curl -v http://localhost:3000/api/v2/home
# This will
$ curl -v localhost:3000/api/items?access_token=OurAccessTokenReturnedByAPI
An example of a request without an access token that returns a 401:

An example with the token returning a successful response:

Perfect! So we can now get to resources with a valid authentication token on our v2 API.
To drive in a little further how our authentication methods work, it is worth understanding the following two concepts:
- Auth tokens from our
devise_auth_tokenflow that are returned and valid for v1 of our API are not valid for our V2 endpoints. - Auth tokens from our Doorkeeper flow that are returned and valid for v2 of our API are not valid for our v1 endpoints.
The following images demonstrate this.


Summary
Today's post demonstrated how to use a basic implementation of authorization tokens with Doorkeeper in order to protect API routes that we could make public for our users.
In the next post in the Devise series, we will look to update this implementation to something more robust with a flow that can create applications and set scopes for what can and cannot be accessed by a user with a Doorkeeper auth token.
Resources and further reading
Photo credit: marekpiwnicki