DEV Community

Code Salley
Code Salley

Posted on • Edited on

Ruby on Rails api authentication with devise and Doorkeeper

Ruby on Rails web authentication with devise is easy to get started with, but how can we handle authentication with devise and doorkeeper in a rails api. This article focuses on steps to add authentication after creating a new project.

Buckle up, required gems installation, Starting with devise.


Setup devise
# add to Gemfile
gem 'devise'

# install gem
bundle install 

# devise initialization.
# for more context on devise refer to official 
rails generate devise:install

# generate User model migration. 
rails generate devise User 

# Run rails migration 
rails db:migrate

Enter fullscreen mode Exit fullscreen mode

Devise docs


Setup Doorkeeper
# add doorkeeper to Gemfile 
gem 'doorkeeper'

# install gem 
bundle install 

# initialize doorkeeper 
rails generate doorkeeper:install 

# Above command integrate doorkeeper into your 
# project and create and initializer file in 
# config/initializers/doorkeeper.rb, add doorkeeper 
# routes to your route file(config/routes.rb) and locale 
# file in config/locales/doorkeeper.en.yml 
# for more insight check official docs. 

# generate doorkeeper migration 
rails generate doorkeeper:migration

# run rails migration 
rails db:migrate

Enter fullscreen mode Exit fullscreen mode

Doorkeeper Official docs

We need two(2) routes for login and signup. In config/routes.rb lets comment out use_doorkeeper and devise routes. Create new routing to look something like this.

# config/routes.rb

# comment or remove doorkeeper routes
# use_doorkeeper

# not exposing devise routes 
devise_for :users, only: [] 

# our auth routes auth/signup and auth/login
scope "auth" do 
  post "/signup",  to: "auth#signup"
  post "/login",  to: "auth#login"
end

Enter fullscreen mode Exit fullscreen mode

doorkeeper configuration needs some adjustment since we're using custom routes.

# config/initializers/doorkeeper.rb

Doorkeeper.configure do
  # 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]) # we need to add this method in our user model
  end

  grant_flows %w[authorization_code client_credentials password]

  use_refresh_token

  client_credentials :from_basic, :from_params

  access_token_methods :from_bearer_authorization, :from_access_token_param, :from_bearer_param

  access_token_expires_in 1.hour
end
Enter fullscreen mode Exit fullscreen mode

lets adjust User model to include authenticate! method

# app/models/user.rb
class User < ApplicationRecord

# add this method to find and authenticate users
  def self.authenticate!(email, password)
    user = find_by(email: email.downcase)
    user if user&.valid_password?(password)
  end
end
Enter fullscreen mode Exit fullscreen mode

One of the very capabilities of doorkeeper is the ability to manage multiple platforms, like a doorkeeper client app for android, web etc. In our database seed (db/seeds.rb) lets create an app for our IOS app like this.

# db/seeds.rb

Doorkeeper::Application.find_or_create_by(name: "IOS APP") do |app|
  app.redirect_uri = "urn:ietf:wg:oauth:2.0:oob"
  app.secret = "my_secret" 
  app.uid = "my_uid"
  app.save!
end

Enter fullscreen mode Exit fullscreen mode

run rails db:seed to insert above doorkeeper app into our database.

Now we need auth controller to be able to handle requests from our signup and login routes. create a file app/controllers/auth_controller.rb

# app/controllers/auth_controller.rb

# signup method
def signup
    client_app = Doorkeeper::Application.find_by(uid: params[:client_id])

    unless client_app
      return render json: { error: I18n.t("doorkeeper.errors.messages.invalid_client") },
          status: :unauthorized
    end

    @user = User.new(user_params)
    @user.save


    unless @user
      render json: { message: "registration failed" }, status: :unprocessable_entity
    end

    @results = model_results(@user, client_app)

    @results
end

def login
    response = strategy.authorize
    @token = response.status == :ok ? response.token : nil
    if @token&.resource_owner_id
      @user ||= User.find(@token.resource_owner_id)
    end

    self.response_body =
      if response.status == :unauthorized
        render json: {error: "unauthorized" }, status: 404
      else
        user_json(@user, @token)
      end
end



private 

# for a more cleaner approach, separate this into concerns or an isolated class.

def model_results(user, client_app, token_type = "Bearer")
   access_token = Doorkeeper::AccessToken.find_or_create_for(
        resource_owner: user.id,
        application:    client_app,
        refresh_token:  generate_refresh_token,
        expires_in:     Doorkeeper.configuration.access_token_expires_in.to_i,
        scopes:         ""
      )

  return { user: user, tokens: {refresh_token: access_token.refresh_token, access_token:  access_token.token }
end


 def generate_refresh_token
    loop do
      token = SecureRandom.hex(32)

      break token unless Doorkeeper::AccessToken.exists?(refresh_token: token)
    end
end


def user_json(user, token)
    {
      user:          user,
      auth: {

        access_token:  token.token,
        refresh_token: token.refresh_token
      }
    }.to_json
end

  def user_params
    params.require(:user).permit(:email ,:password, :password_confirmation)
  end

Enter fullscreen mode Exit fullscreen mode

Finally how do we secure endpoints?
For any controller actions we desire to protect, we place this in our controller.

class ProfileController < ApplicationController
  # this protects profile actions. 
  before_action :doorkeeper_authorize!


  def create 
  end
end
Enter fullscreen mode Exit fullscreen mode

Happy Coding 🎉

Top comments (4)

Collapse
 
tnypxl profile image
tnypxl

If you're a reader stumbling upon this, there will be many changes and fixes required to make this implementation function properly.

Collapse
 
codesalley profile image
Code Salley

Can you be more descriptive. Thanks

Collapse
 
tnypxl profile image
tnypxl • Edited
  • This one isn't your fault, but there is a bug in the gem that makes it look for the doorkeeper initializer before it's been created when running rails g doorkeeper:install. It’s been brought up in doorkeeper’s GitHub as an issue. The work-around is creating the initializer manually with an empty Doorkeeper.configure do; end block and then running rails g doorkeeper:install. You might make a note in the post about it as no fix has been committed.
  • The user_json method is used as though it accepts arguments. But the method is not written to accept any arguments.
  • The user_params method should require :auth and not :users.
  • In the signup method, you are not rendering the model_results as json, so nothing comes across in the response.

I don't see how any of your code could have worked as-is. Typically, for tutorials like this, its a good idea to publish a repository of the working code just in case there are dependency issues or caveats and considerations not covered in the tutorial.

Collapse
 
walem2707 profile image
Walem • Edited

Where is the strategy method, im getting an error in that line strategy.authorize. undefined local variable or method `strategy' thanks