Back to home

Devise Part 11: Authentication Tokens With Doorkeeper main image

Devise Part 11: Authentication Tokens With Doorkeeper

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

  1. Read "Devise Part 10: Devise Token Auth"

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

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

Without an access token

An example with the token returning a successful response:

With a valid access token

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:

  1. 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.
  2. 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

v1 auth token does not work for v2

v2 Doorkeeper token does not work for v1

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

Personal image

Dennis O'Keeffe

@dennisokeeffe92
  • Melbourne, Australia

Hi, I am a professional Software Engineer. Formerly of Culture Amp, UsabilityHub, Present Company and NightGuru.
I am currently working on workingoutloud.dev, Den Dribbles and LandPad .

1,200+ PEOPLE ALREADY JOINED ❤️️

Get fresh posts + news direct to your inbox.

No spam. We only send you relevant content.