Ruby on Rails api authentication with devise and Doorkeeper
Tags: rails, ruby, api, programming
Published: 2022-12-30 20:25:49

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`.

****
<center>Setup devise</center>

```ruby
# 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

```

[Devise docs](https://github.com/heartcombo/devise#getting-started)

**** 
<center> Setup Doorkeeper </center>

```ruby
# 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

```
[Doorkeeper Official docs](https://doorkeeper.gitbook.io/guides/ruby-on-rails/getting-started)


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. 

```ruby
# 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

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

```ruby
# 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
``` 
lets adjust `User` model to include `authenticate!` method 

```ruby
# 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
```

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. 

```ruby
# 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

```
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`

```ruby 
# 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

```
Finally how do we secure endpoints? 
For any controller actions we desire to protect, we place this in our controller. 
```ruby
class ProfileController < ApplicationController
  # this protects profile actions. 
  before_action :doorkeeper_authorize!


  def create 
  end
end
```

Happy Coding 🎉