Devise Part 11: Authentication Tokens With Doorkeeper
Published: Mar 16, 2022
Last updated: Mar 16, 2022
Doorkeeper
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:
Postman - Getting the OAuth token
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:
Without an access token
An example with the token returning a successful response:
With a valid access token
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_token
flow 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.
v1 auth token does not work for v2
v2 Doorkeeper token does not work for v1
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
Dennis O'Keeffe
Melbourne, Australia
1,200+ PEOPLE ALREADY JOINED ❤️️
Get fresh posts + news direct to your inbox.
No spam. We only send you relevant content.
Devise Part 11: Authentication Tokens With Doorkeeper
Introduction